attestation: add snp package

The package holds code shared between SNP-based
attestation implementations on AWS and Azure .
This commit is contained in:
Otto Bittner 2023-10-30 12:31:42 +01:00
parent 635a5d2c0a
commit 5ce55e3449
15 changed files with 608 additions and 489 deletions

View File

@ -15,11 +15,11 @@ go_library(
deps = [
"//internal/attestation",
"//internal/attestation/idkeydigest",
"//internal/attestation/snp",
"//internal/attestation/variant",
"//internal/attestation/vtpm",
"//internal/cloud/azure",
"//internal/config",
"//internal/constants",
"@com_github_edgelesssys_go_azguestattestation//maa",
"@com_github_google_go_sev_guest//abi",
"@com_github_google_go_sev_guest//kds",
@ -48,9 +48,10 @@ go_test(
}),
deps = [
"//internal/attestation",
"//internal/attestation/azure/snp/testdata",
"//internal/attestation/idkeydigest",
"//internal/attestation/simulator",
"//internal/attestation/snp",
"//internal/attestation/snp/testdata",
"//internal/attestation/vtpm",
"//internal/config",
"//internal/logger",

View File

@ -13,6 +13,7 @@ import (
"io"
"github.com/edgelesssys/constellation/v2/internal/attestation"
"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/go-azguestattestation/maa"
@ -65,7 +66,7 @@ func (i *Issuer) getInstanceInfo(ctx context.Context, tpm io.ReadWriteCloser, us
}
}
instanceInfo := azureInstanceInfo{
instanceInfo := snp.InstanceInfo{
VCEK: params.VcekCert,
CertChain: params.VcekChain,
AttestationReport: params.SNPReport,

View File

@ -15,6 +15,7 @@ import (
"testing"
"github.com/edgelesssys/constellation/v2/internal/attestation/simulator"
"github.com/edgelesssys/constellation/v2/internal/attestation/snp"
"github.com/edgelesssys/go-azguestattestation/maa"
tpmclient "github.com/google/go-tpm-tools/client"
"github.com/google/go-tpm/legacy/tpm2"
@ -99,7 +100,7 @@ func TestGetSNPAttestation(t *testing.T) {
assert.Equal(data, maa.gotTokenData)
}
var instanceInfo azureInstanceInfo
var instanceInfo snp.InstanceInfo
err = json.Unmarshal(attestationJSON, &instanceInfo)
require.NoError(err)

View File

@ -15,16 +15,15 @@ import (
"encoding/base64"
"encoding/binary"
"encoding/json"
"encoding/pem"
"errors"
"fmt"
"github.com/edgelesssys/constellation/v2/internal/attestation"
"github.com/edgelesssys/constellation/v2/internal/attestation/idkeydigest"
"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/edgelesssys/constellation/v2/internal/constants"
"github.com/google/go-sev-guest/abi"
"github.com/google/go-sev-guest/kds"
spb "github.com/google/go-sev-guest/proto/sevsnp"
@ -79,7 +78,7 @@ func NewValidator(cfg *config.AzureSEVSNP, log attestation.Logger) *Validator {
log = nopAttestationLogger{}
}
v := &Validator{
hclValidator: &azureInstanceInfo{},
hclValidator: &attestationKey{},
maa: newMAAClient(),
config: cfg,
log: log,
@ -106,18 +105,15 @@ func (v *Validator) getTrustedKey(ctx context.Context, attDoc vtpm.AttestationDo
trustedArk := (*x509.Certificate)(&v.config.AMDRootKey) // ARK, specified in the Constellation config
// fallback certificates, used if not present in THIM response.
cachedCerts := sevSnpCerts{
ask: trustedAsk,
ark: trustedArk,
}
cachedCerts := snp.NewCertificateChain(trustedAsk, trustedArk)
// transform the instanceInfo received from Microsoft into a verifiable attestation report format.
var instanceInfo azureInstanceInfo
var instanceInfo snp.InstanceInfo
if err := json.Unmarshal(attDoc.InstanceInfo, &instanceInfo); err != nil {
return nil, fmt.Errorf("unmarshalling instanceInfo: %w", err)
}
att, err := instanceInfo.attestationWithCerts(v.log, v.getter, cachedCerts)
att, err := instanceInfo.AttestationWithCerts(v.log, v.getter, cachedCerts)
if err != nil {
return nil, fmt.Errorf("parsing attestation report: %w", err)
}
@ -192,7 +188,7 @@ func (v *Validator) getTrustedKey(ctx context.Context, attDoc vtpm.AttestationDo
if err != nil {
return nil, err
}
if err = v.hclValidator.validateAk(instanceInfo.RuntimeData, att.Report.ReportData, pubArea.RSAParameters); err != nil {
if err = v.hclValidator.validate(instanceInfo.RuntimeData, att.Report.ReportData, pubArea.RSAParameters); err != nil {
return nil, fmt.Errorf("validating HCLAkPub: %w", err)
}
@ -239,201 +235,17 @@ func (v *Validator) checkIDKeyDigest(ctx context.Context, report *spb.Attestatio
return nil
}
// azureInstanceInfo contains the necessary information to establish trust in
// an Azure CVM.
type azureInstanceInfo struct {
// VCEK is the PEM-encoded VCEK certificate for the attestation report.
VCEK []byte
// CertChain is the PEM-encoded certificate chain for the attestation report.
CertChain []byte
// AttestationReport is the attestation report from the vTPM (NVRAM) of the CVM.
AttestationReport []byte
// RuntimeData is the Azure runtime data from the vTPM (NVRAM) of the CVM.
RuntimeData []byte
// MAAToken is the token of the MAA for the attestation report, used as a fallback
// if the IDKeyDigest cannot be verified.
MAAToken string
type attestationKey struct {
PublicPart []akPub `json:"keys"`
}
// attestationWithCerts returns a formatted version of the attestation report and its certificates from the instanceInfo.
// Certificates are retrieved in the following precedence:
// 1. ASK or ARK from THIM
// 2. ASK or ARK from fallbackCerts
// 3. ASK or ARK from AMD KDS.
func (a *azureInstanceInfo) attestationWithCerts(logger attestation.Logger, getter trust.HTTPSGetter,
fallbackCerts sevSnpCerts,
) (*spb.Attestation, error) {
report, err := abi.ReportToProto(a.AttestationReport)
if err != nil {
return nil, fmt.Errorf("converting report to proto: %w", err)
}
// Product info as reported through CPUID[EAX=1]
sevProduct := &spb.SevProduct{Name: spb.SevProduct_SEV_PRODUCT_MILAN, Stepping: 0} // Milan-B0
productName := kds.ProductString(sevProduct)
att := &spb.Attestation{
Report: report,
CertificateChain: &spb.CertificateChain{},
Product: sevProduct,
}
// If the VCEK certificate is present, parse it and format it.
vcek, err := a.parseVCEK()
if err != nil {
logger.Warnf("Error parsing VCEK: %v", err)
}
if vcek != nil {
att.CertificateChain.VcekCert = vcek.Raw
} else {
// Otherwise, retrieve it from AMD KDS.
logger.Infof("VCEK certificate not present, falling back to retrieving it from AMD KDS")
vcekURL := kds.VCEKCertURL(productName, report.GetChipId(), kds.TCBVersion(report.GetReportedTcb()))
vcek, err := getter.Get(vcekURL)
if err != nil {
return nil, fmt.Errorf("retrieving VCEK certificate from AMD KDS: %w", err)
}
att.CertificateChain.VcekCert = vcek
}
// If the certificate chain from THIM is present, parse it and format it.
ask, ark, err := a.parseCertChain()
if err != nil {
logger.Warnf("Error parsing certificate chain: %v", err)
}
if ask != nil {
logger.Infof("Using ASK certificate from Azure THIM")
att.CertificateChain.AskCert = ask.Raw
}
if ark != nil {
logger.Infof("Using ARK certificate from Azure THIM")
att.CertificateChain.ArkCert = ark.Raw
}
// If a cached ASK or an ARK from the Constellation config is present, use it.
if att.CertificateChain.AskCert == nil && fallbackCerts.ask != nil {
logger.Infof("Using cached ASK certificate")
att.CertificateChain.AskCert = fallbackCerts.ask.Raw
}
if att.CertificateChain.ArkCert == nil && fallbackCerts.ark != nil {
logger.Infof("Using ARK certificate from %s", constants.ConfigFilename)
att.CertificateChain.ArkCert = fallbackCerts.ark.Raw
}
// Otherwise, retrieve it from AMD KDS.
if att.CertificateChain.AskCert == nil || att.CertificateChain.ArkCert == nil {
logger.Infof(
"Certificate chain not fully present (ARK present: %t, ASK present: %t), falling back to retrieving it from AMD KDS",
(att.CertificateChain.ArkCert != nil),
(att.CertificateChain.AskCert != nil),
)
kdsCertChain, err := trust.GetProductChain(productName, abi.VcekReportSigner, getter)
if err != nil {
return nil, fmt.Errorf("retrieving certificate chain from AMD KDS: %w", err)
}
if att.CertificateChain.AskCert == nil && kdsCertChain.Ask != nil {
logger.Infof("Using ASK certificate from AMD KDS")
att.CertificateChain.AskCert = kdsCertChain.Ask.Raw
}
if att.CertificateChain.ArkCert == nil && kdsCertChain.Ask != nil {
logger.Infof("Using ARK certificate from AMD KDS")
att.CertificateChain.ArkCert = kdsCertChain.Ark.Raw
}
}
return att, nil
}
type sevSnpCerts struct {
ask *x509.Certificate
ark *x509.Certificate
}
// parseCertChain parses the certificate chain from the instanceInfo into x509-formatted ASK and ARK certificates.
// If less than 2 certificates are present, only the present certificate is returned.
// If more than 2 certificates are present, an error is returned.
func (a *azureInstanceInfo) parseCertChain() (ask, ark *x509.Certificate, retErr error) {
rest := bytes.TrimSpace(a.CertChain)
i := 1
var block *pem.Block
for block, rest = pem.Decode(rest); block != nil; block, rest = pem.Decode(rest) {
if i > 2 {
retErr = fmt.Errorf("parse certificate %d: more than 2 certificates in chain", i)
return
}
if block.Type != "CERTIFICATE" {
retErr = fmt.Errorf("parse certificate %d: expected PEM block type 'CERTIFICATE', got '%s'", i, block.Type)
return
}
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
retErr = fmt.Errorf("parse certificate %d: %w", i, err)
return
}
// https://www.amd.com/content/dam/amd/en/documents/epyc-technical-docs/specifications/57230.pdf
// Table 6 and 7
switch cert.Subject.CommonName {
case "SEV-Milan":
ask = cert
case "ARK-Milan":
ark = cert
default:
retErr = fmt.Errorf("parse certificate %d: unexpected subject CN %s", i, cert.Subject.CommonName)
return
}
i++
}
switch {
case i == 1:
retErr = fmt.Errorf("no PEM blocks found")
case len(rest) != 0:
retErr = fmt.Errorf("remaining PEM block is not a valid certificate: %s", rest)
}
return
}
// parseVCEK parses the VCEK certificate from the instanceInfo into an x509-formatted certificate.
// If the VCEK certificate is not present, nil is returned.
func (a *azureInstanceInfo) parseVCEK() (*x509.Certificate, error) {
newlinesTrimmed := bytes.TrimSpace(a.VCEK)
if len(newlinesTrimmed) == 0 {
// VCEK is not present.
return nil, nil
}
block, rest := pem.Decode(newlinesTrimmed)
if block == nil {
return nil, fmt.Errorf("no PEM blocks found")
}
if len(rest) != 0 {
return nil, fmt.Errorf("received more data than expected")
}
if block.Type != "CERTIFICATE" {
return nil, fmt.Errorf("expected PEM block type 'CERTIFICATE', got '%s'", block.Type)
}
vcek, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return nil, fmt.Errorf("parsing VCEK certificate: %w", err)
}
return vcek, nil
}
// validateAk validates that the attestation key from the TPM is trustworthy. The steps are:
// validate validates that the attestation key from the TPM is trustworthy. The steps are:
// 1. runtime data read from the TPM has the same sha256 digest as reported in `report_data` of the SNP report.
// 2. modulus reported in runtime data matches modulus from key at idx 0x81000003.
// 3. exponent reported in runtime data matches exponent from key at idx 0x81000003.
// The function is currently tested manually on a Azure Ubuntu CVM.
func (a *azureInstanceInfo) validateAk(runtimeDataRaw []byte, reportData []byte, rsaParameters *tpm2.RSAParams) error {
var runtimeData runtimeData
if err := json.Unmarshal(runtimeDataRaw, &runtimeData); err != nil {
func (a *attestationKey) validate(runtimeDataRaw []byte, reportData []byte, rsaParameters *tpm2.RSAParams) error {
if err := json.Unmarshal(runtimeDataRaw, a); err != nil {
return fmt.Errorf("unmarshalling json: %w", err)
}
@ -445,10 +257,10 @@ func (a *azureInstanceInfo) validateAk(runtimeDataRaw []byte, reportData []byte,
return errors.New("unexpected runtimeData digest in TPM")
}
if len(runtimeData.Keys) < 1 {
if len(a.PublicPart) < 1 {
return errors.New("did not receive any keys in runtime data")
}
rawN, err := base64.RawURLEncoding.DecodeString(runtimeData.Keys[0].N)
rawN, err := base64.RawURLEncoding.DecodeString(a.PublicPart[0].N)
if err != nil {
return fmt.Errorf("decoding modulus string: %w", err)
}
@ -456,7 +268,7 @@ func (a *azureInstanceInfo) validateAk(runtimeDataRaw []byte, reportData []byte,
return fmt.Errorf("unexpected modulus value in TPM")
}
rawE, err := base64.RawURLEncoding.DecodeString(runtimeData.Keys[0].E)
rawE, err := base64.RawURLEncoding.DecodeString(a.PublicPart[0].E)
if err != nil {
return fmt.Errorf("decoding exponent string: %w", err)
}
@ -478,17 +290,13 @@ func (a *azureInstanceInfo) validateAk(runtimeDataRaw []byte, reportData []byte,
// The HCL is written by Azure, and sits between the Hypervisor and CVM OS.
// The HCL runs in the protected context of the CVM.
type hclAkValidator interface {
validateAk(runtimeDataRaw []byte, reportData []byte, rsaParameters *tpm2.RSAParams) error
validate(runtimeDataRaw []byte, reportData []byte, rsaParameters *tpm2.RSAParams) error
}
// akPub are the public parameters of an RSA attestation key.
type akPub struct {
E string
N string
}
type runtimeData struct {
Keys []akPub
E string `json:"e"`
N string `json:"n"`
}
// nopAttestationLogger is a no-op implementation of AttestationLogger.

View File

@ -10,7 +10,6 @@ import (
"bytes"
"context"
"crypto/sha256"
"crypto/x509"
"encoding/base64"
"encoding/binary"
"encoding/hex"
@ -19,13 +18,13 @@ import (
"fmt"
"os"
"regexp"
"strings"
"testing"
"github.com/edgelesssys/constellation/v2/internal/attestation"
"github.com/edgelesssys/constellation/v2/internal/attestation/azure/snp/testdata"
"github.com/edgelesssys/constellation/v2/internal/attestation/idkeydigest"
"github.com/edgelesssys/constellation/v2/internal/attestation/simulator"
"github.com/edgelesssys/constellation/v2/internal/attestation/snp"
"github.com/edgelesssys/constellation/v2/internal/attestation/snp/testdata"
"github.com/edgelesssys/constellation/v2/internal/attestation/vtpm"
"github.com/edgelesssys/constellation/v2/internal/config"
"github.com/edgelesssys/constellation/v2/internal/logger"
@ -69,264 +68,6 @@ func TestNewValidator(t *testing.T) {
}
}
// TestParseCertChain tests the parsing of the certificate chain.
func TestParseCertChain(t *testing.T) {
defaultCertChain := testdata.CertChain
askOnly := strings.Split(string(defaultCertChain), "-----END CERTIFICATE-----")[0] + "-----END CERTIFICATE-----"
arkOnly := strings.Split(string(defaultCertChain), "-----END CERTIFICATE-----")[1] + "-----END CERTIFICATE-----"
testCases := map[string]struct {
certChain []byte
wantAsk bool
wantArk bool
wantErr bool
}{
"success": {
certChain: defaultCertChain,
wantAsk: true,
wantArk: true,
},
"empty cert chain": {
certChain: []byte{},
wantErr: true,
},
"more than two certificates": {
certChain: append(defaultCertChain, defaultCertChain...),
wantErr: true,
},
"invalid certificate": {
certChain: []byte("invalid"),
wantErr: true,
},
"ark missing": {
certChain: []byte(askOnly),
wantAsk: true,
},
"ask missing": {
certChain: []byte(arkOnly),
wantArk: true,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
instanceInfo := &azureInstanceInfo{
CertChain: tc.certChain,
}
ask, ark, err := instanceInfo.parseCertChain()
if tc.wantErr {
assert.Error(err)
} else {
assert.NoError(err)
assert.Equal(tc.wantAsk, ask != nil)
assert.Equal(tc.wantArk, ark != nil)
}
})
}
}
// TestParseVCEK tests the parsing of the VCEK certificate.
func TestParseVCEK(t *testing.T) {
testCases := map[string]struct {
VCEK []byte
wantVCEK bool
wantErr bool
}{
"success": {
VCEK: testdata.AzureThimVCEK,
wantVCEK: true,
},
"empty": {
VCEK: []byte{},
},
"malformed": {
VCEK: testdata.AzureThimVCEK[:len(testdata.AzureThimVCEK)-100],
wantErr: true,
},
"invalid": {
VCEK: []byte("invalid"),
wantErr: true,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
instanceInfo := &azureInstanceInfo{
VCEK: tc.VCEK,
}
vcek, err := instanceInfo.parseVCEK()
if tc.wantErr {
assert.Error(err)
} else {
assert.NoError(err)
assert.Equal(tc.wantVCEK, vcek != nil)
}
})
}
}
// TestInstanceInfoAttestation tests the basic unmarshalling of the attestation report and the ASK / ARK precedence.
func TestInstanceInfoAttestation(t *testing.T) {
defaultReport := testdata.AttestationReport
testdataArk, testdataAsk := mustCertChainToPem(t, testdata.CertChain)
exampleCert := &x509.Certificate{
Raw: []byte{1, 2, 3},
}
cfg := config.DefaultForAzureSEVSNP()
testCases := map[string]struct {
report []byte
vcek []byte
certChain []byte
fallbackCerts sevSnpCerts
getter *stubHTTPSGetter
expectedArk *x509.Certificate
expectedAsk *x509.Certificate
wantErr bool
}{
"success": {
report: defaultReport,
vcek: testdata.AzureThimVCEK,
certChain: testdata.CertChain,
expectedArk: testdataArk,
expectedAsk: testdataAsk,
},
"retrieve vcek": {
report: defaultReport,
certChain: testdata.CertChain,
getter: newStubHTTPSGetter(
&urlResponseMatcher{
vcekResponse: testdata.AmdKdsVCEK,
wantVcekRequest: true,
},
nil,
),
expectedArk: testdataArk,
expectedAsk: testdataAsk,
},
"retrieve certchain": {
report: defaultReport,
vcek: testdata.AzureThimVCEK,
getter: newStubHTTPSGetter(
&urlResponseMatcher{
certChainResponse: testdata.CertChain,
wantCertChainRequest: true,
},
nil,
),
expectedArk: testdataArk,
expectedAsk: testdataAsk,
},
"use fallback certs": {
report: defaultReport,
vcek: testdata.AzureThimVCEK,
fallbackCerts: sevSnpCerts{
ask: exampleCert,
ark: exampleCert,
},
getter: newStubHTTPSGetter(
&urlResponseMatcher{},
nil,
),
expectedArk: exampleCert,
expectedAsk: exampleCert,
},
"use certchain with fallback certs": {
report: defaultReport,
certChain: testdata.CertChain,
vcek: testdata.AzureThimVCEK,
fallbackCerts: sevSnpCerts{
ask: &x509.Certificate{},
ark: &x509.Certificate{},
},
getter: newStubHTTPSGetter(
&urlResponseMatcher{},
nil,
),
expectedArk: testdataArk,
expectedAsk: testdataAsk,
},
"retrieve vcek and certchain": {
report: defaultReport,
getter: newStubHTTPSGetter(
&urlResponseMatcher{
certChainResponse: testdata.CertChain,
vcekResponse: testdata.AmdKdsVCEK,
wantCertChainRequest: true,
wantVcekRequest: true,
},
nil,
),
expectedArk: testdataArk,
expectedAsk: testdataAsk,
},
"report too short": {
report: defaultReport[:len(defaultReport)-100],
wantErr: true,
},
"corrupted report": {
report: defaultReport[10 : len(defaultReport)-10],
wantErr: true,
},
"certificate fetch error": {
report: defaultReport,
getter: newStubHTTPSGetter(nil, assert.AnError),
wantErr: true,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
require := require.New(t)
// This is important. Without this call, the trust module caches certificates across testcases.
defer trust.ClearProductCertCache()
instanceInfo := azureInstanceInfo{
AttestationReport: tc.report,
CertChain: tc.certChain,
VCEK: tc.vcek,
}
att, err := instanceInfo.attestationWithCerts(logger.NewTest(t), tc.getter, tc.fallbackCerts)
if tc.wantErr {
assert.Error(err)
} else {
require.NoError(err)
assert.NotNil(att)
assert.NotNil(att.CertificateChain)
assert.NotNil(att.Report)
assert.Equal(hex.EncodeToString(att.Report.IdKeyDigest[:]), "57e229e0ffe5fa92d0faddff6cae0e61c926fc9ef9afd20a8b8cfcf7129db9338cbe5bf3f6987733a2bf65d06dc38fc1")
// This is a canary for us: If this fails in the future we possibly downgraded a SVN.
// See https://github.com/google/go-sev-guest/blob/14ac50e9ffcc05cd1d12247b710c65093beedb58/validate/validate.go#L336 for decomposition of the values.
tcbValues := kds.DecomposeTCBVersion(kds.TCBVersion(att.Report.GetLaunchTcb()))
assert.True(tcbValues.BlSpl >= cfg.BootloaderVersion.Value)
assert.True(tcbValues.TeeSpl >= cfg.TEEVersion.Value)
assert.True(tcbValues.SnpSpl >= cfg.SNPVersion.Value)
assert.True(tcbValues.UcodeSpl >= cfg.MicrocodeVersion.Value)
assert.Equal(tc.expectedArk.Raw, att.CertificateChain.ArkCert)
assert.Equal(tc.expectedAsk.Raw, att.CertificateChain.AskCert)
}
})
}
}
func mustCertChainToPem(t *testing.T, certchain []byte) (ark, ask *x509.Certificate) {
t.Helper()
a := azureInstanceInfo{CertChain: certchain}
ask, ark, err := a.parseCertChain()
require.NoError(t, err)
return ark, ask
}
type stubHTTPSGetter struct {
urlResponseMatcher *urlResponseMatcher // maps responses to requested URLs
err error
@ -488,18 +229,18 @@ func TestValidateAk(t *testing.T) {
n := base64.RawURLEncoding.EncodeToString(key.PublicArea().RSAParameters.ModulusRaw)
ak := akPub{E: e, N: n}
runtimeData := runtimeData{Keys: []akPub{ak}}
runtimeData := attestationKey{PublicPart: []akPub{ak}}
defaultRuntimeDataRaw, err := json.Marshal(runtimeData)
require.NoError(err)
defaultInstanceInfo := azureInstanceInfo{RuntimeData: defaultRuntimeDataRaw}
defaultInstanceInfo := snp.InstanceInfo{RuntimeData: defaultRuntimeDataRaw}
sig := sha256.Sum256(defaultRuntimeDataRaw)
defaultReportData := sig[:]
defaultRsaParams := key.PublicArea().RSAParameters
testCases := map[string]struct {
instanceInfo azureInstanceInfo
instanceInfo snp.InstanceInfo
runtimeDataRaw []byte
reportData []byte
rsaParameters *tpm2.RSAParams
@ -552,7 +293,8 @@ func TestValidateAk(t *testing.T) {
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
err = tc.instanceInfo.validateAk(tc.runtimeDataRaw, tc.reportData, tc.rsaParameters)
ak := attestationKey{}
err = ak.validate(tc.runtimeDataRaw, tc.reportData, tc.rsaParameters)
if tc.wantErr {
assert.Error(err)
} else {
@ -985,7 +727,7 @@ func TestTrustedKeyFromSNP(t *testing.T) {
// This is important. Without this call, the trust module caches certificates across testcases.
defer trust.ClearProductCertCache()
instanceInfo, err := newStubAzureInstanceInfo(tc.vcek, tc.certChain, tc.report, tc.runtimeData)
instanceInfo, err := newStubInstanceInfo(tc.vcek, tc.certChain, tc.report, tc.runtimeData)
require.NoError(err)
statement, err := json.Marshal(instanceInfo)
@ -1004,7 +746,7 @@ func TestTrustedKeyFromSNP(t *testing.T) {
}
validator := &Validator{
hclValidator: &instanceInfo,
hclValidator: &stubAttestationKey{},
config: defaultCfg,
log: logger.NewTest(t),
getter: tc.getter,
@ -1050,25 +792,25 @@ func (v *stubAttestationValidator) SNPAttestation(attestation *spb.Attestation,
return validate.SnpAttestation(attestation, options)
}
type stubAzureInstanceInfo struct {
type stubInstanceInfo struct {
AttestationReport []byte
RuntimeData []byte
VCEK []byte
CertChain []byte
}
func newStubAzureInstanceInfo(vcek, certChain []byte, report, runtimeData string) (stubAzureInstanceInfo, error) {
func newStubInstanceInfo(vcek, certChain []byte, report, runtimeData string) (stubInstanceInfo, error) {
validReport, err := hex.DecodeString(report)
if err != nil {
return stubAzureInstanceInfo{}, fmt.Errorf("invalid hex string report: %s", err)
return stubInstanceInfo{}, fmt.Errorf("invalid hex string report: %s", err)
}
decodedRuntime, err := hex.DecodeString(runtimeData)
if err != nil {
return stubAzureInstanceInfo{}, fmt.Errorf("invalid hex string runtimeData: %s", err)
return stubInstanceInfo{}, fmt.Errorf("invalid hex string runtimeData: %s", err)
}
return stubAzureInstanceInfo{
return stubInstanceInfo{
AttestationReport: validReport,
RuntimeData: decodedRuntime,
VCEK: vcek,
@ -1076,9 +818,12 @@ func newStubAzureInstanceInfo(vcek, certChain []byte, report, runtimeData string
}, nil
}
func (s *stubAzureInstanceInfo) validateAk(runtimeDataRaw []byte, reportData []byte, _ *tpm2.RSAParams) error {
var runtimeData runtimeData
if err := json.Unmarshal(runtimeDataRaw, &runtimeData); err != nil {
type stubAttestationKey struct {
PublicPart []akPub
}
func (s *stubAttestationKey) validate(runtimeDataRaw []byte, reportData []byte, _ *tpm2.RSAParams) error {
if err := json.Unmarshal(runtimeDataRaw, s); err != nil {
return fmt.Errorf("unmarshalling json: %w", err)
}

View File

@ -0,0 +1,31 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library")
load("//bazel/go:go_test.bzl", "go_test")
go_library(
name = "snp",
srcs = ["snp.go"],
importpath = "github.com/edgelesssys/constellation/v2/internal/attestation/snp",
visibility = ["//:__subpackages__"],
deps = [
"//internal/attestation",
"//internal/constants",
"@com_github_google_go_sev_guest//abi",
"@com_github_google_go_sev_guest//kds",
"@com_github_google_go_sev_guest//proto/sevsnp",
"@com_github_google_go_sev_guest//verify/trust",
],
)
go_test(
name = "snp_test",
srcs = ["snp_test.go"],
embed = [":snp"],
deps = [
"//internal/attestation/snp/testdata",
"//internal/config",
"//internal/logger",
"@com_github_google_go_sev_guest//kds",
"@com_github_stretchr_testify//assert",
"@com_github_stretchr_testify//require",
],
)

View File

@ -0,0 +1,217 @@
/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
// Package SNP provides types shared by SNP-based attestation implementations.
// It ensures all issuers provide the same types to the verify command.
package snp
import (
"bytes"
"crypto/x509"
"encoding/pem"
"fmt"
"github.com/edgelesssys/constellation/v2/internal/attestation"
"github.com/edgelesssys/constellation/v2/internal/constants"
"github.com/google/go-sev-guest/abi"
"github.com/google/go-sev-guest/kds"
spb "github.com/google/go-sev-guest/proto/sevsnp"
"github.com/google/go-sev-guest/verify/trust"
)
// InstanceInfo contains the necessary information to establish trust in
// an Azure CVM.
type InstanceInfo struct {
// VCEK is the PEM-encoded VCEK certificate for the attestation report.
VCEK []byte
// CertChain is the PEM-encoded certificate chain for the attestation report.
CertChain []byte
// AttestationReport is the attestation report from the vTPM (NVRAM) of the CVM.
AttestationReport []byte
// RuntimeData is the Azure runtime data from the vTPM (NVRAM) of the CVM.
RuntimeData []byte
// MAAToken is the token of the MAA for the attestation report, used as a fallback
// if the IDKeyDigest cannot be verified.
MAAToken string
}
// AttestationWithCerts returns a formatted version of the attestation report and its certificates from the instanceInfo.
// Certificates are retrieved in the following precedence:
// 1. ASK or ARK from issuer. On Azure: THIM. One AWS: not prefilled.
// 2. ASK or ARK from fallbackCerts.
// 3. ASK or ARK from AMD KDS.
func (a *InstanceInfo) AttestationWithCerts(logger attestation.Logger, getter trust.HTTPSGetter,
fallbackCerts CertificateChain,
) (*spb.Attestation, error) {
report, err := abi.ReportToProto(a.AttestationReport)
if err != nil {
return nil, fmt.Errorf("converting report to proto: %w", err)
}
// Product info as reported through CPUID[EAX=1]
sevProduct := &spb.SevProduct{Name: spb.SevProduct_SEV_PRODUCT_MILAN, Stepping: 0} // Milan-B0
productName := kds.ProductString(sevProduct)
att := &spb.Attestation{
Report: report,
CertificateChain: &spb.CertificateChain{},
Product: sevProduct,
}
// If the VCEK certificate is present, parse it and format it.
vcek, err := a.ParseVCEK()
if err != nil {
logger.Warnf("Error parsing VCEK: %v", err)
}
if vcek != nil {
att.CertificateChain.VcekCert = vcek.Raw
} else {
// Otherwise, retrieve it from AMD KDS.
logger.Infof("VCEK certificate not present, falling back to retrieving it from AMD KDS")
vcekURL := kds.VCEKCertURL(productName, report.GetChipId(), kds.TCBVersion(report.GetReportedTcb()))
vcek, err := getter.Get(vcekURL)
if err != nil {
return nil, fmt.Errorf("retrieving VCEK certificate from AMD KDS: %w", err)
}
att.CertificateChain.VcekCert = vcek
}
// If the certificate chain from THIM is present, parse it and format it.
ask, ark, err := a.ParseCertChain()
if err != nil {
logger.Warnf("Error parsing certificate chain: %v", err)
}
if ask != nil {
logger.Infof("Using ASK certificate from Azure THIM")
att.CertificateChain.AskCert = ask.Raw
}
if ark != nil {
logger.Infof("Using ARK certificate from Azure THIM")
att.CertificateChain.ArkCert = ark.Raw
}
// If a cached ASK or an ARK from the Constellation config is present, use it.
if att.CertificateChain.AskCert == nil && fallbackCerts.ask != nil {
logger.Infof("Using cached ASK certificate")
att.CertificateChain.AskCert = fallbackCerts.ask.Raw
}
if att.CertificateChain.ArkCert == nil && fallbackCerts.ark != nil {
logger.Infof("Using ARK certificate from %s", constants.ConfigFilename)
att.CertificateChain.ArkCert = fallbackCerts.ark.Raw
}
// Otherwise, retrieve it from AMD KDS.
if att.CertificateChain.AskCert == nil || att.CertificateChain.ArkCert == nil {
logger.Infof(
"Certificate chain not fully present (ARK present: %t, ASK present: %t), falling back to retrieving it from AMD KDS",
(att.CertificateChain.ArkCert != nil),
(att.CertificateChain.AskCert != nil),
)
kdsCertChain, err := trust.GetProductChain(productName, abi.VcekReportSigner, getter)
if err != nil {
return nil, fmt.Errorf("retrieving certificate chain from AMD KDS: %w", err)
}
if att.CertificateChain.AskCert == nil && kdsCertChain.Ask != nil {
logger.Infof("Using ASK certificate from AMD KDS")
att.CertificateChain.AskCert = kdsCertChain.Ask.Raw
}
if att.CertificateChain.ArkCert == nil && kdsCertChain.Ask != nil {
logger.Infof("Using ARK certificate from AMD KDS")
att.CertificateChain.ArkCert = kdsCertChain.Ark.Raw
}
}
return att, nil
}
type CertificateChain struct {
ask *x509.Certificate
ark *x509.Certificate
}
func NewCertificateChain(ask, ark *x509.Certificate) CertificateChain {
return CertificateChain{
ask: ask,
ark: ark,
}
}
// ParseCertChain parses the certificate chain from the instanceInfo into x509-formatted ASK and ARK certificates.
// If less than 2 certificates are present, only the present certificate is returned.
// If more than 2 certificates are present, an error is returned.
func (a *InstanceInfo) ParseCertChain() (ask, ark *x509.Certificate, retErr error) {
rest := bytes.TrimSpace(a.CertChain)
i := 1
var block *pem.Block
for block, rest = pem.Decode(rest); block != nil; block, rest = pem.Decode(rest) {
if i > 2 {
retErr = fmt.Errorf("parse certificate %d: more than 2 certificates in chain", i)
return
}
if block.Type != "CERTIFICATE" {
retErr = fmt.Errorf("parse certificate %d: expected PEM block type 'CERTIFICATE', got '%s'", i, block.Type)
return
}
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
retErr = fmt.Errorf("parse certificate %d: %w", i, err)
return
}
// https://www.amd.com/content/dam/amd/en/documents/epyc-technical-docs/specifications/57230.pdf
// Table 6 and 7
switch cert.Subject.CommonName {
case "SEV-Milan":
ask = cert
case "ARK-Milan":
ark = cert
default:
retErr = fmt.Errorf("parse certificate %d: unexpected subject CN %s", i, cert.Subject.CommonName)
return
}
i++
}
switch {
case i == 1:
retErr = fmt.Errorf("no PEM blocks found")
case len(rest) != 0:
retErr = fmt.Errorf("remaining PEM block is not a valid certificate: %s", rest)
}
return
}
// ParseVCEK parses the VCEK certificate from the instanceInfo into an x509-formatted certificate.
// If the VCEK certificate is not present, nil is returned.
func (a *InstanceInfo) ParseVCEK() (*x509.Certificate, error) {
newlinesTrimmed := bytes.TrimSpace(a.VCEK)
if len(newlinesTrimmed) == 0 {
// VCEK is not present.
return nil, nil
}
block, rest := pem.Decode(newlinesTrimmed)
if block == nil {
return nil, fmt.Errorf("no PEM blocks found")
}
if len(rest) != 0 {
return nil, fmt.Errorf("received more data than expected")
}
if block.Type != "CERTIFICATE" {
return nil, fmt.Errorf("expected PEM block type 'CERTIFICATE', got '%s'", block.Type)
}
vcek, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return nil, fmt.Errorf("parsing VCEK certificate: %w", err)
}
return vcek, nil
}

View File

@ -0,0 +1,315 @@
/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
package snp
import (
"crypto/x509"
"encoding/hex"
"fmt"
"regexp"
"strings"
"testing"
"github.com/edgelesssys/constellation/v2/internal/attestation/snp/testdata"
"github.com/edgelesssys/constellation/v2/internal/config"
"github.com/edgelesssys/constellation/v2/internal/logger"
"github.com/google/go-sev-guest/kds"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// TestParseCertChain tests the parsing of the certificate chain.
func TestParseCertChain(t *testing.T) {
defaultCertChain := testdata.CertChain
askOnly := strings.Split(string(defaultCertChain), "-----END CERTIFICATE-----")[0] + "-----END CERTIFICATE-----"
arkOnly := strings.Split(string(defaultCertChain), "-----END CERTIFICATE-----")[1] + "-----END CERTIFICATE-----"
testCases := map[string]struct {
certChain []byte
wantAsk bool
wantArk bool
wantErr bool
}{
"success": {
certChain: defaultCertChain,
wantAsk: true,
wantArk: true,
},
"empty cert chain": {
certChain: []byte{},
wantErr: true,
},
"more than two certificates": {
certChain: append(defaultCertChain, defaultCertChain...),
wantErr: true,
},
"invalid certificate": {
certChain: []byte("invalid"),
wantErr: true,
},
"ark missing": {
certChain: []byte(askOnly),
wantAsk: true,
},
"ask missing": {
certChain: []byte(arkOnly),
wantArk: true,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
instanceInfo := &InstanceInfo{
CertChain: tc.certChain,
}
ask, ark, err := instanceInfo.ParseCertChain()
if tc.wantErr {
assert.Error(err)
} else {
assert.NoError(err)
assert.Equal(tc.wantAsk, ask != nil)
assert.Equal(tc.wantArk, ark != nil)
}
})
}
}
// TestParseVCEK tests the parsing of the VCEK certificate.
func TestParseVCEK(t *testing.T) {
testCases := map[string]struct {
VCEK []byte
wantVCEK bool
wantErr bool
}{
"success": {
VCEK: testdata.AzureThimVCEK,
wantVCEK: true,
},
"empty": {
VCEK: []byte{},
},
"malformed": {
VCEK: testdata.AzureThimVCEK[:len(testdata.AzureThimVCEK)-100],
wantErr: true,
},
"invalid": {
VCEK: []byte("invalid"),
wantErr: true,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
instanceInfo := &InstanceInfo{
VCEK: tc.VCEK,
}
vcek, err := instanceInfo.ParseVCEK()
if tc.wantErr {
assert.Error(err)
} else {
assert.NoError(err)
assert.Equal(tc.wantVCEK, vcek != nil)
}
})
}
}
// TestInstanceInfoAttestation tests the basic unmarshalling of the attestation report and the ASK / ARK precedence.
func TestInstanceInfoAttestation(t *testing.T) {
defaultReport := testdata.AttestationReport
testdataArk, testdataAsk := mustCertChainToPem(t, testdata.CertChain)
exampleCert := &x509.Certificate{
Raw: []byte{1, 2, 3},
}
cfg := config.DefaultForAzureSEVSNP()
testCases := map[string]struct {
report []byte
vcek []byte
certChain []byte
fallbackCerts CertificateChain
getter *stubHTTPSGetter
expectedArk *x509.Certificate
expectedAsk *x509.Certificate
wantErr bool
}{
"success": {
report: defaultReport,
vcek: testdata.AzureThimVCEK,
certChain: testdata.CertChain,
expectedArk: testdataArk,
expectedAsk: testdataAsk,
},
"retrieve vcek": {
report: defaultReport,
certChain: testdata.CertChain,
getter: newStubHTTPSGetter(
&urlResponseMatcher{
vcekResponse: testdata.AmdKdsVCEK,
wantVcekRequest: true,
},
nil,
),
expectedArk: testdataArk,
expectedAsk: testdataAsk,
},
"retrieve certchain": {
report: defaultReport,
vcek: testdata.AzureThimVCEK,
getter: newStubHTTPSGetter(
&urlResponseMatcher{
certChainResponse: testdata.CertChain,
wantCertChainRequest: true,
},
nil,
),
expectedArk: testdataArk,
expectedAsk: testdataAsk,
},
"use fallback certs": {
report: defaultReport,
vcek: testdata.AzureThimVCEK,
fallbackCerts: NewCertificateChain(exampleCert, exampleCert),
getter: newStubHTTPSGetter(
&urlResponseMatcher{},
nil,
),
expectedArk: exampleCert,
expectedAsk: exampleCert,
},
"use certchain with fallback certs": {
report: defaultReport,
certChain: testdata.CertChain,
vcek: testdata.AzureThimVCEK,
fallbackCerts: NewCertificateChain(&x509.Certificate{}, &x509.Certificate{}),
getter: newStubHTTPSGetter(
&urlResponseMatcher{},
nil,
),
expectedArk: testdataArk,
expectedAsk: testdataAsk,
},
"retrieve vcek and certchain": {
report: defaultReport,
getter: newStubHTTPSGetter(
&urlResponseMatcher{
certChainResponse: testdata.CertChain,
vcekResponse: testdata.AmdKdsVCEK,
wantCertChainRequest: true,
wantVcekRequest: true,
},
nil,
),
expectedArk: testdataArk,
expectedAsk: testdataAsk,
},
"report too short": {
report: defaultReport[:len(defaultReport)-100],
wantErr: true,
},
"corrupted report": {
report: defaultReport[10 : len(defaultReport)-10],
wantErr: true,
},
"certificate fetch error": {
report: defaultReport,
getter: newStubHTTPSGetter(nil, assert.AnError),
wantErr: true,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
require := require.New(t)
instanceInfo := InstanceInfo{
AttestationReport: tc.report,
CertChain: tc.certChain,
VCEK: tc.vcek,
}
att, err := instanceInfo.AttestationWithCerts(logger.NewTest(t), tc.getter, tc.fallbackCerts)
if tc.wantErr {
assert.Error(err)
} else {
require.NoError(err)
assert.NotNil(att)
assert.NotNil(att.CertificateChain)
assert.NotNil(att.Report)
assert.Equal(hex.EncodeToString(att.Report.IdKeyDigest[:]), "57e229e0ffe5fa92d0faddff6cae0e61c926fc9ef9afd20a8b8cfcf7129db9338cbe5bf3f6987733a2bf65d06dc38fc1")
// This is a canary for us: If this fails in the future we possibly downgraded a SVN.
// See https://github.com/google/go-sev-guest/blob/14ac50e9ffcc05cd1d12247b710c65093beedb58/validate/validate.go#L336 for decomposition of the values.
tcbValues := kds.DecomposeTCBVersion(kds.TCBVersion(att.Report.GetLaunchTcb()))
assert.True(tcbValues.BlSpl >= cfg.BootloaderVersion.Value)
assert.True(tcbValues.TeeSpl >= cfg.TEEVersion.Value)
assert.True(tcbValues.SnpSpl >= cfg.SNPVersion.Value)
assert.True(tcbValues.UcodeSpl >= cfg.MicrocodeVersion.Value)
assert.Equal(tc.expectedArk.Raw, att.CertificateChain.ArkCert)
assert.Equal(tc.expectedAsk.Raw, att.CertificateChain.AskCert)
}
})
}
}
func mustCertChainToPem(t *testing.T, certchain []byte) (ark, ask *x509.Certificate) {
t.Helper()
a := InstanceInfo{CertChain: certchain}
ask, ark, err := a.ParseCertChain()
require.NoError(t, err)
return ark, ask
}
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)
}
}

View File

@ -10,6 +10,6 @@ go_library(
"runtimedata.bin",
"vcek.pem",
],
importpath = "github.com/edgelesssys/constellation/v2/internal/attestation/azure/snp/testdata",
importpath = "github.com/edgelesssys/constellation/v2/internal/attestation/snp/testdata",
visibility = ["//:__subpackages__"],
)