mirror of
https://github.com/edgelesssys/constellation.git
synced 2024-12-25 15:39:37 -05:00
036a4f2ee1
Signed-off-by: Daniel Weiße <dw@edgeless.systems>
284 lines
9.7 KiB
Go
284 lines
9.7 KiB
Go
/*
|
|
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"
|
|
"errors"
|
|
"fmt"
|
|
|
|
"github.com/edgelesssys/constellation/v2/internal/attestation"
|
|
"github.com/google/go-sev-guest/abi"
|
|
"github.com/google/go-sev-guest/client"
|
|
"github.com/google/go-sev-guest/kds"
|
|
spb "github.com/google/go-sev-guest/proto/sevsnp"
|
|
"github.com/google/go-sev-guest/verify/trust"
|
|
"github.com/google/go-tpm-tools/proto/attest"
|
|
)
|
|
|
|
var errNoPemBlocks = errors.New("no PEM blocks found")
|
|
|
|
// 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
|
|
}
|
|
|
|
// GetExtendedReport retrieves the extended SNP report from the CVM.
|
|
func GetExtendedReport(reportData [64]byte) (report, certChain []byte, err error) {
|
|
qp, err := client.GetLeveledQuoteProvider()
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("getting quote provider: %w", err)
|
|
}
|
|
quote, err := qp.GetRawQuoteAtLevel(reportData, 0)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("getting extended report: %w", err)
|
|
}
|
|
|
|
// Parse the report and certificate chain from the quote.
|
|
report = quote
|
|
if len(quote) > abi.ReportSize {
|
|
report = quote[:abi.ReportSize]
|
|
certChain = quote[abi.ReportSize:]
|
|
}
|
|
return report, certChain, nil
|
|
}
|
|
|
|
// 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.
|
|
ReportSigner []byte
|
|
// CertChain is the PEM-encoded certificate chain for the attestation report (ASK+ARK).
|
|
// Intermediate key that validates the ReportSigner and root key.
|
|
CertChain []byte
|
|
// AttestationReport is the attestation report from the vTPM (NVRAM) of the CVM.
|
|
AttestationReport []byte
|
|
Azure *AzureInstanceInfo
|
|
GCP *attest.GCEInstanceInfo
|
|
}
|
|
|
|
// AzureInstanceInfo contains Azure specific information related to SNP attestation.
|
|
type AzureInstanceInfo struct {
|
|
// 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
|
|
}
|
|
|
|
// addReportSigner parses the reportSigner certificate (VCEK/VLEK) from a and adds it to the attestation proto att.
|
|
// If reportSigner is empty and a VLEK is required, an error is returned.
|
|
// If reportSigner is empty and a VCEK is required, the VCEK is retrieved from AMD KDS.
|
|
func (a *InstanceInfo) addReportSigner(att *spb.Attestation, report *spb.Report, productName string, getter trust.HTTPSGetter, logger attestation.Logger) (abi.ReportSigner, error) {
|
|
// If the VCEK certificate is present, parse it and format it.
|
|
reportSigner, err := a.ParseReportSigner()
|
|
if err != nil {
|
|
logger.Warn(fmt.Sprintf("Error parsing report signer: %v", err))
|
|
}
|
|
|
|
signerInfo, err := abi.ParseSignerInfo(report.GetSignerInfo())
|
|
if err != nil {
|
|
return abi.NoneReportSigner, fmt.Errorf("parsing signer info: %w", err)
|
|
}
|
|
|
|
switch signerInfo.SigningKey {
|
|
case abi.VlekReportSigner:
|
|
if reportSigner == nil {
|
|
return abi.NoneReportSigner, fmt.Errorf("VLEK certificate required but not present")
|
|
}
|
|
att.CertificateChain.VlekCert = reportSigner.Raw
|
|
|
|
case abi.VcekReportSigner:
|
|
var vcekData []byte
|
|
|
|
// If no VCEK is present, fetch it from AMD.
|
|
if reportSigner == nil {
|
|
logger.Info("VCEK certificate not present, falling back to retrieving it from AMD KDS")
|
|
vcekURL := kds.VCEKCertURL(productName, report.GetChipId(), kds.TCBVersion(report.GetReportedTcb()))
|
|
vcekData, err = getter.Get(vcekURL)
|
|
if err != nil {
|
|
return abi.NoneReportSigner, fmt.Errorf("retrieving VCEK certificate from AMD KDS: %w", err)
|
|
}
|
|
} else {
|
|
vcekData = reportSigner.Raw
|
|
}
|
|
|
|
att.CertificateChain.VcekCert = vcekData
|
|
}
|
|
|
|
return signerInfo.SigningKey, nil
|
|
}
|
|
|
|
// AttestationWithCerts returns a formatted version of the attestation report and its certificates from the instanceInfo.
|
|
// Certificates are retrieved in the following precedence:
|
|
// 1. ASK from issuer. On Azure: THIM. One AWS: not prefilled. (Go to option 2) On GCP: prefilled.
|
|
// 2. ASK or ARK from fallbackCerts.
|
|
// 3. ASK or ARK from AMD KDS.
|
|
func (a *InstanceInfo) AttestationWithCerts(getter trust.HTTPSGetter,
|
|
fallbackCerts CertificateChain, logger attestation.Logger,
|
|
) (*spb.Attestation, error) {
|
|
report, err := abi.ReportToProto(a.AttestationReport)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("converting report to proto: %w", err)
|
|
}
|
|
|
|
productName := kds.ProductLine(Product())
|
|
|
|
att := &spb.Attestation{
|
|
Report: report,
|
|
CertificateChain: &spb.CertificateChain{},
|
|
Product: Product(),
|
|
}
|
|
|
|
// Add VCEK/VLEK to attestation object.
|
|
signingInfo, err := a.addReportSigner(att, report, productName, getter, logger)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("adding report signer: %w", err)
|
|
}
|
|
|
|
// If a certificate chain was pre-fetched by the Issuer, parse it and format it.
|
|
// Make sure to only use the ask, since using an ark from the Issuer would invalidate security guarantees.
|
|
ask, _, err := a.ParseCertChain()
|
|
if err != nil && !errors.Is(err, errNoPemBlocks) {
|
|
logger.Warn(fmt.Sprintf("Error parsing certificate chain: %v", err))
|
|
}
|
|
if ask != nil {
|
|
logger.Info("Using ASK certificate from pre-fetched certificate chain")
|
|
att.CertificateChain.AskCert = ask.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.Info("Using cached ASK certificate")
|
|
att.CertificateChain.AskCert = fallbackCerts.ask.Raw
|
|
}
|
|
if fallbackCerts.ark != nil {
|
|
logger.Info("Using cached ARK certificate")
|
|
att.CertificateChain.ArkCert = fallbackCerts.ark.Raw
|
|
}
|
|
|
|
// Otherwise, retrieve missing certificates from AMD KDS.
|
|
if att.CertificateChain.AskCert == nil || att.CertificateChain.ArkCert == nil {
|
|
logger.Info(fmt.Sprintf(
|
|
"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, signingInfo, 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.Info("Using ASK certificate from AMD KDS")
|
|
att.CertificateChain.AskCert = kdsCertChain.Ask.Raw
|
|
}
|
|
if att.CertificateChain.ArkCert == nil && kdsCertChain.Ask != nil {
|
|
logger.Info("Using ARK certificate from AMD KDS")
|
|
att.CertificateChain.ArkCert = kdsCertChain.Ark.Raw
|
|
}
|
|
}
|
|
|
|
return att, nil
|
|
}
|
|
|
|
// CertificateChain stores an AMD signing key (ASK) and AMD root key (ARK) certificate.
|
|
type CertificateChain struct {
|
|
ask *x509.Certificate
|
|
ark *x509.Certificate
|
|
}
|
|
|
|
// NewCertificateChain returns a new CertificateChain with the given ASK and ARK certificates.
|
|
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", "SEV-VLEK-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 = errNoPemBlocks
|
|
case len(rest) != 0:
|
|
retErr = fmt.Errorf("remaining PEM block is not a valid certificate: %s", rest)
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
// ParseReportSigner parses the VCEK/VLEK certificate from the instanceInfo into an x509-formatted certificate.
|
|
// If no certificate is present, nil is returned.
|
|
func (a *InstanceInfo) ParseReportSigner() (*x509.Certificate, error) {
|
|
newlinesTrimmed := bytes.TrimSpace(a.ReportSigner)
|
|
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)
|
|
}
|
|
|
|
reportSigner, err := x509.ParseCertificate(block.Bytes)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("parsing VCEK certificate: %w", err)
|
|
}
|
|
|
|
return reportSigner, nil
|
|
}
|