diff --git a/cmd/attested-get/main.go b/cmd/attested-get/main.go index 304350f..31fcc59 100644 --- a/cmd/attested-get/main.go +++ b/cmd/attested-get/main.go @@ -92,6 +92,12 @@ var flags []cli.Flag = []cli.Flag{ Value: false, Usage: "log debug messages", }, + &cli.BoolFlag{ + Name: "verify-ak-certificate", + Value: false, + EnvVars: []string{"VERIFY_AK_CERTIFICATE"}, + Usage: "verify Azure TDX vTPM attestation key certificate chain", + }, } func main() { @@ -108,10 +114,11 @@ func main() { } // createAzureTDXValidator creates an Azure TDX validator without required measurements -func createAzureTDXValidator(log *slog.Logger, overrideAzurev6Tcbinfo bool) atls.Validator { +func createAzureTDXValidator(log *slog.Logger, overrideAzurev6Tcbinfo bool, verifyAKCertificate bool) atls.Validator { attConfig := config.DefaultForAzureTDX() attConfig.SetMeasurements(measurements.M{}) validator := azure_tdx.NewValidator(attConfig, proxy.AttestationLogger{Log: log}) + validator.SetVerifyAKCertificate(verifyAKCertificate) if overrideAzurev6Tcbinfo { azure_tcbinfo_override.OverrideAzureValidatorsForV6SEAMLoader(log, []atls.Validator{validator}) } @@ -132,6 +139,7 @@ func runClient(cCtx *cli.Context) (err error) { attestationTypeStr := cCtx.String("attestation-type") expectedMeasurementsPath := cCtx.String("expected-measurements") overrideAzurev6Tcbinfo := cCtx.Bool("override-azurev6-tcbinfo") + verifyAKCertificate := cCtx.Bool("verify-ak-certificate") // Setup logging log := common.SetupLogger(&common.LoggingOpts{ @@ -156,13 +164,13 @@ func runClient(cCtx *cli.Context) (err error) { var validators []atls.Validator switch attestationType { case proxy.AttestationAzureTDX: - validators = append(validators, createAzureTDXValidator(log, overrideAzurev6Tcbinfo)) + validators = append(validators, createAzureTDXValidator(log, overrideAzurev6Tcbinfo, verifyAKCertificate)) case proxy.AttestationDCAPTDX: validators = append(validators, createDCAPTDXValidator(log)) case proxy.AttestationAuto: // In auto mode, add all validators to support any attestation type log.Info("Auto mode: creating validators for all supported attestation types") - validators = append(validators, createAzureTDXValidator(log, overrideAzurev6Tcbinfo)) + validators = append(validators, createAzureTDXValidator(log, overrideAzurev6Tcbinfo, verifyAKCertificate)) validators = append(validators, createDCAPTDXValidator(log)) default: log.Error("unsupported attestation type, see --help for available options") diff --git a/cmd/proxy-client/main.go b/cmd/proxy-client/main.go index 30421de..aad40cb 100644 --- a/cmd/proxy-client/main.go +++ b/cmd/proxy-client/main.go @@ -75,6 +75,12 @@ var flags []cli.Flag = []cli.Flag{ EnvVars: []string{"DEV_DUMMY_DCAP"}, Usage: "URL of the remote dummy DCAP service. Only with --client-attestation-type dummy.", }, + &cli.BoolFlag{ + Name: "verify-ak-certificate", + EnvVars: []string{"VERIFY_AK_CERTIFICATE"}, + Value: false, + Usage: "verify Azure TDX vTPM attestation key certificate chain", + }, } func main() { @@ -103,6 +109,7 @@ func runClient(cCtx *cli.Context) error { devDummyDcapURL := cCtx.String("dev-dummy-dcap") verifyTLS := cCtx.Bool("verify-tls") + verifyAKCertificate := cCtx.Bool("verify-ak-certificate") log := common.SetupLogger(&common.LoggingOpts{ Debug: logDebug, @@ -145,7 +152,7 @@ func runClient(cCtx *cli.Context) error { } } - validators, err := proxy.CreateAttestationValidatorsFromFile(log, serverMeasurements) + validators, err := proxy.CreateAttestationValidatorsFromFile(log, serverMeasurements, verifyAKCertificate) if err != nil { log.Error("could not create attestation validators from file", "err", err) return err diff --git a/cmd/proxy-server/main.go b/cmd/proxy-server/main.go index d570fa5..516b0d3 100644 --- a/cmd/proxy-server/main.go +++ b/cmd/proxy-server/main.go @@ -92,6 +92,12 @@ var flags []cli.Flag = []cli.Flag{ EnvVars: []string{"DEV_DUMMY_DCAP"}, Usage: "URL of the remote dummy DCAP service. Only with --server-attestation-type dummy.", }, + &cli.BoolFlag{ + Name: "verify-ak-certificate", + EnvVars: []string{"VERIFY_AK_CERTIFICATE"}, + Value: false, + Usage: "verify Azure TDX vTPM attestation key certificate chain", + }, } var log *slog.Logger @@ -120,6 +126,7 @@ func runServer(cCtx *cli.Context) error { overrideAzurev6Tcbinfo := cCtx.Bool("override-azurev6-tcbinfo") logJSON := cCtx.Bool("log-json") logDebug := cCtx.Bool("log-debug") + verifyAKCertificate := cCtx.Bool("verify-ak-certificate") tdx.SetLogDcapQuote(cCtx.Bool("log-dcap-quote")) serverAttestationTypeFlag := cCtx.String("server-attestation-type") @@ -148,7 +155,7 @@ func runServer(cCtx *cli.Context) error { return errors.New("not all of --tls-certificate-path and --tls-private-key-path specified") } - validators, err := proxy.CreateAttestationValidatorsFromFile(log, clientMeasurements) + validators, err := proxy.CreateAttestationValidatorsFromFile(log, clientMeasurements, verifyAKCertificate) if err != nil { log.Error("could not create attestation validators from file", "err", err) return err diff --git a/internal/attestation/azure/tdx/issuer.go b/internal/attestation/azure/tdx/issuer.go index 7265e42..dc9e282 100644 --- a/internal/attestation/azure/tdx/issuer.go +++ b/internal/attestation/azure/tdx/issuer.go @@ -9,6 +9,7 @@ package tdx import ( "bytes" "context" + "crypto/x509" "encoding/base64" "encoding/binary" "encoding/json" @@ -27,6 +28,7 @@ import ( const ( imdsURL = "http://169.254.169.254/acc/tdquote" indexHCLReport = 0x1400001 + tpmAkCertIdx = 0x1C101D0 hclDataOffset = 1216 hclReportTypeOffset = 8 hclReportTypeOffsetStart = hclDataOffset + hclReportTypeOffset @@ -53,6 +55,7 @@ type Issuer struct { *vtpm.Issuer quoteGetter quoteGetter + log attestation.Logger } // NewIssuer initializes a new Azure Issuer. @@ -61,6 +64,7 @@ func NewIssuer(log attestation.Logger) *Issuer { quoteGetter: imdsQuoteGetter{ client: &http.Client{Transport: &http.Transport{Proxy: nil}}, }, + log: log, } i.Issuer = vtpm.NewIssuer( @@ -91,9 +95,17 @@ func (i *Issuer) getInstanceInfo(ctx context.Context, tpm io.ReadWriteCloser, _ return nil, fmt.Errorf("getting quote: %w", err) } + // Read and extract the vTPM AK certificate. If this fails, we log a warning and continue without it + akCert, err := i.readAKCertificateFromTPM(tpm) + if err != nil { + i.log.Warn(fmt.Sprintf("Failed to read AK certificate: %v", err)) + akCert = nil + } + instanceInfo := InstanceInfo{ AttestationReport: quote, RuntimeData: runtimeData, + AkCert: akCert, // Use the clean certificate } instanceInfoJSON, err := json.Marshal(instanceInfo) if err != nil { @@ -102,6 +114,91 @@ func (i *Issuer) getInstanceInfo(ctx context.Context, tpm io.ReadWriteCloser, _ return instanceInfoJSON, nil } +// readAKCertificateFromTPM reads and extracts the attestation key certificate from TPM. +// Returns the clean DER-encoded certificate or an error if reading/extraction fails. +func (i *Issuer) readAKCertificateFromTPM(tpm io.ReadWriteCloser) ([]byte, error) { + certDERRaw, err := tpm2.NVReadEx(tpm, tpmAkCertIdx, tpm2.HandleOwner, "", 0) + if err != nil { + return nil, fmt.Errorf("reading attestation key certificate from TPM: %w", err) + } + + i.log.Debug(fmt.Sprintf("Read %d bytes from TPM AK cert index", len(certDERRaw))) + + // The TPM NV index contains trailing data. We need to extract just the certificate. + // X.509 DER certificates start with 0x30 (SEQUENCE) followed by length encoding + cleanCertDER, err := extractDERCertificate(certDERRaw) + if err != nil { + return nil, fmt.Errorf("extracting certificate from TPM data: %w", err) + } + + i.log.Debug(fmt.Sprintf("Extracted %d bytes certificate from %d bytes TPM data", len(cleanCertDER), len(certDERRaw))) + + // Verify we can parse the extracted certificate + _, err = x509.ParseCertificate(cleanCertDER) + if err != nil { + return nil, fmt.Errorf("parsing extracted attestation key certificate: %w", err) + } + + return cleanCertDER, nil +} + +// extractDERCertificate extracts a clean X.509 DER certificate from raw TPM data. +// The TPM NV index may contain trailing data, so this function parses the DER +// structure to extract exactly the certificate bytes. +// +// X.509 DER certificates use ASN.1 encoding and start with: +// - Tag: 0x30 (SEQUENCE) +// - Length: encoded in one of three forms (short, long-1byte, long-2byte) +// - Content: the certificate data +func extractDERCertificate(certDERRaw []byte) ([]byte, error) { + if len(certDERRaw) < 4 { + return nil, fmt.Errorf("certificate data too short: %d bytes", len(certDERRaw)) + } + + // Verify it starts with DER SEQUENCE tag (0x30) + if certDERRaw[0] != 0x30 { + return nil, fmt.Errorf("invalid certificate format: does not start with DER SEQUENCE tag (0x30), got 0x%02x", certDERRaw[0]) + } + + // Parse the DER length encoding to determine certificate size + var certLen int + lengthByte := certDERRaw[1] + + if lengthByte < 0x80 { + // Short form: length fits in 7 bits (0-127 bytes) + // Format: 0x30 + certLen = int(lengthByte) + 2 // +2 for tag and length bytes + } else if lengthByte == 0x81 { + // Long form with 1 length byte (128-255 bytes) + // Format: 0x30 0x81 + if len(certDERRaw) < 3 { + return nil, fmt.Errorf("truncated DER encoding: expected length byte") + } + certLen = int(certDERRaw[2]) + 3 // +3 for tag, 0x81, and length byte + } else if lengthByte == 0x82 { + // Long form with 2 length bytes (256-65535 bytes) + // Format: 0x30 0x82 + if len(certDERRaw) < 4 { + return nil, fmt.Errorf("truncated DER encoding: expected 2 length bytes") + } + certLen = (int(certDERRaw[2]) << 8) | int(certDERRaw[3]) + certLen += 4 // +4 for tag, 0x82, and two length bytes + } else { + return nil, fmt.Errorf("unsupported DER length encoding: 0x%02x", lengthByte) + } + + // Validate the calculated length + if certLen <= 0 { + return nil, fmt.Errorf("invalid certificate length: %d", certLen) + } + if certLen > len(certDERRaw) { + return nil, fmt.Errorf("invalid certificate length: %d exceeds available data (%d bytes)", certLen, len(certDERRaw)) + } + + // Extract the exact certificate bytes + return certDERRaw[:certLen], nil +} + func parseHCLReport(report []byte) (hwReport, runtimeData []byte, err error) { // First, ensure the extracted report is actually for TDX if len(report) < hclReportTypeOffsetStart+4 { diff --git a/internal/attestation/azure/tdx/tdx.go b/internal/attestation/azure/tdx/tdx.go index eaee616..60c8fe8 100644 --- a/internal/attestation/azure/tdx/tdx.go +++ b/internal/attestation/azure/tdx/tdx.go @@ -23,4 +23,5 @@ package tdx type InstanceInfo struct { AttestationReport []byte RuntimeData []byte + AkCert []byte `json:"akCert,omitempty"` } diff --git a/internal/attestation/azure/tdx/validator.go b/internal/attestation/azure/tdx/validator.go index 48e138e..e216720 100644 --- a/internal/attestation/azure/tdx/validator.go +++ b/internal/attestation/azure/tdx/validator.go @@ -9,8 +9,10 @@ package tdx import ( "context" "crypto" + "crypto/rsa" "crypto/x509" "encoding/json" + "errors" "fmt" "github.com/flashbots/cvm-reverse-proxy/internal/attestation" @@ -18,6 +20,7 @@ import ( "github.com/flashbots/cvm-reverse-proxy/internal/attestation/variant" "github.com/flashbots/cvm-reverse-proxy/internal/attestation/vtpm" "github.com/flashbots/cvm-reverse-proxy/internal/config" + certutil "github.com/flashbots/cvm-reverse-proxy/internal/crypto" "github.com/google/go-tdx-guest/abi" "github.com/google/go-tdx-guest/pcs" @@ -31,6 +34,117 @@ import ( const AZURE_V6_BAD_FMSPC = "90c06f000000" +// microsoftRSADevicesRoot2021 is the root CA certificate used to sign Azure TDX vTPM certificates. +// This is different from the AME root CA used by TrustedLaunch VMs. +// The certificate can be downloaded from: +// http://www.microsoft.com/pkiops/certs/Microsoft%20RSA%20Devices%20Root%20CA%202021.crt +var microsoftRSADevicesRoot2021 = mustParseX509(`-----BEGIN CERTIFICATE----- +MIIFkjCCA3qgAwIBAgIQGWCAkS2F96VGa+6hm2M3rjANBgkqhkiG9w0BAQwFADBa +MQswCQYDVQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSsw +KQYDVQQDEyJNaWNyb3NvZnQgUlNBIERldmljZXMgUm9vdCBDQSAyMDIxMB4XDTIx +MDgyNjIzMzkxOFoXDTQ2MDgyNjIzNDcxNFowWjELMAkGA1UEBhMCVVMxHjAcBgNV +BAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjErMCkGA1UEAxMiTWljcm9zb2Z0IFJT +QSBEZXZpY2VzIFJvb3QgQ0EgMjAyMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCC +AgoCggIBALF4kgr3bAptorWmkrM6u47osmLfg67KxZPE4W74Zw5Bu64tjEuzegcB +6lFkoXi2V4eLdIRshk3l14jul6ghCML/6gh4hYiTExky3XMY05wg0d1o+AdhuyvC +anXvQZratosnL+KhR2qFeagthciIrCibIIKX91LvqRl/Eg8uo82fl30gieB40Sun +Pe/SfMJLb7AYbQ95yHK8G1lTFUHkIfPbAY6SfkOBUpNJ6UAtjlAmIaHYpdcdOayf +qXyhW3+Hf0Ou2wiKYJihCqh3TaI2hqmiv4p4CScug9sDcTyafA6OYLyTe3vx7Krn +BOUvkSkTj80GrXSKCWnrw+bE7z0deptPuLS6+n83ImLsBZ3XYhX4iUPmTRSU9vr7 +q0cZA8P8zAzLaeN+uK14l92u/7TMhkp5etmLE9DMd9MtnsLZSy18UpW4ZlBXxt9Z +w/RFKStlNbK5ILsI2HdSjgkF0DxZtNnCiEQehMu5DBfCdXo1P90iJhfF1MD+2Kh5 +xeuDQEC7Dh3gUSXIkOm/72u1fE52r0uY+aH1TCQGbCrijI9Jf78lFbI7L6Ll3YAa +89MrDs2tAQG0SaJdabh4k5orqaJOgaqrrq61RzcMjlZGI3dOdL+f6romKOccFkm0 +k+gwjvZ9xaJ5i9SB6Lq/GrA8YxzjmKHHVPmGGdm/v93R0oNGfyvxAgMBAAGjVDBS +MA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBSERIYG +AJg/LKqzxYnzrC7J5p0JAzAQBgkrBgEEAYI3FQEEAwIBADANBgkqhkiG9w0BAQwF +AAOCAgEAd3RAo42nyNbVvj+mxZ03VV+ceU6nCdgIS8RZfZBxf+lqupRzKUV9UW59 +IRCSeMH3gHfGSVhmwH1AJHkFIhd5meSShF4lPPmvYMmrbfOrwiUunqz2aix/QkRp +geMOe10wm6dEHHAw/eNi3PWhc+jdGJNV0SdnqcwJg/t5db8Y7RCVW+tG3DtEa63U +B4sGNlBbaUffdSdYL5TCRXm2mkcCWruu/gmDTgoabFmI4j9ss0shsIxwqVVEq2zk +EH1ypZrHSmVrTRh9hPHWpkOxnh9yqpGDXcSll09ZZUBUhx7YUX6p+BTVWnuuyR4T +bXS8P6fUS5Q2WF0WR07BrGYlBqomsEwMhth1SmBKn6tXfQyWkgr4pVl5XkkC7Bfv +pmw90csy8ycwog+x4L9kO1Nr6OPwnJ9V39oMifNDxnvYVBX7EhjoiARPp+97feNJ +YwMt4Os/WSeD++IhBB9xVsrI+jZufySQ02C/w1LBFR6zPy+a+v+6WlvMxDBEDWOj +JyDQ6kzkWxIG35klzLnwHybuIsFIIR1QGL1l47eW2dM4hB9oCay6z3FX5xYBIFvA +yp8up+KbjfH/NIWfPBXhYMW64DagB9P2cW5LBRz+AzDA+JF/OdYpb6vxv3lzjLQb +U9zMFwSrzEF5o2Aa/n+xZ90Naj78AYaTM18DalA17037fjucDN8= +-----END CERTIFICATE-----`) + +// azureVirtualTPMRoot2023 is the root CA for Azure vTPM (used by both Trusted Launch and TDX) +// Source: https://learn.microsoft.com/en-us/azure/virtual-machines/trusted-launch-faq +// Valid until: 2048-06-01 +var azureVirtualTPMRoot2023 = mustParseX509(`-----BEGIN CERTIFICATE----- +MIIFsDCCA5igAwIBAgIQUfQx2iySCIpOKeDZKd5KpzANBgkqhkiG9w0BAQwFADBp +MQswCQYDVQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTow +OAYDVQQDEzFBenVyZSBWaXJ0dWFsIFRQTSBSb290IENlcnRpZmljYXRlIEF1dGhv +cml0eSAyMDIzMB4XDTIzMDYwMTE4MDg1M1oXDTQ4MDYwMTE4MTU0MVowaTELMAkG +A1UEBhMCVVMxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjE6MDgGA1UE +AxMxQXp1cmUgVmlydHVhbCBUUE0gUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkg +MjAyMzCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALoMMwvdRJ7+bW00 +adKE1VemNqJS+268Ure8QcfZXVOsVO22+PL9WRoPnWo0r5dVoomYGbobh4HC72s9 +sGY6BGRe+Ui2LMwuWnirBtOjaJ34r1ZieNMcVNJT/dXW5HN/HLlm/gSKlWzqCEx6 +gFFAQTvyYl/5jYI4Oe05zJ7ojgjK/6ZHXpFysXnyUITJ9qgjn546IJh/G5OMC3mD +fFU7A/GAi+LYaOHSzXj69Lk1vCftNq9DcQHtB7otO0VxFkRLaULcfu/AYHM7FC/S +q6cJb9Au8K/IUhw/5lJSXZawLJwHpcEYzETm2blad0VHsACaLNucZL5wBi8GEusQ +9Wo8W1p1rUCMp89pufxa3Ar9sYZvWeJlvKggWcQVUlhvvIZEnT+fteEvwTdoajl5 +qSvZbDPGCPjb91rSznoiLq8XqgQBBFjnEiTL+ViaZmyZPYUsBvBY3lKXB1l2hgga +hfBIag4j0wcgqlL82SL7pAdGjq0Fou6SKgHnkkrV5CNxUBBVMNCwUoj5mvEjd5mF +7XPgfM98qNABb2Aqtfl+VuCkU/G1XvFoTqS9AkwbLTGFMS9+jCEU2rw6wnKuGv1T +x9iuSdNvsXt8stx4fkVeJvnFpJeAIwBZVgKRSTa3w3099k0mW8qGiMnwCI5SfdZ2 +SJyD4uEmszsnieE6wAWd1tLLg1jvAgMBAAGjVDBSMA4GA1UdDwEB/wQEAwIBhjAP +BgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRL/iZalMH2M8ODSCbd8+WwZLKqlTAQ +BgkrBgEEAYI3FQEEAwIBADANBgkqhkiG9w0BAQwFAAOCAgEALgNAyg8I0ANNO/8I +2BhpTOsbywN2YSmShAmig5h4sCtaJSM1dRXwA+keY6PCXQEt/PRAQAiHNcOF5zbu +OU1Bw/Z5Z7k9okt04eu8CsS2Bpc+POg9js6lBtmigM5LWJCH1goMD0kJYpzkaCzx +1TdD3yjo0xSxgGhabk5Iu1soD3OxhUyIFcxaluhwkiVINt3Jhy7G7VJTlEwkk21A +oOrQxUsJH0f2GXjYShS1r9qLPzLf7ykcOm62jHGmLZVZujBzLIdNk1bljP9VuGW+ +cISBwzkNeEMMFufcL2xh6s/oiUnXicFWvG7E6ioPnayYXrHy3Rh68XLnhfpzeCzv +bz/I4yMV38qGo/cAY2OJpXUuuD/ZbI5rT+lRBEkDW1kxHP8cpwkRwGopV8+gX2KS +UucIIN4l8/rrNDEX8T0b5U+BUqiO7Z5YnxCya/H0ZIwmQnTlLRTU2fW+OGG+xyIr +jMi/0l6/yWPUkIAkNtvS/yO7USRVLPbtGVk3Qre6HcqacCXzEjINcJhGEVg83Y8n +M+Y+a9J0lUnHytMSFZE85h88OseRS2QwqjozUo2j1DowmhSSUv9Na5Ae22ycciBk +EZSq8a4rSlwqthaELNpeoTLUk6iVoUkK/iLvaMvrkdj9yJY1O/gvlfN2aiNTST/2 +bd+PA4RBToG9rXn6vNkUWdbLibU= +-----END CERTIFICATE-----`) + +// globalVirtualTPMCA03 is the intermediate CA that issues TDX vTPM AK certificates +// Source: https://learn.microsoft.com/en-us/azure/virtual-machines/trusted-launch-faq +// Issuer: Azure Virtual TPM Root Certificate Authority 2023 +// Valid: 2025-04-24 to 2027-04-24 +var globalVirtualTPMCA03 = mustParseX509(`-----BEGIN CERTIFICATE----- +MIIFnDCCA4SgAwIBAgITMwAAAAknQOWscnsOpgAAAAAACTANBgkqhkiG9w0BAQwF +ADBpMQswCQYDVQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9u +MTowOAYDVQQDEzFBenVyZSBWaXJ0dWFsIFRQTSBSb290IENlcnRpZmljYXRlIEF1 +dGhvcml0eSAyMDIzMB4XDTI1MDQyNDE4MDExN1oXDTI3MDQyNDE4MDExN1owJTEj +MCEGA1UEAxMaR2xvYmFsIFZpcnR1YWwgVFBNIENBIC0gMDMwggEiMA0GCSqGSIb3 +DQEBAQUAA4IBDwAwggEKAoIBAQDYGYtis5ka0cxQkhU11jslgX6wzjR/UXQIFdUn +8juTUMJl91VokwUPX3WfXeog7mtbWyYWD8SI0BSnchRGlV8u3AhcW61/HetHqmIL +tD0c75UATi+gsTQnpwKPA/m38MGGyXFETr3xHXjilUPfIhmxO4ImuNJ0R95bZYhx +bLYmOZpVUcj8oz980An8HlIqSzrskQR6NiuEmikHkHc1/CpoNunrr8kQNPF6gxex +IrvXsKLUAuUqnNtcQWc/8Er5EN9+TdX6AOjUmKriVGbCInP1m/aC+DWH/+aJ/8aD +pKze6fe7OHh2BL9hxqIsmJAStIh4siRdLYTt8hKGmkdzOWnRAgMBAAGjggF/MIIB +ezASBgNVHRMBAf8ECDAGAQH/AgEAMA4GA1UdDwEB/wQEAwICBDAXBgNVHSUEEDAO +BgVngQUIAQYFZ4EFCAMwHQYDVR0OBBYEFGcJhvj5gV6TrfnJZOcUCtqZywotMB8G +A1UdIwQYMBaAFEv+JlqUwfYzw4NIJt3z5bBksqqVMHYGA1UdHwRvMG0wa6BpoGeG +ZWh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvY3JsL0F6dXJlJTIwVmly +dHVhbCUyMFRQTSUyMFJvb3QlMjBDZXJ0aWZpY2F0ZSUyMEF1dGhvcml0eSUyMDIw +MjMuY3JsMIGDBggrBgEFBQcBAQR3MHUwcwYIKwYBBQUHMAKGZ2h0dHA6Ly93d3cu +bWljcm9zb2Z0LmNvbS9wa2lvcHMvY2VydHMvQXp1cmUlMjBWaXJ0dWFsJTIwVFBN +JTIwUm9vdCUyMENlcnRpZmljYXRlJTIwQXV0aG9yaXR5JTIwMjAyMy5jcnQwDQYJ +KoZIhvcNAQEMBQADggIBAJPP3Z2z1zhzUS3qSRVgyoUVnaxCGuMHzPQAZuoPBVpz +wKnv4HqyjMgT8pBtQqxkqAsg7KiqbPfO97bMCHcuqkkfHjw8yg6IYt01RjUjVPKq +lrsY2iw7hFWNWr8SGMa10JdNYNyf5dxob5+mKAwEOhLzKNwq9rM/uIvZky77pNly +RLt55XEPfBMYdI9I8uQ5Uqmrw7mVJfERMfTBhSQF9BrcajAsaLcs7qEUyj0yUdJf +cgZkfCoUEUSPr3OwLHaYeV1J6VidhIYsYo53sXXal91d60NspYgei2nJFei/+R3E +SWnGbPBW+EQ4FbvZXxu57zUMX9mM7lC+GoXLvA6/vtKShEi9ZXl2PSnBQ/R2A7b3 +AXyg4fmMLFausEk6OiuU8E/bvp+gPLOJ8YrX7SAJVuEn+koJaK5G7os5DMIh7/KM +l9cI9WxPwqoWjp4VBfrF4hDOCmKWrqtFUDQCML8qD8RTxlQKQtgeGAcNDfoAuL9K +VtSG5/iIhuyBEFYEHa3vRWbSaHCUzaHJsTmLcz4cp1VDdepzqZRVuErBzJKFnBXb +zRNW32EFmcAUKZImIsE5dgB7y7eiijf33VWNfWmK05fxzQziWFWRYlET4SVc3jMn +PBiY3N8BfK8EBOYbLvzo0qn2n3SAmPhYX3Ag6vbbIHd4Qc8DQKHRV0PB8D3jPGmD +-----END CERTIFICATE-----`) + // Validator for Azure confidential VM attestation using TDX. type Validator struct { variant.AzureTDX @@ -42,6 +156,8 @@ type Validator struct { tcbOverride func(pcs.TcbInfo) pcs.TcbInfo log attestation.Logger + + verifyAKCertEnabled bool } // NewValidator returns a new Validator for Azure confidential VM attestation using TDX. @@ -70,6 +186,10 @@ func (v *Validator) SetTcbOverride(overrideFn func(pcs.TcbInfo) pcs.TcbInfo) *Va return v } +func (v *Validator) SetVerifyAKCertificate(enabled bool) { + v.verifyAKCertEnabled = enabled +} + func (v *Validator) getTrustedTPMKey(_ context.Context, attDoc vtpm.AttestationDocument, _ []byte) (crypto.PublicKey, error) { var instanceInfo InstanceInfo if err := json.Unmarshal(attDoc.InstanceInfo, &instanceInfo); err != nil { @@ -98,6 +218,14 @@ func (v *Validator) getTrustedTPMKey(_ context.Context, attDoc vtpm.AttestationD return nil, fmt.Errorf("validating HCLAkPub: %w", err) } + // Verify the vTPM AK certificate chain to prevent forging attestation attacks + // This ensures the attestation key is actually signed by Azure's CA + if v.verifyAKCertEnabled { + if err := v.verifyAKCertificate(instanceInfo, &pubArea); err != nil { + return nil, fmt.Errorf("verifying AK certificate: %w", err) + } + } + return pubArea.Key() } @@ -146,6 +274,81 @@ func (v *Validator) validateQuote(tdxQuote *tdx.QuoteV4) error { return nil } +// verifyAKCertificate verifies the vTPM attestation key certificate chain. +// This prevents attacks where an attacker could forge attestation by using their own key. +func (v *Validator) verifyAKCertificate(instanceInfo InstanceInfo, pubArea *tpm2.Public) error { + // Ensure that the AK certificate is provided + if len(instanceInfo.AkCert) == 0 { + return errors.New("no AK certificate provided in instance info") + } + + // Parse the AK certificate + akCert, err := x509.ParseCertificate(instanceInfo.AkCert) + if err != nil { + return fmt.Errorf("parsing attestation key certificate: %w", err) + } + v.log.Debug(fmt.Sprintf("AK Certificate Subject: %s, Issuer: %s", akCert.Subject.String(), akCert.Issuer.String())) + + // Setup certificate pools + roots := x509.NewCertPool() + intermediates := x509.NewCertPool() + + // Add all known Azure's root CAs + // Microsoft RSA Devices Root CA 2021 (for older VMs) + roots.AddCert(microsoftRSADevicesRoot2021) + // Azure Virtual TPM Root Certificate Authority 2023 (for TDX and newer Trusted Launch) + roots.AddCert(azureVirtualTPMRoot2023) + + // Add known Azure's intermediate CAs + // Global Virtual TPM CA - 03 (for TDX VMs) + intermediates.AddCert(globalVirtualTPMCA03) + + // Verify the certificate chain + chains, err := akCert.Verify(x509.VerifyOptions{ + Roots: roots, + Intermediates: intermediates, + KeyUsages: []x509.ExtKeyUsage{x509.ExtKeyUsageAny}, + }) + if err != nil { + v.log.Warn(fmt.Sprintf("Certificate chain verification failed: %v", err)) + return fmt.Errorf("verifying attestation key certificate chain: %w", err) + } + + // Log the verified chain + for i, chain := range chains { + v.log.Debug(fmt.Sprintf("Verified chain %d:", i)) + for j, cert := range chain { + v.log.Debug(fmt.Sprintf(" [%d] %s", j, cert.Subject.String())) + } + } + + // Verify that the public key in the certificate matches the TPM's AK public key + pubKey, err := pubArea.Key() + if err != nil { + return fmt.Errorf("getting public key from TPM: %w", err) + } + + pubKeyRSA, ok := pubKey.(*rsa.PublicKey) + if !ok { + return errors.New("attestation key is not an RSA key") + } + + if !pubKeyRSA.Equal(akCert.PublicKey) { + v.log.Warn("Certificate public key does not match TPM attestation key") + return errors.New("certificate public key does not match attestation key") + } + + return nil +} + +func mustParseX509(pem string) *x509.Certificate { + cert, err := certutil.PemToX509Cert([]byte(pem)) + if err != nil { + panic(err) + } + return cert +} + type hclAkValidator interface { Validate(runtimeDataRaw []byte, reportData []byte, rsaParameters *tpm2.RSAParams) error } diff --git a/proxy/atls_config.go b/proxy/atls_config.go index 7f46c40..881abcf 100644 --- a/proxy/atls_config.go +++ b/proxy/atls_config.go @@ -84,7 +84,7 @@ func CreateAttestationIssuer(log *slog.Logger, attestationType AttestationType) } } -func CreateAttestationValidatorsFromFile(log *slog.Logger, jsonMeasurementsPath string) ([]atls.Validator, error) { +func CreateAttestationValidatorsFromFile(log *slog.Logger, jsonMeasurementsPath string, verifyAKCert bool) ([]atls.Validator, error) { if jsonMeasurementsPath == "" { return nil, nil } @@ -113,9 +113,11 @@ func CreateAttestationValidatorsFromFile(log *slog.Logger, jsonMeasurementsPath case AttestationAzureTDX: attConfig := config.DefaultForAzureTDX() attConfig.SetMeasurements(measurement.Measurements) + validator := azure_tdx.NewValidator(attConfig, AttestationLogger{Log: log}) + validator.SetVerifyAKCertificate(verifyAKCert) validatorsByType[attestationType] = append( validatorsByType[attestationType], - azure_tdx.NewValidator(attConfig, AttestationLogger{Log: log}), + validator, ) case AttestationDCAPTDX: attConfig := &config.QEMUTDX{Measurements: measurements.DefaultsFor(cloudprovider.QEMU, variant.QEMUTDX{})}