attestation: add GCP SEV-SNP attestation logic

This commit is contained in:
Moritz Sanft 2024-04-04 16:33:55 +02:00
parent 5488ba1357
commit afeac3a8e9
No known key found for this signature in database
GPG key ID: 335D28368B1DA615
9 changed files with 851 additions and 5 deletions

View file

@ -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",
],
)

View file

@ -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
}

View file

@ -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)
}

View file

@ -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

View file

@ -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
}

View file

@ -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
}

View file

@ -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",
],
)

View file

@ -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.

View file

@ -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))
}