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

@ -1,15 +0,0 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library")
go_library(
name = "testdata",
srcs = ["testdata.go"],
embedsrcs = [
"attestation.bin",
"vcek.cert",
"certchain.pem",
"runtimedata.bin",
"vcek.pem",
],
importpath = "github.com/edgelesssys/constellation/v2/internal/attestation/azure/snp/testdata",
visibility = ["//:__subpackages__"],
)

View file

@ -1,74 +0,0 @@
-----BEGIN CERTIFICATE-----
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
-----END CERTIFICATE-----
-----BEGIN CERTIFICATE-----
MIIGYzCCBBKgAwIBAgIDAQAAMEYGCSqGSIb3DQEBCjA5oA8wDQYJYIZIAWUDBAIC
BQChHDAaBgkqhkiG9w0BAQgwDQYJYIZIAWUDBAICBQCiAwIBMKMDAgEBMHsxFDAS
BgNVBAsMC0VuZ2luZWVyaW5nMQswCQYDVQQGEwJVUzEUMBIGA1UEBwwLU2FudGEg
Q2xhcmExCzAJBgNVBAgMAkNBMR8wHQYDVQQKDBZBZHZhbmNlZCBNaWNybyBEZXZp
Y2VzMRIwEAYDVQQDDAlBUkstTWlsYW4wHhcNMjAxMDIyMTcyMzA1WhcNNDUxMDIy
MTcyMzA1WjB7MRQwEgYDVQQLDAtFbmdpbmVlcmluZzELMAkGA1UEBhMCVVMxFDAS
BgNVBAcMC1NhbnRhIENsYXJhMQswCQYDVQQIDAJDQTEfMB0GA1UECgwWQWR2YW5j
ZWQgTWljcm8gRGV2aWNlczESMBAGA1UEAwwJQVJLLU1pbGFuMIICIjANBgkqhkiG
9w0BAQEFAAOCAg8AMIICCgKCAgEA0Ld52RJOdeiJlqK2JdsVmD7FktuotWwX1fNg
W41XY9Xz1HEhSUmhLz9Cu9DHRlvgJSNxbeYYsnJfvyjx1MfU0V5tkKiU1EesNFta
1kTA0szNisdYc9isqk7mXT5+KfGRbfc4V/9zRIcE8jlHN61S1ju8X93+6dxDUrG2
SzxqJ4BhqyYmUDruPXJSX4vUc01P7j98MpqOS95rORdGHeI52Naz5m2B+O+vjsC0
60d37jY9LFeuOP4Meri8qgfi2S5kKqg/aF6aPtuAZQVR7u3KFYXP59XmJgtcog05
gmI0T/OitLhuzVvpZcLph0odh/1IPXqx3+MnjD97A7fXpqGd/y8KxX7jksTEzAOg
bKAeam3lm+3yKIcTYMlsRMXPcjNbIvmsBykD//xSniusuHBkgnlENEWx1UcbQQrs
+gVDkuVPhsnzIRNgYvM48Y+7LGiJYnrmE8xcrexekBxrva2V9TJQqnN3Q53kt5vi
Qi3+gCfmkwC0F0tirIZbLkXPrPwzZ0M9eNxhIySb2npJfgnqz55I0u33wh4r0ZNQ
eTGfw03MBUtyuzGesGkcw+loqMaq1qR4tjGbPYxCvpCq7+OgpCCoMNit2uLo9M18
fHz10lOMT8nWAUvRZFzteXCm+7PHdYPlmQwUw3LvenJ/ILXoQPHfbkH0CyPfhl1j
WhJFZasCAwEAAaN+MHwwDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBSFrBrRQ/fI
rFXUxR1BSKvVeErUUzAPBgNVHRMBAf8EBTADAQH/MDoGA1UdHwQzMDEwL6AtoCuG
KWh0dHBzOi8va2RzaW50Zi5hbWQuY29tL3ZjZWsvdjEvTWlsYW4vY3JsMEYGCSqG
SIb3DQEBCjA5oA8wDQYJYIZIAWUDBAICBQChHDAaBgkqhkiG9w0BAQgwDQYJYIZI
AWUDBAICBQCiAwIBMKMDAgEBA4ICAQC6m0kDp6zv4Ojfgy+zleehsx6ol0ocgVel
ETobpx+EuCsqVFRPK1jZ1sp/lyd9+0fQ0r66n7kagRk4Ca39g66WGTJMeJdqYriw
STjjDCKVPSesWXYPVAyDhmP5n2v+BYipZWhpvqpaiO+EGK5IBP+578QeW/sSokrK
dHaLAxG2LhZxj9aF73fqC7OAJZ5aPonw4RE299FVarh1Tx2eT3wSgkDgutCTB1Yq
zT5DuwvAe+co2CIVIzMDamYuSFjPN0BCgojl7V+bTou7dMsqIu/TW/rPCX9/EUcp
KGKqPQ3P+N9r1hjEFY1plBg93t53OOo49GNI+V1zvXPLI6xIFVsh+mto2RtgEX/e
pmMKTNN6psW88qg7c1hTWtN6MbRuQ0vm+O+/2tKBF2h8THb94OvvHHoFDpbCELlq
HnIYhxy0YKXGyaW1NjfULxrrmxVW4wcn5E8GddmvNa6yYm8scJagEi13mhGu4Jqh
3QU3sf8iUSUr09xQDwHtOQUVIqx4maBZPBtSMf+qUDtjXSSq8lfWcd8bLr9mdsUn
JZJ0+tuPMKmBnSH860llKk+VpVQsgqbzDIvOLvD6W1Umq25boxCYJ+TuBoa4s+HH
CViAvgT9kf/rBq1d+ivj6skkHxuzcxbk1xv6ZGxrteJxVH7KlX7YRdZ6eARKwLe4
AFZEAwoKCQ==
-----END CERTIFICATE-----

View file

@ -1 +0,0 @@
{"keys":[{"kid":"HCLAkPub","key_ops":["encrypt"],"kty":"RSA","e":"AQAB","n":"tyFqvAAAf2GFelkW7VcRhJnA2YvY6LjBzeUN2vaMZnZth_tFnWMkK5AXtusyCNel3uip3VGZzTaz5X2tGVjGrs-MVHcap9QdwqUXVW3g9OQ_tEbixcx7-xblUJQkGEQfnbbSdnPI2lvLzOs1Z_0vjeDAxvSQrmags6nY-cJAWH-pgDVJyHtpsUSs_QBWklazDso5WHmnMt9s9MuilWXox0RSyXnUeaQyhY1ju7RTScRnVXuNy67z_EJnwM92drwFb8AUd0SJ_9ohvEYl4aZRDECGo0VrjcSHU-JGJeuWC5VhDBR5EOoCVBBgqe9e83v_lJxI3WLe2ovSIZIIzAmbSQ"}],"vm-configuration":{"console-enabled":true,"current-time":1661455391,"secure-boot":false,"tpm-enabled":true,"vmUniqueId":"B6C98C3B-4EC7-4DA6-BD2F-7D98D20D7B75"}}

View file

@ -1,35 +0,0 @@
/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
// Package testdata contains testing data for an attestation process.
package testdata
import _ "embed"
// AttestationBytes is an example attestation report from a Constellation VM.
//
//go:embed attestation.bin
var AttestationReport []byte
// AzureThimVCEK is an example VCEK certificate (PEM, as returned from Azure THIM) for the AttestationReport.
//
//go:embed vcek.pem
var AzureThimVCEK []byte
// AmdKdsVCEK is an example VCEK certificate (DER, as returned from AMD KDS) for the AttestationReport.
//
//go:embed vcek.cert
var AmdKdsVCEK []byte
// RuntimeData is an example runtime data from the TPM for the AttestationReport.
//
//go:embed runtimedata.bin
var RuntimeData []byte
// CertChain is a valid certificate chain (PEM, as returned from Azure THIM) for the VCEK certificate.
//
//go:embed certchain.pem
var CertChain []byte

Binary file not shown.

View file

@ -1,31 +0,0 @@
-----BEGIN CERTIFICATE-----
MIIFTDCCAvugAwIBAgIBADBGBgkqhkiG9w0BAQowOaAPMA0GCWCGSAFlAwQCAgUA
oRwwGgYJKoZIhvcNAQEIMA0GCWCGSAFlAwQCAgUAogMCATCjAwIBATB7MRQwEgYD
VQQLDAtFbmdpbmVlcmluZzELMAkGA1UEBhMCVVMxFDASBgNVBAcMC1NhbnRhIENs
YXJhMQswCQYDVQQIDAJDQTEfMB0GA1UECgwWQWR2YW5jZWQgTWljcm8gRGV2aWNl
czESMBAGA1UEAwwJU0VWLU1pbGFuMB4XDTIzMDgzMDEyMTUyNFoXDTMwMDgzMDEy
MTUyNFowejEUMBIGA1UECwwLRW5naW5lZXJpbmcxCzAJBgNVBAYTAlVTMRQwEgYD
VQQHDAtTYW50YSBDbGFyYTELMAkGA1UECAwCQ0ExHzAdBgNVBAoMFkFkdmFuY2Vk
IE1pY3JvIERldmljZXMxETAPBgNVBAMMCFNFVi1WQ0VLMHYwEAYHKoZIzj0CAQYF
K4EEACIDYgAEhPX8Cl9uA7PxqNGzeqamJNYJLx/VFE/s3+8qOWtaztKNcn1PaAI4
ndE+yaVfMHsiA8CLTylumpWXcVBHPYV9kPEVrtozhvrrT5Oii9OpZPYHJ7/WPVmM
J3K8/Iz3AshTo4IBFjCCARIwEAYJKwYBBAGceAEBBAMCAQAwFwYJKwYBBAGceAEC
BAoWCE1pbGFuLUIwMBEGCisGAQQBnHgBAwEEAwIBAjARBgorBgEEAZx4AQMCBAMC
AQAwEQYKKwYBBAGceAEDBAQDAgEAMBEGCisGAQQBnHgBAwUEAwIBADARBgorBgEE
AZx4AQMGBAMCAQAwEQYKKwYBBAGceAEDBwQDAgEAMBEGCisGAQQBnHgBAwMEAwIB
BjARBgorBgEEAZx4AQMIBAMCAV0wTQYJKwYBBAGceAEEBECeRKrvAs/Kb926ymac
bP0p4auNl+vJOYVxKKy7E7h0DfMUNtNOhuX4rgzf6zoOGF20beysF2zHfXYcIqG5
3PJbMEYGCSqGSIb3DQEBCjA5oA8wDQYJYIZIAWUDBAICBQChHDAaBgkqhkiG9w0B
AQgwDQYJYIZIAWUDBAICBQCiAwIBMKMDAgEBA4ICAQBoVGgDdFV9gWPHaEOBrHzd
WVYyuuMBH340DDSXbCGlPR6rhgja0qALmkUPG50REQGvoPsikAskwqhzRG2XEDO2
b6+fRPIq3DjEbz/8V89IiYiOZI/ycFACi3EEVECAWbzjXSfiOio1NfbniXP6tWzW
D/8xpd/8N8166bHpgNgMl9pX4i0I9vaTl3qH+jBuSMZ5Q4heTHLB+v4V7q+H6SZo
7htqpaI3keLEhQL/pCP72udMPAzU+/5W/x/t/LD6SbQcQQoHbWDU6kgTDuXabDxl
A4JoEZfatr+/TO6jKQcGtqOLKT8JFGcigUlBi/TBVP+Xs8E4CWYGZZiTpYoLwNAu
yuKOP9VVFViSCqPvzpNs2G+e0zXg2w3te7oMw/l0bD8iQCAS8rR0+r+8pZL4e010
KLZ3yEfA0moXef66k5xyf4y37ZIP189wz6qJ+YXqOujDmeTomCU0SnZXlri6GhbF
19rp2z5/lsZG+W27CRxvzTB3hk+ukZr35vCqNq4Rs+c7/hYcYzzyZ4ysATwdglNF
WddfVw5Qunlu6Ngxr84ifz3HrnUx9bR5DzmFbztrb7IbkZhq7GjImwJULub1viyg
YFa7X3p8b1WllienSEfvbadobbS9HeuLUrWyh0kZjQnz+0Q1UB1/zlzokeQmAYCf
8H3kABPv6hqrFftRNbargQ==
-----END CERTIFICATE-----

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)
}