25 KiB
@suaveplan/x509 — RFC 5280 X.509 v3 parser + certification-path validator
Package: @suaveplan/x509 (NEW)
Status: PROPOSED
Type: new-package (genesis-original; greenfield — no upstream port)
Date: 2026-05-11
Path: packages/security/x509/
Why
@suaveplan/webauthn parses three attestation statement formats — tpm (TPM 2.0 attestation), android-key (Google Hardware Attestation), apple (Apple Anonymous Attestation) — each of which carries an X.509 certificate chain in its x5c array and requires that chain to validate against a pinned set of trust anchors (FIDO MDS root CAs for TPM, Google Hardware Attestation roots for android-key, the Apple WebAuthn Root CA for apple). Without a real RFC 5280 §6.1 path validator the WebAuthn attestation verifiers either trust the leaf blindly (security defect) or ship as half-baked stubs (banned per CLAUDE.md §3 and OpenSpec AGENTS.md §2b). Both options are unacceptable on genesis.
External alternatives (node-forge, pkijs, @peculiar/x509, asn1.js) drag in CommonJS interop hazards, oversize transitive deps, and behaviours that diverge from RFC 5280 strict-profile requirements (most notably around the §6.1.4(f) handling of unrecognised critical extensions, and around DER canonicalisation when re-hashing the TBSCertificate for signature verify). An in-house, RFC-cited, zero-runtime-dep package keeps webauthn universal-tier and lets every future PKI consumer (mTLS handshake validation, Sigstore Fulcio chains, code-signing verify, SCEP enrolment) share one canonical implementation with one error registry, one OTel surface, one ASN.1 codec, and one path-validation algorithm.
The package also unlocks the SLSA / Sigstore work already on genesis: @suaveplan/slsa currently verifies Rekor inclusion proofs but cannot verify the Fulcio-issued code-signing certificate chains those proofs reference, because no X.509 verifier exists. This proposal closes both gaps with one package.
What changes
New package: @suaveplan/x509
- ADDED:
packages/security/x509/package scaffold (package.json,tsconfig.lib.json,vite.config.ts,vitest.config.ts,README.md).
ASN.1 codec
- ADDED:
src/asn1/decoder.ts—decodeAsn1(bytes: Uint8Array, mode?: "der" | "ber"): Asn1Noderecursive TLV decoder covering every production used by X.509 v3 (RFC 5280 Appendix A, A.1, A.2). Strict DER (default) rejects indefinite-length; permissive BER accepts it. - ADDED:
src/asn1/encoder.ts—encodeAsn1(node: Asn1Node): Uint8Arraycanonical DER encoder; round-trip with the decoder is byte-identical for any input the decoder accepted. - ADDED:
src/asn1/oid.ts— OID encode/decode (decodeOid(bytes): string,encodeOid(dotted: string): Uint8Array) per X.690 §8.19. - ADDED:
src/asn1/strings.ts— typed-string decoders forUTF8String,PrintableString,IA5String,TeletexString,BMPString,UniversalString. Encoder picks the most-restrictive form that holds the input (RFC 5280 §4.1.2.4 + §7). - ADDED:
src/asn1/time.ts—UTCTime(RFC 5280 §4.1.2.5.1, two-digit-year 50/49 split) +GeneralizedTime(§4.1.2.5.2, mandatoryZ) decode + encode; returns numeric epoch-ms via injectableClock-compatible interface. - ADDED:
src/asn1/asn1.types.ts—Asn1Tag,Asn1Class,Asn1Nodediscriminated union,Asn1DecodeMode.
PEM framing
- ADDED:
src/pem/pem.ts—decodePem(text: string): PemBlock[]andencodePem(label: string, bytes: Uint8Array): stringper RFC 7468. Multi-cert bundles supported via repeatedBEGIN/ENDblocks;\r\nand\nline endings tolerated. - ADDED:
src/pem/pem.types.ts—PemBlock { label: string; bytes: Uint8Array; }.
Certificate parser
- ADDED:
src/certificate/parse.ts—parseCertificate(bytes: Uint8Array): X509Certificateper RFC 5280 §4.1. Accepts DER and bare PEM (viadecodePem); the result carries the parsed fields plus the canonical TBSCertificate DER bytes (for re-hashing during signature verify) AND the raw outer signature bytes. - ADDED:
src/certificate/certificate.types.ts—X509Certificate,TbsCertificate,Name,RelativeDistinguishedName,AttributeTypeAndValue,Validity,SubjectPublicKeyInfo,AlgorithmIdentifier,Extension,RawExtension. - ADDED:
src/certificate/name.ts— RFC 5280 §4.1.2.4Namedecode + canonical encode (used by name-chaining check during validation).
Extension decoders (RFC 5280 §4.2)
- ADDED:
src/extensions/basic-constraints.ts— RFC 5280 §4.2.1.9cA,pathLenConstraint. - ADDED:
src/extensions/key-usage.ts— RFC 5280 §4.2.1.3 nine-bitBIT STRING→KeyUsageflags. - ADDED:
src/extensions/extended-key-usage.ts— RFC 5280 §4.2.1.12SEQUENCE OF KeyPurposeId. RecognisesserverAuth,clientAuth,codeSigning,emailProtection,timeStamping,OCSPSigning,anyExtendedKeyUsage. - ADDED:
src/extensions/subject-alt-name.ts— RFC 5280 §4.2.1.6 + §4.2.2.6GeneralNamediscriminated union:rfc822Name,dNSName,uniformResourceIdentifier,iPAddress(4-byte v4 / 16-byte v6),directoryName,otherName,x400Address,ediPartyName,registeredID. - ADDED:
src/extensions/name-constraints.ts— RFC 5280 §4.2.1.10permittedSubtrees/excludedSubtreeswithGeneralSubtreedecode + matching helpers (DNS suffix, RFC 822, URI, IP prefix, directory name). - ADDED:
src/extensions/authority-key-id.ts— RFC 5280 §4.2.1.1. - ADDED:
src/extensions/subject-key-id.ts— RFC 5280 §4.2.1.2. - ADDED:
src/extensions/crl-distribution-points.ts— RFC 5280 §4.2.1.13SEQUENCE OF DistributionPointdecode (CRL fetching is out of scope; structure is exposed for sibling packages). - ADDED:
src/extensions/registry.ts— OID-keyed dispatch table; unknown extension →RawExtension { oid, critical, value }.
Signature verify
- ADDED:
src/signature/alg-map.ts— OID →@suaveplan/crypto-curvealgorithm ID table:sha256WithRSAEncryption(1.2.840.113549.1.1.11),sha384WithRSAEncryption(…1.12),sha512WithRSAEncryption(…1.13),ecdsa-with-SHA256(1.2.840.10045.4.3.2),ecdsa-with-SHA384(…4.3.3),ecdsa-with-SHA512(…4.3.4),id-Ed25519(1.3.101.112),id-Ed448(1.3.101.113). - ADDED:
src/signature/verify.ts—verifyCertificateSignature(cert: X509Certificate, parentSpki: SubjectPublicKeyInfo): Promise<boolean>re-encoding TBSCertificate to DER, decoding the outersignatureValueBIT STRING(handling the ECDSASEQUENCE { r, s }shape), dispatching viaalg-mapto@suaveplan/crypto-curve.
Path validation
- ADDED:
src/validate/validate-chain.ts—validateChain(opts: ValidateChainOptions): Promise<ValidateChainResult>implementing RFC 5280 §6.1 12-step algorithm: initialise state (maxPathLength,permittedSubtrees,excludedSubtrees,validPolicyTree); for each cert verify signature, validity, name chaining, name constraints, basic constraints, key usagekeyCertSignon issuers, path-length, critical-extension recognition; bound chain depth. - ADDED:
src/validate/validate-chain.types.ts—ValidateChainOptions { leaf, intermediates, trustAnchors, clock?, signal?, maxChainDepth?, ocspResponder?, hostName?, ekuRequired? },ValidateChainResult { trustedPath: X509Certificate[] }. - ADDED:
src/validate/trust-anchor.ts—TrustAnchorStorevalue object with O(1)findBySkiandfindIssuerlookups; PEM-bundle constructor. - ADDED:
src/validate/ocsp.types.ts—OcspResponderLikeinterface only; implementation lives in a follow-up@suaveplan/x509-ocsppackage.
Helpers
- ADDED:
src/helpers/has-name-match.ts—hasNameMatch(cert: X509Certificate, name: string): booleanagainst CN + every SAN dNSName per RFC 6125. - ADDED:
src/helpers/chains-to.ts—chainsTo(child: X509Certificate, anchor: X509Certificate): booleanheuristic via SKI/AKI. - ADDED:
src/helpers/is-issuer-of.ts— canonical-name equality betweenchild.issuerandparent.subject.
Errors, schemas, observability
- ADDED:
src/errors/error-codes.ts—defineCodes("X509", [...])with ≥18 codes (full list below);X509Error extends SuaveplanError<X509ErrorCode>. - ADDED:
src/schemas/schemas.ts— Zod schemas forParseCertificateOptions,ValidateChainOptions,X509Certificate,GeneralName,KeyUsage,ExtendedKeyUsage,BasicConstraints,NameConstraints,CrlDistributionPoint,AuthorityKeyIdentifier,SubjectKeyIdentifier. - ADDED:
src/observability/metrics.ts—x509.parse_duration_ms+x509.validate_duration_mshistograms;x509.parse_total{outcome},x509.validate_total{outcome},x509.extension_total{oid,critical}counters;x509.chain_lengthhistogram on successful validations.
Benchmarks + root barrel
- ADDED:
src/__benchmarks__/parse.bench.ts,validate.bench.ts,pem.bench.ts— mitata. - ADDED:
src/index.ts— explicit named re-exports (noexport *).
Subpath exports
.— root barrel:parseCertificate,validateChain,decodePem,encodePem,TrustAnchorStore, error codes../asn1—decodeAsn1,encodeAsn1,decodeOid,encodeOid, string + time helpers,Asn1Node../pem—decodePem,encodePem,PemBlock../certificate—parseCertificate,X509Certificate, name codec../extensions— every extension decoder + registry + types../signature—verifyCertificateSignature, alg-map../validate—validateChain,TrustAnchorStore, options/result/OCSP types../helpers—hasNameMatch,chainsTo,isIssuerOf../errors—X509_CODES,X509Error,X509ErrorCode.
Dependencies
@suaveplan/crypto-curve workspace:^— RSA / ECDSA / Ed25519 / Ed448 verify (called byverifyCertificateSignature).@suaveplan/crypto-hash workspace:^— SHA-256/384/512 over the canonical TBSCertificate bytes.@suaveplan/crypto-utils workspace:^— constant-time equality, hex/base64 helpers.@suaveplan/error workspace:^—defineCodes,SuaveplanErrorbase.@suaveplan/types workspace:^—Clockinjection point.@opentelemetry/api catalog:observability— histograms + counters.tslib catalog:build.zod catalog:forms— boundary schemas.- No
node-forge,pkijs,@peculiar/x509,asn1.js, browser globals, or Node built-ins. Pure TypeScript overUint8Array+DataView.
Out of scope
Every capability NOT listed in ## What changes is explicitly out of scope for this delta. The deliberate exclusions:
- OCSP responder implementation —
OcspResponderLikeinterface is exposed andvalidateChainhonours it, but no responder is shipped. Reason: OCSP requires a fetch transport, response signature verify, nonce extension handling (RFC 8954), and AIA URL extraction — a meaningfully separate surface. A follow-up@suaveplan/x509-ocsppackage owns it. - CRL fetching + CRL signature verify —
CrlDistributionPointparsing ships here so callers can extract distribution URLs; actual CRL retrieval, parsing of theCertificateListASN.1 structure (RFC 5280 §5), and revocation-set checking live in a follow-up@suaveplan/x509-crlpackage. Reason: same scope-discipline justification as OCSP — CRL is a separate wire format with its own extension set and its own fetch concerns. - Certificate issuance / signing — no
signCertificate, nocreateCertificateRequest. Reason: issuance is a CA function;@suaveplan/x509-cawould own it (also requires keypair generation + serial-number policy). Not yet requested by any consumer. - Smartcard / PKCS#11 / PKCS#12 interop — out of scope; not requested.
- Policy mapping / policy constraints extensions (RFC 5280 §4.2.1.5 / §4.2.1.11) — these extensions are decoded as
RawExtensionif non-critical; if marked critical they triggerX509.CRITICAL_EXTENSION_UNRECOGNIZEDper RFC 5280 §6.1.4(f). Path validation does NOT honour them. Reason: WebAuthn attestation chains do not use policy mapping; SLSA / Sigstore chains do not use policy mapping. Adding it would double the path-validation algorithm without a consumer requesting it; the §6.1.4(f) failure mode is RFC-compliant.
Source mapping
This package is genesis-original; no ../suaveplan/enterprise/x509.md line range exists for any feature. The companion enterprise doc at enterprise/x509.md is being authored in this same change. Source-of-record is RFC 5280 (plus RFC 6818, RFC 7468, and the IANA Object Identifier registry).
| Requirement | RFC source | Notes |
|---|---|---|
| ASN.1 BER/DER decoder (X.509 subset) | RFC 5280 Appendix A.1 + A.2; X.690 §8 | TLV, primitive + constructed; tags + classes per X.690 §8.1.2 |
| ASN.1 DER encoder | RFC 5280 §4.1.2 + X.690 §10 (DER canonical) | Round-trip with decoder |
| Object Identifier encode/decode | X.690 §8.19 | Subidentifier 7-bit encoding |
| PEM framing | RFC 7468 §5 | -----BEGIN/END CERTIFICATE-----; multi-cert bundles |
| TBSCertificate parser | RFC 5280 §4.1.2 | Every field; v1/v2/v3 version handling |
Name / RDN / AttributeTypeAndValue |
RFC 5280 §4.1.2.4 + §7 | Canonical encoding for name-chaining check |
Validity (UTCTime / GeneralizedTime) |
RFC 5280 §4.1.2.5 | 50/49 split; mandatory Z suffix on GeneralizedTime |
SubjectPublicKeyInfo + AlgorithmIdentifier |
RFC 5280 §4.1.2.7 | RSA / EC / Ed25519 / Ed448 key shapes |
| Extension dispatch + unknown-critical handling | RFC 5280 §4.2 + §6.1.4(f) | Unknown critical → CRITICAL_EXTENSION_UNRECOGNIZED |
| Basic constraints | RFC 5280 §4.2.1.9 | cA, pathLenConstraint |
| Key usage | RFC 5280 §4.2.1.3 | 9-bit BIT STRING |
| Extended key usage | RFC 5280 §4.2.1.12 | OID list; anyExtendedKeyUsage |
| Subject Alternative Name | RFC 5280 §4.2.1.6 + §4.2.2.6 | DNS/IP/URI/email/dirName/otherName |
| Name constraints | RFC 5280 §4.2.1.10 | permitted/excluded; DNS suffix matching |
| Authority key identifier | RFC 5280 §4.2.1.1 | keyIdentifier + optional issuer/serial |
| Subject key identifier | RFC 5280 §4.2.1.2 | keyIdentifier |
| CRL distribution points | RFC 5280 §4.2.1.13 | Parse only; CRL fetch out of scope |
| Signature OID → algorithm ID | RFC 5280 §1.2 + IANA OID registry; RFC 8410 for Ed25519/Ed448 | RSA / ECDSA / Ed25519 / Ed448 |
| Signature verify dispatch | RFC 5280 §4.1.1.3 + §6.1.3(a)(1) | Re-encode TBS for hashing; decode ECDSA SEQUENCE { r, s } |
| Validity period check | RFC 5280 §6.1.3(a)(2) | Injectable Clock |
| Name chaining | RFC 5280 §6.1.3(a)(4) | Canonical Name equality |
| Path validation 12-step | RFC 5280 §6.1 | State per §6.1.2; per-cert per §6.1.3; wrap-up per §6.1.4 |
| Chain depth bound | RFC 5280 §6.1.4(h)/(k) + §6.1.2(j) | Default 16; configurable |
| Self-signed leaf rejection | RFC 5280 §6.1.1 (preconditions) | Unless explicitly in trust anchors |
| Trust anchor store | RFC 5280 §6.1.1(d) | SKI lookup; PEM-bundle constructor |
| OCSP responder interface | RFC 6960 + RFC 8954 (nonce) | Interface only; impl in follow-up package |
Requirements with no source line
| Requirement | Justification |
|---|---|
defineCodes("X509", [...]) ≥18-code registry |
Genesis convention — every package surfaces typed errors via @suaveplan/error. Each code maps to a distinct RFC 5280 §6.1.4 abort condition or parser failure class so callers can branch on root cause. |
| Zod boundary schemas at every public option type | Genesis convention (CLAUDE.md §2 / §11). Catches caller misuse at the boundary; option misuse never silently degrades into a wrong-trust-store outcome. |
OTel surface (x509.parse_duration_ms, x509.validate_duration_ms, parse/validate/extension counters, x509.chain_length) |
Genesis convention — every hot-path package emits histograms + counters. The oid attribute on the extension counter is bounded by the IANA + registered-OID set the parser recognises plus a single "unknown" bucket for unrecognised non-critical extensions; cardinality stays low. |
300-line file budget + 100/100/100/100 coverage + sibling .md per impl file |
CLAUDE.md global rules. |
| Property-based round-trip (fast-check) — parse → DER encode → parse byte-identical | CLAUDE.md "no mocks"; round-trip is the only test that proves the parser + encoder agree on the wire format under arbitrary RFC 5280-valid structures. |
TrustAnchorStore SKI lookup + findIssuer helper |
Path validation needs O(1) issuer lookup or every chain walk is O(n·m); the helper is required for validateChain to meet the perf bound the benchmarks publish. |
OcspResponderLike interface (no impl) |
Allows validateChain to thread OCSP into RFC 5280 §6.1.3(a)(3) without coupling this package to a fetch transport. Implementation in @suaveplan/x509-ocsp (follow-up). |
hasNameMatch / chainsTo / isIssuerOf helpers |
Pre-filtering / display helpers; not part of §6.1 but standard PKI library surface. |
Mitata benchmarks under src/__benchmarks__/ |
CLAUDE.md global benchmark requirement. |
Parity Checklist
This package is greenfield (no upstream in ../suaveplan/packages/). The parity check is therefore against external reference implementations rather than against an upstream package:
| Capability | Reference (pkijs / @peculiar/x509) |
Migrated | Notes |
|---|---|---|---|
| ASN.1 BER/DER codec | ✅ (via asn1js) |
✅ | Hand-rolled X.509 subset; sufficient for every TBSCertificate field |
| PEM framing | ✅ | ✅ | RFC 7468 |
parseCertificate |
✅ | ✅ | Full TBSCertificate per §4.1 |
| Basic constraints | ✅ | ✅ | §4.2.1.9 |
| Key usage | ✅ | ✅ | §4.2.1.3 |
| Extended key usage | ✅ | ✅ | §4.2.1.12 |
| Subject Alternative Name | ✅ | ✅ | §4.2.1.6 + §4.2.2.6 |
| Name constraints | ✅ | ✅ | §4.2.1.10 |
| AKI / SKI | ✅ | ✅ | §4.2.1.1 / §4.2.1.2 |
| CRL distribution points | ✅ | ✅ | Parse only — fetch in follow-up |
| Validity check | ✅ | ✅ | Clock-injectable |
| Signature verify dispatch | ✅ | ✅ | RSA / ECDSA / Ed25519 / Ed448 via @suaveplan/crypto-curve |
| RFC 5280 §6.1 path validation | ✅ | ✅ | 12-step algorithm |
| Trust anchor store | ✅ | ✅ | SKI O(1) lookup |
| OCSP responder integration | ✅ (pkijs ships a client) |
🚫 | Scoped to follow-up @suaveplan/x509-ocsp. Reason: OCSP is a wire protocol with its own fetch transport, response signature verify, and RFC 8954 nonce; separable surface that does not block WebAuthn. |
| CRL fetch + verify | ✅ (pkijs) |
🚫 | Scoped to follow-up @suaveplan/x509-crl. Reason: separate ASN.1 structure (CertificateList, RFC 5280 §5) with its own extension set; not required by WebAuthn (attestation chains don't carry CDPs that matter for the immediate consumer). |
| Certificate issuance / signing | ✅ | 🚫 | Scoped to follow-up @suaveplan/x509-ca. Reason: CA function; no consumer has requested it. |
| Policy mapping / policy constraints | ✅ (full) | 🚫 | Decoded as RawExtension; critical occurrence triggers CRITICAL_EXTENSION_UNRECOGNIZED per RFC 5280 §6.1.4(f). Reason: no consumer chain uses these extensions; full algorithm doubles validation surface without payoff. RFC-compliant degradation. |
| PKCS#12 / PKCS#11 / smartcard | ✅ (pkijs) |
🚫 | Out of scope; not requested. |
No 🚫 row uses "not needed yet" alone — every justification names the concrete reason and (where applicable) the follow-up package that owns it.
Verification approach
Each requirement carries a **Verified by:** line in the spec delta naming concrete it(...) titles inside the test files below.
packages/security/x509/src/asn1/decoder.test.ts— every X.509 production decoded; indefinite-length rejected in DER mode, accepted in BER mode.packages/security/x509/src/asn1/encoder.test.ts— round-trip on every primitive + constructed shape; byte-identical with decoder input.packages/security/x509/src/asn1/oid.test.ts— IANA OID round-trips:1.2.840.113549.1.1.11,1.2.840.10045.4.3.2,1.3.101.112,2.5.29.17,2.5.29.19, …packages/security/x509/src/asn1/strings.test.ts— every typed string round-trips; encoder picks the most-restrictive form.packages/security/x509/src/asn1/time.test.ts—UTCTime50/49 split (490101000000Z→ 2049;500101000000Z→ 1950);GeneralizedTimemandatoryZenforcement.packages/security/x509/src/asn1/asn1.spec-conformance.test.ts— RFC 5280 Appendix B explicit ASN.1 modules parse without error.packages/security/x509/src/pem/pem.test.ts— single block + multi-cert bundle +\r\nvs\n+ label whitelist; malformed input throwsX509.PEM_MALFORMED.packages/security/x509/src/certificate/parse.test.ts— Mozilla NSS / pkijs corpus certificates parse to expected typed fields.packages/security/x509/src/certificate/parse.spec-conformance.test.ts— round-trip property (fast-check): generated valid certs parse → encode → parse byte-identical.packages/security/x509/src/certificate/name.test.ts— canonicalNameencode/decode; equality predicate.packages/security/x509/src/extensions/basic-constraints.test.ts—cA: truewithpathLenConstraint: 3;cA: false; end-entity rejected when used as issuer.packages/security/x509/src/extensions/key-usage.test.ts— every bit position;keyCertSignrequirement enforced for issuers invalidateChain.packages/security/x509/src/extensions/extended-key-usage.test.ts— every recognised KeyPurposeId;anyExtendedKeyUsagepermissive.packages/security/x509/src/extensions/subject-alt-name.test.ts— everyGeneralNamevariant; IPv4 + IPv6 decode; URI parse.packages/security/x509/src/extensions/name-constraints.test.ts— DNS suffix match (*.example.com); IP-prefix match (192.0.2.0/24); excluded-subtree rejection.packages/security/x509/src/extensions/authority-key-id.test.ts—keyIdentifierdecode; matches issuer's SKI.packages/security/x509/src/extensions/subject-key-id.test.ts—keyIdentifierdecode.packages/security/x509/src/extensions/crl-distribution-points.test.ts—fullNameURIs decoded; CRL fetch is out of scope.packages/security/x509/src/extensions/registry.test.ts— OID-keyed dispatch; unknown extension yieldsRawExtension; unknown critical extension marked so the validator can fail.packages/security/x509/src/signature/alg-map.test.ts— every supported OID maps; unknown OID throwsX509.SIGNATURE_ALG_UNSUPPORTED.packages/security/x509/src/signature/verify.test.ts— RSA-SHA256, ECDSA-P256-SHA256, Ed25519 verify against real intermediate-signed-by-root pairs from the Mozilla NSS root store; tampered TBS rejected withX509.SIGNATURE_INVALID.packages/security/x509/src/validate/validate-chain.test.ts— happy path 3-chain; expired cert →X509.CERT_EXPIRED; not-yet-valid →X509.CERT_NOT_YET_VALID; broken name chaining →X509.NAME_CHAIN_BROKEN; depth bound exceeded →X509.CHAIN_TOO_DEEP; non-CA issuer →X509.ISSUER_NOT_CA; missingkeyCertSign→X509.KEY_USAGE_INVALID; name-constraint violation →X509.NAME_CONSTRAINT_VIOLATION; unknown critical extension →X509.CRITICAL_EXTENSION_UNRECOGNIZED; signature invalid →X509.SIGNATURE_INVALID; trust anchor not found →X509.TRUST_ANCHOR_NOT_FOUND.packages/security/x509/src/validate/validate-chain.spec-conformance.test.ts— real WebAuthn TPM attestation chain (Microsoft TPM Root CA → intermediate → leaf) validates; real Apple WebAuthn attestation chain validates.packages/security/x509/src/validate/trust-anchor.test.ts— SKI lookup; PEM-bundle constructor; duplicate-SKI handling.packages/security/x509/src/helpers/has-name-match.test.ts— CN + SAN dNSName matching; wildcard handling per RFC 6125.packages/security/x509/src/helpers/chains-to.test.ts— SKI/AKI heuristic; returns false when AKI absent.packages/security/x509/src/helpers/is-issuer-of.test.ts— canonical name equality across encoding variants.packages/security/x509/src/errors/error-codes.test.ts— every code in registry thrown by ≥1 production path;expectTypeOf<X509ErrorCode>().toEqualTypeOf<…>()pins the literal union.packages/security/x509/src/observability/metrics.test.ts— histograms + counters recorded under in-memoryMeterProvider;x509.extension_total{oid,critical}increments per observed extension;x509.error_total{code}increments on every thrownX509Error.packages/security/x509/src/schemas/schemas.test.ts— every Zod schema rejects malformed options at the boundary.packages/security/x509/src/index.test.ts— every documented subpath export resolves.
Migration impact
None — additive only; this is the first delta of a new package. No existing code in genesis imports from @suaveplan/x509. @suaveplan/webauthn is the gating consumer and is itself pending migration; consumer wiring lands in the WebAuthn migration change once that proposal is opened.
Impact
Affected dependents
bunx turbo ls --filter=...@suaveplan/x509 returns no current dependents (the package does not yet exist on genesis). Pending consumers:
@suaveplan/webauthn— [ ] tests run / [ ] docs reviewed / [x] no action needed in THIS change (reason: webauthn is itself pending migration; consumer wiring lands in the webauthn migration change once that proposal is opened, perSTATUS.md).@suaveplan/slsa— [ ] tests run / [ ] docs reviewed / [x] no action needed in THIS change (reason: slsa currently lacks Fulcio cert-chain verification entirely; adding the@suaveplan/x509dep + wiring lands in a separateslsa-fulcio-verificationchange after this package archives).
No foundation-package fan-out applies (this is a new universal-tier leaf package; foundation deps @suaveplan/error and @suaveplan/types are unchanged).