mirror of
https://github.com/edgelesssys/constellation.git
synced 2025-08-15 02:05:45 -04:00
attestation: add GCP SEV-SNP attestation logic
This commit is contained in:
parent
5488ba1357
commit
afeac3a8e9
9 changed files with 851 additions and 5 deletions
56
internal/attestation/gcp/snp/BUILD.bazel
Normal file
56
internal/attestation/gcp/snp/BUILD.bazel
Normal 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",
|
||||
],
|
||||
)
|
160
internal/attestation/gcp/snp/issuer.go
Normal file
160
internal/attestation/gcp/snp/issuer.go
Normal 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
|
||||
}
|
44
internal/attestation/gcp/snp/issuer_test.go
Normal file
44
internal/attestation/gcp/snp/issuer_test.go
Normal 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)
|
||||
}
|
44
internal/attestation/gcp/snp/snp.go
Normal file
44
internal/attestation/gcp/snp/snp.go
Normal 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
|
231
internal/attestation/gcp/snp/validator.go
Normal file
231
internal/attestation/gcp/snp/validator.go
Normal 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
|
||||
}
|
295
internal/attestation/gcp/snp/validator_test.go
Normal file
295
internal/attestation/gcp/snp/validator_test.go
Normal 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
|
||||
}
|
|
@ -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",
|
||||
],
|
||||
)
|
||||
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue