Revert "attestation: add SNP-based attestation for aws-sev-snp (#1916)" (#1957)

This reverts commit c7d12055d1.
This commit is contained in:
Otto Bittner 2023-06-22 17:08:44 +02:00 committed by GitHub
parent 487fa1e397
commit 7388240943
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 239 additions and 646 deletions

View file

@ -4,7 +4,6 @@ load("//bazel/go:go_test.bzl", "go_test")
go_library(
name = "snp",
srcs = [
"errors.go",
"issuer.go",
"snp.go",
"validator.go",
@ -16,9 +15,9 @@ go_library(
"//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//verify",
"@com_github_aws_aws_sdk_go_v2_config//:config",
"@com_github_aws_aws_sdk_go_v2_feature_ec2_imds//:imds",
"@com_github_aws_aws_sdk_go_v2_service_ec2//:ec2",
"@com_github_google_go_tpm//tpm2",
"@com_github_google_go_tpm_tools//client",
"@com_github_google_go_tpm_tools//proto/attest",
@ -31,7 +30,6 @@ go_test(
"issuer_test.go",
"validator_test.go",
],
data = glob(["testdata/**"]),
embed = [":snp"],
# keep
gotags = select({
@ -41,7 +39,10 @@ go_test(
deps = [
"//internal/attestation/simulator",
"//internal/attestation/vtpm",
"//internal/constants",
"@com_github_aws_aws_sdk_go_v2_feature_ec2_imds//:imds",
"@com_github_aws_aws_sdk_go_v2_service_ec2//:ec2",
"@com_github_aws_aws_sdk_go_v2_service_ec2//types",
"@com_github_aws_smithy_go//middleware",
"@com_github_google_go_tpm_tools//client",
"@com_github_google_go_tpm_tools//proto/attest",
"@com_github_stretchr_testify//assert",

View file

@ -1,48 +0,0 @@
/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
package snp
import "fmt"
// decodeError is used to signal an error during decoding of a public key.
// It only wrapps an error.
type decodeError struct {
inner error
}
// newDecodeError an error in a DecodeError.
func newDecodeError(err error) *decodeError {
return &decodeError{inner: err}
}
func (e *decodeError) Error() string {
return fmt.Sprintf("error decoding public key: %v", e.inner)
}
func (e *decodeError) Unwrap() error {
return e.inner
}
// validationError is used to signal an invalid SNP report.
// It only wrapps an error.
// Used during testing to error conditions more precisely.
type validationError struct {
inner error
}
// newValidationError wraps an error in a ValidationError.
func newValidationError(err error) *validationError {
return &validationError{inner: err}
}
func (e *validationError) Error() string {
return e.inner.Error()
}
func (e *validationError) Unwrap() error {
return e.inner
}

View file

@ -8,17 +8,15 @@ package snp
import (
"context"
"crypto/sha512"
"crypto/x509"
"encoding/json"
"fmt"
"io"
"github.com/aws/aws-sdk-go-v2/feature/ec2/imds"
"github.com/edgelesssys/constellation/v2/internal/attestation"
"github.com/edgelesssys/constellation/v2/internal/attestation/variant"
"github.com/edgelesssys/constellation/v2/internal/attestation/vtpm"
sevclient "github.com/google/go-sev-guest/client"
"github.com/google/go-tpm-tools/client"
tpmclient "github.com/google/go-tpm-tools/client"
)
@ -35,7 +33,7 @@ func NewIssuer(log attestation.Logger) *Issuer {
Issuer: vtpm.NewIssuer(
vtpm.OpenVTPM,
getAttestationKey,
getInstanceInfo,
getInstanceInfo(imds.New(imds.Options{})),
log,
),
}
@ -51,44 +49,18 @@ func getAttestationKey(tpm io.ReadWriter) (*tpmclient.Key, error) {
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.
// getInstanceInfo returns information about the current instance using the aws Metadata SDK.
// The returned bytes will be written into the attestation document.
func getInstanceInfo(_ context.Context, tpm io.ReadWriteCloser, _ []byte) ([]byte, error) {
tpmAk, err := client.AttestationKeyRSA(tpm)
if err != nil {
return nil, fmt.Errorf("error creating RSA Endorsement key: %w", err)
func getInstanceInfo(client awsMetaData) func(context.Context, io.ReadWriteCloser, []byte) ([]byte, error) {
return func(ctx context.Context, _ io.ReadWriteCloser, _ []byte) ([]byte, error) {
ec2InstanceIdentityOutput, err := client.GetInstanceIdentityDocument(ctx, &imds.GetInstanceIdentityDocumentInput{})
if err != nil {
return nil, fmt.Errorf("fetching instance identity document: %w", err)
}
return json.Marshal(ec2InstanceIdentityOutput.InstanceIdentityDocument)
}
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)
}
raw, err := json.Marshal(instanceInfo{Report: report, Certs: certs})
if err != nil {
return nil, fmt.Errorf("marshalling instance info: %w", err)
}
return raw, nil
}
type instanceInfo struct {
// Report contains the marshalled AMD SEV-SNP Report.
Report []byte
// Certs contains the PEM encoded VLEK and ASK certificates, queried from the AMD PSP of the issuing party.
Certs []byte
type awsMetaData interface {
GetInstanceIdentityDocument(context.Context, *imds.GetInstanceIdentityDocumentInput, ...func(*imds.Options)) (*imds.GetInstanceIdentityDocumentOutput, error)
}

View file

@ -7,8 +7,12 @@ SPDX-License-Identifier: AGPL-3.0-only
package snp
import (
"context"
"errors"
"testing"
"github.com/aws/aws-sdk-go-v2/feature/ec2/imds"
"github.com/aws/smithy-go/middleware"
"github.com/edgelesssys/constellation/v2/internal/attestation/simulator"
tpmclient "github.com/google/go-tpm-tools/client"
"github.com/stretchr/testify/assert"
@ -23,7 +27,7 @@ func TestGetAttestationKey(t *testing.T) {
require.NoError(err)
defer tpm.Close()
// create the attestation key in RSA format
// create the attestation ket in RSA format
tpmAk, err := tpmclient.AttestationKeyRSA(tpm)
assert.NoError(err)
assert.NotNil(tpmAk)
@ -36,3 +40,79 @@ func TestGetAttestationKey(t *testing.T) {
// if everything worked fine, tpmAk and getAk are the same key
assert.Equal(tpmAk, getAk)
}
func TestGetInstanceInfo(t *testing.T) {
testCases := map[string]struct {
client stubMetadataAPI
wantErr bool
}{
"invalid region": {
client: stubMetadataAPI{
instanceDoc: imds.InstanceIdentityDocument{
Region: "invalid-region",
},
instanceErr: errors.New("failed"),
},
wantErr: true,
},
"valid region": {
client: stubMetadataAPI{
instanceDoc: imds.InstanceIdentityDocument{
Region: "us-east-2",
},
},
},
"invalid imageID": {
client: stubMetadataAPI{
instanceDoc: imds.InstanceIdentityDocument{
ImageID: "ami-fail",
},
instanceErr: errors.New("failed"),
},
wantErr: true,
},
"valid imageID": {
client: stubMetadataAPI{
instanceDoc: imds.InstanceIdentityDocument{
ImageID: "ami-09e7c7f5617a47830",
},
},
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
tpm, err := simulator.OpenSimulatedTPM()
assert.NoError(err)
defer tpm.Close()
instanceInfoFunc := getInstanceInfo(&tc.client)
assert.NotNil(instanceInfoFunc)
info, err := instanceInfoFunc(context.Background(), tpm, nil)
if tc.wantErr {
assert.Error(err)
assert.Nil(info)
} else {
assert.Nil(err)
assert.NotNil(info)
}
})
}
}
type stubMetadataAPI struct {
instanceDoc imds.InstanceIdentityDocument
instanceErr error
}
func (c *stubMetadataAPI) GetInstanceIdentityDocument(context.Context, *imds.GetInstanceIdentityDocumentInput, ...func(*imds.Options)) (*imds.GetInstanceIdentityDocumentOutput, error) {
output := &imds.InstanceIdentityDocument{}
return &imds.GetInstanceIdentityDocumentOutput{
InstanceIdentityDocument: *output,
ResultMetadata: middleware.Metadata{},
}, c.instanceErr
}

View file

@ -36,10 +36,6 @@ Thus, the hypervisor is still included in the trusted computing base.
This section explains abbreviations used in SNP implementation.
- Platform Security Processor (PSP)
- Certificate Revocation List (CRL)
- Attestation Key (AK)
- AMD Root Key (ARK)

View file

@ -7,213 +7,95 @@ SPDX-License-Identifier: AGPL-3.0-only
package snp
import (
"bytes"
"context"
"crypto"
"crypto/sha512"
"crypto/x509"
"encoding/json"
"encoding/pem"
"errors"
"fmt"
"io"
"net/http"
awsConfig "github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/feature/ec2/imds"
"github.com/aws/aws-sdk-go-v2/service/ec2"
"github.com/edgelesssys/constellation/v2/internal/attestation"
"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/verify"
"github.com/google/go-tpm-tools/proto/attest"
"github.com/google/go-tpm/tpm2"
)
// Validator for AWS TPM attestation.
type Validator struct {
// Embed variant to identify the Validator using varaint.OID().
variant.AWSSEVSNP
// Embed validator to implement Validate method for aTLS handshake.
*vtpm.Validator
// AMD root key. Root of trust for the ASK used during report validation.
ark *x509.Certificate
// kdsClient gets an ASK from somewhere. kdsClient is required for testing.
kdsClient askGetter
// reportValidator validates a SNP report. reportValidator is required for testing.
reportValidator snpReportValidator
getDescribeClient func(context.Context, string) (awsMetadataAPI, error)
}
// NewValidator create a new Validator structure and returns it.
func NewValidator(cfg *config.AWSSEVSNP, log attestation.Logger) *Validator {
v := &Validator{
ark: (*x509.Certificate)(&cfg.AMDRootKey),
kdsClient: kdsClient{http.DefaultClient},
reportValidator: awsValidator{},
}
v := &Validator{}
v.Validator = vtpm.NewValidator(
cfg.Measurements,
v.getTrustedKey,
func(vtpm.AttestationDocument, *attest.MachineState) error { return nil },
getTrustedKey,
v.tpmEnabled,
log,
)
v.getDescribeClient = getEC2Client
return v
}
// getTrustedKeys return the public area of the provides attestation key.
// Normally, the key should be verified here, but currently AWS does not provide means to do so.
func (v *Validator) getTrustedKey(ctx context.Context, attDoc vtpm.AttestationDocument, _ []byte) (crypto.PublicKey, error) {
// Normally, here the trust of this key should be verified, but currently AWS does not provide this feature.
func getTrustedKey(_ context.Context, attDoc vtpm.AttestationDocument, _ []byte) (crypto.PublicKey, error) {
// Copied from https://github.com/edgelesssys/constellation/blob/main/internal/attestation/qemu/validator.go
pubArea, err := tpm2.DecodePublic(attDoc.Attestation.AkPub)
if err != nil {
return nil, newDecodeError(err)
}
pubKey, err := pubArea.Key()
if err != nil {
return nil, fmt.Errorf("getting public key: %w", err)
}
akDigest, err := sha512sum(pubKey)
if err != nil {
return nil, fmt.Errorf("calculating hash of attestation key: %w", err)
}
if err := v.reportValidator.validate(ctx, attDoc, v.kdsClient, v.ark, akDigest); err != nil {
return nil, fmt.Errorf("validating SNP report: %w", err)
return nil, err
}
return pubArea.Key()
}
// 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)
// tpmEnabled verifies if the virtual machine has the tpm2.0 feature enabled.
func (v *Validator) tpmEnabled(attestation vtpm.AttestationDocument, _ *attest.MachineState) error {
// https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/verify-nitrotpm-support-on-ami.html
// 1. Get the vm's ami (from IdentiTyDocument.imageId)
// 2. Check the value of key "TpmSupport": {"Value": "v2.0"}"
ctx := context.Background()
idDocument := imds.InstanceIdentityDocument{}
err := json.Unmarshal(attestation.InstanceInfo, &idDocument)
if err != nil {
return [64]byte{}, fmt.Errorf("marshalling public key: %w", err)
return err
}
return sha512.Sum512(pub), nil
imageID := idDocument.ImageID
client, err := v.getDescribeClient(ctx, idDocument.Region)
if err != nil {
return err
}
// Currently, there seems to be a problem with retrieving image attributes directly.
// Alternatively, parse it from the general output.
imageOutput, err := client.DescribeImages(ctx, &ec2.DescribeImagesInput{ImageIds: []string{imageID}})
if err != nil {
return err
}
if imageOutput.Images[0].TpmSupport == "v2.0" {
return nil
}
return fmt.Errorf("iam image %s does not support TPM v2.0", imageID)
}
// Validate a given SNP report.
type snpReportValidator interface {
validate(ctx context.Context, attestation vtpm.AttestationDocument, kdsClient askGetter, ark *x509.Certificate, ak [64]byte) error
func getEC2Client(ctx context.Context, region string) (awsMetadataAPI, error) {
client, err := awsConfig.LoadDefaultConfig(ctx, awsConfig.WithRegion(region))
if err != nil {
return nil, err
}
return ec2.NewFromConfig(client), nil
}
// Validation logic for the AWS SNP implementation.
type awsValidator struct{}
// validate the report by checking if it has a valid VLEK signature.
// The certificate chain ARK -> ASK -> VLEK is also validated.
// Checks that the report's userData matches the connection's userData.
func (awsValidator) validate(ctx context.Context, attestation vtpm.AttestationDocument, kdsClient askGetter, ark *x509.Certificate, akDigest [64]byte) error {
var info instanceInfo
if err := json.Unmarshal(attestation.InstanceInfo, &info); err != nil {
return newValidationError(fmt.Errorf("unmarshalling instance info: %w", err))
}
vlek, err := getVLEK(info.Certs)
if err != nil {
return newValidationError(fmt.Errorf("parsing certificates from certtable %x: %w", info.Certs, err))
}
ask, err := kdsClient.getASK(ctx)
if err != nil {
return newValidationError(fmt.Errorf("getting ASK: %w", err))
}
if err := ask.CheckSignatureFrom(ark); err != nil {
return newValidationError(fmt.Errorf("verifying ASK signature: %w", err))
}
if err := vlek.CheckSignatureFrom(ask); err != nil {
return newValidationError(fmt.Errorf("verifying VLEK signature: %w", err))
}
if err := verify.SnpReportSignature(info.Report, vlek); err != nil {
return newValidationError(fmt.Errorf("verifying snp report signature: %w", err))
}
report, err := abi.ReportToProto(info.Report)
if err != nil {
return newValidationError(fmt.Errorf("unmarshalling SNP report: %w", err))
}
if !bytes.Equal(report.GetReportData(), akDigest[:]) {
return newValidationError(errors.New("SNP report and attestation statement contain mismatching attestation keys"))
}
return nil
}
// getVLEK parses the certificate table included in an extended SNP report
// and returns the VLEK certificate.
func getVLEK(certs []byte) (vlek *x509.Certificate, err error) {
certTable := abi.CertTable{}
if err = certTable.Unmarshal(certs); err != nil {
return nil, fmt.Errorf("unmarshalling SNP certificate table: %v", err)
}
vlekRaw, err := certTable.GetByGUIDString(abi.VlekGUID)
if err != nil {
return nil, fmt.Errorf("getting VLEK certificate: %v", err)
}
vlek, err = x509.ParseCertificate(vlekRaw)
if err != nil {
return nil, fmt.Errorf("parsing certificate: %w", err)
}
return
}
// Query the AMD key distribution service for an AMD signing key.
type kdsClient struct {
httpClient
}
type httpClient interface {
Do(req *http.Request) (*http.Response, error)
}
// getASK requests the current certificate chain from the AMD KDS API and returns the ASK.
// There is no information on how to handle CRLs in the official AMD docs.
// Once github.com/google/go-sev-guest adds support to check CRLs for VLEK-based certificate chains
// we can check CRLs here.
func (k kdsClient) getASK(ctx context.Context) (*x509.Certificate, error) {
// If there are multiple CPU generations (and with that different API paths to call) in the future,
// we can select the correct path to call based on the information contained in the SNP report.
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://kdsintf.amd.com/vlek/v1/Milan/cert_chain", nil)
if err != nil {
return nil, fmt.Errorf("creating request: %w", err)
}
resp, err := k.Do(req)
if err != nil {
return nil, fmt.Errorf("requesting ASK: %w", err)
}
defer resp.Body.Close()
pemChain, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("reading response body: %w", err)
}
// certificate chain starts with ASK. We hardcode the ARK, so ignore that.
decodedASK, _ := pem.Decode(pemChain)
if decodedASK == nil {
return nil, errors.New("no PEM data found")
}
ask, err := x509.ParseCertificate(decodedASK.Bytes)
if err != nil {
return nil, fmt.Errorf("parsing ASK: %w", err)
}
return ask, nil
}
// askGetter gets an ASK from somewhere.
type askGetter interface {
getASK(ctx context.Context) (*x509.Certificate, error)
type awsMetadataAPI interface {
DescribeImages(ctx context.Context, params *ec2.DescribeImagesInput, optFns ...func(*ec2.Options)) (*ec2.DescribeImagesOutput, error)
}

File diff suppressed because one or more lines are too long