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,
}
formatterFactory := func(output string, provider cloudprovider.Provider, log debugLog) (attestationDocFormatter, error) {
if output == "json" && provider != cloudprovider.Azure {
return nil, errors.New("json output is only supported for Azure")
if output == "json" && (provider != cloudprovider.Azure && provider != cloudprovider.AWS) {
return nil, errors.New("json output is only supported for Azure and AWS")
}
switch output {
case "json":
@ -206,8 +206,7 @@ func (c *verifyCmd) verify(cmd *cobra.Command, verifyClient verifyClient, factor
cmd.Context(),
rawAttestationDoc,
(conf.Provider.Azure == nil && conf.Provider.AWS == nil),
attConfig.GetMeasurements(),
maaURL,
attConfig,
)
if err != nil {
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.
type attestationDocFormatter interface {
// format returns the raw or formatted attestation doc depending on the rawOutput argument.
format(ctx context.Context, docString string, PCRsOnly bool, expectedPCRs measurements.M,
attestationServiceURL string) (string, error)
format(ctx context.Context, docString string, PCRsOnly bool, attestationCfg config.AttestationCfg) (string, error)
}
type jsonAttestationDocFormatter struct {
@ -264,7 +262,7 @@ type jsonAttestationDocFormatter struct {
// format returns the json formatted attestation doc.
func (f *jsonAttestationDocFormatter) format(ctx context.Context, docString string, _ bool,
_ measurements.M, attestationServiceURL string,
attestationCfg config.AttestationCfg,
) (string, error) {
var doc attestationDoc
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 {
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 {
return "", fmt.Errorf("parsing SNP report: %w", err)
}
@ -291,7 +289,7 @@ type rawAttestationDocFormatter struct {
// format returns the raw attestation doc.
func (f *rawAttestationDocFormatter) format(_ context.Context, docString string, _ bool,
_ measurements.M, _ string,
_ config.AttestationCfg,
) (string, error) {
b := &strings.Builder{}
b.WriteString("Attestation Document:\n")
@ -305,7 +303,7 @@ type defaultAttestationDocFormatter struct {
// format returns the formatted attestation doc.
func (f *defaultAttestationDocFormatter) format(ctx context.Context, docString string, PCRsOnly bool,
expectedPCRs measurements.M, attestationServiceURL string,
attestationCfg config.AttestationCfg,
) (string, error) {
b := &strings.Builder{}
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)
}
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)
}
if PCRsOnly {
@ -327,7 +325,7 @@ func (f *defaultAttestationDocFormatter) format(ctx context.Context, docString s
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 {
return "", fmt.Errorf("parsing SNP report: %w", err)
}

View File

@ -233,7 +233,7 @@ type stubAttDocFormatter struct {
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
}
@ -258,7 +258,7 @@ func TestFormat(t *testing.T) {
for name, tc := range testCases {
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 {
assert.Error(t, err)
} else {

View File

@ -22,6 +22,13 @@ import (
"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.
type InstanceInfo struct {
// 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)
}
// 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)
productName := kds.ProductString(Product())
att := &spb.Attestation{
Report: report,
CertificateChain: &spb.CertificateChain{},
Product: sevProduct,
Product: Product(),
}
// Add VCEK/VLEK to attestation object.

View File

@ -7,6 +7,7 @@ SPDX-License-Identifier: AGPL-3.0-only
package config
import (
"bytes"
"crypto/x509"
"encoding/json"
"encoding/pem"
@ -67,6 +68,11 @@ func unmarshalTypedConfig[T AttestationCfg](data []byte) (AttestationCfg, error)
// Certificate is a wrapper around x509.Certificate allowing custom marshaling.
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.
func (c Certificate) MarshalJSON() ([]byte, error) {
if len(c.Raw) == 0 {

View File

@ -1052,7 +1052,7 @@ type AWSSEVSNP struct {
AMDRootKey Certificate `json:"amdRootKey" yaml:"amdRootKey"`
// description: |
// 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.

View File

@ -3,19 +3,16 @@ load("//bazel/go:go_test.bzl", "go_test")
go_library(
name = "verify",
srcs = [
"certchain.go",
"verify.go",
],
srcs = ["verify.go"],
importpath = "github.com/edgelesssys/constellation/v2/internal/verify",
visibility = ["//:__subpackages__"],
deps = [
"//internal/attestation/snp",
"//internal/constants",
"//internal/kubernetes/kubectl",
"//internal/config",
"@com_github_golang_jwt_jwt_v5//:jwt",
"@com_github_google_go_sev_guest//abi",
"@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"
"github.com/edgelesssys/constellation/v2/internal/attestation/snp"
"github.com/edgelesssys/constellation/v2/internal/constants"
"github.com/edgelesssys/constellation/v2/internal/kubernetes/kubectl"
"github.com/edgelesssys/constellation/v2/internal/config"
"github.com/golang-jwt/jwt/v5"
"github.com/google/go-sev-guest/abi"
"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.
type Report struct {
SNPReport SNPReport `json:"snp_report"`
ReportSigner []Certificate `json:"vcek"`
ReportSigner []Certificate `json:"report_signer"`
CertChain []Certificate `json:"cert_chain"`
*AzureReportAddition `json:"azure,omitempty"`
*AWSReportAddition `json:"aws,omitempty"`
@ -51,7 +57,7 @@ type AzureReportAddition struct {
type AWSReportAddition struct{}
// 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)
if err != nil {
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
switch snpReport.SignerInfo.SigningKey {
case abi.VlekReportSigner.String():
certTypeName = "VLEK certificate"
certTypeName = vlekCert
case abi.VcekReportSigner.String():
certTypeName = "VCEK certificate"
certTypeName = vcekCert
default:
return Report{}, errors.New("unknown report signer")
}
reportSigner, err := newCertificates(certTypeName, instanceInfo.ReportSigner, log)
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.
var pemCerts []byte
if instanceInfo.CertChain == nil {
client, err := kubectl.NewFromConfig(constants.AdminConfFilename)
if err != nil {
return Report{}, fmt.Errorf("creating kubectl client: %w", err)
}
pemCerts, err = getCertChainCache(ctx, client, log)
rawCerts := instanceInfo.CertChain
if certTypeName == vlekCert {
rawCerts, err = getCertChain(attestationCfg)
if err != nil {
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 {
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 aws *AWSReportAddition
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 {
return Report{}, fmt.Errorf("parsing MAA token: %w", err)
}
@ -113,6 +117,46 @@ func NewReport(ctx context.Context, instanceInfo snp.InstanceInfo, attestationSe
}, 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.
func (r *Report) FormatString(b *strings.Builder) (string, error) {
if len(r.ReportSigner) != 1 {
@ -163,7 +207,8 @@ type Certificate struct {
CertTypeName string `json:"cert_type_name"`
StructVersion uint8 `json:"struct_version"`
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"`
}
@ -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)
}
cert, err := x509.ParseCertificate(block.Bytes)
certX509, err := x509.ParseCertificate(block.Bytes)
if err != nil {
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 {
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{
Type: "CERTIFICATE",
Bytes: cert.Raw,
})
certs = append(certs, Certificate{
Certificate: *cert,
CertificatePEM: string(certPEM),
CertTypeName: certTypeName,
StructVersion: vcekExts.StructVersion,
ProductName: vcekExts.ProductName,
TCBVersion: newTCBVersion(vcekExts.TCBVersion),
HardwareID: vcekExts.HWID,
})
} else {
certPEM := pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE",
Bytes: cert.Raw,
})
certs = append(certs, Certificate{
Certificate: *cert,
CertificatePEM: string(certPEM),
CertTypeName: certTypeName,
})
}
certPEM := pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE",
Bytes: certX509.Raw,
})
cert := Certificate{
Certificate: *certX509,
CertificatePEM: string(certPEM),
CertTypeName: certTypeName,
}
if ext != nil {
cert.StructVersion = ext.StructVersion
cert.ProductName = ext.ProductName
cert.TCBVersion = newTCBVersion(ext.TCBVersion)
if ext.HWID != nil {
cert.HardwareID = ext.HWID
} else {
cert.CspID = ext.CspID
}
}
certs = append(certs, cert)
i++
}
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, "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
// https://www.amd.com/system/files/TechDocs/57230.pdf
vcekExts, err := kds.VcekCertificateExtensions(&c.Certificate)
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)