mirror of
https://github.com/edgelesssys/constellation.git
synced 2025-01-23 22:01:14 -05:00
attestation: add snp package
The package holds code shared between SNP-based attestation implementations on AWS and Azure .
This commit is contained in:
parent
635a5d2c0a
commit
5ce55e3449
@ -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",
|
||||
|
@ -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,
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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.
|
||||
|
@ -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)
|
||||
}
|
||||
|
||||
|
31
internal/attestation/snp/BUILD.bazel
Normal file
31
internal/attestation/snp/BUILD.bazel
Normal 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",
|
||||
],
|
||||
)
|
217
internal/attestation/snp/snp.go
Normal file
217
internal/attestation/snp/snp.go
Normal 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
|
||||
}
|
315
internal/attestation/snp/snp_test.go
Normal file
315
internal/attestation/snp/snp_test.go
Normal 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)
|
||||
}
|
||||
}
|
@ -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__"],
|
||||
)
|
Loading…
Reference in New Issue
Block a user