first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,155 @@
# `__fixtures__/build-certs` — deterministic real-crypto certificate builders
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## 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.