/** * A1 acceptance (FIX C-1) — the single X.509 issuance primitive. Proves that a leaf assembled by * DER-encoding the TBS ourselves and signing the SERIALIZED TBS via `CaSigner.sign` (the KMS * boundary) is a REAL, tool-parseable, signature-verifiable X.509 v3 certificate for BOTH the * Ed25519 and ECDSA-P256 CA families — and that the dNSName+URI SAN the A3 nginx njs will parse * round-trips byte-correct. */ import 'reflect-metadata' import { describe, test, expect } from 'vitest' import * as x509 from '@peculiar/x509' import { AsnConvert } from '@peculiar/asn1-schema' import { Certificate } from '@peculiar/asn1-x509' import { webcrypto, generateKeyPairSync, randomBytes } from 'node:crypto' import { execFileSync } from 'node:child_process' import { mkdtempSync, writeFileSync, rmSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' import { assembleCertificate, normalizeEcdsaSignatureToDer, type AssembleCertificateInput, } from '../src/ca/x509-assembler.js' import { inProcessCaSigner, inProcessP256CaSigner, type CaSigner } from '../src/boot/ca-wiring.js' x509.cryptoProvider.set(webcrypto) const DAY_MS = 24 * 60 * 60 * 1000 const SAN_DNS = 'alice.terminal.yaojia.wang' const SAN_URI = 'spiffe://relay.example.com/account/a1/host/alice' /** WebCrypto key pair (the `CryptoKeyPair` global is not in this project's TS lib set). */ type KeyPair = { readonly publicKey: CryptoKey; readonly privateKey: CryptoKey } async function genP256(): Promise { return (await webcrypto.subtle.generateKey({ name: 'ECDSA', namedCurve: 'P-256' }, true, ['sign', 'verify'])) as unknown as KeyPair } const ED25519_SPKI_PREFIX = Uint8Array.from([ 0x30, 0x2a, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x70, 0x03, 0x21, 0x00, ]) function derToPem(der: Uint8Array, label = 'CERTIFICATE'): string { const b64 = Buffer.from(der).toString('base64').match(/.{1,64}/g)?.join('\n') ?? '' return `-----BEGIN ${label}-----\n${b64}\n-----END ${label}-----\n` } function leafExtensions(): x509.Extension[] { return [ new x509.SubjectAlternativeNameExtension([ { type: 'dns', value: SAN_DNS }, { type: 'url', value: SAN_URI }, ]), new x509.BasicConstraintsExtension(false, undefined, true), new x509.KeyUsagesExtension(x509.KeyUsageFlags.digitalSignature, true), new x509.ExtendedKeyUsageExtension([x509.ExtendedKeyUsage.clientAuth]), ] } async function importEd25519Public(raw: Uint8Array): Promise { const spki = new Uint8Array(ED25519_SPKI_PREFIX.length + raw.length) spki.set(ED25519_SPKI_PREFIX, 0) spki.set(raw, ED25519_SPKI_PREFIX.length) return webcrypto.subtle.importKey('spki', spki, { name: 'Ed25519' }, true, ['verify']) } async function importP256Public(spkiDer: Uint8Array): Promise { // Copy into a fresh ArrayBuffer-backed view so it satisfies BufferSource (not ArrayBufferLike). return webcrypto.subtle.importKey('spki', new Uint8Array(spkiDer), { name: 'ECDSA', namedCurve: 'P-256' }, true, ['verify']) } async function ed25519SubjectSpki(): Promise { const { publicKey } = generateKeyPairSync('ed25519') return new Uint8Array(publicKey.export({ format: 'der', type: 'spki' })) } function baseInput(overrides: Partial): AssembleCertificateInput { const now = Date.now() return { subjectPublicKey: new Uint8Array(0), subject: 'CN=alice', issuer: 'CN=test-CA', serialNumber: randomBytes(16), notBefore: new Date(now - 60_000), notAfter: new Date(now + DAY_MS), extensions: leafExtensions(), signer: inProcessCaSigner(), sigAlg: 'ed25519', ...overrides, } } describe('x509-assembler — gate (a): assembled leaves re-parse as X.509', () => { test('Ed25519 leaf re-parses via new x509.X509Certificate without throwing', async () => { const der = await assembleCertificate(baseInput({ subjectPublicKey: await ed25519SubjectSpki() })) expect(() => new x509.X509Certificate(der)).not.toThrow() }) test('ECDSA-P256 leaf re-parses via new x509.X509Certificate without throwing', async () => { const subject = await genP256() const der = await assembleCertificate(baseInput({ subjectPublicKey: subject.publicKey, signer: inProcessP256CaSigner(), sigAlg: 'ecdsa-p256' })) expect(() => new x509.X509Certificate(der)).not.toThrow() expect(new x509.X509Certificate(der).subject).toBe('CN=alice') }) }) describe('x509-assembler — gate (b): re-parsed leaf signature verifies against the CA public key', () => { test('Ed25519 leaf verifies against the Ed25519 CA public key', async () => { const ca = inProcessCaSigner() const der = await assembleCertificate(baseInput({ subjectPublicKey: await ed25519SubjectSpki(), signer: ca, sigAlg: 'ed25519' })) const leaf = new x509.X509Certificate(der) const caPub = await importEd25519Public(ca.publicKeyRaw) expect(await leaf.verify({ publicKey: caPub, signatureOnly: true })).toBe(true) }) test('ECDSA-P256 leaf verifies against the P-256 CA public key', async () => { const ca = inProcessP256CaSigner() const subject = await genP256() const der = await assembleCertificate(baseInput({ subjectPublicKey: subject.publicKey, signer: ca, sigAlg: 'ecdsa-p256' })) const leaf = new x509.X509Certificate(der) const caPub = await importP256Public(ca.publicKeyRaw) expect(await leaf.verify({ publicKey: caPub, signatureOnly: true })).toBe(true) }) test('a leaf does NOT verify against a different CA key (negative)', async () => { const ca = inProcessP256CaSigner() const subject = await genP256() const der = await assembleCertificate(baseInput({ subjectPublicKey: subject.publicKey, signer: ca, sigAlg: 'ecdsa-p256' })) const leaf = new x509.X509Certificate(der) const otherCaPub = await importP256Public(inProcessP256CaSigner().publicKeyRaw) expect(await leaf.verify({ publicKey: otherCaPub, signatureOnly: true })).toBe(false) }) }) describe('x509-assembler — gate (c): dNSName + URI SAN round-trips byte-correct', () => { test('Ed25519 leaf SAN carries both the dNSName and the URI', async () => { const der = await assembleCertificate(baseInput({ subjectPublicKey: await ed25519SubjectSpki() })) const san = new x509.X509Certificate(der).getExtension(x509.SubjectAlternativeNameExtension) const names = san!.names.toJSON() expect(names).toContainEqual({ type: 'dns', value: SAN_DNS }) expect(names).toContainEqual({ type: 'url', value: SAN_URI }) }) test('ECDSA-P256 leaf SAN carries both the dNSName and the URI', async () => { const subject = await genP256() const der = await assembleCertificate(baseInput({ subjectPublicKey: subject.publicKey, signer: inProcessP256CaSigner(), sigAlg: 'ecdsa-p256' })) const san = new x509.X509Certificate(der).getExtension(x509.SubjectAlternativeNameExtension) const names = san!.names.toJSON() expect(names).toContainEqual({ type: 'dns', value: SAN_DNS }) expect(names).toContainEqual({ type: 'url', value: SAN_URI }) }) }) describe('x509-assembler — X.509 structural invariants', () => { test('tbsCertificate.signature OID equals certificate.signatureAlgorithm OID (identical)', async () => { const der = await assembleCertificate(baseInput({ subjectPublicKey: await ed25519SubjectSpki() })) const cert = AsnConvert.parse(der.buffer.slice(der.byteOffset, der.byteOffset + der.byteLength) as ArrayBuffer, Certificate) expect(cert.tbsCertificate.signature.algorithm).toBe(cert.signatureAlgorithm.algorithm) expect(cert.signatureAlgorithm.algorithm).toBe('1.3.101.112') }) test('ECDSA-P256 signatureAlgorithm OID is ecdsa-with-SHA256', async () => { const subject = await genP256() const der = await assembleCertificate(baseInput({ subjectPublicKey: subject.publicKey, signer: inProcessP256CaSigner(), sigAlg: 'ecdsa-p256' })) const cert = AsnConvert.parse(der.buffer.slice(der.byteOffset, der.byteOffset + der.byteLength) as ArrayBuffer, Certificate) expect(cert.tbsCertificate.signature.algorithm).toBe('1.2.840.10045.4.3.2') expect(cert.signatureAlgorithm.algorithm).toBe('1.2.840.10045.4.3.2') }) test('a high-bit serial is emitted as a positive INTEGER (0x00-prefixed content octets)', async () => { const highBit = Uint8Array.from([0x80, 0x01, 0x02, 0x03]) const der = await assembleCertificate(baseInput({ subjectPublicKey: await ed25519SubjectSpki(), serialNumber: highBit })) // Inspect the raw INTEGER content octets: the assembler MUST prepend 0x00 so a leading 0x80 is // never read as a negative integer. (@peculiar's serialNumber getter strips it for display.) const cert = AsnConvert.parse(der.buffer.slice(der.byteOffset, der.byteOffset + der.byteLength) as ArrayBuffer, Certificate) const serialBytes = new Uint8Array(cert.tbsCertificate.serialNumber) expect(Array.from(serialBytes)).toEqual([0x00, 0x80, 0x01, 0x02, 0x03]) }) test('subject public key accepted as BOTH a CryptoKey and as SPKI DER', async () => { const kp = await genP256() const spkiDer = new Uint8Array(await webcrypto.subtle.exportKey('spki', kp.publicKey)) const ca = inProcessP256CaSigner() const fromKey = await assembleCertificate(baseInput({ subjectPublicKey: kp.publicKey, signer: ca, sigAlg: 'ecdsa-p256' })) const fromDer = await assembleCertificate(baseInput({ subjectPublicKey: spkiDer, signer: ca, sigAlg: 'ecdsa-p256' })) // Both embed the SAME subjectPublicKeyInfo. expect(new x509.X509Certificate(fromKey).publicKey.rawData.byteLength).toBe(new x509.X509Certificate(fromDer).publicKey.rawData.byteLength) expect(Buffer.from(new x509.X509Certificate(fromKey).publicKey.rawData).equals(Buffer.from(new x509.X509Certificate(fromDer).publicKey.rawData))).toBe(true) }) }) describe('normalizeEcdsaSignatureToDer — both signer output shapes', () => { test('raw P1363 (64 bytes) is converted to a valid DER ECDSA-Sig-Value', async () => { const kp = generateKeyPairSync('ec', { namedCurve: 'P-256' }) const { sign } = await import('node:crypto') const raw = new Uint8Array(sign('sha256', Buffer.from('hello'), { key: kp.privateKey, dsaEncoding: 'ieee-p1363' })) expect(raw.length).toBe(64) const der = normalizeEcdsaSignatureToDer(raw) expect(der[0]).toBe(0x30) // SEQUENCE // openssl-shape signature verifies against the same key (proves the r,s survived intact). const { verify } = await import('node:crypto') expect(verify('sha256', Buffer.from('hello'), { key: kp.publicKey, dsaEncoding: 'der' }, Buffer.from(der))).toBe(true) }) test('an already-DER ECDSA-Sig-Value passes through unchanged', async () => { const kp = generateKeyPairSync('ec', { namedCurve: 'P-256' }) const { sign } = await import('node:crypto') const der = new Uint8Array(sign('sha256', Buffer.from('world'), { key: kp.privateKey, dsaEncoding: 'der' })) const out = normalizeEcdsaSignatureToDer(der) expect(Buffer.from(out).equals(Buffer.from(der))).toBe(true) }) test('garbage that is neither P1363 nor DER throws (fail loud at issuance)', () => { expect(() => normalizeEcdsaSignatureToDer(Uint8Array.from([1, 2, 3, 4, 5]))).toThrow() }) }) describe('x509-assembler — Ed25519 signatureValue length guard (CP2)', () => { /** A CaSigner whose `sign` returns a wrong-length "Ed25519" signature (truncated / malformed). */ function fixedLenEd25519Signer(len: number): CaSigner { return { publicKeyRaw: new Uint8Array(32), async sign() { return new Uint8Array(len) } } } test('a signer returning fewer than 64 bytes is rejected at issuance (not embedded)', async () => { await expect( assembleCertificate( baseInput({ subjectPublicKey: await ed25519SubjectSpki(), signer: fixedLenEd25519Signer(63), sigAlg: 'ed25519' }), ), ).rejects.toThrow(/64 bytes/) }) test('a signer returning more than 64 bytes is rejected too', async () => { await expect( assembleCertificate( baseInput({ subjectPublicKey: await ed25519SubjectSpki(), signer: fixedLenEd25519Signer(65), sigAlg: 'ed25519' }), ), ).rejects.toThrow(/64 bytes/) }) test('a real 64-byte Ed25519 signature still issues fine (no false positive)', async () => { const der = await assembleCertificate(baseInput({ subjectPublicKey: await ed25519SubjectSpki() })) expect(() => new x509.X509Certificate(der)).not.toThrow() }) }) describe('x509-assembler — gate (e): openssl parses & verifies the chain', () => { function hasOpenssl(): boolean { try { execFileSync('openssl', ['version'], { stdio: 'ignore' }) return true } catch { return false } } test('openssl x509 -text parses the P-256 leaf and openssl verify OKs it against the CA', async () => { if (!hasOpenssl()) { // openssl absent — gates (a)-(c) already prove real X.509; note and skip the tool check. expect(true).toBe(true) return } // Self-signed P-256 CA (subject key == signer key) so openssl can build a full chain. const caKeys = generateKeyPairSync('ec', { namedCurve: 'P-256' }) const caSpki = new Uint8Array(caKeys.publicKey.export({ format: 'der', type: 'spki' })) const caSigner: CaSigner = inProcessP256CaSigner(caKeys.privateKey) const now = Date.now() const caDer = await assembleCertificate({ subjectPublicKey: caSpki, subject: 'CN=Test P256 CA', issuer: 'CN=Test P256 CA', serialNumber: Uint8Array.from([0x01]), notBefore: new Date(now - DAY_MS), notAfter: new Date(now + 365 * DAY_MS), extensions: [ new x509.BasicConstraintsExtension(true, undefined, true), new x509.KeyUsagesExtension(x509.KeyUsageFlags.keyCertSign | x509.KeyUsageFlags.cRLSign, true), ], signer: caSigner, sigAlg: 'ecdsa-p256', }) const subject = await genP256() const leafDer = await assembleCertificate(baseInput({ subjectPublicKey: subject.publicKey, issuer: 'CN=Test P256 CA', signer: caSigner, sigAlg: 'ecdsa-p256' })) const dir = mkdtempSync(join(tmpdir(), 'x509asm-')) try { const caPem = join(dir, 'ca.pem') const leafPem = join(dir, 'leaf.pem') writeFileSync(caPem, derToPem(caDer)) writeFileSync(leafPem, derToPem(leafDer)) const text = execFileSync('openssl', ['x509', '-in', leafPem, '-noout', '-text'], { encoding: 'utf8' }) expect(text).toContain('Signature Algorithm: ecdsa-with-SHA256') expect(text).toContain(SAN_DNS) expect(text).toContain(SAN_URI) const verifyOut = execFileSync('openssl', ['verify', '-CAfile', caPem, leafPem], { encoding: 'utf8' }) expect(verifyOut).toContain('OK') } finally { rmSync(dir, { recursive: true, force: true }) } }) })