Files
skills/docs/security-stack/security/x509/src/__fixtures__/build-certs.md
2026-05-29 08:20:56 +01:00

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 + 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

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 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.