|
8 | 8 | import { verify as nodeVerify,X509Certificate } from 'node:crypto' |
9 | 9 | import { readFileSync } from 'node:fs' |
10 | 10 |
|
| 11 | +import { AsnConvert } from '@peculiar/asn1-schema' |
| 12 | +import { Certificate } from '@peculiar/asn1-x509' |
| 13 | +import { X509Certificate as PeculiarCert } from '@peculiar/x509' |
| 14 | + |
11 | 15 | import { Attestation, type Report } from '#src/proto/sevsnp.ts' |
12 | 16 |
|
| 17 | +// assertKDSLeafFormat enforces the AMD KDS cert-format invariants on the |
| 18 | +// VLEK/VCEK leaf — the checks go-sev-guest's validateKDSCertificateProductNonspecific |
| 19 | +// makes. The leaf is attacker-supplied (unlike the fingerprint-pinned ARK/ASK), |
| 20 | +// so its format is constrained: X.509 v3, signed by the AMD root key with |
| 21 | +// RSASSA-PSS/SHA-384, carrying an ECDSA P-384 public key. |
| 22 | +function assertKDSLeafFormat(der: Uint8Array): void { |
| 23 | + const cert = new PeculiarCert(Buffer.from(der).toString('base64')) |
| 24 | + |
| 25 | + const sig = cert.signatureAlgorithm as { name: string, hash?: { name: string } } |
| 26 | + if(sig.name !== 'RSA-PSS' || sig.hash?.name !== 'SHA-384') { |
| 27 | + throw new Error(`SEV: leaf cert signed with ${sig.name}/${sig.hash?.name}, want RSASSA-PSS/SHA-384`) |
| 28 | + } |
| 29 | + |
| 30 | + const key = cert.publicKey.algorithm as { name: string, namedCurve?: string } |
| 31 | + if(key.name !== 'ECDSA' || key.namedCurve !== 'P-384') { |
| 32 | + throw new Error(`SEV: leaf cert key is ${key.name}/${key.namedCurve}, want ECDSA P-384`) |
| 33 | + } |
| 34 | + |
| 35 | + // tbsCertificate.version is 0-indexed: 2 == X.509 v3. |
| 36 | + const version = AsnConvert.parse(Buffer.from(der), Certificate).tbsCertificate.version |
| 37 | + if(version !== 2) { |
| 38 | + throw new Error(`SEV: leaf cert is X.509 version ${version + 1}, want v3`) |
| 39 | + } |
| 40 | +} |
| 41 | + |
13 | 42 | const SIGNATURE_OFFSET = 0x2A0 |
14 | 43 | const REPORT_SIZE = 0x4A0 |
15 | 44 | const REPORT_VERSION3 = 3 |
@@ -133,15 +162,35 @@ export function verifySevReport( |
133 | 162 | throw new Error('SEV: attestation missing report or certificate chain') |
134 | 163 | } |
135 | 164 |
|
| 165 | + // Guest policy + VMPL: the report signature/chain prove genuine AMD hardware |
| 166 | + // but NOT that the guest is non-debuggable or at the highest privilege. A |
| 167 | + // DEBUG-policy guest lets the host decrypt/tamper its memory; reject it, and |
| 168 | + // require VMPL 0. (Mirrors assertSnpReportSafe in the Go verifier.) |
| 169 | + if((report.policy & (1n << 19n)) !== 0n) { |
| 170 | + throw new Error('SEV: guest policy permits DEBUG (host can decrypt guest memory)') |
| 171 | + } |
| 172 | + |
| 173 | + if(report.vmpl !== 0) { |
| 174 | + throw new Error(`SEV: report VMPL=${report.vmpl}, require 0`) |
| 175 | + } |
| 176 | + |
| 177 | + // Choose the signer cert by the report's own SignerInfo.SigningKey (0=VCEK, |
| 178 | + // 1=VLEK), not by which cert field happens to be present — otherwise an |
| 179 | + // attacker could attach the other key type. Then chain it to the matching |
| 180 | + // pinned AMD bundle. |
| 181 | + const signingKey = (Number(report.signerInfo) >> 2) & 0x7 |
| 182 | + const wantVlek = signingKey === 1 |
136 | 183 | const vlek = chain.vlekCert?.length ? Buffer.from(chain.vlekCert) : undefined |
137 | 184 | const vcek = chain.vcekCert?.length ? Buffer.from(chain.vcekCert) : undefined |
138 | | - const signerDer = vlek ?? vcek |
| 185 | + const signerDer = wantVlek ? vlek : vcek |
139 | 186 | if(!signerDer) { |
140 | | - throw new Error('SEV: certificate chain has neither VLEK nor VCEK') |
| 187 | + throw new Error(`SEV: report declares ${wantVlek ? 'VLEK' : 'VCEK'} signer but that cert is absent`) |
141 | 188 | } |
142 | 189 |
|
| 190 | + assertKDSLeafFormat(signerDer) |
143 | 191 | const signer = new X509Certificate(signerDer) |
144 | | - const bundle = parsePemBundle(vlek ? VLEK_BUNDLE_PEM : VCEK_BUNDLE_PEM) |
| 192 | + |
| 193 | + const bundle = parsePemBundle(wantVlek ? VLEK_BUNDLE_PEM : VCEK_BUNDLE_PEM) |
145 | 194 | verifyChainToAmdRoot(signer, bundle, now) |
146 | 195 |
|
147 | 196 | const signed = reportSignedComponent(report) |
|
0 commit comments