import { describe, expect, it } from 'vitest' import { X509Certificate, createPublicKey, verify } from 'node:crypto' import { generateIdentity, generateP256Identity } from '../src/keys/identity.js' import { buildCsr } from '../src/enroll/csr.js' // --- minimal DER reader (test-only) — walks the PKCS#10 outer SEQUENCE into its 3 children ------- interface Tlv { readonly tag: number /** the full tag+length+value bytes (what was signed, for the CertificationRequestInfo). */ readonly tlv: Uint8Array readonly content: Uint8Array } function readTlv(buf: Uint8Array, off: number): { node: Tlv; next: number } { const tag = buf[off]! let i = off + 1 const first = buf[i]! let len: number if (first < 0x80) { len = first i += 1 } else { const n = first & 0x7f len = 0 for (let k = 0; k < n; k++) len = (len << 8) | buf[i + 1 + k]! i += 1 + n } return { node: { tag, tlv: buf.subarray(off, i + len), content: buf.subarray(i, i + len) }, next: i + len } } function pemToDer(pem: string): Uint8Array { const b64 = pem.replace(/-----[A-Z ]+-----/g, '').replace(/\s+/g, '') return new Uint8Array(Buffer.from(b64, 'base64')) } /** The three children of the PKCS#10 outer SEQUENCE: [certificationRequestInfo, sigAlg, signature]. */ function csrChildren(pem: string): readonly Tlv[] { const der = pemToDer(pem) const outer = readTlv(der, 0).node const children: Tlv[] = [] let p = 0 while (p < outer.content.length) { const { node, next } = readTlv(outer.content, p) children.push(node) p = next } return children } describe('PKCS#10 CSR (T4)', () => { it('emits a PEM CERTIFICATE REQUEST', () => { const csr = buildCsr(generateIdentity(), 'host-42.term.example.com') expect(csr).toContain('-----BEGIN CERTIFICATE REQUEST-----') expect(csr).toContain('-----END CERTIFICATE REQUEST-----') }) it('does not contain private-key material (INV4)', () => { const id = generateIdentity() const csr = buildCsr(id, 'host-42') expect(csr).not.toContain('PRIVATE KEY') expect(csr).not.toContain(id.exportPrivatePkcs8Pem().split('\n')[1]!) }) it('produces a syntactically decodable DER structure', () => { // Node can't parse a CSR directly, but the base64 body must be valid DER (a SEQUENCE). const csr = buildCsr(generateIdentity(), 'host-42') const b64 = csr .replace(/-----[A-Z ]+-----/g, '') .replace(/\s+/g, '') const der = Buffer.from(b64, 'base64') expect(der[0]).toBe(0x30) // outer SEQUENCE tag expect(der.length).toBeGreaterThan(64) // sanity: X509Certificate exists (crypto available) — unrelated smoke to keep import used expect(typeof X509Certificate).toBe('function') }) }) describe('P-256 PKCS#10 CSR (FIX H-host-2)', () => { it('emits a PEM CERTIFICATE REQUEST for a P-256 identity', () => { const csr = buildCsr(generateP256Identity(), 'alice.terminal.yaojia.wang') expect(csr).toContain('-----BEGIN CERTIFICATE REQUEST-----') expect(csr).toContain('-----END CERTIFICATE REQUEST-----') expect(csr).not.toContain('PRIVATE KEY') }) it('round-trips as a valid PKCS#10: signatureAlgorithm is ecdsa-with-SHA256', () => { const csr = buildCsr(generateP256Identity(), 'alice.terminal.yaojia.wang') const [, sigAlg] = csrChildren(csr) // sigAlg = SEQUENCE { OID 1.2.840.10045.4.3.2 } (no parameters, RFC 5758 §3.2) const oidTlv = readTlv(sigAlg!.content, 0).node expect(Array.from(oidTlv.content)).toEqual([0x2a, 0x86, 0x48, 0xce, 0x3d, 0x04, 0x03, 0x02]) // no parameters: the sigAlg SEQUENCE holds ONLY the OID. expect(oidTlv.tlv.length).toBe(sigAlg!.content.length) }) it('self-signature verifies over the CertificationRequestInfo (verifyCsrPoPEc semantics)', () => { const id = generateP256Identity() const csr = buildCsr(id, 'alice.terminal.yaojia.wang') const [reqInfo, , sigVal] = csrChildren(csr) // signatureValue BIT STRING content = 0x00 (unused bits) || DER ECDSA-Sig-Value. expect(sigVal!.tag).toBe(0x03) const signature = sigVal!.content.subarray(1) const pub = createPublicKey({ key: Buffer.from(id.publicKey), format: 'der', type: 'spki' }) // Verify over the EXACT CertificationRequestInfo bytes that buildCsr signed. expect(verify('sha256', reqInfo!.tlv, pub, signature)).toBe(true) // Tampering with the signed body breaks PoP. const tampered = Uint8Array.from(reqInfo!.tlv) tampered[tampered.length - 1] = tampered[tampered.length - 1]! ^ 0xff expect(verify('sha256', tampered, pub, signature)).toBe(false) }) it('embeds the identity EC SPKI verbatim as the CSR subjectPublicKeyInfo', () => { const id = generateP256Identity() const csr = buildCsr(id, 'alice.terminal.yaojia.wang') const [reqInfo] = csrChildren(csr) // requestInfo = SEQUENCE { version, name, spki, [0] attributes }; the spki is the 3rd child. const inner = reqInfo!.content const version = readTlv(inner, 0) const name = readTlv(inner, version.next) const spki = readTlv(inner, name.next).node expect(Buffer.from(spki.tlv).equals(Buffer.from(id.publicKey))).toBe(true) }) })