From 776294f18d74a7a367575ccac6a03663d2ada8eb Mon Sep 17 00:00:00 2001 From: Dionna Glaze Date: Tue, 16 Jan 2024 20:41:36 +0000 Subject: [PATCH] Add configfs-tsm support for attestation reports Given Dan Williams's configfs-tsm patch set to the Linux kernel, all attestation reports ought to be requested through configfs and not ioctls. This marks the deprecation of the Device interface for reports, but not for keys. Signed-off-by: Dionna Glaze --- abi/abi.go | 21 ++++++++ client/client.go | 13 +++++ client/client_linux.go | 101 +++++++++++++++++++++++++++++++++++++++ client/client_macos.go | 29 +++++++++++ client/client_windows.go | 29 +++++++++++ go.mod | 1 + go.sum | 2 + testing/client/client.go | 75 ++++++++++++++++++++++++++++- testing/mocks.go | 37 ++++++++++++++ testing/test_cases.go | 21 ++++++++ verify/verify_test.go | 48 +++++++++++++++++++ 11 files changed, 376 insertions(+), 1 deletion(-) 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