first commit
This commit is contained in:
@@ -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.
|
||||
Reference in New Issue
Block a user