verify: move CSP-specific code to internal/verify

With the introduction of SNP-based attestation on AWS
some of the information in the report (MAAToken) is not
applicable to all attestation reports anymore.
Thus, make verify cmd CSP-agnostic and move
CSP-specific logic to internal/verify.
Also make internal/attestation/snp CSP aware.
This commit is contained in:
Otto Bittner 2023-11-07 15:19:31 +01:00
parent 59b096e279
commit cdc91b50bc
13 changed files with 665 additions and 531 deletions

View File

@ -88,9 +88,6 @@ go_library(
"//internal/verify", "//internal/verify",
"//internal/versions", "//internal/versions",
"//verify/verifyproto", "//verify/verifyproto",
"@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_tpm_tools//proto/tpm", "@com_github_google_go_tpm_tools//proto/tpm",
"@com_github_google_uuid//:uuid", "@com_github_google_uuid//:uuid",
"@com_github_mattn_go_isatty//:go-isatty", "@com_github_mattn_go_isatty//:go-isatty",

View File

@ -9,16 +9,11 @@ package cmd
import ( import (
"bytes" "bytes"
"context" "context"
"crypto/x509"
"encoding/base64" "encoding/base64"
"encoding/json" "encoding/json"
"encoding/pem"
"errors" "errors"
"fmt" "fmt"
"io"
"net" "net"
"net/http"
"net/url"
"sort" "sort"
"strconv" "strconv"
"strings" "strings"
@ -40,9 +35,6 @@ import (
"github.com/edgelesssys/constellation/v2/internal/state" "github.com/edgelesssys/constellation/v2/internal/state"
"github.com/edgelesssys/constellation/v2/internal/verify" "github.com/edgelesssys/constellation/v2/internal/verify"
"github.com/edgelesssys/constellation/v2/verify/verifyproto" "github.com/edgelesssys/constellation/v2/verify/verifyproto"
"github.com/golang-jwt/jwt/v5"
"github.com/google/go-sev-guest/abi"
"github.com/google/go-sev-guest/kds"
"github.com/spf13/afero" "github.com/spf13/afero"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"github.com/spf13/pflag" "github.com/spf13/pflag"
@ -213,7 +205,7 @@ func (c *verifyCmd) verify(cmd *cobra.Command, verifyClient verifyClient, factor
attDocOutput, err := formatter.format( attDocOutput, err := formatter.format(
cmd.Context(), cmd.Context(),
rawAttestationDoc, rawAttestationDoc,
conf.Provider.Azure == nil, (conf.Provider.Azure == nil && conf.Provider.AWS == nil),
attConfig.GetMeasurements(), attConfig.GetMeasurements(),
maaURL, maaURL,
) )
@ -274,34 +266,20 @@ type jsonAttestationDocFormatter struct {
func (f *jsonAttestationDocFormatter) format(ctx context.Context, docString string, _ bool, func (f *jsonAttestationDocFormatter) format(ctx context.Context, docString string, _ bool,
_ measurements.M, attestationServiceURL string, _ measurements.M, attestationServiceURL string,
) (string, error) { ) (string, error) {
instanceInfo, err := extractAzureInstanceInfo(docString) var doc attestationDoc
if err != nil { if err := json.Unmarshal([]byte(docString), &doc); err != nil {
return "", fmt.Errorf("unmarshal instance info: %w", err) return "", fmt.Errorf("unmarshal attestation document: %w", err)
} }
snpReport, err := newSNPReport(instanceInfo.AttestationReport)
instanceInfo, err := extractInstanceInfo(doc)
if err != nil {
return "", fmt.Errorf("unmarshalling instance info: %w", err)
}
report, err := verify.NewReport(ctx, instanceInfo, attestationServiceURL, f.log)
if err != nil { if err != nil {
return "", fmt.Errorf("parsing SNP report: %w", err) return "", fmt.Errorf("parsing SNP report: %w", err)
} }
vcek, err := newCertificates("VCEK certificate", instanceInfo.VCEK, f.log)
if err != nil {
return "", fmt.Errorf("parsing VCEK certificate: %w", err)
}
certChain, err := newCertificates("Certificate chain", instanceInfo.CertChain, f.log)
if err != nil {
return "", fmt.Errorf("parsing certificate chain: %w", err)
}
maaToken, err := newMAAToken(ctx, instanceInfo.MAAToken, attestationServiceURL)
if err != nil {
return "", fmt.Errorf("parsing MAA token: %w", err)
}
report := verify.Report{
SNPReport: snpReport,
VCEK: vcek,
CertChain: certChain,
MAAToken: maaToken,
}
jsonBytes, err := json.Marshal(report) jsonBytes, err := json.Marshal(report)
return string(jsonBytes), err return string(jsonBytes), err
@ -344,97 +322,17 @@ func (f *defaultAttestationDocFormatter) format(ctx context.Context, docString s
return b.String(), nil return b.String(), nil
} }
instanceInfoString, err := base64.StdEncoding.DecodeString(doc.InstanceInfo) instanceInfo, err := extractInstanceInfo(doc)
if err != nil { if err != nil {
return "", fmt.Errorf("decode instance info: %w", err) return "", fmt.Errorf("unmarshalling instance info: %w", err)
} }
var instanceInfo snp.InstanceInfo report, err := verify.NewReport(ctx, instanceInfo, attestationServiceURL, f.log)
if err := json.Unmarshal(instanceInfoString, &instanceInfo); err != nil {
return "", fmt.Errorf("unmarshal instance info: %w", err)
}
if err := f.parseCerts(b, "VCEK certificate", instanceInfo.VCEK); err != nil {
return "", fmt.Errorf("print VCEK certificate: %w", err)
}
if err := f.parseCerts(b, "Certificate chain", instanceInfo.CertChain); err != nil {
return "", fmt.Errorf("print certificate chain: %w", err)
}
snpReport, err := newSNPReport(instanceInfo.AttestationReport)
if err != nil { if err != nil {
return "", fmt.Errorf("parsing SNP report: %w", err) return "", fmt.Errorf("parsing SNP report: %w", err)
} }
f.buildSNPReport(b, snpReport)
if err := parseMAAToken(ctx, b, instanceInfo.MAAToken, attestationServiceURL); err != nil {
return "", fmt.Errorf("print MAA token: %w", err)
}
return b.String(), nil return report.FormatString(b)
}
// parseCerts parses the PEM certificates and writes their details to the output builder.
func (f *defaultAttestationDocFormatter) parseCerts(b *strings.Builder, certTypeName string, cert []byte) error {
newlinesTrimmed := strings.TrimSpace(string(cert))
formattedCert := strings.ReplaceAll(newlinesTrimmed, "\n", "\n\t\t") + "\n"
b.WriteString(fmt.Sprintf("\tRaw %s:\n\t\t%s", certTypeName, formattedCert))
f.log.Debugf("Decoding PEM certificate: %s", certTypeName)
i := 1
var rest []byte
var block *pem.Block
for block, rest = pem.Decode([]byte(newlinesTrimmed)); block != nil; block, rest = pem.Decode(rest) {
f.log.Debugf("Parsing PEM block: %d", i)
if block.Type != "CERTIFICATE" {
return fmt.Errorf("parse %s: expected PEM block type 'CERTIFICATE', got '%s'", certTypeName, block.Type)
}
cert, err := x509.ParseCertificate(block.Bytes)
if err != nil {
return fmt.Errorf("parse %s: %w", certTypeName, err)
}
writeIndentfln(b, 1, "%s (%d):", certTypeName, i)
writeIndentfln(b, 2, "Serial Number: %s", cert.SerialNumber)
writeIndentfln(b, 2, "Subject: %s", cert.Subject)
writeIndentfln(b, 2, "Issuer: %s", cert.Issuer)
writeIndentfln(b, 2, "Not Before: %s", cert.NotBefore)
writeIndentfln(b, 2, "Not After: %s", cert.NotAfter)
writeIndentfln(b, 2, "Signature Algorithm: %s", cert.SignatureAlgorithm)
writeIndentfln(b, 2, "Public Key Algorithm: %s", cert.PublicKeyAlgorithm)
if certTypeName == "VCEK certificate" {
// Extensions documented in Table 8 and Table 9 of
// https://www.amd.com/system/files/TechDocs/57230.pdf
vcekExts, err := kds.VcekCertificateExtensions(cert)
if err != nil {
return fmt.Errorf("parsing VCEK certificate extensions: %w", err)
}
writeIndentfln(b, 2, "Struct version: %d", vcekExts.StructVersion)
writeIndentfln(b, 2, "Product name: %s", vcekExts.ProductName)
tcb := kds.DecomposeTCBVersion(vcekExts.TCBVersion)
writeIndentfln(b, 2, "Secure Processor bootloader SVN: %d", tcb.BlSpl)
writeIndentfln(b, 2, "Secure Processor operating system SVN: %d", tcb.TeeSpl)
writeIndentfln(b, 2, "SVN 4 (reserved): %d", tcb.Spl4)
writeIndentfln(b, 2, "SVN 5 (reserved): %d", tcb.Spl5)
writeIndentfln(b, 2, "SVN 6 (reserved): %d", tcb.Spl6)
writeIndentfln(b, 2, "SVN 7 (reserved): %d", tcb.Spl7)
writeIndentfln(b, 2, "SEV-SNP firmware SVN: %d", tcb.SnpSpl)
writeIndentfln(b, 2, "Microcode SVN: %d", tcb.UcodeSpl)
writeIndentfln(b, 2, "Hardware ID: %x", vcekExts.HWID)
}
i++
}
if i == 1 {
return fmt.Errorf("parse %s: no PEM blocks found", certTypeName)
}
if len(rest) != 0 {
return fmt.Errorf("parse %s: remaining PEM block is not a valid certificate: %s", certTypeName, rest)
}
return nil
} }
// parseQuotes parses the base64-encoded quotes and writes their details to the output builder. // parseQuotes parses the base64-encoded quotes and writes their details to the output builder.
@ -465,139 +363,6 @@ func (f *defaultAttestationDocFormatter) parseQuotes(b *strings.Builder, quotes
return nil return nil
} }
func (f *defaultAttestationDocFormatter) buildSNPReport(b *strings.Builder, report verify.SNPReport) {
writeTCB := func(tcb verify.TCBVersion) {
writeIndentfln(b, 3, "Secure Processor bootloader SVN: %d", tcb.Bootloader)
writeIndentfln(b, 3, "Secure Processor operating system SVN: %d", tcb.TEE)
writeIndentfln(b, 3, "SVN 4 (reserved): %d", tcb.Spl4)
writeIndentfln(b, 3, "SVN 5 (reserved): %d", tcb.Spl5)
writeIndentfln(b, 3, "SVN 6 (reserved): %d", tcb.Spl6)
writeIndentfln(b, 3, "SVN 7 (reserved): %d", tcb.Spl7)
writeIndentfln(b, 3, "SEV-SNP firmware SVN: %d", tcb.SNP)
writeIndentfln(b, 3, "Microcode SVN: %d", tcb.Microcode)
}
writeIndentfln(b, 1, "SNP Report:")
writeIndentfln(b, 2, "Version: %d", report.Version)
writeIndentfln(b, 2, "Guest SVN: %d", report.GuestSvn)
writeIndentfln(b, 2, "Policy:")
writeIndentfln(b, 3, "ABI Minor: %d", report.PolicyABIMinor)
writeIndentfln(b, 3, "ABI Major: %d", report.PolicyABIMajor)
writeIndentfln(b, 3, "Symmetric Multithreading enabled: %t", report.PolicySMT)
writeIndentfln(b, 3, "Migration agent enabled: %t", report.PolicyMigrationAgent)
writeIndentfln(b, 3, "Debugging enabled (host decryption of VM): %t", report.PolicyDebug)
writeIndentfln(b, 3, "Single socket enabled: %t", report.PolicySingleSocket)
writeIndentfln(b, 2, "Family ID: %x", report.FamilyID)
writeIndentfln(b, 2, "Image ID: %x", report.ImageID)
writeIndentfln(b, 2, "VMPL: %d", report.Vmpl)
writeIndentfln(b, 2, "Signature Algorithm: %d", report.SignatureAlgo)
writeIndentfln(b, 2, "Current TCB:")
writeTCB(report.CurrentTCB)
writeIndentfln(b, 2, "Platform Info:")
writeIndentfln(b, 3, "Symmetric Multithreading enabled (SMT): %t", report.PlatformInfo.SMT)
writeIndentfln(b, 3, "Transparent secure memory encryption (TSME): %t", report.PlatformInfo.TSME)
writeIndentfln(b, 2, "Signer Info:")
writeIndentfln(b, 3, "Author Key Enabled: %t", report.SignerInfo.AuthorKey)
writeIndentfln(b, 3, "Chip ID Masking: %t", report.SignerInfo.MaskChipKey)
writeIndentfln(b, 3, "Signing Type: %s", report.SignerInfo.SigningKey)
writeIndentfln(b, 2, "Report Data: %x", report.ReportData)
writeIndentfln(b, 2, "Measurement: %x", report.Measurement)
writeIndentfln(b, 2, "Host Data: %x", report.HostData)
writeIndentfln(b, 2, "ID Key Digest: %x", report.IDKeyDigest)
writeIndentfln(b, 2, "Author Key Digest: %x", report.AuthorKeyDigest)
writeIndentfln(b, 2, "Report ID: %x", report.ReportID)
writeIndentfln(b, 2, "Report ID MA: %x", report.ReportIDMa)
writeIndentfln(b, 2, "Reported TCB:")
writeTCB(report.ReportedTCB)
writeIndentfln(b, 2, "Chip ID: %x", report.ChipID)
writeIndentfln(b, 2, "Committed TCB:")
writeTCB(report.CommittedTCB)
writeIndentfln(b, 2, "Current Build: %d", report.CurrentBuild)
writeIndentfln(b, 2, "Current Minor: %d", report.CurrentMinor)
writeIndentfln(b, 2, "Current Major: %d", report.CurrentMajor)
writeIndentfln(b, 2, "Committed Build: %d", report.CommittedBuild)
writeIndentfln(b, 2, "Committed Minor: %d", report.CommittedMinor)
writeIndentfln(b, 2, "Committed Major: %d", report.CommittedMajor)
writeIndentfln(b, 2, "Launch TCB:")
writeTCB(report.LaunchTCB)
writeIndentfln(b, 2, "Signature (DER):")
writeIndentfln(b, 3, "%x", report.Signature)
}
func parseMAAToken(ctx context.Context, b *strings.Builder, rawToken, attestationServiceURL string) error {
var claims verify.MaaTokenClaims
_, err := jwt.ParseWithClaims(rawToken, &claims, keyFromJKUFunc(ctx, attestationServiceURL), jwt.WithIssuedAt())
if err != nil {
return fmt.Errorf("parsing token: %w", err)
}
out, err := json.MarshalIndent(claims, "\t\t", " ")
if err != nil {
return fmt.Errorf("marshaling claims: %w", err)
}
b.WriteString("\tMicrosoft Azure Attestation Token:\n\t")
b.WriteString(string(out))
return nil
}
// keyFromJKUFunc returns a function that gets the JSON Web Key URI from the token
// and fetches the key from that URI. The keys are then parsed, and the key with
// the kid that matches the token header is returned.
func keyFromJKUFunc(ctx context.Context, webKeysURLBase string) func(token *jwt.Token) (any, error) {
return func(token *jwt.Token) (any, error) {
webKeysURL, err := url.JoinPath(webKeysURLBase, "certs")
if err != nil {
return nil, fmt.Errorf("joining web keys base URL with path: %w", err)
}
if token.Header["alg"] != "RS256" {
return nil, fmt.Errorf("invalid signing algorithm: %s", token.Header["alg"])
}
kid, ok := token.Header["kid"].(string)
if !ok {
return nil, fmt.Errorf("invalid kid: %v", token.Header["kid"])
}
jku, ok := token.Header["jku"].(string)
if !ok {
return nil, fmt.Errorf("invalid jku: %v", token.Header["jku"])
}
if jku != webKeysURL {
return nil, fmt.Errorf("jku from token (%s) does not match configured attestation service (%s)", jku, webKeysURL)
}
keySetBytes, err := httpGet(ctx, jku)
if err != nil {
return nil, fmt.Errorf("getting signing keys from jku %s: %w", jku, err)
}
var rawKeySet struct {
Keys []struct {
X5c [][]byte
Kid string
}
}
if err := json.Unmarshal(keySetBytes, &rawKeySet); err != nil {
return nil, err
}
for _, key := range rawKeySet.Keys {
if key.Kid != kid {
continue
}
cert, err := x509.ParseCertificate(key.X5c[0])
if err != nil {
return nil, fmt.Errorf("parsing certificate: %w", err)
}
return cert.PublicKey, nil
}
return nil, fmt.Errorf("no key found for kid %s", kid)
}
}
// attestationDoc is the attestation document returned by the verifier. // attestationDoc is the attestation document returned by the verifier.
type attestationDoc struct { type attestationDoc struct {
Attestation struct { Attestation struct {
@ -664,176 +429,7 @@ func writeIndentfln(b *strings.Builder, indentLvl int, format string, args ...an
b.WriteString(fmt.Sprintf(format+"\n", args...)) b.WriteString(fmt.Sprintf(format+"\n", args...))
} }
func httpGet(ctx context.Context, url string) ([]byte, error) { func extractInstanceInfo(doc attestationDoc) (snp.InstanceInfo, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, http.NoBody)
if err != nil {
return nil, err
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, errors.New(resp.Status)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
return body, nil
}
func newCertificates(certTypeName string, cert []byte, log debugLog) (certs []verify.Certificate, err error) {
newlinesTrimmed := strings.TrimSpace(string(cert))
log.Debugf("Decoding PEM certificate: %s", certTypeName)
i := 1
var rest []byte
var block *pem.Block
for block, rest = pem.Decode([]byte(newlinesTrimmed)); block != nil; block, rest = pem.Decode(rest) {
log.Debugf("Parsing PEM block: %d", i)
if block.Type != "CERTIFICATE" {
return certs, fmt.Errorf("parse %s: expected PEM block type 'CERTIFICATE', got '%s'", certTypeName, block.Type)
}
cert, 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)
if err != nil {
return certs, fmt.Errorf("parsing VCEK certificate extensions: %w", err)
}
certPEM := pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE",
Bytes: cert.Raw,
})
certs = append(certs, verify.Certificate{
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, verify.Certificate{
CertificatePEM: string(certPEM),
CertTypeName: certTypeName,
})
}
i++
}
if i == 1 {
return certs, fmt.Errorf("parse %s: no PEM blocks found", certTypeName)
}
if len(rest) != 0 {
return certs, fmt.Errorf("parse %s: remaining PEM block is not a valid certificate: %s", certTypeName, rest)
}
return certs, nil
}
func newSNPReport(reportBytes []byte) (res verify.SNPReport, err error) {
report, err := abi.ReportToProto(reportBytes)
if err != nil {
return res, fmt.Errorf("parsing report to proto: %w", err)
}
policy, err := abi.ParseSnpPolicy(report.Policy)
if err != nil {
return res, fmt.Errorf("parsing policy: %w", err)
}
platformInfo, err := abi.ParseSnpPlatformInfo(report.PlatformInfo)
if err != nil {
return res, fmt.Errorf("parsing platform info: %w", err)
}
signature, err := abi.ReportToSignatureDER(reportBytes)
if err != nil {
return res, fmt.Errorf("parsing signature: %w", err)
}
signerInfo, err := abi.ParseSignerInfo(report.SignerInfo)
if err != nil {
return res, fmt.Errorf("parsing signer info: %w", err)
}
return verify.SNPReport{
Version: report.Version,
GuestSvn: report.GuestSvn,
PolicyABIMinor: policy.ABIMinor,
PolicyABIMajor: policy.ABIMajor,
PolicySMT: policy.SMT,
PolicyMigrationAgent: policy.MigrateMA,
PolicyDebug: policy.Debug,
PolicySingleSocket: policy.SingleSocket,
FamilyID: report.FamilyId,
ImageID: report.ImageId,
Vmpl: report.Vmpl,
SignatureAlgo: report.SignatureAlgo,
CurrentTCB: newTCBVersion(kds.TCBVersion(report.CurrentTcb)),
PlatformInfo: verify.PlatformInfo{
SMT: platformInfo.SMTEnabled,
TSME: platformInfo.TSMEEnabled,
},
SignerInfo: verify.SignerInfo{
AuthorKey: signerInfo.AuthorKeyEn,
MaskChipKey: signerInfo.MaskChipKey,
SigningKey: signerInfo.SigningKey.String(),
},
ReportData: report.ReportData,
Measurement: report.Measurement,
HostData: report.HostData,
IDKeyDigest: report.IdKeyDigest,
AuthorKeyDigest: report.AuthorKeyDigest,
ReportID: report.ReportId,
ReportIDMa: report.ReportIdMa,
ReportedTCB: newTCBVersion(kds.TCBVersion(report.ReportedTcb)),
ChipID: report.ChipId,
CommittedTCB: newTCBVersion(kds.TCBVersion(report.CommittedTcb)),
CurrentBuild: report.CurrentBuild,
CurrentMinor: report.CurrentMinor,
CurrentMajor: report.CurrentMajor,
CommittedBuild: report.CommittedBuild,
CommittedMinor: report.CommittedMinor,
CommittedMajor: report.CommittedMajor,
LaunchTCB: newTCBVersion(kds.TCBVersion(report.LaunchTcb)),
Signature: signature,
}, nil
}
func newMAAToken(ctx context.Context, rawToken, attestationServiceURL string) (verify.MaaTokenClaims, error) {
var claims verify.MaaTokenClaims
_, err := jwt.ParseWithClaims(rawToken, &claims, keyFromJKUFunc(ctx, attestationServiceURL), jwt.WithIssuedAt())
return claims, err
}
func newTCBVersion(tcbVersion kds.TCBVersion) (res verify.TCBVersion) {
tcb := kds.DecomposeTCBVersion(tcbVersion)
return verify.TCBVersion{
Bootloader: tcb.BlSpl,
TEE: tcb.TeeSpl,
SNP: tcb.SnpSpl,
Microcode: tcb.UcodeSpl,
Spl4: tcb.Spl4,
Spl5: tcb.Spl5,
Spl6: tcb.Spl6,
Spl7: tcb.Spl7,
}
}
func extractAzureInstanceInfo(docString string) (snp.InstanceInfo, error) {
var doc attestationDoc
if err := json.Unmarshal([]byte(docString), &doc); err != nil {
return snp.InstanceInfo{}, fmt.Errorf("unmarshal attestation document: %w", err)
}
instanceInfoString, err := base64.StdEncoding.DecodeString(doc.InstanceInfo) instanceInfoString, err := base64.StdEncoding.DecodeString(doc.InstanceInfo)
if err != nil { if err != nil {
return snp.InstanceInfo{}, fmt.Errorf("decode instance info: %w", err) return snp.InstanceInfo{}, fmt.Errorf("decode instance info: %w", err)

View File

@ -268,82 +268,6 @@ func TestFormat(t *testing.T) {
} }
} }
func TestParseCerts(t *testing.T) {
validCert := `-----BEGIN CERTIFICATE-----
MIIFTDCCAvugAwIBAgIBADBGBgkqhkiG9w0BAQowOaAPMA0GCWCGSAFlAwQCAgUA
oRwwGgYJKoZIhvcNAQEIMA0GCWCGSAFlAwQCAgUAogMCATCjAwIBATB7MRQwEgYD
VQQLDAtFbmdpbmVlcmluZzELMAkGA1UEBhMCVVMxFDASBgNVBAcMC1NhbnRhIENs
YXJhMQswCQYDVQQIDAJDQTEfMB0GA1UECgwWQWR2YW5jZWQgTWljcm8gRGV2aWNl
czESMBAGA1UEAwwJU0VWLU1pbGFuMB4XDTIyMTEyMzIyMzM0N1oXDTI5MTEyMzIy
MzM0N1owejEUMBIGA1UECwwLRW5naW5lZXJpbmcxCzAJBgNVBAYTAlVTMRQwEgYD
VQQHDAtTYW50YSBDbGFyYTELMAkGA1UECAwCQ0ExHzAdBgNVBAoMFkFkdmFuY2Vk
IE1pY3JvIERldmljZXMxETAPBgNVBAMMCFNFVi1WQ0VLMHYwEAYHKoZIzj0CAQYF
K4EEACIDYgAEVGm4GomfpkiziqEYP61nfKaz5OjDLr8Y0POrv4iAnFVHAmBT81Ms
gfSLKL5r3V3mNzl1Zh7jwSBft14uhGdwpARoK0YNQc4OvptqVIiv2RprV53DMzge
rtwiumIargiCo4IBFjCCARIwEAYJKwYBBAGceAEBBAMCAQAwFwYJKwYBBAGceAEC
BAoWCE1pbGFuLUIwMBEGCisGAQQBnHgBAwEEAwIBAzARBgorBgEEAZx4AQMCBAMC
AQAwEQYKKwYBBAGceAEDBAQDAgEAMBEGCisGAQQBnHgBAwUEAwIBADARBgorBgEE
AZx4AQMGBAMCAQAwEQYKKwYBBAGceAEDBwQDAgEAMBEGCisGAQQBnHgBAwMEAwIB
CDARBgorBgEEAZx4AQMIBAMCAXMwTQYJKwYBBAGceAEEBEB80kCZ1oAyCjWC6w3m
xOz+i4t6dFjk/Bqhm7+Jscf8D62CXtlwcKc4aM9CdO4LuKlwpdTU80VNQc6ZEuMF
VzbRMEYGCSqGSIb3DQEBCjA5oA8wDQYJYIZIAWUDBAICBQChHDAaBgkqhkiG9w0B
AQgwDQYJYIZIAWUDBAICBQCiAwIBMKMDAgEBA4ICAQCN1qBYOywoZWGnQvk6u0Oh
5zkEKykXU6sK8hA6L65rQcqWUjEHDa9AZUpx3UuCmpPc24dx6DTHc58M7TxcyKry
8s4CvruBKFbQ6B8MHnH6k07MzsmiBnsiIhAscZ0ipGm6h8e/VM/6ULrAcVSxZ+Mh
D/IogZAuCQARsGQ4QYXBT8Qc5mLnTkx30m1rZVlp1VcN4ngOo/1tz1jj1mfpG2zv
wNcQa9LwAzRLnnmLpxXA2OMbl7AaTWQenpL9rzBON2sg4OBl6lVhaSU0uBbFyCmR
RvBqKC0iDD6TvyIikkMq05v5YwIKFYw++ICndz+fKcLEULZbziAsZ52qjM8iPVHC
pN0yhVOr2g22F9zxlGH3WxTl9ymUytuv3vJL/aJiQM+n/Ri90Sc05EK4oIJ3+BS8
yu5cVy9o2cQcOcQ8rhQh+Kv1sR9xrs25EXZF8KEETfhoJnN6KY1RwG7HsOfAQ3dV
LWInQRaC/8JPyVS2zbd0+NRBJOnq4/quv/P3C4SBP98/ZuGrqN59uifyqC3Kodkl
WkG/2UdhiLlCmOtsU+BYDZrSiYK1R9FNnlQCOGrkuVxpDwa2TbbvEEzQP7RXxotA
KlxejvrY4VuK8agNqvffVofbdIIperK65K4+0mYIb+A6fU8QQHlCbti4ERSZ6UYD
F/SjRih31+SAtWb42jueAA==
-----END CERTIFICATE-----
`
validCertExpected := "\tRaw Some Cert:\n\t\t-----BEGIN CERTIFICATE-----\n\t\tMIIFTDCCAvugAwIBAgIBADBGBgkqhkiG9w0BAQowOaAPMA0GCWCGSAFlAwQCAgUA\n\t\toRwwGgYJKoZIhvcNAQEIMA0GCWCGSAFlAwQCAgUAogMCATCjAwIBATB7MRQwEgYD\n\t\tVQQLDAtFbmdpbmVlcmluZzELMAkGA1UEBhMCVVMxFDASBgNVBAcMC1NhbnRhIENs\n\t\tYXJhMQswCQYDVQQIDAJDQTEfMB0GA1UECgwWQWR2YW5jZWQgTWljcm8gRGV2aWNl\n\t\tczESMBAGA1UEAwwJU0VWLU1pbGFuMB4XDTIyMTEyMzIyMzM0N1oXDTI5MTEyMzIy\n\t\tMzM0N1owejEUMBIGA1UECwwLRW5naW5lZXJpbmcxCzAJBgNVBAYTAlVTMRQwEgYD\n\t\tVQQHDAtTYW50YSBDbGFyYTELMAkGA1UECAwCQ0ExHzAdBgNVBAoMFkFkdmFuY2Vk\n\t\tIE1pY3JvIERldmljZXMxETAPBgNVBAMMCFNFVi1WQ0VLMHYwEAYHKoZIzj0CAQYF\n\t\tK4EEACIDYgAEVGm4GomfpkiziqEYP61nfKaz5OjDLr8Y0POrv4iAnFVHAmBT81Ms\n\t\tgfSLKL5r3V3mNzl1Zh7jwSBft14uhGdwpARoK0YNQc4OvptqVIiv2RprV53DMzge\n\t\trtwiumIargiCo4IBFjCCARIwEAYJKwYBBAGceAEBBAMCAQAwFwYJKwYBBAGceAEC\n\t\tBAoWCE1pbGFuLUIwMBEGCisGAQQBnHgBAwEEAwIBAzARBgorBgEEAZx4AQMCBAMC\n\t\tAQAwEQYKKwYBBAGceAEDBAQDAgEAMBEGCisGAQQBnHgBAwUEAwIBADARBgorBgEE\n\t\tAZx4AQMGBAMCAQAwEQYKKwYBBAGceAEDBwQDAgEAMBEGCisGAQQBnHgBAwMEAwIB\n\t\tCDARBgorBgEEAZx4AQMIBAMCAXMwTQYJKwYBBAGceAEEBEB80kCZ1oAyCjWC6w3m\n\t\txOz+i4t6dFjk/Bqhm7+Jscf8D62CXtlwcKc4aM9CdO4LuKlwpdTU80VNQc6ZEuMF\n\t\tVzbRMEYGCSqGSIb3DQEBCjA5oA8wDQYJYIZIAWUDBAICBQChHDAaBgkqhkiG9w0B\n\t\tAQgwDQYJYIZIAWUDBAICBQCiAwIBMKMDAgEBA4ICAQCN1qBYOywoZWGnQvk6u0Oh\n\t\t5zkEKykXU6sK8hA6L65rQcqWUjEHDa9AZUpx3UuCmpPc24dx6DTHc58M7TxcyKry\n\t\t8s4CvruBKFbQ6B8MHnH6k07MzsmiBnsiIhAscZ0ipGm6h8e/VM/6ULrAcVSxZ+Mh\n\t\tD/IogZAuCQARsGQ4QYXBT8Qc5mLnTkx30m1rZVlp1VcN4ngOo/1tz1jj1mfpG2zv\n\t\twNcQa9LwAzRLnnmLpxXA2OMbl7AaTWQenpL9rzBON2sg4OBl6lVhaSU0uBbFyCmR\n\t\tRvBqKC0iDD6TvyIikkMq05v5YwIKFYw++ICndz+fKcLEULZbziAsZ52qjM8iPVHC\n\t\tpN0yhVOr2g22F9zxlGH3WxTl9ymUytuv3vJL/aJiQM+n/Ri90Sc05EK4oIJ3+BS8\n\t\tyu5cVy9o2cQcOcQ8rhQh+Kv1sR9xrs25EXZF8KEETfhoJnN6KY1RwG7HsOfAQ3dV\n\t\tLWInQRaC/8JPyVS2zbd0+NRBJOnq4/quv/P3C4SBP98/ZuGrqN59uifyqC3Kodkl\n\t\tWkG/2UdhiLlCmOtsU+BYDZrSiYK1R9FNnlQCOGrkuVxpDwa2TbbvEEzQP7RXxotA\n\t\tKlxejvrY4VuK8agNqvffVofbdIIperK65K4+0mYIb+A6fU8QQHlCbti4ERSZ6UYD\n\t\tF/SjRih31+SAtWb42jueAA==\n\t\t-----END CERTIFICATE-----\n\tSome Cert (1):\n\t\tSerial Number: 0\n\t\tSubject: CN=SEV-VCEK,OU=Engineering,O=Advanced Micro Devices,L=Santa Clara,ST=CA,C=US\n\t\tIssuer: CN=SEV-Milan,OU=Engineering,O=Advanced Micro Devices,L=Santa Clara,ST=CA,C=US\n\t\tNot Before: 2022-11-23 22:33:47 +0000 UTC\n\t\tNot After: 2029-11-23 22:33:47 +0000 UTC\n\t\tSignature Algorithm: SHA384-RSAPSS\n\t\tPublic Key Algorithm: ECDSA\n"
testCases := map[string]struct {
cert []byte
expected string
wantErr bool
}{
"one cert": {
cert: []byte(validCert),
expected: validCertExpected,
},
"one cert with extra newlines": {
cert: []byte("\n\n" + validCert + "\n\n"),
expected: validCertExpected,
},
"invalid cert": {
cert: []byte("invalid"),
wantErr: true,
},
"no cert": {
wantErr: true,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
b := &strings.Builder{}
formatter := &defaultAttestationDocFormatter{
log: logger.NewTest(t),
}
err := formatter.parseCerts(b, "Some Cert", tc.cert)
if tc.wantErr {
assert.Error(err)
} else {
assert.NoError(err)
assert.Equal(tc.expected, b.String())
}
})
}
}
func TestVerifyClient(t *testing.T) { func TestVerifyClient(t *testing.T) {
testCases := map[string]struct { testCases := map[string]struct {
attestationDoc atls.FakeAttestationDoc attestationDoc atls.FakeAttestationDoc

View File

@ -67,11 +67,13 @@ func (i *Issuer) getInstanceInfo(ctx context.Context, tpm io.ReadWriteCloser, us
} }
instanceInfo := snp.InstanceInfo{ instanceInfo := snp.InstanceInfo{
VCEK: params.VcekCert, ReportSigner: params.VcekCert,
CertChain: params.VcekChain, CertChain: params.VcekChain,
AttestationReport: params.SNPReport, AttestationReport: params.SNPReport,
RuntimeData: params.RuntimeData, Azure: &snp.AzureInstanceInfo{
MAAToken: maaToken, RuntimeData: params.RuntimeData,
MAAToken: maaToken,
},
} }
statement, err := json.Marshal(instanceInfo) statement, err := json.Marshal(instanceInfo)
if err != nil { if err != nil {

View File

@ -104,11 +104,11 @@ func TestGetSNPAttestation(t *testing.T) {
err = json.Unmarshal(attestationJSON, &instanceInfo) err = json.Unmarshal(attestationJSON, &instanceInfo)
require.NoError(err) require.NoError(err)
assert.Equal(params.VcekCert, instanceInfo.VCEK) assert.Equal(params.VcekCert, instanceInfo.ReportSigner)
assert.Equal(params.VcekChain, instanceInfo.CertChain) assert.Equal(params.VcekChain, instanceInfo.CertChain)
assert.Equal(params.SNPReport, instanceInfo.AttestationReport) assert.Equal(params.SNPReport, instanceInfo.AttestationReport)
assert.Equal(params.RuntimeData, instanceInfo.RuntimeData) assert.Equal(params.RuntimeData, instanceInfo.Azure.RuntimeData)
assert.Equal(tc.maaToken, instanceInfo.MAAToken) assert.Equal(tc.maaToken, instanceInfo.Azure.MAAToken)
}) })
} }
} }

View File

@ -179,7 +179,10 @@ func (v *Validator) getTrustedKey(ctx context.Context, attDoc vtpm.AttestationDo
} }
// Custom check of the IDKeyDigests, taking care of the WarnOnly / MAAFallback cases, // Custom check of the IDKeyDigests, taking care of the WarnOnly / MAAFallback cases,
// but also double-checking the IDKeyDigests if the enforcement policy is set to Equal. // but also double-checking the IDKeyDigests if the enforcement policy is set to Equal.
if err := v.checkIDKeyDigest(ctx, att, instanceInfo.MAAToken, extraData); err != nil { if instanceInfo.Azure == nil {
return nil, errors.New("missing Azure info from instanceInfo")
}
if err := v.checkIDKeyDigest(ctx, att, instanceInfo.Azure.MAAToken, extraData); err != nil {
return nil, fmt.Errorf("checking IDKey digests: %w", err) return nil, fmt.Errorf("checking IDKey digests: %w", err)
} }
@ -188,7 +191,7 @@ func (v *Validator) getTrustedKey(ctx context.Context, attDoc vtpm.AttestationDo
if err != nil { if err != nil {
return nil, err return nil, err
} }
if err = v.hclValidator.validate(instanceInfo.RuntimeData, att.Report.ReportData, pubArea.RSAParameters); err != nil { if err = v.hclValidator.validate(instanceInfo.Azure.RuntimeData, att.Report.ReportData, pubArea.RSAParameters); err != nil {
return nil, fmt.Errorf("validating HCLAkPub: %w", err) return nil, fmt.Errorf("validating HCLAkPub: %w", err)
} }

View File

@ -233,7 +233,7 @@ func TestValidateAk(t *testing.T) {
defaultRuntimeDataRaw, err := json.Marshal(runtimeData) defaultRuntimeDataRaw, err := json.Marshal(runtimeData)
require.NoError(err) require.NoError(err)
defaultInstanceInfo := snp.InstanceInfo{RuntimeData: defaultRuntimeDataRaw} defaultInstanceInfo := snp.InstanceInfo{Azure: &snp.AzureInstanceInfo{RuntimeData: defaultRuntimeDataRaw}}
sig := sha256.Sum256(defaultRuntimeDataRaw) sig := sha256.Sum256(defaultRuntimeDataRaw)
defaultReportData := sig[:] defaultReportData := sig[:]
@ -794,9 +794,13 @@ func (v *stubAttestationValidator) SNPAttestation(attestation *spb.Attestation,
type stubInstanceInfo struct { type stubInstanceInfo struct {
AttestationReport []byte AttestationReport []byte
RuntimeData []byte ReportSigner []byte
VCEK []byte
CertChain []byte CertChain []byte
Azure *stubAzureInstanceInfo
}
type stubAzureInstanceInfo struct {
RuntimeData []byte
} }
func newStubInstanceInfo(vcek, certChain []byte, report, runtimeData string) (stubInstanceInfo, error) { func newStubInstanceInfo(vcek, certChain []byte, report, runtimeData string) (stubInstanceInfo, error) {
@ -812,9 +816,11 @@ func newStubInstanceInfo(vcek, certChain []byte, report, runtimeData string) (st
return stubInstanceInfo{ return stubInstanceInfo{
AttestationReport: validReport, AttestationReport: validReport,
RuntimeData: decodedRuntime, ReportSigner: vcek,
VCEK: vcek,
CertChain: certChain, CertChain: certChain,
Azure: &stubAzureInstanceInfo{
RuntimeData: decodedRuntime,
},
}, nil }, nil
} }

View File

@ -22,15 +22,21 @@ import (
"github.com/google/go-sev-guest/verify/trust" "github.com/google/go-sev-guest/verify/trust"
) )
// InstanceInfo contains the necessary information to establish trust in // InstanceInfo contains the necessary information to establish trust in a SNP CVM.
// an Azure CVM.
type InstanceInfo struct { type InstanceInfo struct {
// VCEK is the PEM-encoded VCEK certificate for the attestation report. // ReportSigner is the PEM-encoded ReportSigner/VLEK certificate for the attestation report.
VCEK []byte // Public key that validates the report's signature.
// CertChain is the PEM-encoded certificate chain for the attestation report. 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 CertChain []byte
// AttestationReport is the attestation report from the vTPM (NVRAM) of the CVM. // AttestationReport is the attestation report from the vTPM (NVRAM) of the CVM.
AttestationReport []byte AttestationReport []byte
Azure *AzureInstanceInfo
}
// 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 is the Azure runtime data from the vTPM (NVRAM) of the CVM.
RuntimeData []byte RuntimeData []byte
// MAAToken is the token of the MAA for the attestation report, used as a fallback // MAAToken is the token of the MAA for the attestation report, used as a fallback
@ -126,11 +132,13 @@ func (a *InstanceInfo) AttestationWithCerts(logger attestation.Logger, getter tr
return att, nil return att, nil
} }
// CertificateChain stores an AMD signing key (ASK) and AMD root key (ARK) certificate.
type CertificateChain struct { type CertificateChain struct {
ask *x509.Certificate ask *x509.Certificate
ark *x509.Certificate ark *x509.Certificate
} }
// NewCertificateChain returns a new CertificateChain with the given ASK and ARK certificates.
func NewCertificateChain(ask, ark *x509.Certificate) CertificateChain { func NewCertificateChain(ask, ark *x509.Certificate) CertificateChain {
return CertificateChain{ return CertificateChain{
ask: ask, ask: ask,
@ -191,7 +199,7 @@ func (a *InstanceInfo) ParseCertChain() (ask, ark *x509.Certificate, retErr erro
// ParseVCEK parses the VCEK certificate from the instanceInfo into an x509-formatted certificate. // ParseVCEK parses the VCEK certificate from the instanceInfo into an x509-formatted certificate.
// If the VCEK certificate is not present, nil is returned. // If the VCEK certificate is not present, nil is returned.
func (a *InstanceInfo) ParseVCEK() (*x509.Certificate, error) { func (a *InstanceInfo) ParseVCEK() (*x509.Certificate, error) {
newlinesTrimmed := bytes.TrimSpace(a.VCEK) newlinesTrimmed := bytes.TrimSpace(a.ReportSigner)
if len(newlinesTrimmed) == 0 { if len(newlinesTrimmed) == 0 {
// VCEK is not present. // VCEK is not present.
return nil, nil return nil, nil

View File

@ -110,7 +110,7 @@ func TestParseVCEK(t *testing.T) {
assert := assert.New(t) assert := assert.New(t)
instanceInfo := &InstanceInfo{ instanceInfo := &InstanceInfo{
VCEK: tc.VCEK, ReportSigner: tc.VCEK,
} }
vcek, err := instanceInfo.ParseVCEK() vcek, err := instanceInfo.ParseVCEK()
@ -235,7 +235,7 @@ func TestInstanceInfoAttestation(t *testing.T) {
instanceInfo := InstanceInfo{ instanceInfo := InstanceInfo{
AttestationReport: tc.report, AttestationReport: tc.report,
CertChain: tc.certChain, CertChain: tc.certChain,
VCEK: tc.vcek, ReportSigner: tc.vcek,
} }
att, err := instanceInfo.AttestationWithCerts(logger.NewTest(t), tc.getter, tc.fallbackCerts) att, err := instanceInfo.AttestationWithCerts(logger.NewTest(t), tc.getter, tc.fallbackCerts)

View File

@ -1,9 +1,31 @@
load("@io_bazel_rules_go//go:def.bzl", "go_library") load("@io_bazel_rules_go//go:def.bzl", "go_library")
load("//bazel/go:go_test.bzl", "go_test")
go_library( go_library(
name = "verify", name = "verify",
srcs = ["verify.go"], srcs = [
"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 = ["@com_github_golang_jwt_jwt_v5//:jwt"], deps = [
"//internal/attestation/snp",
"//internal/constants",
"//internal/kubernetes/kubectl",
"@com_github_golang_jwt_jwt_v5//:jwt",
"@com_github_google_go_sev_guest//abi",
"@com_github_google_go_sev_guest//kds",
],
)
go_test(
name = "verify_test",
srcs = ["verify_test.go"],
embed = [":verify"],
deps = [
"//internal/attestation/snp/testdata",
"//internal/logger",
"@com_github_stretchr_testify//assert",
],
) )

View File

@ -0,0 +1,29 @@
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

@ -9,29 +9,256 @@ Package verify provides the types for the verify report in JSON format.
The package provides an interface for constellation verify and The package provides an interface for constellation verify and
the attestationconfigapi upload tool through JSON serialization. the attestationconfigapi upload tool through JSON serialization.
It exposes a CSP-agnostic interface for printing Reports that may include CSP-specific information.
*/ */
package verify package verify
import ( import (
"context"
"crypto/x509"
"encoding/json"
"encoding/pem"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"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/golang-jwt/jwt/v5" "github.com/golang-jwt/jwt/v5"
"github.com/google/go-sev-guest/abi"
"github.com/google/go-sev-guest/kds"
) )
// 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"`
VCEK []Certificate `json:"vcek"` ReportSigner []Certificate `json:"vcek"`
CertChain []Certificate `json:"cert_chain"` CertChain []Certificate `json:"cert_chain"`
MAAToken MaaTokenClaims `json:"maa_token"` *AzureReportAddition `json:"azure,omitempty"`
*AWSReportAddition `json:"aws,omitempty"`
}
// AzureReportAddition contains attestation report data specific to Azure.
type AzureReportAddition struct {
MAAToken MaaTokenClaims `json:"maa_token"`
}
// AWSReportAddition contains attestation report data specific to AWS.
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) {
snpReport, err := newSNPReport(instanceInfo.AttestationReport)
if err != nil {
return Report{}, fmt.Errorf("parsing SNP report: %w", err)
}
var certTypeName string
switch snpReport.SignerInfo.SigningKey {
case abi.VlekReportSigner.String():
certTypeName = "VLEK certificate"
case abi.VcekReportSigner.String():
certTypeName = "VCEK certificate"
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)
}
// 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)
if err != nil {
return Report{}, fmt.Errorf("getting certificate chain cache: %w", err)
}
} else {
pemCerts = instanceInfo.CertChain
}
certChain, err := newCertificates("Certificate chain", pemCerts, log)
if err != nil {
return Report{}, fmt.Errorf("parsing certificate chain: %w", err)
}
var azure *AzureReportAddition
var aws *AWSReportAddition
if instanceInfo.Azure != nil {
maaToken, err := newMAAToken(ctx, instanceInfo.Azure.MAAToken, attestationServiceURL)
if err != nil {
return Report{}, fmt.Errorf("parsing MAA token: %w", err)
}
azure = &AzureReportAddition{
MAAToken: maaToken,
}
}
return Report{
SNPReport: snpReport,
ReportSigner: reportSigner,
CertChain: certChain,
AzureReportAddition: azure,
AWSReportAddition: aws,
}, 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 {
return "", fmt.Errorf("expected exactly one report signing certificate, found %d", len(r.ReportSigner))
}
if err := formatCertificates(b, r.ReportSigner); err != nil {
return "", fmt.Errorf("building report signing certificate string: %w", err)
}
if err := formatCertificates(b, r.CertChain); err != nil {
return "", fmt.Errorf("building certificate chain string: %w", err)
}
r.SNPReport.formatString(b)
if r.AzureReportAddition != nil {
if err := r.AzureReportAddition.MAAToken.formatString(b); err != nil {
return "", fmt.Errorf("error building MAAToken string : %w", err)
}
}
return b.String(), nil
}
func formatCertificates(b *strings.Builder, certs []Certificate) error {
for i, cert := range certs {
if i == 0 {
b.WriteString(fmt.Sprintf("\tRaw %s:\n", cert.CertTypeName))
}
newlinesTrimmed := strings.TrimSpace(cert.CertificatePEM)
formattedCert := strings.ReplaceAll(newlinesTrimmed, "\n", "\n\t\t") + "\n"
b.WriteString(fmt.Sprintf("\t\t%s", formattedCert))
}
for i, cert := range certs {
// Use 1-based indexing for user output.
if err := cert.formatString(b, i+1); err != nil {
return fmt.Errorf("error printing certificate chain: %w", err)
}
}
return nil
} }
// Certificate contains the certificate data and additional information. // Certificate contains the certificate data and additional information.
type Certificate struct { type Certificate struct {
CertificatePEM string `json:"certificate"` x509.Certificate `json:"-"`
CertTypeName string `json:"cert_type_name"` CertificatePEM string `json:"certificate"`
StructVersion uint8 `json:"struct_version"` CertTypeName string `json:"cert_type_name"`
ProductName string `json:"product_name"` StructVersion uint8 `json:"struct_version"`
HardwareID []byte `json:"hardware_id"` ProductName string `json:"product_name"`
TCBVersion TCBVersion `json:"tcb_version"` HardwareID []byte `json:"hardware_id"`
TCBVersion TCBVersion `json:"tcb_version"`
}
// newCertificates parses a list of PEM encoded certificate and returns a slice of Certificate objects.
func newCertificates(certTypeName string, cert []byte, log debugLog) (certs []Certificate, err error) {
newlinesTrimmed := strings.TrimSpace(string(cert))
log.Debugf("Decoding PEM certificate: %s", certTypeName)
i := 1
var rest []byte
var block *pem.Block
for block, rest = pem.Decode([]byte(newlinesTrimmed)); block != nil; block, rest = pem.Decode(rest) {
log.Debugf("Parsing PEM block: %d", i)
if block.Type != "CERTIFICATE" {
return certs, fmt.Errorf("parse %s: expected PEM block type 'CERTIFICATE', got '%s'", certTypeName, block.Type)
}
cert, 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)
if err != nil {
return certs, fmt.Errorf("parsing VCEK certificate extensions: %w", 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,
})
}
i++
}
if i == 1 {
return certs, fmt.Errorf("parse %s: no PEM blocks found", certTypeName)
}
if len(rest) != 0 {
return certs, fmt.Errorf("parse %s: remaining PEM block is not a valid certificate: %s", certTypeName, rest)
}
return certs, nil
}
// formatString builds a string representation of a certificate that is inteded for console output.
func (c *Certificate) formatString(b *strings.Builder, idx int) error {
writeIndentfln(b, 1, "%s (%d):", c.CertTypeName, idx)
writeIndentfln(b, 2, "Serial Number: %s", c.Certificate.SerialNumber)
writeIndentfln(b, 2, "Subject: %s", c.Certificate.Subject)
writeIndentfln(b, 2, "Issuer: %s", c.Certificate.Issuer)
writeIndentfln(b, 2, "Not Before: %s", c.Certificate.NotBefore)
writeIndentfln(b, 2, "Not After: %s", c.Certificate.NotAfter)
writeIndentfln(b, 2, "Signature Algorithm: %s", c.Certificate.SignatureAlgorithm)
writeIndentfln(b, 2, "Public Key Algorithm: %s", c.Certificate.PublicKeyAlgorithm)
if c.CertTypeName == "VCEK certificate" {
// 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)
}
writeIndentfln(b, 2, "Struct version: %d", vcekExts.StructVersion)
writeIndentfln(b, 2, "Product name: %s", vcekExts.ProductName)
tcb := kds.DecomposeTCBVersion(vcekExts.TCBVersion)
writeIndentfln(b, 2, "Secure Processor bootloader SVN: %d", tcb.BlSpl)
writeIndentfln(b, 2, "Secure Processor operating system SVN: %d", tcb.TeeSpl)
writeIndentfln(b, 2, "SVN 4 (reserved): %d", tcb.Spl4)
writeIndentfln(b, 2, "SVN 5 (reserved): %d", tcb.Spl5)
writeIndentfln(b, 2, "SVN 6 (reserved): %d", tcb.Spl6)
writeIndentfln(b, 2, "SVN 7 (reserved): %d", tcb.Spl7)
writeIndentfln(b, 2, "SEV-SNP firmware SVN: %d", tcb.SnpSpl)
writeIndentfln(b, 2, "Microcode SVN: %d", tcb.UcodeSpl)
writeIndentfln(b, 2, "Hardware ID: %x", vcekExts.HWID)
}
return nil
} }
// TCBVersion contains the TCB version data. // TCBVersion contains the TCB version data.
@ -46,6 +273,33 @@ type TCBVersion struct {
Spl7 uint8 `json:"spl7"` Spl7 uint8 `json:"spl7"`
} }
// formatString builds a string representation of a TCB version that is inteded for console output.
func (t *TCBVersion) formatString(b *strings.Builder) {
writeIndentfln(b, 3, "Secure Processor bootloader SVN: %d", t.Bootloader)
writeIndentfln(b, 3, "Secure Processor operating system SVN: %d", t.TEE)
writeIndentfln(b, 3, "SVN 4 (reserved): %d", t.Spl4)
writeIndentfln(b, 3, "SVN 5 (reserved): %d", t.Spl5)
writeIndentfln(b, 3, "SVN 6 (reserved): %d", t.Spl6)
writeIndentfln(b, 3, "SVN 7 (reserved): %d", t.Spl7)
writeIndentfln(b, 3, "SEV-SNP firmware SVN: %d", t.SNP)
writeIndentfln(b, 3, "Microcode SVN: %d", t.Microcode)
}
// newTCBVersion creates a TCB version from a kds.TCBVersion.
func newTCBVersion(tcbVersion kds.TCBVersion) TCBVersion {
tcb := kds.DecomposeTCBVersion(tcbVersion)
return TCBVersion{
Bootloader: tcb.BlSpl,
TEE: tcb.TeeSpl,
SNP: tcb.SnpSpl,
Microcode: tcb.UcodeSpl,
Spl4: tcb.Spl4,
Spl5: tcb.Spl5,
Spl6: tcb.Spl6,
Spl7: tcb.Spl7,
}
}
// PlatformInfo contains the platform information. // PlatformInfo contains the platform information.
type PlatformInfo struct { type PlatformInfo struct {
SMT bool `json:"smt"` SMT bool `json:"smt"`
@ -96,6 +350,125 @@ type SNPReport struct {
Signature []byte `json:"signature"` Signature []byte `json:"signature"`
} }
// newSNPReport parses a marshalled SNP report and returns a SNPReport object.
func newSNPReport(reportBytes []byte) (SNPReport, error) {
report, err := abi.ReportToProto(reportBytes)
if err != nil {
return SNPReport{}, fmt.Errorf("parsing report to proto: %w", err)
}
policy, err := abi.ParseSnpPolicy(report.Policy)
if err != nil {
return SNPReport{}, fmt.Errorf("parsing policy: %w", err)
}
platformInfo, err := abi.ParseSnpPlatformInfo(report.PlatformInfo)
if err != nil {
return SNPReport{}, fmt.Errorf("parsing platform info: %w", err)
}
signature, err := abi.ReportToSignatureDER(reportBytes)
if err != nil {
return SNPReport{}, fmt.Errorf("parsing signature: %w", err)
}
signerInfo, err := abi.ParseSignerInfo(report.SignerInfo)
if err != nil {
return SNPReport{}, fmt.Errorf("parsing signer info: %w", err)
}
return SNPReport{
Version: report.Version,
GuestSvn: report.GuestSvn,
PolicyABIMinor: policy.ABIMinor,
PolicyABIMajor: policy.ABIMajor,
PolicySMT: policy.SMT,
PolicyMigrationAgent: policy.MigrateMA,
PolicyDebug: policy.Debug,
PolicySingleSocket: policy.SingleSocket,
FamilyID: report.FamilyId,
ImageID: report.ImageId,
Vmpl: report.Vmpl,
SignatureAlgo: report.SignatureAlgo,
CurrentTCB: newTCBVersion(kds.TCBVersion(report.CurrentTcb)),
PlatformInfo: PlatformInfo{
SMT: platformInfo.SMTEnabled,
TSME: platformInfo.TSMEEnabled,
},
SignerInfo: SignerInfo{
AuthorKey: signerInfo.AuthorKeyEn,
MaskChipKey: signerInfo.MaskChipKey,
SigningKey: signerInfo.SigningKey.String(),
},
ReportData: report.ReportData,
Measurement: report.Measurement,
HostData: report.HostData,
IDKeyDigest: report.IdKeyDigest,
AuthorKeyDigest: report.AuthorKeyDigest,
ReportID: report.ReportId,
ReportIDMa: report.ReportIdMa,
ReportedTCB: newTCBVersion(kds.TCBVersion(report.ReportedTcb)),
ChipID: report.ChipId,
CommittedTCB: newTCBVersion(kds.TCBVersion(report.CommittedTcb)),
CurrentBuild: report.CurrentBuild,
CurrentMinor: report.CurrentMinor,
CurrentMajor: report.CurrentMajor,
CommittedBuild: report.CommittedBuild,
CommittedMinor: report.CommittedMinor,
CommittedMajor: report.CommittedMajor,
LaunchTCB: newTCBVersion(kds.TCBVersion(report.LaunchTcb)),
Signature: signature,
}, nil
}
// formatString builds a string representation of a SNP report that is inteded for console output.
func (s *SNPReport) formatString(b *strings.Builder) {
writeIndentfln(b, 1, "SNP Report:")
writeIndentfln(b, 2, "Version: %d", s.Version)
writeIndentfln(b, 2, "Guest SVN: %d", s.GuestSvn)
writeIndentfln(b, 2, "Policy:")
writeIndentfln(b, 3, "ABI Minor: %d", s.PolicyABIMinor)
writeIndentfln(b, 3, "ABI Major: %d", s.PolicyABIMajor)
writeIndentfln(b, 3, "Symmetric Multithreading enabled: %t", s.PolicySMT)
writeIndentfln(b, 3, "Migration agent enabled: %t", s.PolicyMigrationAgent)
writeIndentfln(b, 3, "Debugging enabled (host decryption of VM): %t", s.PolicyDebug)
writeIndentfln(b, 3, "Single socket enabled: %t", s.PolicySingleSocket)
writeIndentfln(b, 2, "Family ID: %x", s.FamilyID)
writeIndentfln(b, 2, "Image ID: %x", s.ImageID)
writeIndentfln(b, 2, "VMPL: %d", s.Vmpl)
writeIndentfln(b, 2, "Signature Algorithm: %d", s.SignatureAlgo)
writeIndentfln(b, 2, "Current TCB:")
s.CurrentTCB.formatString(b)
writeIndentfln(b, 2, "Platform Info:")
writeIndentfln(b, 3, "Symmetric Multithreading enabled (SMT): %t", s.PlatformInfo.SMT)
writeIndentfln(b, 3, "Transparent secure memory encryption (TSME): %t", s.PlatformInfo.TSME)
writeIndentfln(b, 2, "Signer Info:")
writeIndentfln(b, 3, "Author Key Enabled: %t", s.SignerInfo.AuthorKey)
writeIndentfln(b, 3, "Chip ID Masking: %t", s.SignerInfo.MaskChipKey)
writeIndentfln(b, 3, "Signing Type: %s", s.SignerInfo.SigningKey)
writeIndentfln(b, 2, "Report Data: %x", s.ReportData)
writeIndentfln(b, 2, "Measurement: %x", s.Measurement)
writeIndentfln(b, 2, "Host Data: %x", s.HostData)
writeIndentfln(b, 2, "ID Key Digest: %x", s.IDKeyDigest)
writeIndentfln(b, 2, "Author Key Digest: %x", s.AuthorKeyDigest)
writeIndentfln(b, 2, "Report ID: %x", s.ReportID)
writeIndentfln(b, 2, "Report ID MA: %x", s.ReportIDMa)
writeIndentfln(b, 2, "Reported TCB:")
s.ReportedTCB.formatString(b)
writeIndentfln(b, 2, "Chip ID: %x", s.ChipID)
writeIndentfln(b, 2, "Committed TCB:")
s.CommittedTCB.formatString(b)
writeIndentfln(b, 2, "Current Build: %d", s.CurrentBuild)
writeIndentfln(b, 2, "Current Minor: %d", s.CurrentMinor)
writeIndentfln(b, 2, "Current Major: %d", s.CurrentMajor)
writeIndentfln(b, 2, "Committed Build: %d", s.CommittedBuild)
writeIndentfln(b, 2, "Committed Minor: %d", s.CommittedMinor)
writeIndentfln(b, 2, "Committed Major: %d", s.CommittedMajor)
writeIndentfln(b, 2, "Launch TCB:")
s.LaunchTCB.formatString(b)
writeIndentfln(b, 2, "Signature (DER):")
writeIndentfln(b, 3, "%x", s.Signature)
}
// MaaTokenClaims contains the MAA token claims. // MaaTokenClaims contains the MAA token claims.
type MaaTokenClaims struct { type MaaTokenClaims struct {
jwt.RegisteredClaims jwt.RegisteredClaims
@ -174,3 +547,113 @@ type MaaTokenClaims struct {
} `json:"x-ms-runtime,omitempty"` } `json:"x-ms-runtime,omitempty"`
XMsVer string `json:"x-ms-ver,omitempty"` XMsVer string `json:"x-ms-ver,omitempty"`
} }
// newMAAToken parses a MAA token and returns a MaaTokenClaims object.
func newMAAToken(ctx context.Context, rawToken, attestationServiceURL string) (MaaTokenClaims, error) {
var claims MaaTokenClaims
_, err := jwt.ParseWithClaims(rawToken, &claims, keyFromJKUFunc(ctx, attestationServiceURL), jwt.WithIssuedAt())
return claims, err
}
// formatString builds a string representation of a MAA token that is inteded for console output.
func (m *MaaTokenClaims) formatString(b *strings.Builder) error {
out, err := json.MarshalIndent(m, "\t\t", " ")
if err != nil {
return fmt.Errorf("marshaling claims: %w", err)
}
b.WriteString("\tMicrosoft Azure Attestation Token:\n\t")
b.WriteString(string(out))
return nil
}
// writeIndentfln writes a formatted string to the builder with the given indentation level
// and a newline at the end.
func writeIndentfln(b *strings.Builder, indentLvl int, format string, args ...any) {
for i := 0; i < indentLvl; i++ {
b.WriteByte('\t')
}
b.WriteString(fmt.Sprintf(format+"\n", args...))
}
// keyFromJKUFunc returns a function that gets the JSON Web Key URI from the token
// and fetches the key from that URI. The keys are then parsed, and the key with
// the kid that matches the token header is returned.
func keyFromJKUFunc(ctx context.Context, webKeysURLBase string) func(token *jwt.Token) (any, error) {
return func(token *jwt.Token) (any, error) {
webKeysURL, err := url.JoinPath(webKeysURLBase, "certs")
if err != nil {
return nil, fmt.Errorf("joining web keys base URL with path: %w", err)
}
if token.Header["alg"] != "RS256" {
return nil, fmt.Errorf("invalid signing algorithm: %s", token.Header["alg"])
}
kid, ok := token.Header["kid"].(string)
if !ok {
return nil, fmt.Errorf("invalid kid: %v", token.Header["kid"])
}
jku, ok := token.Header["jku"].(string)
if !ok {
return nil, fmt.Errorf("invalid jku: %v", token.Header["jku"])
}
if jku != webKeysURL {
return nil, fmt.Errorf("jku from token (%s) does not match configured attestation service (%s)", jku, webKeysURL)
}
keySetBytes, err := httpGet(ctx, jku)
if err != nil {
return nil, fmt.Errorf("getting signing keys from jku %s: %w", jku, err)
}
var rawKeySet struct {
Keys []struct {
X5c [][]byte
Kid string
}
}
if err := json.Unmarshal(keySetBytes, &rawKeySet); err != nil {
return nil, err
}
for _, key := range rawKeySet.Keys {
if key.Kid != kid {
continue
}
cert, err := x509.ParseCertificate(key.X5c[0])
if err != nil {
return nil, fmt.Errorf("parsing certificate: %w", err)
}
return cert.PublicKey, nil
}
return nil, fmt.Errorf("no key found for kid %s", kid)
}
}
func httpGet(ctx context.Context, url string) ([]byte, error) {
req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, http.NoBody)
if err != nil {
return nil, err
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, errors.New(resp.Status)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
return body, nil
}
type debugLog interface {
Debugf(format string, args ...any)
}

View File

@ -0,0 +1,64 @@
/*
Copyright (c) Edgeless Systems GmbH
SPDX-License-Identifier: AGPL-3.0-only
*/
package verify
import (
"strings"
"testing"
"github.com/edgelesssys/constellation/v2/internal/attestation/snp/testdata"
"github.com/edgelesssys/constellation/v2/internal/logger"
"github.com/stretchr/testify/assert"
)
func TestParseCerts(t *testing.T) {
validCertExpected := "\tRaw Some Cert:\n\t\t-----BEGIN CERTIFICATE-----\n\t\tMIIFTDCCAvugAwIBAgIBADBGBgkqhkiG9w0BAQowOaAPMA0GCWCGSAFlAwQCAgUA\n\t\toRwwGgYJKoZIhvcNAQEIMA0GCWCGSAFlAwQCAgUAogMCATCjAwIBATB7MRQwEgYD\n\t\tVQQLDAtFbmdpbmVlcmluZzELMAkGA1UEBhMCVVMxFDASBgNVBAcMC1NhbnRhIENs\n\t\tYXJhMQswCQYDVQQIDAJDQTEfMB0GA1UECgwWQWR2YW5jZWQgTWljcm8gRGV2aWNl\n\t\tczESMBAGA1UEAwwJU0VWLU1pbGFuMB4XDTIzMDgzMDEyMTUyNFoXDTMwMDgzMDEy\n\t\tMTUyNFowejEUMBIGA1UECwwLRW5naW5lZXJpbmcxCzAJBgNVBAYTAlVTMRQwEgYD\n\t\tVQQHDAtTYW50YSBDbGFyYTELMAkGA1UECAwCQ0ExHzAdBgNVBAoMFkFkdmFuY2Vk\n\t\tIE1pY3JvIERldmljZXMxETAPBgNVBAMMCFNFVi1WQ0VLMHYwEAYHKoZIzj0CAQYF\n\t\tK4EEACIDYgAEhPX8Cl9uA7PxqNGzeqamJNYJLx/VFE/s3+8qOWtaztKNcn1PaAI4\n\t\tndE+yaVfMHsiA8CLTylumpWXcVBHPYV9kPEVrtozhvrrT5Oii9OpZPYHJ7/WPVmM\n\t\tJ3K8/Iz3AshTo4IBFjCCARIwEAYJKwYBBAGceAEBBAMCAQAwFwYJKwYBBAGceAEC\n\t\tBAoWCE1pbGFuLUIwMBEGCisGAQQBnHgBAwEEAwIBAjARBgorBgEEAZx4AQMCBAMC\n\t\tAQAwEQYKKwYBBAGceAEDBAQDAgEAMBEGCisGAQQBnHgBAwUEAwIBADARBgorBgEE\n\t\tAZx4AQMGBAMCAQAwEQYKKwYBBAGceAEDBwQDAgEAMBEGCisGAQQBnHgBAwMEAwIB\n\t\tBjARBgorBgEEAZx4AQMIBAMCAV0wTQYJKwYBBAGceAEEBECeRKrvAs/Kb926ymac\n\t\tbP0p4auNl+vJOYVxKKy7E7h0DfMUNtNOhuX4rgzf6zoOGF20beysF2zHfXYcIqG5\n\t\t3PJbMEYGCSqGSIb3DQEBCjA5oA8wDQYJYIZIAWUDBAICBQChHDAaBgkqhkiG9w0B\n\t\tAQgwDQYJYIZIAWUDBAICBQCiAwIBMKMDAgEBA4ICAQBoVGgDdFV9gWPHaEOBrHzd\n\t\tWVYyuuMBH340DDSXbCGlPR6rhgja0qALmkUPG50REQGvoPsikAskwqhzRG2XEDO2\n\t\tb6+fRPIq3DjEbz/8V89IiYiOZI/ycFACi3EEVECAWbzjXSfiOio1NfbniXP6tWzW\n\t\tD/8xpd/8N8166bHpgNgMl9pX4i0I9vaTl3qH+jBuSMZ5Q4heTHLB+v4V7q+H6SZo\n\t\t7htqpaI3keLEhQL/pCP72udMPAzU+/5W/x/t/LD6SbQcQQoHbWDU6kgTDuXabDxl\n\t\tA4JoEZfatr+/TO6jKQcGtqOLKT8JFGcigUlBi/TBVP+Xs8E4CWYGZZiTpYoLwNAu\n\t\tyuKOP9VVFViSCqPvzpNs2G+e0zXg2w3te7oMw/l0bD8iQCAS8rR0+r+8pZL4e010\n\t\tKLZ3yEfA0moXef66k5xyf4y37ZIP189wz6qJ+YXqOujDmeTomCU0SnZXlri6GhbF\n\t\t19rp2z5/lsZG+W27CRxvzTB3hk+ukZr35vCqNq4Rs+c7/hYcYzzyZ4ysATwdglNF\n\t\tWddfVw5Qunlu6Ngxr84ifz3HrnUx9bR5DzmFbztrb7IbkZhq7GjImwJULub1viyg\n\t\tYFa7X3p8b1WllienSEfvbadobbS9HeuLUrWyh0kZjQnz+0Q1UB1/zlzokeQmAYCf\n\t\t8H3kABPv6hqrFftRNbargQ==\n\t\t-----END CERTIFICATE-----\n\tSome Cert (1):\n\t\tSerial Number: 0\n\t\tSubject: CN=SEV-VCEK,OU=Engineering,O=Advanced Micro Devices,L=Santa Clara,ST=CA,C=US\n\t\tIssuer: CN=SEV-Milan,OU=Engineering,O=Advanced Micro Devices,L=Santa Clara,ST=CA,C=US\n\t\tNot Before: 2023-08-30 12:15:24 +0000 UTC\n\t\tNot After: 2030-08-30 12:15:24 +0000 UTC\n\t\tSignature Algorithm: SHA384-RSAPSS\n\t\tPublic Key Algorithm: ECDSA\n"
testCases := map[string]struct {
cert []byte
expected string
wantErr bool
}{
"one cert": {
cert: testdata.AzureThimVCEK,
expected: validCertExpected,
},
"one cert with extra newlines": {
cert: []byte("\n\n" + string(testdata.AzureThimVCEK) + "\n\n"),
expected: validCertExpected,
},
"invalid cert": {
cert: []byte("invalid"),
wantErr: true,
},
"no cert": {
wantErr: true,
},
}
for name, tc := range testCases {
t.Run(name, func(t *testing.T) {
assert := assert.New(t)
b := &strings.Builder{}
certs, err := newCertificates("Some Cert", tc.cert, logger.NewTest(t))
if err != nil {
assert.True(tc.wantErr)
return
}
err = formatCertificates(b, certs)
if tc.wantErr {
assert.Error(err)
} else {
assert.NoError(err)
assert.Equal(tc.expected, b.String())
}
})
}
}