From afeac3a8e9c83fa454bf3fe379cf3ad1d494c45d Mon Sep 17 00:00:00 2001 From: Moritz Sanft <58110325+msanft@users.noreply.github.com> Date: Thu, 4 Apr 2024 16:33:55 +0200 Subject: [PATCH] attestation: add GCP SEV-SNP attestation logic --- internal/attestation/gcp/snp/BUILD.bazel | 56 ++++ internal/attestation/gcp/snp/issuer.go | 160 ++++++++++ internal/attestation/gcp/snp/issuer_test.go | 44 +++ internal/attestation/gcp/snp/snp.go | 44 +++ internal/attestation/gcp/snp/validator.go | 231 ++++++++++++++ .../attestation/gcp/snp/validator_test.go | 295 ++++++++++++++++++ internal/attestation/snp/BUILD.bazel | 1 + internal/attestation/snp/snp.go | 2 + internal/attestation/vtpm/attestation.go | 23 +- 9 files changed, 851 insertions(+), 5 deletions(-) create mode 100644 internal/attestation/gcp/snp/BUILD.bazel create mode 100644 internal/attestation/gcp/snp/issuer.go create mode 100644 internal/attestation/gcp/snp/issuer_test.go create mode 100644 internal/attestation/gcp/snp/snp.go create mode 100644 internal/attestation/gcp/snp/validator.go create mode 100644 internal/attestation/gcp/snp/validator_test.go diff --git a/internal/attestation/gcp/snp/BUILD.bazel b/internal/attestation/gcp/snp/BUILD.bazel new file mode 100644 index 000000000..3056543f4 --- /dev/null +++ b/internal/attestation/gcp/snp/BUILD.bazel @@ -0,0 +1,56 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") +load("//bazel/go:go_test.bzl", "go_test") + +go_library( + name = "snp", + srcs = [ + "issuer.go", + "snp.go", + "validator.go", + ], + importpath = "github.com/edgelesssys/constellation/v2/internal/attestation/gcp/snp", + visibility = ["//:__subpackages__"], + deps = [ + "//internal/attestation", + "//internal/attestation/gcp", + "//internal/attestation/snp", + "//internal/attestation/variant", + "//internal/attestation/vtpm", + "//internal/config", + "@com_github_google_go_sev_guest//abi", + "@com_github_google_go_sev_guest//client", + "@com_github_google_go_sev_guest//kds", + "@com_github_google_go_sev_guest//proto/sevsnp", + "@com_github_google_go_sev_guest//validate", + "@com_github_google_go_sev_guest//verify", + "@com_github_google_go_sev_guest//verify/trust", + "@com_github_google_go_tpm//legacy/tpm2", + "@com_github_google_go_tpm_tools//client", + "@com_github_google_go_tpm_tools//proto/attest", + ], +) + +go_test( + name = "snp_test", + srcs = [ + "issuer_test.go", + "validator_test.go", + ], + embed = [":snp"], + deps = [ + "//internal/attestation", + "//internal/attestation/aws/snp/testdata", + "//internal/attestation/simulator", + "//internal/attestation/snp", + "//internal/attestation/vtpm", + "//internal/config", + "//internal/logger", + "@com_github_google_go_sev_guest//abi", + "@com_github_google_go_sev_guest//proto/sevsnp", + "@com_github_google_go_sev_guest//verify", + "@com_github_google_go_tpm_tools//client", + "@com_github_google_go_tpm_tools//proto/attest", + "@com_github_stretchr_testify//assert", + "@com_github_stretchr_testify//require", + ], +) diff --git a/internal/attestation/gcp/snp/issuer.go b/internal/attestation/gcp/snp/issuer.go new file mode 100644 index 000000000..951253e77 --- /dev/null +++ b/internal/attestation/gcp/snp/issuer.go @@ -0,0 +1,160 @@ +/* +Copyright (c) Edgeless Systems GmbH + +SPDX-License-Identifier: AGPL-3.0-only +*/ + +package snp + +import ( + "context" + "crypto/sha512" + "crypto/x509" + "encoding/json" + "encoding/pem" + "fmt" + "io" + + "github.com/edgelesssys/constellation/v2/internal/attestation" + "github.com/edgelesssys/constellation/v2/internal/attestation/gcp" + "github.com/edgelesssys/constellation/v2/internal/attestation/snp" + "github.com/edgelesssys/constellation/v2/internal/attestation/variant" + "github.com/edgelesssys/constellation/v2/internal/attestation/vtpm" + + "github.com/google/go-sev-guest/abi" + sevclient "github.com/google/go-sev-guest/client" + "github.com/google/go-tpm-tools/client" + tpmclient "github.com/google/go-tpm-tools/client" + "github.com/google/go-tpm-tools/proto/attest" +) + +// Issuer issues SEV-SNP attestations. +type Issuer struct { + variant.GCPSEVSNP + *vtpm.Issuer +} + +// NewIssuer creates a SEV-SNP based issuer for GCP. +func NewIssuer(log attestation.Logger) *Issuer { + return &Issuer{ + Issuer: vtpm.NewIssuer( + vtpm.OpenVTPM, + getAttestationKey, + getInstanceInfo, + log, + ), + } +} + +// getAttestationKey returns a new attestation key. +func getAttestationKey(tpm io.ReadWriter) (*tpmclient.Key, error) { + tpmAk, err := client.GceAttestationKeyRSA(tpm) + if err != nil { + return nil, fmt.Errorf("creating RSA Endorsement key: %w", err) + } + + return tpmAk, nil +} + +// getInstanceInfo generates an extended SNP report, i.e. the report and any loaded certificates. +// Report generation is triggered by sending ioctl syscalls to the SNP guest device, the AMD PSP generates the report. +// The returned bytes will be written into the attestation document. +func getInstanceInfo(_ context.Context, tpm io.ReadWriteCloser, _ []byte) ([]byte, error) { + tpmAk, err := client.GceAttestationKeyRSA(tpm) + if err != nil { + return nil, fmt.Errorf("creating RSA Endorsement key: %w", err) + } + + encoded, err := x509.MarshalPKIXPublicKey(tpmAk.PublicKey()) + if err != nil { + return nil, fmt.Errorf("marshalling public key: %w", err) + } + + akDigest := sha512.Sum512(encoded) + + device, err := sevclient.OpenDevice() + if err != nil { + return nil, fmt.Errorf("opening sev device: %w", err) + } + defer device.Close() + + report, certs, err := sevclient.GetRawExtendedReportAtVmpl(device, akDigest, 0) + if err != nil { + return nil, fmt.Errorf("getting extended report: %w", err) + } + + vcek, err := pemEncodedVCEK(certs) + if err != nil { + return nil, fmt.Errorf("parsing vlek: %w", err) + } + + gceInstanceInfo, err := gceInstanceInfo() + if err != nil { + return nil, fmt.Errorf("getting GCE instance info: %w", err) + } + + raw, err := json.Marshal(snp.InstanceInfo{ + AttestationReport: report, + ReportSigner: vcek, + GCP: gceInstanceInfo, + }) + if err != nil { + return nil, fmt.Errorf("marshalling instance info: %w", err) + } + + return raw, nil +} + +// gceInstanceInfo returns the instance info for a GCE instance from the metadata API. +func gceInstanceInfo() (*attest.GCEInstanceInfo, error) { + c := gcp.MetadataClient{} + + instanceName, err := c.InstanceName() + if err != nil { + return nil, fmt.Errorf("getting instance name: %w", err) + } + + projectID, err := c.ProjectID() + if err != nil { + return nil, fmt.Errorf("getting project ID: %w", err) + } + + zone, err := c.Zone() + if err != nil { + return nil, fmt.Errorf("getting zone: %w", err) + } + + return &attest.GCEInstanceInfo{ + InstanceName: instanceName, + ProjectId: projectID, + Zone: zone, + }, nil +} + +// pemEncodedVCEK takes a marshalled SNP certificate table and returns the PEM-encoded VCEK certificate. +// AMD documentation on certificate tables can be found in section 4.1.8.1, revision 2.03 "SEV-ES Guest-Hypervisor Communication Block Standardization". +// https://www.amd.com/content/dam/amd/en/documents/epyc-technical-docs/specifications/56421.pdf +func pemEncodedVCEK(certs []byte) ([]byte, error) { + certTable := abi.CertTable{} + if err := certTable.Unmarshal(certs); err != nil { + return nil, fmt.Errorf("unmarshalling SNP certificate table: %w", err) + } + + vcekRaw, err := certTable.GetByGUIDString(abi.VcekGUID) + if err != nil { + return nil, fmt.Errorf("getting VCEK certificate: %w", err) + } + + // An optional check for certificate well-formedness. vlekRaw == cert.Raw. + cert, err := x509.ParseCertificate(vcekRaw) + if err != nil { + return nil, fmt.Errorf("parsing certificate: %w", err) + } + + certPEM := pem.EncodeToMemory(&pem.Block{ + Type: "CERTIFICATE", + Bytes: cert.Raw, + }) + + return certPEM, nil +} diff --git a/internal/attestation/gcp/snp/issuer_test.go b/internal/attestation/gcp/snp/issuer_test.go new file mode 100644 index 000000000..3f2f24699 --- /dev/null +++ b/internal/attestation/gcp/snp/issuer_test.go @@ -0,0 +1,44 @@ +/* +Copyright (c) Edgeless Systems GmbH + +SPDX-License-Identifier: AGPL-3.0-only +*/ + +package snp + +import ( + "os" + "testing" + + "github.com/edgelesssys/constellation/v2/internal/attestation/simulator" + tpmclient "github.com/google/go-tpm-tools/client" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGetAttestationKey(t *testing.T) { + cgo := os.Getenv("CGO_ENABLED") + if cgo == "0" { + t.Skip("skipping test because CGO is disabled and tpm simulator requires it") + } + + require := require.New(t) + assert := assert.New(t) + + tpm, err := simulator.OpenSimulatedTPM() + require.NoError(err) + defer tpm.Close() + + // create the attestation key in RSA format + tpmAk, err := tpmclient.AttestationKeyRSA(tpm) + assert.NoError(err) + assert.NotNil(tpmAk) + + // get the cached, already created key + getAk, err := getAttestationKey(tpm) + assert.NoError(err) + assert.NotNil(getAk) + + // if everything worked fine, tpmAk and getAk are the same key + assert.Equal(tpmAk, getAk) +} diff --git a/internal/attestation/gcp/snp/snp.go b/internal/attestation/gcp/snp/snp.go new file mode 100644 index 000000000..f81a6df2c --- /dev/null +++ b/internal/attestation/gcp/snp/snp.go @@ -0,0 +1,44 @@ +/* +Copyright (c) Edgeless Systems GmbH + +SPDX-License-Identifier: AGPL-3.0-only +*/ + +/* +# GCP SEV-SNP Attestation + +Google offers [confidential VMs], utilizing AMD SEV-SNP to provide memory encryption. + +Each SEV-SNP VM comes with a [virtual Trusted Platform Module (vTPM)]. +This vTPM can be used to generate encryption keys unique to the VM or to attest the platform's boot chain. +We can use the vTPM to verify the VM is running on AMD SEV-SNP enabled hardware and booted the expected OS image, allowing us to bootstrap a constellation cluster. + +# Issuer + +Retrieves an SEV-SNP attestation statement for the VM it's running in. Then, it generates a TPM attestation statement, binding the SEV-SNP attestation statement to it by including its hash in the TPM attestation statement. +Without binding the SEV-SNP attestation statement to the TPM attestation statement, the SEV-SNP attestation statement could be used in a different VM. Furthermore, it's important to first create the SEV-SNP attestation statement +and then the TPM attestation statement, as otherwise, a non-CVM could be used to create a valid TPM attestation statement, and then later swap the SEV-SNP attestation statement with one from a CVM. +Additionally project ID, zone, and instance name are fetched from the metadata server and attached to the attestation statement. + +# Validator + +First, it verifies the SEV-SNP attestation statement by checking the signatures and claims. Then, it verifies the TPM attestation by using a +public key provided by Google's API corresponding to the project ID, zone, instance name tuple attached to the attestation document, and confirms whether the SEV-SNP attestation statement is bound to the TPM attestation statement. + +# Problems + + - We have to trust Google + + Since the vTPM is provided by Google, and they could do whatever they want with it, we have no save proof of the VMs actually being confidential. + + - The provided vTPM has no endorsement certificate for its attestation key + + Without a certificate signing the authenticity of any endorsement keys we have no way of establishing a chain of trust. + Instead, we have to rely on Google's API to provide us with the public key of the vTPM's endorsement key. + +[GCP Confidential VMs]: https://cloud.google.com/compute/confidential-vm/docs/about-cvm +[GCP Virtual Trusted Platform Module (vTPM)]: https://cloud.google.com/security/shielded-cloud/shielded-vm#vtpm +[GCP Monitoring docs]: https://cloud.google.com/compute/confidential-vm/docs/monitoring +[AMD SEV-SNP whitepaper]: https://www.amd.com/system/files/TechDocs/SEV-SNP-strengthening-vm-isolation-with-integrity-protection-and-more.pdf#page=7 +*/ +package snp diff --git a/internal/attestation/gcp/snp/validator.go b/internal/attestation/gcp/snp/validator.go new file mode 100644 index 000000000..a2f4afeb9 --- /dev/null +++ b/internal/attestation/gcp/snp/validator.go @@ -0,0 +1,231 @@ +/* +Copyright (c) Edgeless Systems GmbH + +SPDX-License-Identifier: AGPL-3.0-only +*/ + +package snp + +import ( + "context" + "crypto" + "crypto/sha512" + "crypto/x509" + "encoding/json" + "fmt" + + "github.com/edgelesssys/constellation/v2/internal/attestation" + "github.com/edgelesssys/constellation/v2/internal/attestation/gcp" + "github.com/edgelesssys/constellation/v2/internal/attestation/snp" + "github.com/edgelesssys/constellation/v2/internal/attestation/variant" + "github.com/edgelesssys/constellation/v2/internal/attestation/vtpm" + "github.com/edgelesssys/constellation/v2/internal/config" + "github.com/google/go-sev-guest/abi" + "github.com/google/go-sev-guest/kds" + "github.com/google/go-sev-guest/proto/sevsnp" + "github.com/google/go-sev-guest/validate" + "github.com/google/go-sev-guest/verify" + "github.com/google/go-sev-guest/verify/trust" + "github.com/google/go-tpm-tools/proto/attest" + "github.com/google/go-tpm/legacy/tpm2" +) + +// Validator for GCP SEV-SNP / TPM attestation. +type Validator struct { + variant.GCPSEVSNP + *vtpm.Validator + cfg *config.GCPSEVSNP + + // reportValidator validates a SNP report and is required for testing. + reportValidator snpReportValidator + + // gceKeyGetter gets the public key of the EK from the GCE metadata API. + gceKeyGetter func(ctx context.Context, attDoc vtpm.AttestationDocument, _ []byte) (crypto.PublicKey, error) + + log attestation.Logger +} + +// NewValidator creates a new Validator. +func NewValidator(cfg *config.GCPSEVSNP, log attestation.Logger) (*Validator, error) { + getGCEKey, err := gcp.TrustedKeyGetter(variant.GCPSEVSNP{}, gcp.NewRESTClient) + if err != nil { + return nil, fmt.Errorf("create trusted key getter: %v", err) + } + + v := &Validator{ + cfg: cfg, + reportValidator: &gcpValidator{httpsGetter: trust.DefaultHTTPSGetter(), verifier: &reportVerifierImpl{}, validator: &reportValidatorImpl{}}, + gceKeyGetter: getGCEKey, + log: log, + } + + v.Validator = vtpm.NewValidator( + cfg.Measurements, + v.getTrustedKey, + v.validateCVM, + log, + ) + return v, nil +} + +// getTrustedKey returns TPM endorsement key provided through the GCE metadata API. +func (v *Validator) getTrustedKey(ctx context.Context, attDoc vtpm.AttestationDocument, _ []byte) (crypto.PublicKey, error) { + ekPub, err := v.gceKeyGetter(ctx, attDoc, nil) + if err != nil { + return nil, fmt.Errorf("getting TPM endorsement key: %w", err) + } + + return ekPub, nil +} + +// validateCVM validates the SEV-SNP attestation document. +func (v *Validator) validateCVM(attDoc vtpm.AttestationDocument, state *attest.MachineState) error { + pubArea, err := tpm2.DecodePublic(attDoc.Attestation.AkPub) + if err != nil { + return fmt.Errorf("decoding public area: %w", err) + } + + pubKey, err := pubArea.Key() + if err != nil { + return fmt.Errorf("getting public key: %w", err) + } + + akDigest, err := sha512sum(pubKey) + if err != nil { + return fmt.Errorf("calculating hash of attestation key: %w", err) + } + + if err := v.reportValidator.validate(attDoc, (*x509.Certificate)(&v.cfg.AMDSigningKey), (*x509.Certificate)(&v.cfg.AMDRootKey), akDigest, v.cfg, v.log); err != nil { + return fmt.Errorf("validating SNP report: %w", err) + } + return nil +} + +// sha512sum PEM-encodes a public key and calculates the SHA512 hash of the encoded key. +func sha512sum(key crypto.PublicKey) ([64]byte, error) { + pub, err := x509.MarshalPKIXPublicKey(key) + if err != nil { + return [64]byte{}, fmt.Errorf("marshalling public key: %w", err) + } + + return sha512.Sum512(pub), nil +} + +// snpReportValidator validates a given SNP report. +type snpReportValidator interface { + validate(attestation vtpm.AttestationDocument, ask *x509.Certificate, ark *x509.Certificate, ak [64]byte, config *config.GCPSEVSNP, log attestation.Logger) error +} + +// gcpValidator implements the validation for GCP SEV-SNP attestation. +// The properties exist for unittesting. +type gcpValidator struct { + verifier reportVerifier + validator reportValidator + httpsGetter trust.HTTPSGetter +} + +type reportVerifier interface { + SnpAttestation(att *sevsnp.Attestation, opts *verify.Options) error +} +type reportValidator interface { + SnpAttestation(att *sevsnp.Attestation, opts *validate.Options) error +} + +type reportValidatorImpl struct{} + +func (r *reportValidatorImpl) SnpAttestation(att *sevsnp.Attestation, opts *validate.Options) error { + return validate.SnpAttestation(att, opts) +} + +type reportVerifierImpl struct{} + +func (r *reportVerifierImpl) SnpAttestation(att *sevsnp.Attestation, opts *verify.Options) error { + return verify.SnpAttestation(att, opts) +} + +// validate the report by checking if it has a valid VCEK signature. +// The certificate chain ARK -> ASK -> VCEK is also validated. +// Checks that the report's userData matches the connection's userData. +func (a *gcpValidator) validate(attestation vtpm.AttestationDocument, ask *x509.Certificate, ark *x509.Certificate, akDigest [64]byte, config *config.GCPSEVSNP, log attestation.Logger) error { + var info snp.InstanceInfo + if err := json.Unmarshal(attestation.InstanceInfo, &info); err != nil { + return fmt.Errorf("unmarshalling instance info: %w", err) + } + + certchain := snp.NewCertificateChain(ask, ark) + + att, err := info.AttestationWithCerts(a.httpsGetter, certchain, log) + if err != nil { + return fmt.Errorf("getting attestation with certs: %w", err) + } + + verifyOpts, err := getVerifyOpts(att) + if err != nil { + return fmt.Errorf("getting verify options: %w", err) + } + + if err := a.verifier.SnpAttestation(att, verifyOpts); err != nil { + return fmt.Errorf("verifying SNP attestation: %w", err) + } + + validateOpts := &validate.Options{ + // Check that the attestation key's digest is included in the report. + ReportData: akDigest[:], + GuestPolicy: abi.SnpPolicy{ + Debug: false, // Debug means the VM can be decrypted by the host for debugging purposes and thus is not allowed. + SMT: true, // Allow Simultaneous Multi-Threading (SMT). Normally, we would want to disable SMT + // but GCP machines are currently facing issues if it's disabled + }, + VMPL: new(int), // Checks that Virtual Machine Privilege Level (VMPL) is 0. + // This checks that the reported LaunchTCB version is equal or greater than the minimum specified in the config. + // We don't specify Options.MinimumTCB as it only restricts the allowed TCB for Current_ and Reported_TCB. + // Because we allow Options.ProvisionalFirmware, there is not security gained in also checking Current_ and Reported_TCB. + // We always have to check Launch_TCB as this value indicated the smallest TCB version a VM has seen during + // it's lifetime. + MinimumLaunchTCB: kds.TCBParts{ + BlSpl: config.BootloaderVersion.Value, // Bootloader + TeeSpl: config.TEEVersion.Value, // TEE (Secure OS) + SnpSpl: config.SNPVersion.Value, // SNP + UcodeSpl: config.MicrocodeVersion.Value, // Microcode + }, + // Check that CurrentTCB >= CommittedTCB. + PermitProvisionalFirmware: true, + } + + // Checks if the attestation report matches the given constraints. + // Some constraints are implicitly checked by validate.SnpAttestation: + // - the report is not expired + if err := a.validator.SnpAttestation(att, validateOpts); err != nil { + return fmt.Errorf("validating SNP attestation: %w", err) + } + + return nil +} + +func getVerifyOpts(att *sevsnp.Attestation) (*verify.Options, error) { + ask, err := x509.ParseCertificate(att.CertificateChain.AskCert) + if err != nil { + return &verify.Options{}, fmt.Errorf("parsing ASK certificate: %w", err) + } + ark, err := x509.ParseCertificate(att.CertificateChain.ArkCert) + if err != nil { + return &verify.Options{}, fmt.Errorf("parsing ARK certificate: %w", err) + } + + verifyOpts := &verify.Options{ + DisableCertFetching: true, + TrustedRoots: map[string][]*trust.AMDRootCerts{ + "Milan": { + { + Product: "Milan", + ProductCerts: &trust.ProductCerts{ + Ask: ask, + Ark: ark, + }, + }, + }, + }, + } + + return verifyOpts, nil +} diff --git a/internal/attestation/gcp/snp/validator_test.go b/internal/attestation/gcp/snp/validator_test.go new file mode 100644 index 000000000..5772c357e --- /dev/null +++ b/internal/attestation/gcp/snp/validator_test.go @@ -0,0 +1,295 @@ +/* +Copyright (c) Edgeless Systems GmbH + +SPDX-License-Identifier: AGPL-3.0-only +*/ + +package snp + +import ( + "bytes" + "context" + "crypto" + "crypto/x509" + "encoding/base64" + "encoding/hex" + "encoding/json" + "encoding/pem" + "errors" + "fmt" + "regexp" + "testing" + + "github.com/edgelesssys/constellation/v2/internal/attestation" + "github.com/edgelesssys/constellation/v2/internal/attestation/aws/snp/testdata" + "github.com/edgelesssys/constellation/v2/internal/attestation/snp" + "github.com/edgelesssys/constellation/v2/internal/attestation/vtpm" + "github.com/edgelesssys/constellation/v2/internal/config" + "github.com/edgelesssys/constellation/v2/internal/logger" + "github.com/google/go-sev-guest/abi" + "github.com/google/go-sev-guest/proto/sevsnp" + spb "github.com/google/go-sev-guest/proto/sevsnp" + "github.com/google/go-sev-guest/verify" + "github.com/google/go-tpm-tools/proto/attest" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGetTrustedKey(t *testing.T) { + validator := func() *Validator { return &Validator{reportValidator: stubGCPValidator{}} } + testCases := map[string]struct { + akPub []byte + info []byte + wantErr bool + }{ + "null byte docs": { + akPub: []byte{0x00, 0x00, 0x00, 0x00}, + info: []byte{0x00, 0x00, 0x00, 0x00}, + wantErr: true, + }, + "nil": { + akPub: nil, + info: nil, + wantErr: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + assert := assert.New(t) + out, err := validator().getTrustedKey( + context.Background(), + vtpm.AttestationDocument{ + Attestation: &attest.Attestation{ + AkPub: tc.akPub, + }, + InstanceInfo: tc.info, + }, + nil, + ) + + if tc.wantErr { + assert.Error(err) + } else { + assert.NoError(err) + } + + assert.Nil(out) + }) + } +} + +// TestValidateSNPReport has to setup the following to run ValidateSNPReport: +// - parse ARK certificate from constants.go. +// - parse cached ASK certificate. +// - parse cached SNP report. +// - parse cached AK hash. Hash and SNP report have to match. +// - parse cache VLEK cert. +func TestValidateSNPReport(t *testing.T) { + require := require.New(t) + certs, err := loadCerts(testdata.CertChain) + require.NoError(err) + ark := certs[1] + ask := certs[0] + + // reportTransformer unpacks the base64 encoded report, applies the given transformations and re-encodes it. + reportTransformer := func(reportHex string, transformations func(*spb.Report)) string { + rawReport, err := base64.StdEncoding.DecodeString(reportHex) + require.NoError(err) + report, err := abi.ReportToProto(rawReport) + require.NoError(err) + transformations(report) + reportBytes, err := abi.ReportToAbiBytes(report) + require.NoError(err) + return base64.StdEncoding.EncodeToString(reportBytes) + } + + testCases := map[string]struct { + ak string + report string + reportTransformer func(string, func(*spb.Report)) string + verifier reportVerifier + validator reportValidator + wantErr bool + }{ + "success": { + ak: testdata.AKDigest, + report: testdata.SNPReport, + verifier: &reportVerifierImpl{}, + validator: &reportValidatorImpl{}, + }, + "invalid report data": { + ak: testdata.AKDigest, + report: reportTransformer(testdata.SNPReport, func(r *spb.Report) { + r.ReportData = make([]byte, 64) + }), + verifier: &stubReportVerifier{}, + validator: &reportValidatorImpl{}, + wantErr: true, + }, + "invalid report signature": { + ak: testdata.AKDigest, + report: reportTransformer(testdata.SNPReport, func(r *spb.Report) { r.Signature[0]++ }), + verifier: &reportVerifierImpl{}, + validator: &reportValidatorImpl{}, + wantErr: true, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + assert := assert.New(t) + + hash, err := hex.DecodeString(tc.ak) + require.NoError(err) + + report, err := base64.StdEncoding.DecodeString(tc.report) + require.NoError(err) + + info := snp.InstanceInfo{AttestationReport: report, ReportSigner: testdata.VLEK} + infoMarshalled, err := json.Marshal(info) + require.NoError(err) + + v := gcpValidator{httpsGetter: newStubHTTPSGetter(&urlResponseMatcher{}, nil), verifier: tc.verifier, validator: tc.validator} + err = v.validate(vtpm.AttestationDocument{InstanceInfo: infoMarshalled}, ask, ark, [64]byte(hash), config.DefaultForGCPSEVSNP(), logger.NewTest(t)) + if tc.wantErr { + assert.Error(err) + } else { + assert.NoError(err) + } + }) + } +} + +type stubHTTPSGetter struct { + urlResponseMatcher *urlResponseMatcher // maps responses to requested URLs + err error +} + +func newStubHTTPSGetter(urlResponseMatcher *urlResponseMatcher, err error) *stubHTTPSGetter { + return &stubHTTPSGetter{ + urlResponseMatcher: urlResponseMatcher, + err: err, + } +} + +func (s *stubHTTPSGetter) Get(url string) ([]byte, error) { + if s.err != nil { + return nil, s.err + } + return s.urlResponseMatcher.match(url) +} + +type urlResponseMatcher struct { + certChainResponse []byte + wantCertChainRequest bool + vcekResponse []byte + wantVcekRequest bool +} + +func (m *urlResponseMatcher) match(url string) ([]byte, error) { + switch { + case url == "https://kdsintf.amd.com/vcek/v1/Milan/cert_chain": + if !m.wantCertChainRequest { + return nil, fmt.Errorf("unexpected cert_chain request") + } + return m.certChainResponse, nil + case regexp.MustCompile(`https:\/\/kdsintf.amd.com\/vcek\/v1\/Milan\/.*`).MatchString(url): + if !m.wantVcekRequest { + return nil, fmt.Errorf("unexpected VCEK request") + } + return m.vcekResponse, nil + default: + return nil, fmt.Errorf("unexpected URL: %s", url) + } +} + +func TestSha512sum(t *testing.T) { + testCases := map[string]struct { + key string + hash string + match bool + }{ + "success": { + // Generated using: rsa.GenerateKey(rand.Reader, 1024). + key: "30819f300d06092a864886f70d010101050003818d0030818902818100d4b2f072a32fa98456eb7f5938e2ff361fb64d698ea91e003d34bfc5374b814c16ba9ae3ec392ef6d48cf79b63067e338aa941219a7bcdf18aa43cd38bbe5567504838a3b1dca482035458853c5a171709dfae9df551815010bdfbc6df733cde84c4f7a5b0591d9cda9db087fb411ee3e2a4f19ad50c8331712ecdc5dd7ce34b0203010001", + hash: "2d6fe5ec59d7240b8a4c27c2ff27ba1071105fa50d45543768fcbabf9ee3cb8f8fa0afa51e08e053af30f6d11066ebfd47e75bda5ccc085c115d7e1896f3c62f", + match: true, + }, + "mismatching hash": { + key: "30819f300d06092a864886f70d010101050003818d0030818902818100d4b2f072a32fa98456eb7f5938e2ff361fb64d698ea91e003d34bfc5374b814c16ba9ae3ec392ef6d48cf79b63067e338aa941219a7bcdf18aa43cd38bbe5567504838a3b1dca482035458853c5a171709dfae9df551815010bdfbc6df733cde84c4f7a5b0591d9cda9db087fb411ee3e2a4f19ad50c8331712ecdc5dd7ce34b0203010001", + hash: "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + match: false, + }, + } + + for name, tc := range testCases { + t.Run(name, func(t *testing.T) { + assert := assert.New(t) + require := require.New(t) + + newKey, err := loadKeyFromHex(tc.key) + require.NoError(err) + + // Function under test: + hash, err := sha512sum(newKey) + assert.NoError(err) + + expected, err := hex.DecodeString(tc.hash) + require.NoError(err) + + if tc.match { + assert.True(bytes.Equal(expected, hash[:]), fmt.Sprintf("expected hash %x, got %x", expected, hash)) + } else { + assert.False(bytes.Equal(expected, hash[:]), fmt.Sprintf("expected mismatching hashes, got %x", hash)) + } + }) + } +} + +func loadKeyFromHex(key string) (crypto.PublicKey, error) { + decoded, err := hex.DecodeString(key) + if err != nil { + return nil, err + } + + return x509.ParsePKIXPublicKey(decoded) +} + +// loadCachedCertChain loads a valid ARK and ASK from the testdata folder. +func loadCerts(pemData []byte) ([]*x509.Certificate, error) { + var certs []*x509.Certificate + + for len(pemData) > 0 { + var block *pem.Block + block, pemData = pem.Decode(pemData) + if block == nil { + break + } + + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return nil, err + } + + certs = append(certs, cert) + } + + if len(certs) == 0 { + return nil, errors.New("no valid certificates found") + } + + return certs, nil +} + +type stubGCPValidator struct{} + +func (stubGCPValidator) validate(_ vtpm.AttestationDocument, _ *x509.Certificate, _ *x509.Certificate, _ [64]byte, _ *config.GCPSEVSNP, _ attestation.Logger) error { + return nil +} + +type stubReportVerifier struct{} + +func (stubReportVerifier) SnpAttestation(_ *sevsnp.Attestation, _ *verify.Options) error { + return nil +} diff --git a/internal/attestation/snp/BUILD.bazel b/internal/attestation/snp/BUILD.bazel index 700a3aa86..1a1bd2504 100644 --- a/internal/attestation/snp/BUILD.bazel +++ b/internal/attestation/snp/BUILD.bazel @@ -13,6 +13,7 @@ go_library( "@com_github_google_go_sev_guest//kds", "@com_github_google_go_sev_guest//proto/sevsnp", "@com_github_google_go_sev_guest//verify/trust", + "@com_github_google_go_tpm_tools//proto/attest", ], ) diff --git a/internal/attestation/snp/snp.go b/internal/attestation/snp/snp.go index 95cba55bf..e9db5f5a5 100644 --- a/internal/attestation/snp/snp.go +++ b/internal/attestation/snp/snp.go @@ -20,6 +20,7 @@ import ( "github.com/google/go-sev-guest/kds" spb "github.com/google/go-sev-guest/proto/sevsnp" "github.com/google/go-sev-guest/verify/trust" + "github.com/google/go-tpm-tools/proto/attest" ) // Product returns the SEV product info currently supported by Constellation's SNP attestation. @@ -39,6 +40,7 @@ type InstanceInfo struct { // AttestationReport is the attestation report from the vTPM (NVRAM) of the CVM. AttestationReport []byte Azure *AzureInstanceInfo + GCP *attest.GCEInstanceInfo } // AzureInstanceInfo contains Azure specific information related to SNP attestation. diff --git a/internal/attestation/vtpm/attestation.go b/internal/attestation/vtpm/attestation.go index 77c396b9a..f0e233f5d 100644 --- a/internal/attestation/vtpm/attestation.go +++ b/internal/attestation/vtpm/attestation.go @@ -9,10 +9,12 @@ package vtpm import ( "context" "crypto" + "crypto/sha256" "encoding/json" "errors" "fmt" "io" + "slices" "github.com/google/go-sev-guest/proto/sevsnp" tpmClient "github.com/google/go-tpm-tools/client" @@ -125,10 +127,6 @@ func (i *Issuer) Issue(ctx context.Context, userData []byte, nonce []byte) (res // Create an attestation using the loaded key extraData := attestation.MakeExtraData(userData, nonce) - tpmAttestation, err := aK.Attest(tpmClient.AttestOpts{Nonce: extraData}) - if err != nil { - return nil, fmt.Errorf("creating attestation: %w", err) - } // Fetch instance info of the VM instanceInfo, err := i.getInstanceInfo(ctx, tpm, extraData) @@ -136,6 +134,13 @@ func (i *Issuer) Issue(ctx context.Context, userData []byte, nonce []byte) (res return nil, fmt.Errorf("fetching instance info: %w", err) } + tpmNonce := makeTpmNonce(instanceInfo, extraData) + + tpmAttestation, err := aK.Attest(tpmClient.AttestOpts{Nonce: tpmNonce[:]}) + if err != nil { + return nil, fmt.Errorf("creating attestation: %w", err) + } + attDoc := AttestationDocument{ Attestation: tpmAttestation, InstanceInfo: instanceInfo, @@ -208,11 +213,13 @@ func (v *Validator) Validate(ctx context.Context, attDocRaw []byte, nonce []byte return nil, fmt.Errorf("validating attestation public key: %w", err) } + tpmNonce := makeTpmNonce(attDoc.InstanceInfo, extraData) + // Verify the TPM attestation state, err := tpmServer.VerifyAttestation( attDoc.Attestation, tpmServer.VerifyOpts{ - Nonce: extraData, + Nonce: tpmNonce[:], TrustedAKs: []crypto.PublicKey{aKP}, AllowSHA1: false, }, @@ -287,3 +294,9 @@ func GetSelectedMeasurements(open TPMOpenFunc, selection tpm2.PCRSelection) (mea return m, nil } + +// makeTpmNonce creates a nonce for the TPM attestation and returns it in its marshaled form. +func makeTpmNonce(instanceInfo []byte, extraData []byte) [32]byte { + // Finding: GCP nonces cannot be larger than 32 bytes. + return sha256.Sum256(slices.Concat(instanceInfo, extraData)) +}