mirror of
https://github.com/edgelesssys/constellation.git
synced 2024-12-24 23:19:39 -05:00
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:
parent
07eed0e319
commit
84d8bd8110
@ -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)
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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.
|
||||
|
@ -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 {
|
||||
|
@ -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.
|
||||
|
@ -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",
|
||||
],
|
||||
)
|
||||
|
||||
|
@ -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
|
||||
}
|
@ -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)
|
||||
|
Loading…
Reference in New Issue
Block a user