# `__fixtures__/build-certs` — deterministic real-crypto certificate builders _(no submodules)_ ## Purpose This module emits real, signature-verifiable X.509 certificates entirely from in-process code: hand-rolled ASN.1 DER via the package's own `encodeAsn1`, signed with real Ed25519 and ECDSA P-256 keys. Outputs are deterministic because the private keys are hardcoded test constants and both signing primitives are deterministic — Ed25519 by RFC 8032 and ECDSA by RFC 6979 `k` selection in `@noble/curves`. This makes every test that consumes a fixture both reproducible and runnable offline without network or shell-out to `openssl`. The fixtures replace what would otherwise require a Mozilla NSS test PKI: they cover root → intermediate → leaf chains under both Ed25519 and ECDSA P-256, plus single-issue mutants (expired leaf, not-yet-valid leaf, self-signed leaf, name-constraint violator, critical-unknown extension, oversize chain, non-CA intermediate, no-keyCertSign intermediate, IP-prefix excluded subtree, EKU client-only, EKU anyExtendedKeyUsage, post-2050 GeneralizedTime cert, unknown algorithm OID). ## Features - **Deterministic Ed25519 + ECDSA P-256 chains** with consistent SKI ↔ AKI wiring across root/intermediate/leaf. - **Hand-rolled DER** through `encodeAsn1` + `encodeOid` + `encodeTime` so fixtures exercise the same encoder used by signature verify's TBS re-encoding path. - **Real signature bytes** over canonical TBS DER. The fixture pipeline feeds the parser's expected raw shape — no shortcuts via mocks. - **One mutant per validation failure mode** the spec enumerates (expired, not-yet-valid, self-signed, name-constraint, critical-unknown, etc.). - **Oversize chain builder** of caller-specified depth for the `CHAIN_TOO_DEEP` and `maxChainDepth` override scenarios. - **`tamperTbs`** helper for the signature-tampering test path. - **RSA-2048 SPKI constant** plus an `spkiRsa()` builder for the signature-algorithm-rejection branch (no live RSA keygen required). ## Usage: Basic Example ```ts import { buildEcdsaP256Chain, } from "@suaveplan/x509/__fixtures__/build-certs"; import { validateChain } from "@suaveplan/x509"; const { rootCert, intermediateCert, leafCert } = buildEcdsaP256Chain(); await validateChain({ leaf: leafCert, intermediates: intermediateCert === undefined ? [] : [intermediateCert], trustAnchors: [rootCert], clock: () => Date.UTC(2025, 5, 1), }); ``` ## Usage: Advanced Example ```ts import { buildOversizeChain, tamperTbs, } from "@suaveplan/x509/__fixtures__/build-certs"; import { parseCertificate, validateChain } from "@suaveplan/x509"; // Build a 17-cert chain to exercise CHAIN_TOO_DEEP under default options. const { rootCert, intermediateCerts, leafCert } = buildOversizeChain(17); await expect( validateChain({ leaf: leafCert, intermediates: intermediateCerts, trustAnchors: [rootCert], clock: () => Date.UTC(2025, 5, 1), }) ).rejects.toThrow(/CHAIN_TOO_DEEP/); // Flip a TBS byte to drive SIGNATURE_INVALID through verifyCertificateSignature. const { leafDer } = buildEcdsaP256Chain(); const tampered = parseCertificate(tamperTbs(leafDer)); ``` ## API Reference - `buildEd25519Chain(): BuiltChain` — root → intermediate → leaf signed with Ed25519. SKI and AKI wired across all three. - `buildEcdsaP256Chain(): BuiltChain` — same shape with ECDSA P-256 + SHA-256. - `buildRsaSha256Chain` — alias of `buildEcdsaP256Chain` for tests that only need a chain whose leaf signature dispatches through the `verifyCertificateSignature` happy path. - `buildExpiredLeaf(): { chain: BuiltChain; clockMs: number }` — leaf whose `notAfter` is one second before `clockMs` (drives `CERT_EXPIRED`). - `buildNotYetValidLeaf(): { chain: BuiltChain; clockMs: number }` — leaf whose `notBefore` is one second after `clockMs`. - `buildSelfSignedLeaf(): BuiltChain` — issuer === subject Ed25519 certificate. - `buildNameConstraintViolation(): BuiltChain` — intermediate with `permittedSubtrees = [dNSName .example.com]`, leaf with SAN `host.other.com`. - `buildIpExcludedChain(): BuiltChain` — intermediate with `excludedSubtrees = [iPAddress 10.0.0.0/8]`, leaf with SAN `iPAddress 10.1.2.3`. - `buildCriticalUnknownLeaf(): BuiltChain` — leaf with critical extension at unrecognised OID. - `buildOversizeChain(totalCerts): { rootCert; intermediateCerts; leafCert }` — chain of caller-specified depth. - `buildNonCaIntermediateChain(): BuiltChain` — intermediate with `basicConstraints.cA = false`. - `buildNoCertSignIntermediateChain(): BuiltChain` — intermediate whose `keyUsage` omits `keyCertSign`. - `buildPathLenViolation(): { rootCert; intermediateCerts; leafCert }` — chain where `pathLenConstraint` would be exceeded. - `buildAnyEkuLeaf(): BuiltChain` — leaf with EKU = `anyExtendedKeyUsage`. - `buildClientAuthOnlyLeaf(): BuiltChain` — leaf with only `clientAuth` in EKU. - `buildUnknownAlgChain(): BuiltChain` — leaf signed with an OID outside the algorithm map. - `buildPostUnixYearCert(): BuiltChain` — root with `notAfter` past 2050. - `buildOverLongSerialDer(): Uint8Array` — DER with a 21-byte serial number for `CERTIFICATE_MALFORMED` testing. - `buildUnsupportedVersionDer(): Uint8Array` — DER with TBS `version = 3` to drive `UNSUPPORTED_VERSION`. - `tamperTbs(der: Uint8Array): Uint8Array` — flips an inner TBS byte while preserving outer envelope shapes for signature-tampering tests. - `RSA_N_HEX`, `hexToBytes`, `spkiRsa()` — hex constants + SPKI builder for the RSA test surface. ## Implementation Notes The builders emit canonical DER through the same `encodeAsn1` the package's production code calls, so any encoder regression breaks fixture tests first. Signing uses `ed25519.sign(tbsBytes, sk)` directly for Ed25519 and `sha256` + `p256.sign` + DER re-wrap (`SEQUENCE { r INTEGER, s INTEGER }`) for ECDSA. The output of `p256.sign` is RFC 6979-deterministic, so the SEQUENCE bytes are reproducible across runs. `buildOversizeChain(totalCerts)` derives `totalCerts` distinct private keys by filling 32-byte buffers with the byte value `0x80 + i`. This stays well within the Curve25519 valid-scalar range while remaining cheap to compute. Name-constraints fixtures wrap `dNSName` and `iPAddress` GeneralName entries exactly as RFC 5280 §4.2.1.10 requires. The IP-prefix builder concatenates prefix bytes with a network-mask byte string so the on-the-wire body matches the production decoder's expected shape (`base.value = "10.0.0.0/8"` in parsed form, address+mask bytes on the wire). The RSA SPKI helper carries a fixed 2048-bit modulus + e = 65 537 so tests that touch the signature-algorithm map can exercise the RSA branches without paying for a 4-second Miller-Rabin keygen cycle. The fixture does not include the private key — RSA signature-verify tests target the algorithm-resolution path only; the actual rsaVerify call would simply return false against an unrelated signature, which is what the `SIGNATURE_INVALID` test path asserts.