mirror of
https://github.com/edgelesssys/constellation.git
synced 2024-10-01 01:36:09 -04:00
713 lines
27 KiB
Go
713 lines
27 KiB
Go
/*
|
|
Copyright (c) Edgeless Systems GmbH
|
|
|
|
SPDX-License-Identifier: AGPL-3.0-only
|
|
*/
|
|
|
|
/*
|
|
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/config"
|
|
"github.com/golang-jwt/jwt/v5"
|
|
"github.com/google/go-sev-guest/abi"
|
|
"github.com/google/go-sev-guest/kds"
|
|
"github.com/google/go-sev-guest/verify/trust"
|
|
)
|
|
|
|
const (
|
|
vcekCert = "VCEK certificate"
|
|
vlekCert = "VLEK certificate"
|
|
certificateChain = "certificate chain"
|
|
)
|
|
|
|
// Report contains the entire data reported by constellation verify.
|
|
type Report struct {
|
|
SNPReport SNPReport `json:"snp_report"`
|
|
ReportSigner []Certificate `json:"report_signer"`
|
|
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, attestationCfg config.AttestationCfg, 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 = vlekCert
|
|
case abi.VcekReportSigner.String():
|
|
certTypeName = vcekCert
|
|
default:
|
|
return Report{}, errors.New("unknown report signer")
|
|
}
|
|
|
|
reportSigner, err := newCertificates(certTypeName, instanceInfo.ReportSigner, log)
|
|
if err != nil {
|
|
return Report{}, fmt.Errorf("parsing %s: %w", certTypeName, err)
|
|
}
|
|
|
|
// check if issuer included certChain before parsing. If not included, manually collect from the cluster.
|
|
rawCerts := instanceInfo.CertChain
|
|
if certTypeName == vlekCert {
|
|
rawCerts, err = getCertChain(attestationCfg)
|
|
if err != nil {
|
|
return Report{}, fmt.Errorf("getting certificate chain cache: %w", err)
|
|
}
|
|
}
|
|
|
|
certChain, err := newCertificates(certificateChain, rawCerts, log)
|
|
if err != nil {
|
|
return Report{}, fmt.Errorf("parsing certificate chain: %w", err)
|
|
}
|
|
|
|
var azure *AzureReportAddition
|
|
var aws *AWSReportAddition
|
|
if instanceInfo.Azure != nil {
|
|
cfg, ok := attestationCfg.(*config.AzureSEVSNP)
|
|
if !ok {
|
|
return Report{}, fmt.Errorf("expected config type *config.AzureSEVSNP, got %T", attestationCfg)
|
|
}
|
|
maaToken, err := newMAAToken(ctx, instanceInfo.Azure.MAAToken, cfg.FirmwareSignerConfig.MAAURL)
|
|
if err != nil {
|
|
return Report{}, fmt.Errorf("parsing MAA token: %w", err)
|
|
}
|
|
azure = &AzureReportAddition{
|
|
MAAToken: maaToken,
|
|
}
|
|
}
|
|
|
|
return Report{
|
|
SNPReport: snpReport,
|
|
ReportSigner: reportSigner,
|
|
CertChain: certChain,
|
|
AzureReportAddition: azure,
|
|
AWSReportAddition: aws,
|
|
}, nil
|
|
}
|
|
|
|
// inverse of newCertificates.
|
|
// ideally, duplicate encoding/decoding would be removed.
|
|
// AWS specific.
|
|
func getCertChain(cfg config.AttestationCfg) ([]byte, error) {
|
|
awsCfg, ok := cfg.(*config.AWSSEVSNP)
|
|
if !ok {
|
|
return nil, fmt.Errorf("expected config type *config.AWSSEVSNP, got %T", cfg)
|
|
}
|
|
|
|
if awsCfg.AMDRootKey.Equal(config.Certificate{}) {
|
|
return nil, errors.New("no AMD root key configured")
|
|
}
|
|
|
|
if awsCfg.AMDSigningKey.Equal(config.Certificate{}) {
|
|
certs, err := trust.GetProductChain(kds.ProductString(snp.Product()), abi.VlekReportSigner, trust.DefaultHTTPSGetter())
|
|
if err != nil {
|
|
return nil, fmt.Errorf("getting product certificate chain: %w", err)
|
|
}
|
|
// we want an ASVK, but GetProductChain currently does not use the ASVK field.
|
|
if certs.Ask == nil {
|
|
return nil, errors.New("no ASVK certificate available")
|
|
}
|
|
awsCfg.AMDSigningKey = config.Certificate(*certs.Ask)
|
|
}
|
|
|
|
// ARK
|
|
certChain := pem.EncodeToMemory(&pem.Block{
|
|
Type: "CERTIFICATE",
|
|
Bytes: awsCfg.AMDRootKey.Raw,
|
|
})
|
|
|
|
// append ASK
|
|
certChain = append(certChain, pem.EncodeToMemory(&pem.Block{
|
|
Type: "CERTIFICATE",
|
|
Bytes: awsCfg.AMDSigningKey.Raw,
|
|
})...)
|
|
|
|
return certChain, nil
|
|
}
|
|
|
|
// FormatString builds a string representation of a report that is inteded for console output.
|
|
func (r *Report) FormatString(b *strings.Builder) (string, error) {
|
|
if len(r.ReportSigner) != 1 {
|
|
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 {
|
|
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,omitempty"`
|
|
CspID string `json:"csp_id,omitempty"`
|
|
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.Debug(fmt.Sprintf("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.Debug(fmt.Sprintf("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)
|
|
}
|
|
|
|
certX509, err := x509.ParseCertificate(block.Bytes)
|
|
if err != nil {
|
|
return certs, fmt.Errorf("parse %s: %w", certTypeName, err)
|
|
}
|
|
|
|
var ext *kds.Extensions
|
|
switch certTypeName {
|
|
case vcekCert:
|
|
ext, err = kds.VcekCertificateExtensions(certX509)
|
|
if err != nil {
|
|
return certs, fmt.Errorf("parsing %s extensions: %w", certTypeName, err)
|
|
}
|
|
case vlekCert:
|
|
ext, err = kds.VlekCertificateExtensions(certX509)
|
|
if err != nil {
|
|
return certs, fmt.Errorf("parsing %s extensions: %w", certTypeName, err)
|
|
}
|
|
}
|
|
certPEM := pem.EncodeToMemory(&pem.Block{
|
|
Type: "CERTIFICATE",
|
|
Bytes: certX509.Raw,
|
|
})
|
|
cert := Certificate{
|
|
Certificate: *certX509,
|
|
CertificatePEM: string(certPEM),
|
|
CertTypeName: certTypeName,
|
|
}
|
|
|
|
if ext != nil {
|
|
cert.StructVersion = ext.StructVersion
|
|
cert.ProductName = ext.ProductName
|
|
cert.TCBVersion = newTCBVersion(ext.TCBVersion)
|
|
if ext.HWID != nil {
|
|
cert.HardwareID = ext.HWID
|
|
} else {
|
|
cert.CspID = ext.CspID
|
|
}
|
|
}
|
|
|
|
certs = append(certs, cert)
|
|
|
|
i++
|
|
}
|
|
if i == 1 {
|
|
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 == vcekCert {
|
|
// Extensions documented in Table 8 and Table 9 of
|
|
// https://www.amd.com/system/files/TechDocs/57230.pdf
|
|
vcekExts, err := kds.VcekCertificateExtensions(&c.Certificate)
|
|
if err != nil {
|
|
return fmt.Errorf("parsing %s extensions: %w", c.CertTypeName, 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.
|
|
type TCBVersion struct {
|
|
Bootloader uint8 `json:"bootloader"`
|
|
TEE uint8 `json:"tee"`
|
|
SNP uint8 `json:"snp"`
|
|
Microcode uint8 `json:"microcode"`
|
|
Spl4 uint8 `json:"spl4"`
|
|
Spl5 uint8 `json:"spl5"`
|
|
Spl6 uint8 `json:"spl6"`
|
|
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"`
|
|
TSME bool `json:"tsme"`
|
|
}
|
|
|
|
// SignerInfo contains the signer information.
|
|
type SignerInfo struct {
|
|
AuthorKey bool `json:"author_key_en"`
|
|
MaskChipKey bool `json:"mask_chip_key"`
|
|
SigningKey string `json:"signing_key"`
|
|
}
|
|
|
|
// SNPReport contains the SNP report data.
|
|
type SNPReport struct {
|
|
Version uint32 `json:"version"`
|
|
GuestSvn uint32 `json:"guest_svn"`
|
|
PolicyABIMinor uint8 `json:"policy_abi_minor"`
|
|
PolicyABIMajor uint8 `json:"policy_abi_major"`
|
|
PolicySMT bool `json:"policy_symmetric_multi_threading"`
|
|
PolicyMigrationAgent bool `json:"policy_migration_agent"`
|
|
PolicyDebug bool `json:"policy_debug"`
|
|
PolicySingleSocket bool `json:"policy_single_socket"`
|
|
FamilyID []byte `json:"family_id"`
|
|
ImageID []byte `json:"image_id"`
|
|
Vmpl uint32 `json:"vmpl"`
|
|
SignatureAlgo uint32 `json:"signature_algo"`
|
|
CurrentTCB TCBVersion `json:"current_tcb"`
|
|
PlatformInfo PlatformInfo `json:"platform_info"`
|
|
SignerInfo SignerInfo `json:"signer_info"`
|
|
ReportData []byte `json:"report_data"`
|
|
Measurement []byte `json:"measurement"`
|
|
HostData []byte `json:"host_data"`
|
|
IDKeyDigest []byte `json:"id_key_digest"`
|
|
AuthorKeyDigest []byte `json:"author_key_digest"`
|
|
ReportID []byte `json:"report_id"`
|
|
ReportIDMa []byte `json:"report_id_ma"`
|
|
ReportedTCB TCBVersion `json:"reported_tcb"`
|
|
ChipID []byte `json:"chip_id"`
|
|
CommittedTCB TCBVersion `json:"committed_tcb"`
|
|
CurrentBuild uint32 `json:"current_build"`
|
|
CurrentMinor uint32 `json:"current_minor"`
|
|
CurrentMajor uint32 `json:"current_major"`
|
|
CommittedBuild uint32 `json:"committed_build"`
|
|
CommittedMinor uint32 `json:"committed_minor"`
|
|
CommittedMajor uint32 `json:"committed_major"`
|
|
LaunchTCB TCBVersion `json:"launch_tcb"`
|
|
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
|
|
Secureboot bool `json:"secureboot,omitempty"`
|
|
XMsAttestationType string `json:"x-ms-attestation-type,omitempty"`
|
|
XMsAzurevmAttestationProtocolVer string `json:"x-ms-azurevm-attestation-protocol-ver,omitempty"`
|
|
XMsAzurevmAttestedPcrs []int `json:"x-ms-azurevm-attested-pcrs,omitempty"`
|
|
XMsAzurevmBootdebugEnabled bool `json:"x-ms-azurevm-bootdebug-enabled,omitempty"`
|
|
XMsAzurevmDbvalidated bool `json:"x-ms-azurevm-dbvalidated,omitempty"`
|
|
XMsAzurevmDbxvalidated bool `json:"x-ms-azurevm-dbxvalidated,omitempty"`
|
|
XMsAzurevmDebuggersdisabled bool `json:"x-ms-azurevm-debuggersdisabled,omitempty"`
|
|
XMsAzurevmDefaultSecurebootkeysvalidated bool `json:"x-ms-azurevm-default-securebootkeysvalidated,omitempty"`
|
|
XMsAzurevmElamEnabled bool `json:"x-ms-azurevm-elam-enabled,omitempty"`
|
|
XMsAzurevmFlightsigningEnabled bool `json:"x-ms-azurevm-flightsigning-enabled,omitempty"`
|
|
XMsAzurevmHvciPolicy int `json:"x-ms-azurevm-hvci-policy,omitempty"`
|
|
XMsAzurevmHypervisordebugEnabled bool `json:"x-ms-azurevm-hypervisordebug-enabled,omitempty"`
|
|
XMsAzurevmIsWindows bool `json:"x-ms-azurevm-is-windows,omitempty"`
|
|
XMsAzurevmKerneldebugEnabled bool `json:"x-ms-azurevm-kerneldebug-enabled,omitempty"`
|
|
XMsAzurevmOsbuild string `json:"x-ms-azurevm-osbuild,omitempty"`
|
|
XMsAzurevmOsdistro string `json:"x-ms-azurevm-osdistro,omitempty"`
|
|
XMsAzurevmOstype string `json:"x-ms-azurevm-ostype,omitempty"`
|
|
XMsAzurevmOsversionMajor int `json:"x-ms-azurevm-osversion-major,omitempty"`
|
|
XMsAzurevmOsversionMinor int `json:"x-ms-azurevm-osversion-minor,omitempty"`
|
|
XMsAzurevmSigningdisabled bool `json:"x-ms-azurevm-signingdisabled,omitempty"`
|
|
XMsAzurevmTestsigningEnabled bool `json:"x-ms-azurevm-testsigning-enabled,omitempty"`
|
|
XMsAzurevmVmid string `json:"x-ms-azurevm-vmid,omitempty"`
|
|
XMsIsolationTee struct {
|
|
XMsAttestationType string `json:"x-ms-attestation-type,omitempty"`
|
|
XMsComplianceStatus string `json:"x-ms-compliance-status,omitempty"`
|
|
XMsRuntime struct {
|
|
Keys []struct {
|
|
E string `json:"e,omitempty"`
|
|
KeyOps []string `json:"key_ops,omitempty"`
|
|
Kid string `json:"kid,omitempty"`
|
|
Kty string `json:"kty,omitempty"`
|
|
N string `json:"n,omitempty"`
|
|
} `json:"keys,omitempty"`
|
|
VMConfiguration struct {
|
|
ConsoleEnabled bool `json:"console-enabled,omitempty"`
|
|
CurrentTime int `json:"current-time,omitempty"`
|
|
SecureBoot bool `json:"secure-boot,omitempty"`
|
|
TpmEnabled bool `json:"tpm-enabled,omitempty"`
|
|
VMUniqueID string `json:"vmUniqueId,omitempty"`
|
|
} `json:"vm-configuration,omitempty"`
|
|
} `json:"x-ms-runtime,omitempty"`
|
|
XMsSevsnpvmAuthorkeydigest string `json:"x-ms-sevsnpvm-authorkeydigest,omitempty"`
|
|
XMsSevsnpvmBootloaderSvn int `json:"x-ms-sevsnpvm-bootloader-svn,omitempty"`
|
|
XMsSevsnpvmFamilyID string `json:"x-ms-sevsnpvm-familyId,omitempty"`
|
|
XMsSevsnpvmGuestsvn int `json:"x-ms-sevsnpvm-guestsvn,omitempty"`
|
|
XMsSevsnpvmHostdata string `json:"x-ms-sevsnpvm-hostdata,omitempty"`
|
|
XMsSevsnpvmIdkeydigest string `json:"x-ms-sevsnpvm-idkeydigest,omitempty"`
|
|
XMsSevsnpvmImageID string `json:"x-ms-sevsnpvm-imageId,omitempty"`
|
|
XMsSevsnpvmIsDebuggable bool `json:"x-ms-sevsnpvm-is-debuggable,omitempty"`
|
|
XMsSevsnpvmLaunchmeasurement string `json:"x-ms-sevsnpvm-launchmeasurement,omitempty"`
|
|
XMsSevsnpvmMicrocodeSvn int `json:"x-ms-sevsnpvm-microcode-svn,omitempty"`
|
|
XMsSevsnpvmMigrationAllowed bool `json:"x-ms-sevsnpvm-migration-allowed,omitempty"`
|
|
XMsSevsnpvmReportdata string `json:"x-ms-sevsnpvm-reportdata,omitempty"`
|
|
XMsSevsnpvmReportid string `json:"x-ms-sevsnpvm-reportid,omitempty"`
|
|
XMsSevsnpvmSmtAllowed bool `json:"x-ms-sevsnpvm-smt-allowed,omitempty"`
|
|
XMsSevsnpvmSnpfwSvn int `json:"x-ms-sevsnpvm-snpfw-svn,omitempty"`
|
|
XMsSevsnpvmTeeSvn int `json:"x-ms-sevsnpvm-tee-svn,omitempty"`
|
|
XMsSevsnpvmVmpl int `json:"x-ms-sevsnpvm-vmpl,omitempty"`
|
|
} `json:"x-ms-isolation-tee,omitempty"`
|
|
XMsPolicyHash string `json:"x-ms-policy-hash,omitempty"`
|
|
XMsRuntime struct {
|
|
ClientPayload struct {
|
|
Nonce string `json:"nonce,omitempty"`
|
|
} `json:"client-payload,omitempty"`
|
|
Keys []struct {
|
|
E string `json:"e,omitempty"`
|
|
KeyOps []string `json:"key_ops,omitempty"`
|
|
Kid string `json:"kid,omitempty"`
|
|
Kty string `json:"kty,omitempty"`
|
|
N string `json:"n,omitempty"`
|
|
} `json:"keys,omitempty"`
|
|
} `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 {
|
|
Debug(msg string, args ...any)
|
|
}
|