constellation/internal/attestation/snp/snp.go
Daniel Weiße 036a4f2ee1
deps: remove obsolete Go replace to upgrade go-sev-guest (#3107)
Signed-off-by: Daniel Weiße <dw@edgeless.systems>
2024-05-16 15:48:44 +02:00

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
}