/** * PKCS#10 CSR over the Ed25519 identity — PLAN_RELAY_AGENT T4. * * Node has no built-in CSR generator, so this is a compact, self-contained DER encoder that * emits a standard PKCS#10 CertificationRequest signed with the in-process Ed25519 key (INV4 — * only the PUBLIC key + a signature leave; the private key is never serialized here). * * NOTE (cross-plan / open Q#1): the exact SPIFFE-style cert PROFILE (SAN = subdomain vs a SPIFFE * URI) is set by P3's CA. This builds the standard subject-CN PKCS#10 shape; when P3 freezes its * profile, extend `buildCsr`'s attributes here. That is the single integration point. */ import type { AgentIdentity } from '../keys/identity.js' // --- minimal DER encoding helpers ------------------------------------------------------------- function derLen(len: number): Uint8Array { if (len < 0x80) return Uint8Array.from([len]) const bytes: number[] = [] let n = len while (n > 0) { bytes.unshift(n & 0xff) n >>= 8 } return Uint8Array.from([0x80 | bytes.length, ...bytes]) } function tlv(tag: number, value: Uint8Array): Uint8Array { const len = derLen(value.length) const out = new Uint8Array(1 + len.length + value.length) out[0] = tag out.set(len, 1) out.set(value, 1 + len.length) return out } function concat(chunks: readonly Uint8Array[]): Uint8Array { const total = chunks.reduce((s, c) => s + c.length, 0) const out = new Uint8Array(total) let off = 0 for (const c of chunks) { out.set(c, off) off += c.length } return out } const SEQUENCE = 0x30 const SET = 0x31 const INTEGER = 0x02 const BIT_STRING = 0x03 const OID = 0x06 const UTF8_STRING = 0x0c const CONTEXT_0 = 0xa0 // OID 2.5.4.3 (commonName) and 1.3.101.112 (Ed25519) as pre-encoded DER value bytes. const OID_CN = Uint8Array.from([0x55, 0x04, 0x03]) const OID_ED25519 = Uint8Array.from([0x2b, 0x65, 0x70]) /** SubjectPublicKeyInfo DER for a raw Ed25519 public key (fixed 44-byte structure). */ function spkiFromRawEd25519(raw: Uint8Array): Uint8Array { const algId = tlv(SEQUENCE, tlv(OID, OID_ED25519)) const pubBits = tlv(BIT_STRING, concat([Uint8Array.from([0x00]), raw])) return tlv(SEQUENCE, concat([algId, pubBits])) } /** X.501 Name with a single CN= RDN. */ function nameFromCn(cn: string): Uint8Array { const atv = tlv(SEQUENCE, concat([tlv(OID, OID_CN), tlv(UTF8_STRING, new TextEncoder().encode(cn))])) const rdn = tlv(SET, atv) return tlv(SEQUENCE, rdn) } function toPem(der: Uint8Array, label: string): string { const b64 = Buffer.from(der).toString('base64') const lines = b64.match(/.{1,64}/g) ?? [] return `-----BEGIN ${label}-----\n${lines.join('\n')}\n-----END ${label}-----\n` } /** * Build a PKCS#10 CSR (PEM) for `id` with subject CN=`subject`, signed by the Ed25519 key. * The private key is used in-process only; never serialized into the output (INV4). */ export function buildCsr(id: AgentIdentity, subject: string): string { const version = tlv(INTEGER, Uint8Array.from([0x00])) const name = nameFromCn(subject) const spki = spkiFromRawEd25519(id.publicKey) const attributes = tlv(CONTEXT_0, new Uint8Array(0)) // [0] IMPLICIT empty SET OF Attribute const requestInfo = tlv(SEQUENCE, concat([version, name, spki, attributes])) const signature = id.sign(requestInfo) // Ed25519 over CertificationRequestInfo const sigAlg = tlv(SEQUENCE, tlv(OID, OID_ED25519)) const sigBits = tlv(BIT_STRING, concat([Uint8Array.from([0x00]), signature])) const csr = tlv(SEQUENCE, concat([requestInfo, sigAlg, sigBits])) return toPem(csr, 'CERTIFICATE REQUEST') }