7.0 KiB
__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+encodeTimeso 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_DEEPandmaxChainDepthoverride scenarios. tamperTbshelper 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
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
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 ofbuildEcdsaP256Chainfor tests that only need a chain whose leaf signature dispatches through theverifyCertificateSignaturehappy path.buildExpiredLeaf(): { chain: BuiltChain; clockMs: number }— leaf whosenotAfteris one second beforeclockMs(drivesCERT_EXPIRED).buildNotYetValidLeaf(): { chain: BuiltChain; clockMs: number }— leaf whosenotBeforeis one second afterclockMs.buildSelfSignedLeaf(): BuiltChain— issuer === subject Ed25519 certificate.buildNameConstraintViolation(): BuiltChain— intermediate withpermittedSubtrees = [dNSName .example.com], leaf with SANhost.other.com.buildIpExcludedChain(): BuiltChain— intermediate withexcludedSubtrees = [iPAddress 10.0.0.0/8], leaf with SANiPAddress 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 withbasicConstraints.cA = false.buildNoCertSignIntermediateChain(): BuiltChain— intermediate whosekeyUsageomitskeyCertSign.buildPathLenViolation(): { rootCert; intermediateCerts; leafCert }— chain wherepathLenConstraintwould be exceeded.buildAnyEkuLeaf(): BuiltChain— leaf with EKU =anyExtendedKeyUsage.buildClientAuthOnlyLeaf(): BuiltChain— leaf with onlyclientAuthin EKU.buildUnknownAlgChain(): BuiltChain— leaf signed with an OID outside the algorithm map.buildPostUnixYearCert(): BuiltChain— root withnotAfterpast 2050.buildOverLongSerialDer(): Uint8Array— DER with a 21-byte serial number forCERTIFICATE_MALFORMEDtesting.buildUnsupportedVersionDer(): Uint8Array— DER with TBSversion = 3to driveUNSUPPORTED_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.