Skip to content

Commit 5b1495f

Browse files
authored
[SEC] SEV-SNP verifier: reject DEBUG/VMPL, pick signer by SignerInfo, KDS leaf-format checks (#89)
H1: reject guest policy DEBUG bit (host-decryptable guest) + non-zero VMPL — the report sig/chain prove genuine AMD HW but not a non-debug, VMPL-0 guest (mirrors the Go assertSnpReportSafe). H4: choose VLEK vs VCEK by the report's SignerInfo.SigningKey ((info>>2)&7), not by which cert field is present (else an attacker attaches the other type); and assertKDSLeafFormat enforces the AMD KDS cert-format invariants on the attacker-supplied leaf (go-sev-guest's validateKDSCertificateProductNonspecific): X.509 v3, RSASSA-PSS/SHA-384 signature, ECDSA P-384 key. (KeyUsage not checked — genuine VLEK/VCEK leaves carry no KeyUsage extension; ARK/ASK stay pinned by fingerprint.) Uses @peculiar/x509 + @peculiar/asn1-x509. Fixtures 7/7 green.
1 parent fd2f859 commit 5b1495f

1 file changed

Lines changed: 52 additions & 3 deletions

File tree

src/server/utils/sev-snp/sev-report.ts

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,37 @@
88
import { verify as nodeVerify,X509Certificate } from 'node:crypto'
99
import { readFileSync } from 'node:fs'
1010

11+
import { AsnConvert } from '@peculiar/asn1-schema'
12+
import { Certificate } from '@peculiar/asn1-x509'
13+
import { X509Certificate as PeculiarCert } from '@peculiar/x509'
14+
1115
import { Attestation, type Report } from '#src/proto/sevsnp.ts'
1216

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+
1342
const SIGNATURE_OFFSET = 0x2A0
1443
const REPORT_SIZE = 0x4A0
1544
const REPORT_VERSION3 = 3
@@ -133,15 +162,35 @@ export function verifySevReport(
133162
throw new Error('SEV: attestation missing report or certificate chain')
134163
}
135164

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
136183
const vlek = chain.vlekCert?.length ? Buffer.from(chain.vlekCert) : undefined
137184
const vcek = chain.vcekCert?.length ? Buffer.from(chain.vcekCert) : undefined
138-
const signerDer = vlek ?? vcek
185+
const signerDer = wantVlek ? vlek : vcek
139186
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`)
141188
}
142189

190+
assertKDSLeafFormat(signerDer)
143191
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)
145194
verifyChainToAmdRoot(signer, bundle, now)
146195

147196
const signed = reportSignedComponent(report)

0 commit comments

Comments
 (0)