/** * A1 acceptance — ECDSA-P256 PKCS#10 parse + proof-of-possession (`ca/csr-ec.ts`), mirroring the * Ed25519 `ca/csr.ts` tests. Gate (d): a valid P-256 CSR yields `ok:true` with the correct embedded * pubkey; any tampered / mismatched / non-P256 / malformed CSR yields the UNIFORM `ok:false` result * with an empty embedded pubkey and no distinguishing detail (fail-closed). */ import 'reflect-metadata' import { describe, test, expect } from 'vitest' import * as x509 from '@peculiar/x509' import { webcrypto } from 'node:crypto' import { verifyCsrPoPEc, buildCsrEc } from '../src/ca/csr-ec.js' x509.cryptoProvider.set(webcrypto) /** WebCrypto key pair (the `CryptoKeyPair` global is not in this project's TS lib set). */ type KeyPair = { readonly publicKey: CryptoKey; readonly privateKey: CryptoKey } describe('verifyCsrPoPEc — gate (d): valid P-256 CSR', () => { test('a valid P-256 CSR → ok:true with the embedded SPKI matching the generating key', async () => { const { der, keys } = await buildCsrEc('CN=alice') const res = await verifyCsrPoPEc(der) expect(res.ok).toBe(true) expect(res.embeddedPubSpki.length).toBeGreaterThan(0) const exported = new Uint8Array(await webcrypto.subtle.exportKey('spki', keys.publicKey)) expect(Buffer.from(res.embeddedPubSpki).equals(Buffer.from(exported))).toBe(true) }) test('the embedded SPKI re-imports as an EC P-256 verifying key', async () => { const { der } = await buildCsrEc('CN=bob') const { embeddedPubSpki } = await verifyCsrPoPEc(der) const key = await webcrypto.subtle.importKey('spki', new Uint8Array(embeddedPubSpki), { name: 'ECDSA', namedCurve: 'P-256' }, true, ['verify']) expect(key.type).toBe('public') }) }) describe('verifyCsrPoPEc — fail-closed & uniform rejection', () => { test('a tampered CSR (flipped signature byte) → ok:false uniform', async () => { const { der } = await buildCsrEc('CN=carol') const tampered = new Uint8Array(der) tampered[tampered.length - 5]! ^= 0xff const res = await verifyCsrPoPEc(tampered) expect(res).toEqual({ ok: false, embeddedPubSpki: new Uint8Array(0) }) }) test('a CSR whose embedded key was swapped (PoP mismatch) → ok:false uniform', async () => { // Splice: take request-A's body but leave request-B's signature by concatenating mismatched // parts is fragile; instead flip several bytes inside the public-key region to break PoP. const { der } = await buildCsrEc('CN=dave') const mangled = new Uint8Array(der) // Corrupt a byte early in the structure (subject/pubkey area) so the self-signature no longer // matches the tbs — still parseable DER shape but PoP fails. mangled[25]! ^= 0xff const res = await verifyCsrPoPEc(mangled) expect(res).toEqual({ ok: false, embeddedPubSpki: new Uint8Array(0) }) }) test('malformed / non-DER garbage → ok:false uniform (no throw)', async () => { const res = await verifyCsrPoPEc(Uint8Array.from([1, 2, 3, 4, 5])) expect(res).toEqual({ ok: false, embeddedPubSpki: new Uint8Array(0) }) }) test('empty input → ok:false uniform', async () => { const res = await verifyCsrPoPEc(new Uint8Array(0)) expect(res).toEqual({ ok: false, embeddedPubSpki: new Uint8Array(0) }) }) test('a valid non-P256 CSR (Ed25519 key) → ok:false uniform (curve enforced)', async () => { const keys = (await webcrypto.subtle.generateKey({ name: 'Ed25519' }, true, ['sign', 'verify'])) as unknown as KeyPair const csr = await x509.Pkcs10CertificateRequestGenerator.create({ name: 'CN=ed-device', keys, signingAlgorithm: { name: 'Ed25519' }, }) const res = await verifyCsrPoPEc(new Uint8Array(csr.rawData)) expect(res).toEqual({ ok: false, embeddedPubSpki: new Uint8Array(0) }) }) test('a valid P-384 CSR → ok:false uniform (only P-256 accepted)', async () => { const keys = (await webcrypto.subtle.generateKey({ name: 'ECDSA', namedCurve: 'P-384' }, true, ['sign', 'verify'])) as unknown as KeyPair const csr = await x509.Pkcs10CertificateRequestGenerator.create({ name: 'CN=p384-device', keys, signingAlgorithm: { name: 'ECDSA', hash: 'SHA-384' }, }) const res = await verifyCsrPoPEc(new Uint8Array(csr.rawData)) expect(res).toEqual({ ok: false, embeddedPubSpki: new Uint8Array(0) }) }) test('CP3: each rejection returns a FRESH failure object (no shared module-level singleton)', async () => { const a = await verifyCsrPoPEc(new Uint8Array(0)) const b = await verifyCsrPoPEc(new Uint8Array(0)) expect(a).toEqual({ ok: false, embeddedPubSpki: new Uint8Array(0) }) // Distinct instances (result AND its embedded array) so one caller can never mutate another's. expect(a).not.toBe(b) expect(a.embeddedPubSpki).not.toBe(b.embeddedPubSpki) }) })