diff --git a/abi/abi.go b/abi/abi.go index e4a5cff..1f503ed 100644 --- a/abi/abi.go +++ b/abi/abi.go @@ -493,6 +493,27 @@ func ReportToProto(data []uint8) (*pb.Report, error) { return r, nil } +// ReportCertsToProto creates a pb.Attestation from the report and certificate table represented in +// data. The report is expected to take exactly abi.ReportSize bytes, followed by the certificate +// table. +func ReportCertsToProto(data []uint8) (*pb.Attestation, error) { + var certs []uint8 + report := data + if len(data) >= ReportSize { + report = data[:ReportSize] + certs = data[ReportSize:] + } + mreport, err := ReportToProto(report) + if err != nil { + return nil, err + } + table := new(CertTable) + if err := table.Unmarshal(certs); err != nil { + return nil, err + } + return &pb.Attestation{Report: mreport, CertificateChain: table.Proto()}, nil +} + func checkReportSizes(r *pb.Report) error { if len(r.FamilyId) != FamilyIDSize { return fmt.Errorf("report family_id length is %d, expect %d", len(r.FamilyId), FamilyIDSize) diff --git a/client/client.go b/client/client.go index 5f48b60..93ac948 100644 --- a/client/client.go +++ b/client/client.go @@ -39,6 +39,19 @@ type Device interface { Product() *pb.SevProduct } +// LeveledQuoteProvider encapsulates calls to collect an extended attestation report at a given +// privilege level. +type LeveledQuoteProvider interface { + IsSupported() bool + GetRawQuoteAtLevel(reportData [64]byte, vmpl uint) ([]uint8, error) +} + +// QuoteProvider encapsulates calls to collect an extended attestation report. +type QuoteProvider interface { + IsSupported() bool + GetRawQuote(reportData [64]byte) ([]uint8, error) +} + // UseDefaultSevGuest returns true iff -sev_guest_device_path=default. func UseDefaultSevGuest() bool { return *sevGuestPath == "default" diff --git a/client/client_linux.go b/client/client_linux.go index 4a83e21..99f2886 100644 --- a/client/client_linux.go +++ b/client/client_linux.go @@ -22,6 +22,8 @@ import ( "fmt" "time" + "github.com/google/go-configfs-tsm/configfs/linuxtsm" + "github.com/google/go-configfs-tsm/report" "github.com/google/go-sev-guest/abi" labi "github.com/google/go-sev-guest/client/linuxabi" spb "github.com/google/go-sev-guest/proto/sevsnp" @@ -123,3 +125,102 @@ func (d *LinuxDevice) Ioctl(command uintptr, req any) (uintptr, error) { func (d *LinuxDevice) Product() *spb.SevProduct { return abi.SevProduct() } + +// LinuxIoctlQuoteProvider implements the QuoteProvider interface to fetch +// attestation quote via the deprecated /dev/sev-guest ioctl. +type LinuxIoctlQuoteProvider struct{} + +// IsSupported checks if TSM client can be created to use /dev/sev-guest ioctl. +func (p *LinuxIoctlQuoteProvider) IsSupported() bool { + d, err := OpenDevice() + if err != nil { + return false + } + d.Close() + return true +} + +// GetRawQuoteAtLevel returns byte format attestation plus certificate table via /dev/sev-guest ioctl. +func (p *LinuxIoctlQuoteProvider) GetRawQuoteAtLevel(reportData [64]byte, level uint) ([]uint8, error) { + d, err := OpenDevice() + if err != nil { + return nil, err + } + defer d.Close() + report, certs, err := GetRawExtendedReportAtVmpl(d, reportData, int(level)) + if err != nil { + return nil, err + } + return append(report, certs...), nil +} + +// GetRawQuote returns byte format attestation plus certificate table via /dev/sev-guest ioctl. +func (p *LinuxIoctlQuoteProvider) GetRawQuote(reportData [64]byte) ([]uint8, error) { + d, err := OpenDevice() + if err != nil { + return nil, err + } + defer d.Close() + report, certs, err := GetRawExtendedReport(d, reportData) + if err != nil { + return nil, err + } + return append(report, certs...), nil +} + +// LinuxConfigFsQuoteProvider implements the QuoteProvider interface to fetch +// attestation quote via ConfigFS. +type LinuxConfigFsQuoteProvider struct{} + +// IsSupported checks if TSM client can be created to use ConfigFS system. +func (p *LinuxConfigFsQuoteProvider) IsSupported() bool { + _, err := linuxtsm.MakeClient() + return err == nil +} + +// GetRawQuoteAtLevel returns byte format attestation plus certificate table via ConfigFS. +func (p *LinuxConfigFsQuoteProvider) GetRawQuoteAtLevel(reportData [64]byte, level uint) ([]uint8, error) { + req := &report.Request{ + InBlob: reportData[:], + GetAuxBlob: true, + Privilege: &report.Privilege{ + Level: level, + }, + } + resp, err := linuxtsm.GetReport(req) + if err != nil { + return nil, err + } + return append(resp.OutBlob, resp.AuxBlob...), nil +} + +// GetRawQuote returns byte format attestation plus certificate table via ConfigFS. +func (p *LinuxConfigFsQuoteProvider) GetRawQuote(reportData [64]byte) ([]uint8, error) { + req := &report.Request{ + InBlob: reportData[:], + GetAuxBlob: true, + } + resp, err := linuxtsm.GetReport(req) + if err != nil { + return nil, err + } + return append(resp.OutBlob, resp.AuxBlob...), nil +} + +// GetQuoteProvider returns a supported SEV-SNP QuoteProvider. +func GetQuoteProvider() (QuoteProvider, error) { + preferred := &LinuxConfigFsQuoteProvider{} + if !preferred.IsSupported() { + return &LinuxIoctlQuoteProvider{}, nil + } + return preferred, nil +} + +// GetLeveledQuoteProvider returns a supported SEV-SNP LeveledQuoteProvider. +func GetLeveledQuoteProvider() (QuoteProvider, error) { + preferred := &LinuxConfigFsQuoteProvider{} + if !preferred.IsSupported() { + return &LinuxIoctlQuoteProvider{}, nil + } + return preferred, nil +} diff --git a/client/client_macos.go b/client/client_macos.go index 824272a..b53c506 100644 --- a/client/client_macos.go +++ b/client/client_macos.go @@ -26,6 +26,7 @@ import ( const DefaultSevGuestDevicePath = "unknown" // MacOSDevice implements the Device interface with Linux ioctls. +// Deprecated: Use MacOSQuoteProvider. type MacOSDevice struct{} // Open is not supported on MacOS. @@ -52,3 +53,31 @@ func (*MacOSDevice) Ioctl(_ uintptr, _ any) (uintptr, error) { func (*MacOSDevice) Product() *spb.SevProduct { return &spb.SevProduct{} } + +// MacOSQuoteProvider implements the QuoteProvider interface with Linux's configfs-tsm. +type MacOSQuoteProvider struct{} + +// IsSupported checks if the quote provider is supported. +func (*MacOSQuoteProvider) IsSupported() bool { + return false +} + +// GetRawQuote returns byte format attestation plus certificate table via ConfigFS. +func (*MacOSQuoteProvider) GetRawQuote(reportData [64]byte) ([]byte, error) { + return nil, fmt.Errorf("MacOS is unsupported") +} + +// GetRawQuoteAtLevel returns byte format attestation plus certificate table via ConfigFS. +func (*MacOSQuoteProvider) GetRawQuoteAtLevel(reportData [64]byte, level uint) ([]byte, error) { + return nil, fmt.Errorf("MacOS is unsupported") +} + +// GetQuoteProvider returns a supported SEV-SNP QuoteProvider. +func GetQuoteProvider() (QuoteProvider, error) { + return nil, fmt.Errorf("MacOS is unsupported") +} + +// GetLeveledQuoteProvider returns a supported SEV-SNP LeveledQuoteProvider. +func GetLeveledQuoteProvider() (LeveledQuoteProvider, error) { + return nil, fmt.Errorf("MacOS is unsupported") +} diff --git a/client/client_windows.go b/client/client_windows.go index 281c610..c92c22d 100644 --- a/client/client_windows.go +++ b/client/client_windows.go @@ -23,6 +23,7 @@ import ( ) // WindowsDevice implements the Device interface with Linux ioctls. +// Deprecated: Use WindowsQuoteProvider. type WindowsDevice struct{} // Open is not supported on Windows. @@ -50,3 +51,31 @@ func (*WindowsDevice) Ioctl(_ uintptr, _ any) (uintptr, error) { func (*WindowsDevice) Product() *spb.SevProduct { return &spb.SevProduct{} } + +// WindowsQuoteProvider implements the QuoteProvider interface with Linux's configfs-tsm. +type WindowsQuoteProvider struct{} + +// IsSupported checks if the quote provider is supported. +func (*WindowsQuoteProvider) IsSupported() bool { + return false +} + +// GetRawQuote returns byte format attestation plus certificate table via ConfigFS. +func (*WindowsQuoteProvider) GetRawQuote(reportData [64]byte) ([]byte, error) { + return nil, fmt.Errorf("Windows is unsupported") +} + +// GetRawQuoteAtLevel returns byte format attestation plus certificate table via ConfigFS. +func (*WindowsQuoteProvider) GetRawQuoteAtLevel(reportData [64]byte, level uint) ([]byte, error) { + return nil, fmt.Errorf("Windows is unsupported") +} + +// GetQuoteProvider returns a supported SEV-SNP QuoteProvider. +func GetQuoteProvider() (QuoteProvider, error) { + return nil, fmt.Errorf("Windows is unsupported") +} + +// GetLeveledQuoteProvider returns a supported SEV-SNP LeveledQuoteProvider. +func GetLeveledQuoteProvider() (LeveledQuoteProvider, error) { + return nil, fmt.Errorf("Windows is unsupported") +} diff --git a/go.mod b/go.mod index fb7d0c7..339ebd5 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.19 require ( github.com/golang/protobuf v1.5.3 github.com/google/go-cmp v0.5.7 + github.com/google/go-configfs-tsm v0.2.2 github.com/google/logger v1.1.1 github.com/pborman/uuid v1.2.1 github.com/pkg/errors v0.9.1 diff --git a/go.sum b/go.sum index c67444a..fb4a588 100644 --- a/go.sum +++ b/go.sum @@ -5,6 +5,8 @@ github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiu github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o= github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= +github.com/google/go-configfs-tsm v0.2.2 h1:YnJ9rXIOj5BYD7/0DNnzs8AOp7UcvjfTvt215EWcs98= +github.com/google/go-configfs-tsm v0.2.2/go.mod h1:EL1GTDFMb5PZQWDviGfZV9n87WeGTR/JUg13RfwkgRo= github.com/google/logger v1.1.1 h1:+6Z2geNxc9G+4D4oDO9njjjn2d0wN5d7uOo0vOIW1NQ= github.com/google/logger v1.1.1/go.mod h1:BkeJZ+1FhQ+/d087r4dzojEg1u2ZX+ZqG1jTUrLM+zQ= github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= diff --git a/testing/client/client.go b/testing/client/client.go index f7d1bee..3deb19d 100644 --- a/testing/client/client.go +++ b/testing/client/client.go @@ -29,7 +29,7 @@ func SkipUnmockableTestCase(tc *test.TestCase) bool { return !client.UseDefaultSevGuest() && tc.FwErr != 0 } -// GetSevGuest is a cross-platform testing helper function that retrives the +// GetSevGuest is a cross-platform testing helper function that retrieves the // appropriate SEV-guest device from the flags passed into "go test". // // If using a test guest device, this will also produce a fake AMD-SP that produces the signed @@ -101,3 +101,76 @@ func GetSevGuest(tcs []test.TestCase, opts *test.DeviceOptions, tb testing.TB) ( } return client, nil, badSnpRoot, kdsImpl } + +// GetSevQuoteProvider is a cross-platform testing helper function that retrieves the +// appropriate SEV-guest device from the flags passed into "go test". +// +// If using a test guest device, this will also produce a fake AMD-SP that produces the signed +// versions of given attestation reports based on different nonce input. Its returned roots of trust +// are based on the fake's signing credentials. +func GetSevQuoteProvider(tcs []test.TestCase, opts *test.DeviceOptions, tb testing.TB) (client.QuoteProvider, map[string][]*trust.AMDRootCerts, map[string][]*trust.AMDRootCerts, trust.HTTPSGetter) { + tb.Helper() + if client.UseDefaultSevGuest() { + sevQp, err := test.TcQuoteProvider(tcs, opts) + if err != nil { + tb.Fatalf("failed to create test device: %v", err) + } + goodSnpRoot := map[string][]*trust.AMDRootCerts{ + "Milan": { + { + Product: "Milan", + ProductCerts: &trust.ProductCerts{ + Ask: sevQp.Signer.Ask, + Ark: sevQp.Signer.Ark, + Asvk: sevQp.Signer.Asvk, + }, + }, + }, + } + badSnpRoot := map[string][]*trust.AMDRootCerts{ + "Milan": { + { + Product: "Milan", + ProductCerts: &trust.ProductCerts{ + // No ASK, oops. + Ask: sevQp.Signer.Ark, + Ark: sevQp.Signer.Ark, + Asvk: sevQp.Signer.Ark, + }, + }, + }, + } + fakekds, err := test.FakeKDSFromSigner(sevQp.Signer) + if err != nil { + tb.Fatalf("failed to create fake KDS from signer: %v", err) + } + return sevQp, goodSnpRoot, badSnpRoot, fakekds + } + + client, err := client.GetQuoteProvider() + if err != nil { + tb.Fatalf("Failed to open SEV guest device: %v", err) + } + kdsImpl := test.GetKDS(tb) + + badSnpRoot := make(map[string][]*trust.AMDRootCerts) + for product, rootCerts := range trust.DefaultRootCerts { + // Supplement the defaults with the missing x509 certificates. + pc, err := trust.GetProductChain(product, abi.VcekReportSigner, kdsImpl) + if err != nil { + tb.Fatalf("failed to get product chain for %q: %v", product, err) + } + // By removing the ASK intermediate, we ensure that the attestation will never verify. + badSnpRoot[product] = []*trust.AMDRootCerts{{ + Product: product, + ProductCerts: &trust.ProductCerts{ + Ark: pc.Ark, + Ask: pc.Ark, + Asvk: pc.Ark, + }, + AskSev: rootCerts.ArkSev, + ArkSev: rootCerts.AskSev, + }} + } + return client, nil, badSnpRoot, kdsImpl +} diff --git a/testing/mocks.go b/testing/mocks.go index cb40121..e9d465b 100644 --- a/testing/mocks.go +++ b/testing/mocks.go @@ -148,6 +148,43 @@ func (d *Device) Product() *spb.SevProduct { return d.SevProduct } +// QuoteProvider represents a SEV-SNP backed configfs-tsm with pre-programmed responses to attestations. +type QuoteProvider struct { + ReportDataRsp map[string]any + Certs []byte + Signer *AmdSigner + SevProduct *spb.SevProduct +} + +// IsSupported returns true +func (*QuoteProvider) IsSupported() bool { + return true +} + +// GetRawQuote returns the raw report assigned for given reportData. +func (p *QuoteProvider) GetRawQuote(reportData [64]byte) ([]uint8, error) { + mockRspI, ok := p.ReportDataRsp[hex.EncodeToString(reportData[:])] + if !ok { + return nil, fmt.Errorf("test error: no response for %v", reportData) + } + mockRsp, ok := mockRspI.(*GetReportResponse) + if !ok { + return nil, fmt.Errorf("test error: incorrect response type %v", mockRspI) + } + if mockRsp.FwErr != 0 { + return nil, syscall.Errno(unix.EIO) + } + report := mockRsp.Resp.Data[:abi.ReportSize] + r, s, err := p.Signer.Sign(abi.SignedComponent(report)) + if err != nil { + return nil, fmt.Errorf("test error: could not sign report: %v", err) + } + if err := abi.SetSignature(r, s, report); err != nil { + return nil, fmt.Errorf("test error: could not set signature: %v", err) + } + return append(report, p.Certs...), nil +} + // GetResponse controls how often (Occurrences) a certain response should be // provided. type GetResponse struct { diff --git a/testing/test_cases.go b/testing/test_cases.go index c883f2a..4c3ef25 100644 --- a/testing/test_cases.go +++ b/testing/test_cases.go @@ -261,3 +261,24 @@ func TcDevice(tcs []TestCase, opts *DeviceOptions) (*Device, error) { SevProduct: opts.Product, }, nil } + +// TcQuoteProvider returns a mock quote provider populated from test cases' inputs and expected outputs. +func TcQuoteProvider(tcs []TestCase, opts *DeviceOptions) (*QuoteProvider, error) { + certs, signer, err := makeTestCerts(opts) + if err != nil { + return nil, fmt.Errorf("test failure creating certificates: %v", err) + } + responses := map[string]any{} + for _, tc := range tcs { + responses[hex.EncodeToString(tc.Input[:])] = &GetReportResponse{ + Resp: labi.SnpReportRespABI{Data: tc.Output}, + FwErr: tc.FwErr, + } + } + return &QuoteProvider{ + ReportDataRsp: responses, + Certs: certs, + Signer: signer, + SevProduct: opts.Product, + }, nil +} diff --git a/verify/verify_test.go b/verify/verify_test.go index 99d90c1..e3dd54f 100644 --- a/verify/verify_test.go +++ b/verify/verify_test.go @@ -428,6 +428,8 @@ func TestCRLRootValidity(t *testing.T) { } } +// TestOpenGetExtendedReportVerifyClose tests the SnpAttestation function for the deprecated ioctl +// API. func TestOpenGetExtendedReportVerifyClose(t *testing.T) { trust.ClearProductCertCache() tests := test.TestCases() @@ -546,6 +548,52 @@ func TestOpenGetExtendedReportVerifyClose(t *testing.T) { } } +// TestGetQuoteProviderVerify tests the SnpAttestation function for the configfs-tsm report API. +func TestGetQuoteProviderVerify(t *testing.T) { + trust.ClearProductCertCache() + tests := test.TestCases() + qp, goodRoots, badRoots, kds := testclient.GetSevQuoteProvider(tests, &test.DeviceOptions{Now: time.Now()}, t) + // Trust the test device's root certs. + options := &Options{ + TrustedRoots: goodRoots, + Getter: kds, + Product: testProduct(t), + DisableCertFetching: *requireCache && !sg.UseDefaultSevGuest(), + } + badOptions := &Options{TrustedRoots: badRoots, Getter: kds, Product: testProduct(t)} + for _, tc := range tests { + // configfs-tsm doesn't support the key choice parameter for getting an attestation report, and + // it doesn't return firmware error codes. + if testclient.SkipUnmockableTestCase(&tc) || tc.EK == test.KeyChoiceVlek { + t.Run(tc.Name, func(t *testing.T) { t.Skip() }) + continue + } + t.Run(tc.Name+"_", func(t *testing.T) { + reportcerts, err := qp.GetRawQuote(tc.Input) + ereport, _ := abi.ReportCertsToProto(reportcerts) + if tc.FwErr != abi.Success { + if err == nil { + t.Fatalf("(d, %v) = %v. Unexpected success given firmware error: %v", tc.Input, ereport, tc.FwErr) + } + } else if !test.Match(err, tc.WantErr) { + t.Fatalf("(d, %v) = %v, %v. Want err: %v", tc.Input, ereport, err, tc.WantErr) + } + if tc.WantErr == "" { + var wantAttestationErr string + if err := SnpAttestation(ereport, options); !test.Match(err, wantAttestationErr) { + t.Errorf("SnpAttestation(%v) = %v. Want err: %q", ereport, err, wantAttestationErr) + } + + wantBad := "error verifying VCEK certificate" + if err := SnpAttestation(ereport, badOptions); !test.Match(err, wantBad) { + t.Errorf("SnpAttestation(_) bad root test errored unexpectedly: %v, want %s", + err, wantBad) + } + } + }) + } +} + func TestRealAttestationVerification(t *testing.T) { trust.ClearProductCertCache() var nonce [64]byte