diff --git a/cli/internal/cmd/BUILD.bazel b/cli/internal/cmd/BUILD.bazel index 3f927b33f..9b5a8ca95 100644 --- a/cli/internal/cmd/BUILD.bazel +++ b/cli/internal/cmd/BUILD.bazel @@ -88,9 +88,6 @@ go_library( "//internal/verify", "//internal/versions", "//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_uuid//:uuid", "@com_github_mattn_go_isatty//:go-isatty", diff --git a/cli/internal/cmd/verify.go b/cli/internal/cmd/verify.go index 42bb0f8a3..59d3bd570 100644 --- a/cli/internal/cmd/verify.go +++ b/cli/internal/cmd/verify.go @@ -9,16 +9,11 @@ package cmd import ( "bytes" "context" - "crypto/x509" "encoding/base64" "encoding/json" - "encoding/pem" "errors" "fmt" - "io" "net" - "net/http" - "net/url" "sort" "strconv" "strings" @@ -40,9 +35,6 @@ import ( "github.com/edgelesssys/constellation/v2/internal/state" "github.com/edgelesssys/constellation/v2/internal/verify" "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/cobra" "github.com/spf13/pflag" @@ -213,7 +205,7 @@ func (c *verifyCmd) verify(cmd *cobra.Command, verifyClient verifyClient, factor attDocOutput, err := formatter.format( cmd.Context(), rawAttestationDoc, - conf.Provider.Azure == nil, + (conf.Provider.Azure == nil && conf.Provider.AWS == nil), attConfig.GetMeasurements(), maaURL, ) @@ -274,34 +266,20 @@ type jsonAttestationDocFormatter struct { func (f *jsonAttestationDocFormatter) format(ctx context.Context, docString string, _ bool, _ measurements.M, attestationServiceURL string, ) (string, error) { - instanceInfo, err := extractAzureInstanceInfo(docString) - if err != nil { - return "", fmt.Errorf("unmarshal instance info: %w", err) + var doc attestationDoc + if err := json.Unmarshal([]byte(docString), &doc); err != nil { + 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 { 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) return string(jsonBytes), err @@ -344,97 +322,17 @@ func (f *defaultAttestationDocFormatter) format(ctx context.Context, docString s return b.String(), nil } - instanceInfoString, err := base64.StdEncoding.DecodeString(doc.InstanceInfo) + instanceInfo, err := extractInstanceInfo(doc) if err != nil { - return "", fmt.Errorf("decode instance info: %w", err) + return "", fmt.Errorf("unmarshalling instance info: %w", err) } - var instanceInfo snp.InstanceInfo - 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) + report, err := verify.NewReport(ctx, instanceInfo, attestationServiceURL, f.log) if err != nil { 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 -} - -// 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 + return report.FormatString(b) } // 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 } -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. type attestationDoc 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...)) } -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 -} - -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) - } - +func extractInstanceInfo(doc attestationDoc) (snp.InstanceInfo, error) { instanceInfoString, err := base64.StdEncoding.DecodeString(doc.InstanceInfo) if err != nil { return snp.InstanceInfo{}, fmt.Errorf("decode instance info: %w", err) diff --git a/cli/internal/cmd/verify_test.go b/cli/internal/cmd/verify_test.go index f381c86b8..a874125c3 100644 --- a/cli/internal/cmd/verify_test.go +++ b/cli/internal/cmd/verify_test.go @@ -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) { testCases := map[string]struct { attestationDoc atls.FakeAttestationDoc diff --git a/internal/attestation/azure/snp/issuer.go b/internal/attestation/azure/snp/issuer.go index dbba2cefb..3280a731c 100644 --- a/internal/attestation/azure/snp/issuer.go +++ b/internal/attestation/azure/snp/issuer.go @@ -67,11 +67,13 @@ func (i *Issuer) getInstanceInfo(ctx context.Context, tpm io.ReadWriteCloser, us } instanceInfo := snp.InstanceInfo{ - VCEK: params.VcekCert, + ReportSigner: params.VcekCert, CertChain: params.VcekChain, AttestationReport: params.SNPReport, - RuntimeData: params.RuntimeData, - MAAToken: maaToken, + Azure: &snp.AzureInstanceInfo{ + RuntimeData: params.RuntimeData, + MAAToken: maaToken, + }, } statement, err := json.Marshal(instanceInfo) if err != nil { diff --git a/internal/attestation/azure/snp/issuer_test.go b/internal/attestation/azure/snp/issuer_test.go index 3ecc9e29c..81f6d6df1 100644 --- a/internal/attestation/azure/snp/issuer_test.go +++ b/internal/attestation/azure/snp/issuer_test.go @@ -104,11 +104,11 @@ func TestGetSNPAttestation(t *testing.T) { err = json.Unmarshal(attestationJSON, &instanceInfo) require.NoError(err) - assert.Equal(params.VcekCert, instanceInfo.VCEK) + assert.Equal(params.VcekCert, instanceInfo.ReportSigner) assert.Equal(params.VcekChain, instanceInfo.CertChain) assert.Equal(params.SNPReport, instanceInfo.AttestationReport) - assert.Equal(params.RuntimeData, instanceInfo.RuntimeData) - assert.Equal(tc.maaToken, instanceInfo.MAAToken) + assert.Equal(params.RuntimeData, instanceInfo.Azure.RuntimeData) + assert.Equal(tc.maaToken, instanceInfo.Azure.MAAToken) }) } } diff --git a/internal/attestation/azure/snp/validator.go b/internal/attestation/azure/snp/validator.go index fa31893a4..5c7651d89 100644 --- a/internal/attestation/azure/snp/validator.go +++ b/internal/attestation/azure/snp/validator.go @@ -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, // 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) } @@ -188,7 +191,7 @@ func (v *Validator) getTrustedKey(ctx context.Context, attDoc vtpm.AttestationDo if err != nil { 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) } diff --git a/internal/attestation/azure/snp/validator_test.go b/internal/attestation/azure/snp/validator_test.go index e3443bf05..2b2c9a241 100644 --- a/internal/attestation/azure/snp/validator_test.go +++ b/internal/attestation/azure/snp/validator_test.go @@ -233,7 +233,7 @@ func TestValidateAk(t *testing.T) { defaultRuntimeDataRaw, err := json.Marshal(runtimeData) require.NoError(err) - defaultInstanceInfo := snp.InstanceInfo{RuntimeData: defaultRuntimeDataRaw} + defaultInstanceInfo := snp.InstanceInfo{Azure: &snp.AzureInstanceInfo{RuntimeData: defaultRuntimeDataRaw}} sig := sha256.Sum256(defaultRuntimeDataRaw) defaultReportData := sig[:] @@ -794,9 +794,13 @@ func (v *stubAttestationValidator) SNPAttestation(attestation *spb.Attestation, type stubInstanceInfo struct { AttestationReport []byte - RuntimeData []byte - VCEK []byte + ReportSigner []byte CertChain []byte + Azure *stubAzureInstanceInfo +} + +type stubAzureInstanceInfo struct { + RuntimeData []byte } 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{ AttestationReport: validReport, - RuntimeData: decodedRuntime, - VCEK: vcek, + ReportSigner: vcek, CertChain: certChain, + Azure: &stubAzureInstanceInfo{ + RuntimeData: decodedRuntime, + }, }, nil } diff --git a/internal/attestation/snp/snp.go b/internal/attestation/snp/snp.go index 281b9c662..a25c6f5d0 100644 --- a/internal/attestation/snp/snp.go +++ b/internal/attestation/snp/snp.go @@ -22,15 +22,21 @@ import ( "github.com/google/go-sev-guest/verify/trust" ) -// InstanceInfo contains the necessary information to establish trust in -// an Azure CVM. +// InstanceInfo contains the necessary information to establish trust in a SNP CVM. type InstanceInfo struct { - // VCEK is the PEM-encoded VCEK certificate for the attestation report. - VCEK []byte - // CertChain is the PEM-encoded certificate chain for the attestation report. + // ReportSigner is the PEM-encoded ReportSigner/VLEK certificate for the attestation report. + // Public key that validates the 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 +} + +// 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 @@ -126,11 +132,13 @@ func (a *InstanceInfo) AttestationWithCerts(logger attestation.Logger, getter tr 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, @@ -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. // If the VCEK certificate is not present, nil is returned. func (a *InstanceInfo) ParseVCEK() (*x509.Certificate, error) { - newlinesTrimmed := bytes.TrimSpace(a.VCEK) + newlinesTrimmed := bytes.TrimSpace(a.ReportSigner) if len(newlinesTrimmed) == 0 { // VCEK is not present. return nil, nil diff --git a/internal/attestation/snp/snp_test.go b/internal/attestation/snp/snp_test.go index bfc0f225b..ac9443869 100644 --- a/internal/attestation/snp/snp_test.go +++ b/internal/attestation/snp/snp_test.go @@ -110,7 +110,7 @@ func TestParseVCEK(t *testing.T) { assert := assert.New(t) instanceInfo := &InstanceInfo{ - VCEK: tc.VCEK, + ReportSigner: tc.VCEK, } vcek, err := instanceInfo.ParseVCEK() @@ -235,7 +235,7 @@ func TestInstanceInfoAttestation(t *testing.T) { instanceInfo := InstanceInfo{ AttestationReport: tc.report, CertChain: tc.certChain, - VCEK: tc.vcek, + ReportSigner: tc.vcek, } att, err := instanceInfo.AttestationWithCerts(logger.NewTest(t), tc.getter, tc.fallbackCerts) diff --git a/internal/verify/BUILD.bazel b/internal/verify/BUILD.bazel index 1d0250704..c52589085 100644 --- a/internal/verify/BUILD.bazel +++ b/internal/verify/BUILD.bazel @@ -1,9 +1,31 @@ load("@io_bazel_rules_go//go:def.bzl", "go_library") +load("//bazel/go:go_test.bzl", "go_test") go_library( name = "verify", - srcs = ["verify.go"], + srcs = [ + "certchain.go", + "verify.go", + ], importpath = "github.com/edgelesssys/constellation/v2/internal/verify", 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", + ], ) diff --git a/internal/verify/certchain.go b/internal/verify/certchain.go new file mode 100644 index 000000000..c3629032f --- /dev/null +++ b/internal/verify/certchain.go @@ -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 +} diff --git a/internal/verify/verify.go b/internal/verify/verify.go index 4b7bb53c9..b1e87aad8 100644 --- a/internal/verify/verify.go +++ b/internal/verify/verify.go @@ -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 attestationconfigapi upload tool through JSON serialization. +It exposes a CSP-agnostic interface for printing Reports that may include CSP-specific information. */ package verify 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/google/go-sev-guest/abi" + "github.com/google/go-sev-guest/kds" ) // Report contains the entire data reported by constellation verify. type Report struct { - SNPReport SNPReport `json:"snp_report"` - VCEK []Certificate `json:"vcek"` - CertChain []Certificate `json:"cert_chain"` - MAAToken MaaTokenClaims `json:"maa_token"` + SNPReport SNPReport `json:"snp_report"` + ReportSigner []Certificate `json:"vcek"` + CertChain []Certificate `json:"cert_chain"` + *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. type Certificate struct { - CertificatePEM string `json:"certificate"` - CertTypeName string `json:"cert_type_name"` - StructVersion uint8 `json:"struct_version"` - ProductName string `json:"product_name"` - HardwareID []byte `json:"hardware_id"` - TCBVersion TCBVersion `json:"tcb_version"` + x509.Certificate `json:"-"` + CertificatePEM string `json:"certificate"` + CertTypeName string `json:"cert_type_name"` + StructVersion uint8 `json:"struct_version"` + ProductName string `json:"product_name"` + 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. @@ -46,6 +273,33 @@ type TCBVersion struct { 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. type PlatformInfo struct { SMT bool `json:"smt"` @@ -96,6 +350,125 @@ type SNPReport struct { 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. type MaaTokenClaims struct { jwt.RegisteredClaims @@ -174,3 +547,113 @@ type MaaTokenClaims struct { } `json:"x-ms-runtime,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) +} diff --git a/internal/verify/verify_test.go b/internal/verify/verify_test.go new file mode 100644 index 000000000..b0fdf3c5b --- /dev/null +++ b/internal/verify/verify_test.go @@ -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()) + } + }) + } +}