verify: query vlek ASK from KDS if not set

The user can choose to supply an intermediate
certificate through the config, like they can
for the root key. If none is supplied,
the KDS is queried for a valid ASK.
This commit is contained in:
Otto Bittner 2023-11-07 12:17:08 +01:00
parent 07eed0e319
commit 84d8bd8110
8 changed files with 132 additions and 102 deletions

View File

@ -105,8 +105,8 @@ func runVerify(cmd *cobra.Command, _ []string) error {
log: log, log: log,
} }
formatterFactory := func(output string, provider cloudprovider.Provider, log debugLog) (attestationDocFormatter, error) { formatterFactory := func(output string, provider cloudprovider.Provider, log debugLog) (attestationDocFormatter, error) {
if output == "json" && provider != cloudprovider.Azure { if output == "json" && (provider != cloudprovider.Azure && provider != cloudprovider.AWS) {
return nil, errors.New("json output is only supported for Azure") return nil, errors.New("json output is only supported for Azure and AWS")
} }
switch output { switch output {
case "json": case "json":
@ -206,8 +206,7 @@ func (c *verifyCmd) verify(cmd *cobra.Command, verifyClient verifyClient, factor
cmd.Context(), cmd.Context(),
rawAttestationDoc, rawAttestationDoc,
(conf.Provider.Azure == nil && conf.Provider.AWS == nil), (conf.Provider.Azure == nil && conf.Provider.AWS == nil),
attConfig.GetMeasurements(), attConfig,
maaURL,
) )
if err != nil { if err != nil {
return fmt.Errorf("printing attestation document: %w", err) return fmt.Errorf("printing attestation document: %w", err)
@ -254,8 +253,7 @@ func (c *verifyCmd) validateEndpointFlag(cmd *cobra.Command, stateFile *state.St
// an attestationDocFormatter formats the attestation document. // an attestationDocFormatter formats the attestation document.
type attestationDocFormatter interface { type attestationDocFormatter interface {
// format returns the raw or formatted attestation doc depending on the rawOutput argument. // format returns the raw or formatted attestation doc depending on the rawOutput argument.
format(ctx context.Context, docString string, PCRsOnly bool, expectedPCRs measurements.M, format(ctx context.Context, docString string, PCRsOnly bool, attestationCfg config.AttestationCfg) (string, error)
attestationServiceURL string) (string, error)
} }
type jsonAttestationDocFormatter struct { type jsonAttestationDocFormatter struct {
@ -264,7 +262,7 @@ type jsonAttestationDocFormatter struct {
// format returns the json formatted attestation doc. // format returns the json formatted attestation doc.
func (f *jsonAttestationDocFormatter) format(ctx context.Context, docString string, _ bool, func (f *jsonAttestationDocFormatter) format(ctx context.Context, docString string, _ bool,
_ measurements.M, attestationServiceURL string, attestationCfg config.AttestationCfg,
) (string, error) { ) (string, error) {
var doc attestationDoc var doc attestationDoc
if err := json.Unmarshal([]byte(docString), &doc); err != nil { if err := json.Unmarshal([]byte(docString), &doc); err != nil {
@ -275,7 +273,7 @@ func (f *jsonAttestationDocFormatter) format(ctx context.Context, docString stri
if err != nil { if err != nil {
return "", fmt.Errorf("unmarshalling instance info: %w", err) return "", fmt.Errorf("unmarshalling instance info: %w", err)
} }
report, err := verify.NewReport(ctx, instanceInfo, attestationServiceURL, f.log) report, err := verify.NewReport(ctx, instanceInfo, attestationCfg, f.log)
if err != nil { if err != nil {
return "", fmt.Errorf("parsing SNP report: %w", err) return "", fmt.Errorf("parsing SNP report: %w", err)
} }
@ -291,7 +289,7 @@ type rawAttestationDocFormatter struct {
// format returns the raw attestation doc. // format returns the raw attestation doc.
func (f *rawAttestationDocFormatter) format(_ context.Context, docString string, _ bool, func (f *rawAttestationDocFormatter) format(_ context.Context, docString string, _ bool,
_ measurements.M, _ string, _ config.AttestationCfg,
) (string, error) { ) (string, error) {
b := &strings.Builder{} b := &strings.Builder{}
b.WriteString("Attestation Document:\n") b.WriteString("Attestation Document:\n")
@ -305,7 +303,7 @@ type defaultAttestationDocFormatter struct {
// format returns the formatted attestation doc. // format returns the formatted attestation doc.
func (f *defaultAttestationDocFormatter) format(ctx context.Context, docString string, PCRsOnly bool, func (f *defaultAttestationDocFormatter) format(ctx context.Context, docString string, PCRsOnly bool,
expectedPCRs measurements.M, attestationServiceURL string, attestationCfg config.AttestationCfg,
) (string, error) { ) (string, error) {
b := &strings.Builder{} b := &strings.Builder{}
b.WriteString("Attestation Document:\n") b.WriteString("Attestation Document:\n")
@ -315,7 +313,7 @@ func (f *defaultAttestationDocFormatter) format(ctx context.Context, docString s
return "", fmt.Errorf("unmarshal attestation document: %w", err) return "", fmt.Errorf("unmarshal attestation document: %w", err)
} }
if err := f.parseQuotes(b, doc.Attestation.Quotes, expectedPCRs); err != nil { if err := f.parseQuotes(b, doc.Attestation.Quotes, attestationCfg.GetMeasurements()); err != nil {
return "", fmt.Errorf("parse quote: %w", err) return "", fmt.Errorf("parse quote: %w", err)
} }
if PCRsOnly { if PCRsOnly {
@ -327,7 +325,7 @@ func (f *defaultAttestationDocFormatter) format(ctx context.Context, docString s
return "", fmt.Errorf("unmarshalling instance info: %w", err) return "", fmt.Errorf("unmarshalling instance info: %w", err)
} }
report, err := verify.NewReport(ctx, instanceInfo, attestationServiceURL, f.log) report, err := verify.NewReport(ctx, instanceInfo, attestationCfg, f.log)
if err != nil { if err != nil {
return "", fmt.Errorf("parsing SNP report: %w", err) return "", fmt.Errorf("parsing SNP report: %w", err)
} }

View File

@ -233,7 +233,7 @@ type stubAttDocFormatter struct {
formatErr error formatErr error
} }
func (f *stubAttDocFormatter) format(_ context.Context, _ string, _ bool, _ measurements.M, _ string) (string, error) { func (f *stubAttDocFormatter) format(_ context.Context, _ string, _ bool, _ config.AttestationCfg) (string, error) {
return "", f.formatErr return "", f.formatErr
} }
@ -258,7 +258,7 @@ func TestFormat(t *testing.T) {
for name, tc := range testCases { for name, tc := range testCases {
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
_, err := tc.formatter.format(context.Background(), tc.doc, false, nil, "") _, err := tc.formatter.format(context.Background(), tc.doc, false, nil)
if tc.wantErr { if tc.wantErr {
assert.Error(t, err) assert.Error(t, err)
} else { } else {

View File

@ -22,6 +22,13 @@ import (
"github.com/google/go-sev-guest/verify/trust" "github.com/google/go-sev-guest/verify/trust"
) )
// Product returns the SEV product info currently supported by Constellation's SNP attestation.
func Product() *spb.SevProduct {
// sevProduct is the product info of the SEV platform as reported through CPUID[EAX=1].
// It may become necessary in the future to differentiate among CSP vendors.
return &spb.SevProduct{Name: spb.SevProduct_SEV_PRODUCT_MILAN, Stepping: 0} // Milan-B0
}
// InstanceInfo contains the necessary information to establish trust in a SNP CVM. // InstanceInfo contains the necessary information to establish trust in a SNP CVM.
type InstanceInfo struct { type InstanceInfo struct {
// ReportSigner is the PEM-encoded certificate used to validate the attestation report's signature. // ReportSigner is the PEM-encoded certificate used to validate the attestation report's signature.
@ -99,14 +106,12 @@ func (a *InstanceInfo) AttestationWithCerts(getter trust.HTTPSGetter,
return nil, fmt.Errorf("converting report to proto: %w", err) return nil, fmt.Errorf("converting report to proto: %w", err)
} }
// Product info as reported through CPUID[EAX=1] productName := kds.ProductString(Product())
sevProduct := &spb.SevProduct{Name: spb.SevProduct_SEV_PRODUCT_MILAN, Stepping: 0} // Milan-B0
productName := kds.ProductString(sevProduct)
att := &spb.Attestation{ att := &spb.Attestation{
Report: report, Report: report,
CertificateChain: &spb.CertificateChain{}, CertificateChain: &spb.CertificateChain{},
Product: sevProduct, Product: Product(),
} }
// Add VCEK/VLEK to attestation object. // Add VCEK/VLEK to attestation object.

View File

@ -7,6 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
package config package config
import ( import (
"bytes"
"crypto/x509" "crypto/x509"
"encoding/json" "encoding/json"
"encoding/pem" "encoding/pem"
@ -67,6 +68,11 @@ func unmarshalTypedConfig[T AttestationCfg](data []byte) (AttestationCfg, error)
// Certificate is a wrapper around x509.Certificate allowing custom marshaling. // Certificate is a wrapper around x509.Certificate allowing custom marshaling.
type Certificate x509.Certificate type Certificate x509.Certificate
// Equal returns true if the certificates are equal.
func (c Certificate) Equal(other Certificate) bool {
return bytes.Equal(c.Raw, other.Raw)
}
// MarshalJSON marshals the certificate to PEM. // MarshalJSON marshals the certificate to PEM.
func (c Certificate) MarshalJSON() ([]byte, error) { func (c Certificate) MarshalJSON() ([]byte, error) {
if len(c.Raw) == 0 { if len(c.Raw) == 0 {

View File

@ -1052,7 +1052,7 @@ type AWSSEVSNP struct {
AMDRootKey Certificate `json:"amdRootKey" yaml:"amdRootKey"` AMDRootKey Certificate `json:"amdRootKey" yaml:"amdRootKey"`
// description: | // description: |
// AMD Signing Key certificate used to verify the SEV-SNP VCEK / VLEK certificate. // AMD Signing Key certificate used to verify the SEV-SNP VCEK / VLEK certificate.
AMDSigningKey Certificate `json:"amdSigningKey,omitempty" yaml:"amdSigningKey,omitempty" validate:"len=0"` AMDSigningKey Certificate `json:"amdSigningKey,omitempty" yaml:"amdSigningKey,omitempty"`
} }
// AWSNitroTPM is the configuration for AWS Nitro TPM attestation. // AWSNitroTPM is the configuration for AWS Nitro TPM attestation.

View File

@ -3,19 +3,16 @@ load("//bazel/go:go_test.bzl", "go_test")
go_library( go_library(
name = "verify", name = "verify",
srcs = [ srcs = ["verify.go"],
"certchain.go",
"verify.go",
],
importpath = "github.com/edgelesssys/constellation/v2/internal/verify", importpath = "github.com/edgelesssys/constellation/v2/internal/verify",
visibility = ["//:__subpackages__"], visibility = ["//:__subpackages__"],
deps = [ deps = [
"//internal/attestation/snp", "//internal/attestation/snp",
"//internal/constants", "//internal/config",
"//internal/kubernetes/kubectl",
"@com_github_golang_jwt_jwt_v5//:jwt", "@com_github_golang_jwt_jwt_v5//:jwt",
"@com_github_google_go_sev_guest//abi", "@com_github_google_go_sev_guest//abi",
"@com_github_google_go_sev_guest//kds", "@com_github_google_go_sev_guest//kds",
"@com_github_google_go_sev_guest//verify/trust",
], ],
) )

View File

@ -1,29 +0,0 @@
package verify
import (
"context"
"fmt"
"github.com/edgelesssys/constellation/v2/internal/constants"
"github.com/edgelesssys/constellation/v2/internal/kubernetes/kubectl"
)
func getCertChainCache(ctx context.Context, kubectl *kubectl.Kubectl, log debugLog) ([]byte, error) {
log.Debugf("Retrieving certificate chain from cache")
cm, err := kubectl.GetConfigMap(ctx, constants.ConstellationNamespace, constants.SevSnpCertCacheConfigMapName)
if err != nil {
return nil, fmt.Errorf("getting certificate chain cache configmap: %w", err)
}
var result []byte
ask, ok := cm.Data[constants.CertCacheAskKey]
if ok {
result = append(result, ask...)
}
ark, ok := cm.Data[constants.CertCacheArkKey]
if ok {
result = append(result, ark...)
}
return result, nil
}

View File

@ -26,17 +26,23 @@ import (
"strings" "strings"
"github.com/edgelesssys/constellation/v2/internal/attestation/snp" "github.com/edgelesssys/constellation/v2/internal/attestation/snp"
"github.com/edgelesssys/constellation/v2/internal/constants" "github.com/edgelesssys/constellation/v2/internal/config"
"github.com/edgelesssys/constellation/v2/internal/kubernetes/kubectl"
"github.com/golang-jwt/jwt/v5" "github.com/golang-jwt/jwt/v5"
"github.com/google/go-sev-guest/abi" "github.com/google/go-sev-guest/abi"
"github.com/google/go-sev-guest/kds" "github.com/google/go-sev-guest/kds"
"github.com/google/go-sev-guest/verify/trust"
)
const (
vcekCert = "VCEK certificate"
vlekCert = "VLEK certificate"
certificateChain = "certificate chain"
) )
// Report contains the entire data reported by constellation verify. // Report contains the entire data reported by constellation verify.
type Report struct { type Report struct {
SNPReport SNPReport `json:"snp_report"` SNPReport SNPReport `json:"snp_report"`
ReportSigner []Certificate `json:"vcek"` ReportSigner []Certificate `json:"report_signer"`
CertChain []Certificate `json:"cert_chain"` CertChain []Certificate `json:"cert_chain"`
*AzureReportAddition `json:"azure,omitempty"` *AzureReportAddition `json:"azure,omitempty"`
*AWSReportAddition `json:"aws,omitempty"` *AWSReportAddition `json:"aws,omitempty"`
@ -51,7 +57,7 @@ type AzureReportAddition struct {
type AWSReportAddition struct{} type AWSReportAddition struct{}
// NewReport transforms a snp.InstanceInfo object into a Report. // NewReport transforms a snp.InstanceInfo object into a Report.
func NewReport(ctx context.Context, instanceInfo snp.InstanceInfo, attestationServiceURL string, log debugLog) (Report, error) { func NewReport(ctx context.Context, instanceInfo snp.InstanceInfo, attestationCfg config.AttestationCfg, log debugLog) (Report, error) {
snpReport, err := newSNPReport(instanceInfo.AttestationReport) snpReport, err := newSNPReport(instanceInfo.AttestationReport)
if err != nil { if err != nil {
return Report{}, fmt.Errorf("parsing SNP report: %w", err) return Report{}, fmt.Errorf("parsing SNP report: %w", err)
@ -60,34 +66,28 @@ func NewReport(ctx context.Context, instanceInfo snp.InstanceInfo, attestationSe
var certTypeName string var certTypeName string
switch snpReport.SignerInfo.SigningKey { switch snpReport.SignerInfo.SigningKey {
case abi.VlekReportSigner.String(): case abi.VlekReportSigner.String():
certTypeName = "VLEK certificate" certTypeName = vlekCert
case abi.VcekReportSigner.String(): case abi.VcekReportSigner.String():
certTypeName = "VCEK certificate" certTypeName = vcekCert
default: default:
return Report{}, errors.New("unknown report signer") return Report{}, errors.New("unknown report signer")
} }
reportSigner, err := newCertificates(certTypeName, instanceInfo.ReportSigner, log) reportSigner, err := newCertificates(certTypeName, instanceInfo.ReportSigner, log)
if err != nil { if err != nil {
return Report{}, fmt.Errorf("parsing VCEK certificate: %w", err) return Report{}, fmt.Errorf("parsing %s: %w", certTypeName, err)
} }
// check if issuer included certChain before parsing. If not included, manually collect from the cluster. // check if issuer included certChain before parsing. If not included, manually collect from the cluster.
var pemCerts []byte rawCerts := instanceInfo.CertChain
if instanceInfo.CertChain == nil { if certTypeName == vlekCert {
client, err := kubectl.NewFromConfig(constants.AdminConfFilename) rawCerts, err = getCertChain(attestationCfg)
if err != nil {
return Report{}, fmt.Errorf("creating kubectl client: %w", err)
}
pemCerts, err = getCertChainCache(ctx, client, log)
if err != nil { if err != nil {
return Report{}, fmt.Errorf("getting certificate chain cache: %w", err) return Report{}, fmt.Errorf("getting certificate chain cache: %w", err)
} }
} else {
pemCerts = instanceInfo.CertChain
} }
certChain, err := newCertificates("Certificate chain", pemCerts, log) certChain, err := newCertificates(certificateChain, rawCerts, log)
if err != nil { if err != nil {
return Report{}, fmt.Errorf("parsing certificate chain: %w", err) return Report{}, fmt.Errorf("parsing certificate chain: %w", err)
} }
@ -95,7 +95,11 @@ func NewReport(ctx context.Context, instanceInfo snp.InstanceInfo, attestationSe
var azure *AzureReportAddition var azure *AzureReportAddition
var aws *AWSReportAddition var aws *AWSReportAddition
if instanceInfo.Azure != nil { if instanceInfo.Azure != nil {
maaToken, err := newMAAToken(ctx, instanceInfo.Azure.MAAToken, attestationServiceURL) cfg, ok := attestationCfg.(*config.AzureSEVSNP)
if !ok {
return Report{}, fmt.Errorf("expected config type *config.AzureSEVSNP, got %T", attestationCfg)
}
maaToken, err := newMAAToken(ctx, instanceInfo.Azure.MAAToken, cfg.FirmwareSignerConfig.MAAURL)
if err != nil { if err != nil {
return Report{}, fmt.Errorf("parsing MAA token: %w", err) return Report{}, fmt.Errorf("parsing MAA token: %w", err)
} }
@ -113,6 +117,46 @@ func NewReport(ctx context.Context, instanceInfo snp.InstanceInfo, attestationSe
}, nil }, nil
} }
// inverse of newCertificates.
// ideally, duplicate encoding/decoding would be removed.
// AWS specific.
func getCertChain(cfg config.AttestationCfg) ([]byte, error) {
awsCfg, ok := cfg.(*config.AWSSEVSNP)
if !ok {
return nil, fmt.Errorf("expected config type *config.AWSSEVSNP, got %T", cfg)
}
if awsCfg.AMDRootKey.Equal(config.Certificate{}) {
return nil, errors.New("no AMD root key configured")
}
if awsCfg.AMDSigningKey.Equal(config.Certificate{}) {
certs, err := trust.GetProductChain(kds.ProductString(snp.Product()), abi.VlekReportSigner, trust.DefaultHTTPSGetter())
if err != nil {
return nil, fmt.Errorf("getting product certificate chain: %w", err)
}
// we want an ASVK, but GetProductChain currently does not use the ASVK field.
if certs.Ask == nil {
return nil, errors.New("no ASVK certificate available")
}
awsCfg.AMDSigningKey = config.Certificate(*certs.Ask)
}
// ARK
certChain := pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE",
Bytes: awsCfg.AMDRootKey.Raw,
})
// append ASK
certChain = append(certChain, pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE",
Bytes: awsCfg.AMDSigningKey.Raw,
})...)
return certChain, nil
}
// FormatString builds a string representation of a report that is inteded for console output. // FormatString builds a string representation of a report that is inteded for console output.
func (r *Report) FormatString(b *strings.Builder) (string, error) { func (r *Report) FormatString(b *strings.Builder) (string, error) {
if len(r.ReportSigner) != 1 { if len(r.ReportSigner) != 1 {
@ -163,7 +207,8 @@ type Certificate struct {
CertTypeName string `json:"cert_type_name"` CertTypeName string `json:"cert_type_name"`
StructVersion uint8 `json:"struct_version"` StructVersion uint8 `json:"struct_version"`
ProductName string `json:"product_name"` ProductName string `json:"product_name"`
HardwareID []byte `json:"hardware_id"` HardwareID []byte `json:"hardware_id,omitempty"`
CspID string `json:"csp_id,omitempty"`
TCBVersion TCBVersion `json:"tcb_version"` TCBVersion TCBVersion `json:"tcb_version"`
} }
@ -181,39 +226,47 @@ func newCertificates(certTypeName string, cert []byte, log debugLog) (certs []Ce
return certs, fmt.Errorf("parse %s: expected PEM block type 'CERTIFICATE', got '%s'", certTypeName, block.Type) return certs, fmt.Errorf("parse %s: expected PEM block type 'CERTIFICATE', got '%s'", certTypeName, block.Type)
} }
cert, err := x509.ParseCertificate(block.Bytes) certX509, err := x509.ParseCertificate(block.Bytes)
if err != nil { if err != nil {
return certs, fmt.Errorf("parse %s: %w", certTypeName, err) return certs, fmt.Errorf("parse %s: %w", certTypeName, err)
} }
if certTypeName == "VCEK certificate" {
vcekExts, err := kds.VcekCertificateExtensions(cert) var ext *kds.Extensions
switch certTypeName {
case vcekCert:
ext, err = kds.VcekCertificateExtensions(certX509)
if err != nil { if err != nil {
return certs, fmt.Errorf("parsing VCEK certificate extensions: %w", err) return certs, fmt.Errorf("parsing %s extensions: %w", certTypeName, err)
}
case vlekCert:
ext, err = kds.VlekCertificateExtensions(certX509)
if err != nil {
return certs, fmt.Errorf("parsing %s extensions: %w", certTypeName, err)
}
} }
certPEM := pem.EncodeToMemory(&pem.Block{ certPEM := pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE", Type: "CERTIFICATE",
Bytes: cert.Raw, Bytes: certX509.Raw,
}) })
certs = append(certs, Certificate{ cert := Certificate{
Certificate: *cert, Certificate: *certX509,
CertificatePEM: string(certPEM), CertificatePEM: string(certPEM),
CertTypeName: certTypeName, CertTypeName: certTypeName,
StructVersion: vcekExts.StructVersion, }
ProductName: vcekExts.ProductName,
TCBVersion: newTCBVersion(vcekExts.TCBVersion), if ext != nil {
HardwareID: vcekExts.HWID, cert.StructVersion = ext.StructVersion
}) cert.ProductName = ext.ProductName
cert.TCBVersion = newTCBVersion(ext.TCBVersion)
if ext.HWID != nil {
cert.HardwareID = ext.HWID
} else { } else {
certPEM := pem.EncodeToMemory(&pem.Block{ cert.CspID = ext.CspID
Type: "CERTIFICATE",
Bytes: cert.Raw,
})
certs = append(certs, Certificate{
Certificate: *cert,
CertificatePEM: string(certPEM),
CertTypeName: certTypeName,
})
} }
}
certs = append(certs, cert)
i++ i++
} }
if i == 1 { if i == 1 {
@ -236,12 +289,12 @@ func (c *Certificate) formatString(b *strings.Builder, idx int) error {
writeIndentfln(b, 2, "Signature Algorithm: %s", c.Certificate.SignatureAlgorithm) writeIndentfln(b, 2, "Signature Algorithm: %s", c.Certificate.SignatureAlgorithm)
writeIndentfln(b, 2, "Public Key Algorithm: %s", c.Certificate.PublicKeyAlgorithm) writeIndentfln(b, 2, "Public Key Algorithm: %s", c.Certificate.PublicKeyAlgorithm)
if c.CertTypeName == "VCEK certificate" { if c.CertTypeName == vcekCert {
// Extensions documented in Table 8 and Table 9 of // Extensions documented in Table 8 and Table 9 of
// https://www.amd.com/system/files/TechDocs/57230.pdf // https://www.amd.com/system/files/TechDocs/57230.pdf
vcekExts, err := kds.VcekCertificateExtensions(&c.Certificate) vcekExts, err := kds.VcekCertificateExtensions(&c.Certificate)
if err != nil { if err != nil {
return fmt.Errorf("parsing VCEK certificate extensions: %w", err) return fmt.Errorf("parsing %s extensions: %w", c.CertTypeName, err)
} }
writeIndentfln(b, 2, "Struct version: %d", vcekExts.StructVersion) writeIndentfln(b, 2, "Struct version: %d", vcekExts.StructVersion)