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//kds",
|
||||||
"@com_github_google_go_sev_guest//proto/sevsnp",
|
"@com_github_google_go_sev_guest//proto/sevsnp",
|
||||||
"@com_github_google_go_sev_guest//verify/trust",
|
"@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"
|
"github.com/google/go-sev-guest/kds"
|
||||||
spb "github.com/google/go-sev-guest/proto/sevsnp"
|
spb "github.com/google/go-sev-guest/proto/sevsnp"
|
||||||
"github.com/google/go-sev-guest/verify/trust"
|
"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.
|
// 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 is the attestation report from the vTPM (NVRAM) of the CVM.
|
||||||
AttestationReport []byte
|
AttestationReport []byte
|
||||||
Azure *AzureInstanceInfo
|
Azure *AzureInstanceInfo
|
||||||
|
GCP *attest.GCEInstanceInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
// AzureInstanceInfo contains Azure specific information related to SNP attestation.
|
// AzureInstanceInfo contains Azure specific information related to SNP attestation.
|
||||||
|
|
|
@ -9,10 +9,12 @@ package vtpm
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"crypto"
|
"crypto"
|
||||||
|
"crypto/sha256"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"slices"
|
||||||
|
|
||||||
"github.com/google/go-sev-guest/proto/sevsnp"
|
"github.com/google/go-sev-guest/proto/sevsnp"
|
||||||
tpmClient "github.com/google/go-tpm-tools/client"
|
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
|
// Create an attestation using the loaded key
|
||||||
extraData := attestation.MakeExtraData(userData, nonce)
|
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
|
// Fetch instance info of the VM
|
||||||
instanceInfo, err := i.getInstanceInfo(ctx, tpm, extraData)
|
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)
|
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{
|
attDoc := AttestationDocument{
|
||||||
Attestation: tpmAttestation,
|
Attestation: tpmAttestation,
|
||||||
InstanceInfo: instanceInfo,
|
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)
|
return nil, fmt.Errorf("validating attestation public key: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
tpmNonce := makeTpmNonce(attDoc.InstanceInfo, extraData)
|
||||||
|
|
||||||
// Verify the TPM attestation
|
// Verify the TPM attestation
|
||||||
state, err := tpmServer.VerifyAttestation(
|
state, err := tpmServer.VerifyAttestation(
|
||||||
attDoc.Attestation,
|
attDoc.Attestation,
|
||||||
tpmServer.VerifyOpts{
|
tpmServer.VerifyOpts{
|
||||||
Nonce: extraData,
|
Nonce: tpmNonce[:],
|
||||||
TrustedAKs: []crypto.PublicKey{aKP},
|
TrustedAKs: []crypto.PublicKey{aKP},
|
||||||
AllowSHA1: false,
|
AllowSHA1: false,
|
||||||
},
|
},
|
||||||
|
@ -287,3 +294,9 @@ func GetSelectedMeasurements(open TPMOpenFunc, selection tpm2.PCRSelection) (mea
|
||||||
|
|
||||||
return m, nil
|
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