/** * Real X.509 leaf issuance (INV14). After the shared `assertLeafGate` passes, emit an X.509 v3 * Ed25519 leaf whose subject key is the enrolled agent pubkey and whose only SAN is the host's * SPIFFE-ID URI — signed by the intermediate Ed25519 key. This is what `relay-auth`'s * `verifyAgentCert` accepts (it walks leaf → intermediate → self-signed root and parses the * `URI:spiffe://relay./account//host/` SAN). * * The SPIFFE-ID is built with relay-auth's OWN builder (`spiffeIdFor`, deep-imported) so the emitted * SAN can never drift from the verifier's parser. The intermediate PRIVATE key is a WebCrypto * `CryptoKey` imported non-extractable (never serialised back out — INV9). * * `reflect-metadata` must load before `@peculiar/x509` (tsyringe polyfill) — keep it first. */ import 'reflect-metadata' import * as x509 from '@peculiar/x509' import { webcrypto, randomBytes } from 'node:crypto' import { spiffeIdFor } from 'relay-auth/src/agent/spiffe.js' import type { HostStore } from '../store/ports.js' import { assertLeafGate, DEFAULT_LEAF_TTL_SEC, type LeafSigner } from './sign.js' x509.cryptoProvider.set(webcrypto) /** Backdate notBefore slightly to tolerate small clock skew between control-plane and relay. */ const CLOCK_SKEW_SEC = 60 export interface RealLeafSignerDeps { readonly hosts: HostStore /** Intermediate Ed25519 PRIVATE signing key (WebCrypto, non-extractable). */ readonly intermediateKey: CryptoKey /** Intermediate subject as a Name — used verbatim as the leaf issuer so `checkIssued` matches. */ readonly issuerName: x509.Name /** DER of [intermediate, root] returned to the agent as its CA bundle (INV14). */ readonly caChainDer: readonly Uint8Array[] /** Bare trust domain; the SPIFFE builder prepends `relay.`. */ readonly trustDomain: string readonly leafTtlSec?: number } /** Import a raw 32-byte Ed25519 public key as a verifying WebCrypto CryptoKey (via SPKI DER). */ async function importEd25519Public(raw: Uint8Array): Promise { const prefix = Uint8Array.from([ 0x30, 0x2a, 0x30, 0x05, 0x06, 0x03, 0x2b, 0x65, 0x70, 0x03, 0x21, 0x00, ]) const spki = new Uint8Array(prefix.length + raw.length) spki.set(prefix, 0) spki.set(raw, prefix.length) return webcrypto.subtle.importKey('spki', spki, { name: 'Ed25519' }, true, ['verify']) } /** * Build the production leaf signer. Every issued leaf: X.509 v3, Ed25519 subject = agentPubkey, * URI SAN = the host's SPIFFE-ID, CA:false, KeyUsage digitalSignature, EKU clientAuth, validity * [now-skew, now+ttl]. Returns leaf DER + the injected CA chain DER; `redeem.ts` PEM-wraps both. */ export function createRealLeafSigner(deps: RealLeafSignerDeps): LeafSigner { const ttl = deps.leafTtlSec ?? DEFAULT_LEAF_TTL_SEC return { async signHostLeaf(hostId, agentPubkey, csr) { const host = await assertLeafGate(deps.hosts, hostId, agentPubkey, csr) const spiffe = spiffeIdFor(host.accountId, host.hostId, deps.trustDomain) const subjectKey = await importEd25519Public(agentPubkey) const now = Date.now() const leaf = await x509.X509CertificateGenerator.create({ serialNumber: randomBytes(16).toString('hex'), subject: `CN=${host.hostId}`, issuer: deps.issuerName, notBefore: new Date(now - CLOCK_SKEW_SEC * 1000), notAfter: new Date(now + ttl * 1000), publicKey: subjectKey, signingKey: deps.intermediateKey, signingAlgorithm: { name: 'Ed25519' }, extensions: [ new x509.SubjectAlternativeNameExtension([{ type: 'url', value: spiffe }]), new x509.BasicConstraintsExtension(false, undefined, true), new x509.KeyUsagesExtension(x509.KeyUsageFlags.digitalSignature, true), new x509.ExtendedKeyUsageExtension([x509.ExtendedKeyUsage.clientAuth]), ], }) return { cert: new Uint8Array(leaf.rawData), caChain: deps.caChainDer } }, } } export interface LoadRealLeafSignerInput { readonly hosts: HostStore /** Intermediate Ed25519 private key, PKCS#8 PEM. */ readonly intermediateKeyPem: string /** Intermediate certificate, PEM (single block). */ readonly intermediateCertPem: string /** Self-signed root certificate, PEM (single block). */ readonly rootCertPem: string readonly trustDomain: string readonly leafTtlSec?: number } function pemToDer(pem: string): ArrayBuffer { const body = pem.replace(/-----BEGIN [^-]+-----/g, '').replace(/-----END [^-]+-----/g, '').replace(/\s+/g, '') const bytes = Buffer.from(body, 'base64') return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength) as ArrayBuffer } /** * Construct a real leaf signer from PEM material (boot path). Imports the intermediate private key * (non-extractable — never re-serialised, INV9) and derives the issuer Name + CA chain DER from the * certs. THROWS on unreadable/malformed material so the control-plane fails fast at boot. */ export async function loadRealLeafSigner(input: LoadRealLeafSignerInput): Promise { const intermediateKey = await webcrypto.subtle.importKey( 'pkcs8', pemToDer(input.intermediateKeyPem), { name: 'Ed25519' }, false, // non-extractable: the raw private key can never leave the process (INV9) ['sign'], ) const intermediateCert = new x509.X509Certificate(input.intermediateCertPem) const rootCert = new x509.X509Certificate(input.rootCertPem) return createRealLeafSigner({ hosts: input.hosts, intermediateKey, issuerName: intermediateCert.subjectName, caChainDer: [new Uint8Array(intermediateCert.rawData), new Uint8Array(rootCert.rawData)], trustDomain: input.trustDomain, ...(input.leafTtlSec !== undefined ? { leafTtlSec: input.leafTtlSec } : {}), }) }