Move docs/ package mirror into document-navigator plugin

Relocate the ~6k-file docs tree from the repo root into
plugins/document-navigator/docs/ so the plugin is self-contained and
portable. docs/superpowers/ (specs and plans) stays at the repo root.

Moved via cp+rm rather than git mv because directory rename() fails with
EPERM on this WSL /mnt DrvFs mount; git rename-detection pairs the
identical-content files at commit time.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-30 01:10:00 +01:00
parent fe3fedf42e
commit 9bf56a806b
6022 changed files with 12437 additions and 0 deletions

View File

@@ -0,0 +1,205 @@
# @suaveplan/x509 - RFC 5280 X.509 v3 certificate parser and certification-path validator. Universal: pure TypeScript over Uint8Array/DataView; signature verify delegated to @suaveplan/crypto-rsa and @suaveplan/crypto-curve.
## Summary
RFC 5280 X.509 v3 certificate parser and certification-path validator. Universal (zero browser, Node, or React deps). Pure TypeScript over `Uint8Array` / `DataView`; signature verify delegated to `@suaveplan/crypto-rsa` (RSA-PKCS1v1.5, RSA-PSS) and `@suaveplan/crypto-curve` (ECDSA-P{256,384,521}, Ed25519).
<!-- begin:auto-docs -->
## Documentation tree
- [__fixtures__](./src/__fixtures__/build-certs.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [build-certs](./src/__fixtures__/build-certs.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [decoder](./src/asn1/decoder.md) — Implements `decodeAsn1(bytes, mode)` covering every ASN.1 production used by RFC 5280 Appendix A: primitive scalars (`I…
- [encoder](./src/asn1/encoder.md) — Implements `encodeAsn1(node)` producing canonical DER bytes per X.690 paragraph 10 for any `Asn1Node` the decoder accep…
- [oid](./src/asn1/oid.md) — Encodes and decodes ASN.1 OBJECT IDENTIFIER bodies per X.690 paragraph 8.19. OIDs in X.509 identify algorithms, extensi…
- [strings](./src/asn1/strings.md) — Decodes and encodes the typed ASN.1 string types used inside X.509 Names and SubjectAltName entries: `UTF8String`, `Pri…
- [time](./src/asn1/time.md) — Decodes and encodes the two time formats RFC 5280 paragraph 4.1.2.5 permits inside a certificate's `Validity` field: `U…
- [name](./src/certificate/name.md) — Decodes RFC 5280 paragraph 4.1.2.4 `Name` (`RDNSequence`) structures into a typed shape carrying both the raw DER bytes…
- [parse](./src/certificate/parse.md) — Implements `parseCertificate(bytes)` decoding a DER- or PEM-encoded `Certificate` into a typed `X509Certificate` value…
- [errors](./src/errors/error-codes.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [error-codes](./src/errors/error-codes.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [authority-key-id](./src/extensions/authority-key-id.md) — Decodes the `authority-key-id` extension per RFC 5280 paragraph 4.2. Every public field in the resulting type is captur…
- [basic-constraints](./src/extensions/basic-constraints.md) — Decodes the basic-constraints extension into `BasicConstraints { cA: boolean; pathLenConstraint?: number }`. During pat…
- [crl-distribution-points](./src/extensions/crl-distribution-points.md) — Decodes the `crl-distribution-points` extension per RFC 5280 paragraph 4.2. Every public field in the resulting type is…
- [extended-key-usage](./src/extensions/extended-key-usage.md) — Decodes the `extended-key-usage` extension per RFC 5280 paragraph 4.2. Every public field in the resulting type is capt…
- [key-usage](./src/extensions/key-usage.md) — Decodes the `key-usage` extension per RFC 5280 paragraph 4.2. Every public field in the resulting type is captured from…
- [name-constraints](./src/extensions/name-constraints.md) — Decodes the `name-constraints` extension per RFC 5280 paragraph 4.2. Every public field in the resulting type is captur…
- [registry](./src/extensions/registry.md) — Decodes the `registry` extension per RFC 5280 paragraph 4.2. Every public field in the resulting type is captured from…
- [subject-alt-name](./src/extensions/subject-alt-name.md) — Decodes the `subject-alt-name` extension per RFC 5280 paragraph 4.2. Every public field in the resulting type is captur…
- [subject-key-id](./src/extensions/subject-key-id.md) — Decodes the `subject-key-id` extension per RFC 5280 paragraph 4.2. Every public field in the resulting type is captured…
- [chains-to](./src/helpers/chains-to.md) — The `helpers/chains-to` module exposes a pre-filtering / display helper around the parsed certificate data. It is indep…
- [has-name-match](./src/helpers/has-name-match.md) — The `helpers/has-name-match` module exposes a pre-filtering / display helper around the parsed certificate data. It is…
- [is-issuer-of](./src/helpers/is-issuer-of.md) — The `helpers/is-issuer-of` module exposes a pre-filtering / display helper around the parsed certificate data. It is in…
- [observability](./src/observability/metrics.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [metrics](./src/observability/metrics.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [pem](./src/pem/pem.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [pem](./src/pem/pem.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [schemas](./src/schemas/schemas.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [schemas](./src/schemas/schemas.md) — <!-- begin:auto-docs --> _(no submodules)_ <!-- end:auto-docs -->
- [alg-map](./src/signature/alg-map.md) — The `signature/alg-map` module is part of the certificate signature-verify pipeline. Together with the rest of the `sig…
- [decode-ecdsa-sig](./src/signature/decode-ecdsa-sig.md) — The `signature/decode-ecdsa-sig` module is part of the certificate signature-verify pipeline. Together with the rest of…
- [verify](./src/signature/verify.md) — The `signature/verify` module is part of the certificate signature-verify pipeline. Together with the rest of the `sign…
- [trust-anchor](./src/validate/trust-anchor.md) — `validate/trust-anchor` is the core of the certificate path-validation pipeline. `validateChain` walks the certificate…
- [validate-chain](./src/validate/validate-chain.md) — `validate/validate-chain` is the core of the certificate path-validation pipeline. `validateChain` walks the certificat…
<!-- end:auto-docs --> - [build-certs](./src/__fixtures__/build-certs.md) — <!-- begin:auto-docs --> _(no submodules)_- [decoder](./src/asn1/decoder.md) — Implements `decodeAsn1(bytes, mode)` covering every ASN.1 production used by RFC 5280 Appendix A: primitive scalars (`I…
- [encoder](./src/asn1/encoder.md) — Implements `encodeAsn1(node)` producing canonical DER bytes per X.690 paragraph 10 for any `Asn1Node` the decoder accep…
- [oid](./src/asn1/oid.md) — Encodes and decodes ASN.1 OBJECT IDENTIFIER bodies per X.690 paragraph 8.19. OIDs in X.509 identify algorithms, extensi…
- [strings](./src/asn1/strings.md) — Decodes and encodes the typed ASN.1 string types used inside X.509 Names and SubjectAltName entries: `UTF8String`, `Pri…
- [time](./src/asn1/time.md) — Decodes and encodes the two time formats RFC 5280 paragraph 4.1.2.5 permits inside a certificate's `Validity` field: `U…
- [name](./src/certificate/name.md) — Decodes RFC 5280 paragraph 4.1.2.4 `Name` (`RDNSequence`) structures into a typed shape carrying both the raw DER bytes…
- [parse](./src/certificate/parse.md) — Implements `parseCertificate(bytes)` decoding a DER- or PEM-encoded `Certificate` into a typed `X509Certificate` value…
- [errors](./src/errors/error-codes.md) — <!-- begin:auto-docs --> _(no submodules)_- [error-codes](./src/errors/error-codes.md) — <!-- begin:auto-docs --> _(no submodules)_- [authority-key-id](./src/extensions/authority-key-id.md) — Decodes the `authority-key-id` extension per RFC 5280 paragraph 4.2. Every public field in the resulting type is captur…
- [basic-constraints](./src/extensions/basic-constraints.md) — Decodes the basic-constraints extension into `BasicConstraints { cA: boolean; pathLenConstraint?: number }`. During pat…
- [crl-distribution-points](./src/extensions/crl-distribution-points.md) — Decodes the `crl-distribution-points` extension per RFC 5280 paragraph 4.2. Every public field in the resulting type is…
- [extended-key-usage](./src/extensions/extended-key-usage.md) — Decodes the `extended-key-usage` extension per RFC 5280 paragraph 4.2. Every public field in the resulting type is capt…
- [key-usage](./src/extensions/key-usage.md) — Decodes the `key-usage` extension per RFC 5280 paragraph 4.2. Every public field in the resulting type is captured from…
- [name-constraints](./src/extensions/name-constraints.md) — Decodes the `name-constraints` extension per RFC 5280 paragraph 4.2. Every public field in the resulting type is captur…
- [registry](./src/extensions/registry.md) — Decodes the `registry` extension per RFC 5280 paragraph 4.2. Every public field in the resulting type is captured from…
- [subject-alt-name](./src/extensions/subject-alt-name.md) — Decodes the `subject-alt-name` extension per RFC 5280 paragraph 4.2. Every public field in the resulting type is captur…
- [subject-key-id](./src/extensions/subject-key-id.md) — Decodes the `subject-key-id` extension per RFC 5280 paragraph 4.2. Every public field in the resulting type is captured…
- [chains-to](./src/helpers/chains-to.md) — The `helpers/chains-to` module exposes a pre-filtering / display helper around the parsed certificate data. It is indep…
- [has-name-match](./src/helpers/has-name-match.md) — The `helpers/has-name-match` module exposes a pre-filtering / display helper around the parsed certificate data. It is…
- [is-issuer-of](./src/helpers/is-issuer-of.md) — The `helpers/is-issuer-of` module exposes a pre-filtering / display helper around the parsed certificate data. It is in…
- [observability](./src/observability/metrics.md) — <!-- begin:auto-docs --> _(no submodules)_- [metrics](./src/observability/metrics.md) — <!-- begin:auto-docs --> _(no submodules)_- [pem](./src/pem/pem.md) — <!-- begin:auto-docs --> _(no submodules)_- [pem](./src/pem/pem.md) — <!-- begin:auto-docs --> _(no submodules)_- [schemas](./src/schemas/schemas.md) — <!-- begin:auto-docs --> _(no submodules)_- [schemas](./src/schemas/schemas.md) — <!-- begin:auto-docs --> _(no submodules)_- [alg-map](./src/signature/alg-map.md) — The `signature/alg-map` module is part of the certificate signature-verify pipeline. Together with the rest of the `sig…
- [decode-ecdsa-sig](./src/signature/decode-ecdsa-sig.md) — The `signature/decode-ecdsa-sig` module is part of the certificate signature-verify pipeline. Together with the rest of…
- [verify](./src/signature/verify.md) — The `signature/verify` module is part of the certificate signature-verify pipeline. Together with the rest of the `sign…
- [trust-anchor](./src/validate/trust-anchor.md) — `validate/trust-anchor` is the core of the certificate path-validation pipeline. `validateChain` walks the certificate…
- [validate-chain](./src/validate/validate-chain.md) — `validate/validate-chain` is the core of the certificate path-validation pipeline. `validateChain` walks the certificat…
- [build-certs](./src/__fixtures__/build-certs.md) — <!-- begin:auto-docs --> _(no submodules)_- [decoder](./src/asn1/decoder.md) — Implements `decodeAsn1(bytes, mode)` covering every ASN.1 production used by RFC 5280 Appendix A: primitive scalars (`I…
- [encoder](./src/asn1/encoder.md) — Implements `encodeAsn1(node)` producing canonical DER bytes per X.690 paragraph 10 for any `Asn1Node` the decoder accep…
- [oid](./src/asn1/oid.md) — Encodes and decodes ASN.1 OBJECT IDENTIFIER bodies per X.690 paragraph 8.19. OIDs in X.509 identify algorithms, extensi…
- [strings](./src/asn1/strings.md) — Decodes and encodes the typed ASN.1 string types used inside X.509 Names and SubjectAltName entries: `UTF8String`, `Pri…
- [time](./src/asn1/time.md) — Decodes and encodes the two time formats RFC 5280 paragraph 4.1.2.5 permits inside a certificate's `Validity` field: `U…
- [name](./src/certificate/name.md) — Decodes RFC 5280 paragraph 4.1.2.4 `Name` (`RDNSequence`) structures into a typed shape carrying both the raw DER bytes…
- [parse](./src/certificate/parse.md) — Implements `parseCertificate(bytes)` decoding a DER- or PEM-encoded `Certificate` into a typed `X509Certificate` value…
- [errors](./src/errors/error-codes.md) — <!-- begin:auto-docs --> _(no submodules)_- [error-codes](./src/errors/error-codes.md) — <!-- begin:auto-docs --> _(no submodules)_- [authority-key-id](./src/extensions/authority-key-id.md) — Decodes the `authority-key-id` extension per RFC 5280 paragraph 4.2. Every public field in the resulting type is captur…
- [basic-constraints](./src/extensions/basic-constraints.md) — Decodes the basic-constraints extension into `BasicConstraints { cA: boolean; pathLenConstraint?: number }`. During pat…
- [crl-distribution-points](./src/extensions/crl-distribution-points.md) — Decodes the `crl-distribution-points` extension per RFC 5280 paragraph 4.2. Every public field in the resulting type is…
- [extended-key-usage](./src/extensions/extended-key-usage.md) — Decodes the `extended-key-usage` extension per RFC 5280 paragraph 4.2. Every public field in the resulting type is capt…
- [key-usage](./src/extensions/key-usage.md) — Decodes the `key-usage` extension per RFC 5280 paragraph 4.2. Every public field in the resulting type is captured from…
- [name-constraints](./src/extensions/name-constraints.md) — Decodes the `name-constraints` extension per RFC 5280 paragraph 4.2. Every public field in the resulting type is captur…
- [registry](./src/extensions/registry.md) — Decodes the `registry` extension per RFC 5280 paragraph 4.2. Every public field in the resulting type is captured from…
- [subject-alt-name](./src/extensions/subject-alt-name.md) — Decodes the `subject-alt-name` extension per RFC 5280 paragraph 4.2. Every public field in the resulting type is captur…
- [subject-key-id](./src/extensions/subject-key-id.md) — Decodes the `subject-key-id` extension per RFC 5280 paragraph 4.2. Every public field in the resulting type is captured…
- [chains-to](./src/helpers/chains-to.md) — The `helpers/chains-to` module exposes a pre-filtering / display helper around the parsed certificate data. It is indep…
- [has-name-match](./src/helpers/has-name-match.md) — The `helpers/has-name-match` module exposes a pre-filtering / display helper around the parsed certificate data. It is…
- [is-issuer-of](./src/helpers/is-issuer-of.md) — The `helpers/is-issuer-of` module exposes a pre-filtering / display helper around the parsed certificate data. It is in…
- [observability](./src/observability/metrics.md) — <!-- begin:auto-docs --> _(no submodules)_- [metrics](./src/observability/metrics.md) — <!-- begin:auto-docs --> _(no submodules)_- [pem](./src/pem/pem.md) — <!-- begin:auto-docs --> _(no submodules)_- [pem](./src/pem/pem.md) — <!-- begin:auto-docs --> _(no submodules)_- [schemas](./src/schemas/schemas.md) — <!-- begin:auto-docs --> _(no submodules)_- [schemas](./src/schemas/schemas.md) — <!-- begin:auto-docs --> _(no submodules)_- [alg-map](./src/signature/alg-map.md) — The `signature/alg-map` module is part of the certificate signature-verify pipeline. Together with the rest of the `sig…
- [decode-ecdsa-sig](./src/signature/decode-ecdsa-sig.md) — The `signature/decode-ecdsa-sig` module is part of the certificate signature-verify pipeline. Together with the rest of…
- [verify](./src/signature/verify.md) — The `signature/verify` module is part of the certificate signature-verify pipeline. Together with the rest of the `sign…
- [trust-anchor](./src/validate/trust-anchor.md) — `validate/trust-anchor` is the core of the certificate path-validation pipeline. `validateChain` walks the certificate…
- [validate-chain](./src/validate/validate-chain.md) — `validate/validate-chain` is the core of the certificate path-validation pipeline. `validateChain` walks the certificat…
- [build-certs](./src/__fixtures__/build-certs.md) — <!-- begin:auto-docs --> _(no submodules)_- [decoder](./src/asn1/decoder.md) — Implements `decodeAsn1(bytes, mode)` covering every ASN.1 production used by RFC 5280 Appendix A: primitive scalars (`I…
- [encoder](./src/asn1/encoder.md) — Implements `encodeAsn1(node)` producing canonical DER bytes per X.690 paragraph 10 for any `Asn1Node` the decoder accep…
- [oid](./src/asn1/oid.md) — Encodes and decodes ASN.1 OBJECT IDENTIFIER bodies per X.690 paragraph 8.19. OIDs in X.509 identify algorithms, extensi…
- [strings](./src/asn1/strings.md) — Decodes and encodes the typed ASN.1 string types used inside X.509 Names and SubjectAltName entries: `UTF8String`, `Pri…
- [time](./src/asn1/time.md) — Decodes and encodes the two time formats RFC 5280 paragraph 4.1.2.5 permits inside a certificate's `Validity` field: `U…
- [name](./src/certificate/name.md) — Decodes RFC 5280 paragraph 4.1.2.4 `Name` (`RDNSequence`) structures into a typed shape carrying both the raw DER bytes…
- [parse](./src/certificate/parse.md) — Implements `parseCertificate(bytes)` decoding a DER- or PEM-encoded `Certificate` into a typed `X509Certificate` value…
- [errors](./src/errors/error-codes.md) — <!-- begin:auto-docs --> _(no submodules)_- [error-codes](./src/errors/error-codes.md) — <!-- begin:auto-docs --> _(no submodules)_- [authority-key-id](./src/extensions/authority-key-id.md) — Decodes the `authority-key-id` extension per RFC 5280 paragraph 4.2. Every public field in the resulting type is captur…
- [basic-constraints](./src/extensions/basic-constraints.md) — Decodes the basic-constraints extension into `BasicConstraints { cA: boolean; pathLenConstraint?: number }`. During pat…
- [crl-distribution-points](./src/extensions/crl-distribution-points.md) — Decodes the `crl-distribution-points` extension per RFC 5280 paragraph 4.2. Every public field in the resulting type is…
- [extended-key-usage](./src/extensions/extended-key-usage.md) — Decodes the `extended-key-usage` extension per RFC 5280 paragraph 4.2. Every public field in the resulting type is capt…
- [key-usage](./src/extensions/key-usage.md) — Decodes the `key-usage` extension per RFC 5280 paragraph 4.2. Every public field in the resulting type is captured from…
- [name-constraints](./src/extensions/name-constraints.md) — Decodes the `name-constraints` extension per RFC 5280 paragraph 4.2. Every public field in the resulting type is captur…
- [registry](./src/extensions/registry.md) — Decodes the `registry` extension per RFC 5280 paragraph 4.2. Every public field in the resulting type is captured from…
- [subject-alt-name](./src/extensions/subject-alt-name.md) — Decodes the `subject-alt-name` extension per RFC 5280 paragraph 4.2. Every public field in the resulting type is captur…
- [subject-key-id](./src/extensions/subject-key-id.md) — Decodes the `subject-key-id` extension per RFC 5280 paragraph 4.2. Every public field in the resulting type is captured…
- [chains-to](./src/helpers/chains-to.md) — The `helpers/chains-to` module exposes a pre-filtering / display helper around the parsed certificate data. It is indep…
- [has-name-match](./src/helpers/has-name-match.md) — The `helpers/has-name-match` module exposes a pre-filtering / display helper around the parsed certificate data. It is…
- [is-issuer-of](./src/helpers/is-issuer-of.md) — The `helpers/is-issuer-of` module exposes a pre-filtering / display helper around the parsed certificate data. It is in…
- [observability](./src/observability/metrics.md) — <!-- begin:auto-docs --> _(no submodules)_- [metrics](./src/observability/metrics.md) — <!-- begin:auto-docs --> _(no submodules)_- [pem](./src/pem/pem.md) — <!-- begin:auto-docs --> _(no submodules)_- [pem](./src/pem/pem.md) — <!-- begin:auto-docs --> _(no submodules)_- [schemas](./src/schemas/schemas.md) — <!-- begin:auto-docs --> _(no submodules)_- [schemas](./src/schemas/schemas.md) — <!-- begin:auto-docs --> _(no submodules)_- [alg-map](./src/signature/alg-map.md) — The `signature/alg-map` module is part of the certificate signature-verify pipeline. Together with the rest of the `sig…
- [decode-ecdsa-sig](./src/signature/decode-ecdsa-sig.md) — The `signature/decode-ecdsa-sig` module is part of the certificate signature-verify pipeline. Together with the rest of…
- [verify](./src/signature/verify.md) — The `signature/verify` module is part of the certificate signature-verify pipeline. Together with the rest of the `sign…
- [trust-anchor](./src/validate/trust-anchor.md) — `validate/trust-anchor` is the core of the certificate path-validation pipeline. `validateChain` walks the certificate…
- [validate-chain](./src/validate/validate-chain.md) — `validate/validate-chain` is the core of the certificate path-validation pipeline. `validateChain` walks the certificat…
- [build-certs](./src/__fixtures__/build-certs.md) — <!-- begin:auto-docs --> _(no submodules)_- [decoder](./src/asn1/decoder.md) — Implements `decodeAsn1(bytes, mode)` covering every ASN.1 production used by RFC 5280 Appendix A: primitive scalars (`I…
- [encoder](./src/asn1/encoder.md) — Implements `encodeAsn1(node)` producing canonical DER bytes per X.690 paragraph 10 for any `Asn1Node` the decoder accep…
- [oid](./src/asn1/oid.md) — Encodes and decodes ASN.1 OBJECT IDENTIFIER bodies per X.690 paragraph 8.19. OIDs in X.509 identify algorithms, extensi…
- [strings](./src/asn1/strings.md) — Decodes and encodes the typed ASN.1 string types used inside X.509 Names and SubjectAltName entries: `UTF8String`, `Pri…
- [time](./src/asn1/time.md) — Decodes and encodes the two time formats RFC 5280 paragraph 4.1.2.5 permits inside a certificate's `Validity` field: `U…
- [name](./src/certificate/name.md) — Decodes RFC 5280 paragraph 4.1.2.4 `Name` (`RDNSequence`) structures into a typed shape carrying both the raw DER bytes…
- [parse](./src/certificate/parse.md) — Implements `parseCertificate(bytes)` decoding a DER- or PEM-encoded `Certificate` into a typed `X509Certificate` value…
- [errors](./src/errors/error-codes.md) — <!-- begin:auto-docs --> _(no submodules)_ - [error-codes](./src/errors/error-codes.md) — <!-- begin:auto-docs --> _(no submodules)_- [authority-key-id](./src/extensions/authority-key-id.md) — Decodes the `authority-key-id` extension per RFC 5280 paragraph 4.2. Every public field in the resulting type is captur…
- [basic-constraints](./src/extensions/basic-constraints.md) — Decodes the basic-constraints extension into `BasicConstraints { cA: boolean; pathLenConstraint?: number }`. During pat…
- [crl-distribution-points](./src/extensions/crl-distribution-points.md) — Decodes the `crl-distribution-points` extension per RFC 5280 paragraph 4.2. Every public field in the resulting type is…
- [extended-key-usage](./src/extensions/extended-key-usage.md) — Decodes the `extended-key-usage` extension per RFC 5280 paragraph 4.2. Every public field in the resulting type is capt…
- [key-usage](./src/extensions/key-usage.md) — Decodes the `key-usage` extension per RFC 5280 paragraph 4.2. Every public field in the resulting type is captured from…
- [name-constraints](./src/extensions/name-constraints.md) — Decodes the `name-constraints` extension per RFC 5280 paragraph 4.2. Every public field in the resulting type is captur…
- [registry](./src/extensions/registry.md) — Decodes the `registry` extension per RFC 5280 paragraph 4.2. Every public field in the resulting type is captured from…
- [subject-alt-name](./src/extensions/subject-alt-name.md) — Decodes the `subject-alt-name` extension per RFC 5280 paragraph 4.2. Every public field in the resulting type is captur…
- [subject-key-id](./src/extensions/subject-key-id.md) — Decodes the `subject-key-id` extension per RFC 5280 paragraph 4.2. Every public field in the resulting type is captured…
- [chains-to](./src/helpers/chains-to.md) — The `helpers/chains-to` module exposes a pre-filtering / display helper around the parsed certificate data. It is indep…
- [has-name-match](./src/helpers/has-name-match.md) — The `helpers/has-name-match` module exposes a pre-filtering / display helper around the parsed certificate data. It is…
- [is-issuer-of](./src/helpers/is-issuer-of.md) — The `helpers/is-issuer-of` module exposes a pre-filtering / display helper around the parsed certificate data. It is in…
- [observability](./src/observability/metrics.md) — <!-- begin:auto-docs --> _(no submodules)_ - [metrics](./src/observability/metrics.md) — <!-- begin:auto-docs --> _(no submodules)_- [pem](./src/pem/pem.md) — <!-- begin:auto-docs --> _(no submodules)_ - [pem](./src/pem/pem.md) — <!-- begin:auto-docs --> _(no submodules)_- [schemas](./src/schemas/schemas.md) — <!-- begin:auto-docs --> _(no submodules)_ - [schemas](./src/schemas/schemas.md) — <!-- begin:auto-docs --> _(no submodules)_- [alg-map](./src/signature/alg-map.md) — The `signature/alg-map` module is part of the certificate signature-verify pipeline. Together with the rest of the `sig…
- [decode-ecdsa-sig](./src/signature/decode-ecdsa-sig.md) — The `signature/decode-ecdsa-sig` module is part of the certificate signature-verify pipeline. Together with the rest of…
- [verify](./src/signature/verify.md) — The `signature/verify` module is part of the certificate signature-verify pipeline. Together with the rest of the `sign…
- [trust-anchor](./src/validate/trust-anchor.md) — `validate/trust-anchor` is the core of the certificate path-validation pipeline. `validateChain` walks the certificate…
- [validate-chain](./src/validate/validate-chain.md) — `validate/validate-chain` is the core of the certificate path-validation pipeline. `validateChain` walks the certificat…
## Install
```bash
bun add @suaveplan/x509
```
## Quick start
```ts
import { parseCertificate, validateChain, TrustAnchorStore } from "@suaveplan/x509";
const leaf = parseCertificate(leafDerBytes);
const intermediate = parseCertificate(intermediateDerBytes);
const root = parseCertificate(rootDerBytes);
const result = await validateChain({
leaf,
intermediates: [intermediate],
trustAnchors: [root],
});
console.log(result.trustedPath.length); // 3
```
## Modules
| Module | Purpose |
|---|---|
| [`asn1/decoder`](./src/asn1/decoder.md) | TLV decoder for the X.509 ASN.1 subset |
| [`asn1/encoder`](./src/asn1/encoder.md) | Canonical DER encoder (round-trip with decoder) |
| [`asn1/oid`](./src/asn1/oid.md) | OBJECT IDENTIFIER encode / decode |
| [`asn1/strings`](./src/asn1/strings.md) | Typed-string decoders (UTF8 / Printable / IA5 / BMP / Universal / Teletex) |
| [`asn1/time`](./src/asn1/time.md) | UTCTime and GeneralizedTime codec |
| [`pem/pem`](./src/pem/pem.md) | RFC 7468 PEM framing |
| [`certificate/parse`](./src/certificate/parse.md) | RFC 5280 §4.1 Certificate parser |
| [`certificate/name`](./src/certificate/name.md) | Name codec + canonical comparison |
| [`extensions/basic-constraints`](./src/extensions/basic-constraints.md) | RFC 5280 §4.2.1.9 |
| [`extensions/key-usage`](./src/extensions/key-usage.md) | RFC 5280 §4.2.1.3 |
| [`extensions/extended-key-usage`](./src/extensions/extended-key-usage.md) | RFC 5280 §4.2.1.12 |
| [`extensions/subject-alt-name`](./src/extensions/subject-alt-name.md) | RFC 5280 §4.2.1.6 + §4.2.2.6 |
| [`extensions/name-constraints`](./src/extensions/name-constraints.md) | RFC 5280 §4.2.1.10 |
| [`extensions/authority-key-id`](./src/extensions/authority-key-id.md) | RFC 5280 §4.2.1.1 |
| [`extensions/subject-key-id`](./src/extensions/subject-key-id.md) | RFC 5280 §4.2.1.2 |
| [`extensions/crl-distribution-points`](./src/extensions/crl-distribution-points.md) | RFC 5280 §4.2.1.13 (parse only) |
| [`extensions/registry`](./src/extensions/registry.md) | OID-keyed dispatch table |
| [`signature/alg-map`](./src/signature/alg-map.md) | Signature-algorithm OID lookup |
| [`signature/verify`](./src/signature/verify.md) | Signature verify dispatch |
| [`signature/decode-ecdsa-sig`](./src/signature/decode-ecdsa-sig.md) | ECDSA-Sig-Value DER decoder |
| [`validate/validate-chain`](./src/validate/validate-chain.md) | RFC 5280 §6.1 path-validation algorithm |
| [`validate/trust-anchor`](./src/validate/trust-anchor.md) | SKI-indexed trust-anchor store |
| [`helpers/has-name-match`](./src/helpers/has-name-match.md) | RFC 6125 host-name match |
| [`helpers/chains-to`](./src/helpers/chains-to.md) | SKI/AKI chaining heuristic |
| [`helpers/is-issuer-of`](./src/helpers/is-issuer-of.md) | Canonical name equality |
| [`errors/error-codes`](./src/errors/error-codes.md) | Typed error registry |
| [`observability/metrics`](./src/observability/metrics.md) | OpenTelemetry surface |
| [`schemas/schemas`](./src/schemas/schemas.md) | Zod boundary schemas |
## Out of scope
- __OCSP__ — interface only (`OcspResponderLike`); implementation in follow-up `@suaveplan/x509-ocsp`.
- __CRL fetching__ — distribution-point parsing only; implementation in follow-up `@suaveplan/x509-crl`.
- __Certificate issuance / signing__ — follow-up `@suaveplan/x509-ca`.
- __Policy mapping / policy constraints__ — decoded as `RawExtension`; critical occurrence triggers `CRITICAL_EXTENSION_UNRECOGNIZED` per RFC 5280 §6.1.4(f).
- __PKCS#12 / PKCS#11 / smartcard__ — not in scope.

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.

View File

@@ -0,0 +1,57 @@
# `asn1/decoder` — TLV decoder for the X.509 ASN.1 subset
## Purpose
Implements `decodeAsn1(bytes, mode)` covering every ASN.1 production used by RFC 5280 Appendix A: primitive scalars (`INTEGER`, `BIT STRING`, `OCTET STRING`, `OBJECT IDENTIFIER`, `NULL`, `BOOLEAN`), string types, time types, and constructed types (`SEQUENCE`, `SET`, `[n] EXPLICIT/IMPLICIT`). The output is a discriminated-union `Asn1Node` tree the encoder can round-trip byte-identically. Strict DER mode (default) rejects indefinite-length encodings; permissive BER mode accepts them.
## Features
- Long-form tag-number decoding (X.690 paragraph 8.1.2.4) with overflow guard and minimality check.
- Definite-length decoding (X.690 paragraph 8.1.3.3-5) with maximum 4-octet length and minimality check in DER mode.
- Indefinite-length decoding in BER mode (X.690 paragraph 8.1.3.6) terminated by `00 00` end-of-contents marker.
- Trailing-bytes rejection on the outermost item (both modes).
- Sub-array views — primitive `content` references a copied `Uint8Array` slice so the caller can store / hash it without re-entering the decoder.
## Usage: Basic Example
```ts
import { decodeAsn1 } from "@suaveplan/x509/asn1";
const node = decodeAsn1(new Uint8Array([0x30, 0x03, 0x02, 0x01, 0x05]));
// node.tag.tagNumber === 16 (SEQUENCE)
// node.children[0].tag.tagNumber === 2 (INTEGER) carrying byte 0x05
```
## Usage: Advanced Example
```ts
import { decodeAsn1, decodeAsn1Stream } from "@suaveplan/x509/asn1";
// Permissive BER mode accepts indefinite-length encoding (legacy systems).
const ber = decodeAsn1(berBytes, "ber");
// Decode N consecutive items extracted from an outer SEQUENCE body.
const seq = decodeAsn1(outerBytes);
if ("children" in seq) {
for (const child of seq.children) {
// ... structural pattern-match ...
}
}
// Or decode a raw body stream:
const items = decodeAsn1Stream(rawBody);
```
## API Reference
- `decodeAsn1(bytes: Uint8Array, mode?: "der" | "ber"): Asn1Node` — decode one TLV; throws `X509Error(ASN1_MALFORMED)` on any structural failure or trailing bytes.
- `decodeAsn1Stream(bytes: Uint8Array, mode?): readonly Asn1Node[]` — decode N consecutive TLVs across an entire buffer; the buffer must be fully consumed.
- `UNIVERSAL_TAGS` — the universal-class tag numbers referenced by callers.
## Implementation Notes
- The decoder makes no allocations beyond the `Asn1Node` tree and per-node `Uint8Array` copies of primitive content. Constructed bodies are walked through without copying.
- `tagNumber` overflow is bounded at 24-bit; long-form tag numbers larger than that throw `ASN1_MALFORMED`. X.509 never uses tag numbers anywhere near that magnitude.
- Length encoding accepts the 4-octet maximum (`>>>` to ~4 GiB), which covers every realistic certificate input on the wire (largest known WebPKI certs are under 64 KiB).
- BER mode is accepted but the `validateChain` boundary throws on BER inputs by passing `"der"` explicitly; BER support exists so callers parsing legacy data feeds (e.g. older PKCS#7 / CMS) can opt in.
- `decodeAsn1` rejects an indefinite-length form on a primitive tag — primitive types in DER and BER both use definite-length encoding only.

View File

@@ -0,0 +1,52 @@
# `asn1/encoder` — canonical DER encoder
## Purpose
Implements `encodeAsn1(node)` producing canonical DER bytes per X.690 paragraph 10 for any `Asn1Node` the decoder accepted. The decoder/encoder pair round-trips byte-identically — `encodeAsn1(decodeAsn1(bytes, "der"))` equals `bytes` for every input the strict decoder accepted. This is critical for re-hashing the TBSCertificate during signature verify: the parsed structure can be re-serialised and digested without trusting the input layout.
## Features
- Canonical short / long-form tag encoding (X.690 paragraph 8.1.2.4) with overflow-bounded tag numbers.
- Minimal-length encoding (X.690 paragraph 10.1) — single-octet form for length < 128, multi-octet form otherwise.
- Constructed node body recursion — children are concatenated in declared order.
- `makeConstructed` / `makePrimitive` helpers for hand-constructing nodes when synthesising test certificates.
## Usage: Basic Example
```ts
import { decodeAsn1, encodeAsn1 } from "@suaveplan/x509/asn1";
const bytes = new Uint8Array([0x02, 0x01, 0x05]);
const node = decodeAsn1(bytes);
const out = encodeAsn1(node);
// out is byte-identical to bytes.
```
## Usage: Advanced Example
```ts
import {
encodeAsn1,
makeConstructed,
makePrimitive,
UNIVERSAL_TAGS,
} from "@suaveplan/x509/asn1";
const seq = makeConstructed("universal", UNIVERSAL_TAGS.SEQUENCE, [
makePrimitive("universal", UNIVERSAL_TAGS.INTEGER, new Uint8Array([0x05])),
makePrimitive("universal", UNIVERSAL_TAGS.OCTET_STRING, new Uint8Array([0xaa, 0xbb])),
]);
const der = encodeAsn1(seq);
```
## API Reference
- `encodeAsn1(node: Asn1Node): Uint8Array` — canonical DER serialise; throws `X509Error(ASN1_MALFORMED)` if a tag number is negative.
- `makeConstructed(tagClass, tagNumber, children)` — node-builder helper.
- `makePrimitive(tagClass, tagNumber, content)` — node-builder helper.
## Implementation Notes
- Encoding sub-traverses constructed children once to compute the body length, then emits tag-length-body. This is two passes per constructed node; the additional pass over already-decoded structure is cheap compared to the cost of hashing the resulting DER for signature verify.
- `makeConstructed` does not validate child consistency — callers are responsible for arranging children in the order the wire format requires (e.g. `SET OF AttributeTypeAndValue` should be canonically ordered if the caller intends DER round-trip semantics).
- The encoder accepts the same tag-number range the decoder produces; tag-number overflow is guarded at both ends.

View File

@@ -0,0 +1,52 @@
# `asn1/oid` — OBJECT IDENTIFIER encode / decode
## Purpose
Encodes and decodes ASN.1 OBJECT IDENTIFIER bodies per X.690 paragraph 8.19. OIDs in X.509 identify algorithms, extensions, key purposes, name attributes, and policies — every dispatch site in this package keys on a dotted-decimal string returned by `decodeOid`. The encoder is needed only for synthesising test fixtures since `parseCertificate` never re-emits OIDs onto the wire.
## Features
- Folded first-arc decoding (X.690 paragraph 8.19.4): the leading sub-identifier splits into two arcs by the `value < 80` / `value >= 80` rule.
- Minimal sub-identifier check — leading `0x80` byte on a sub-identifier is rejected as non-minimal.
- Truncation guard — a sub-identifier that ends mid-byte (high-bit set on final byte) is rejected.
- Symmetric encoder — produces the same byte sequence the decoder would consume.
## Usage: Basic Example
```ts
import { decodeOid, encodeOid } from "@suaveplan/x509/asn1";
const dotted = decodeOid(new Uint8Array([0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x01, 0x0b]));
// "1.2.840.113549.1.1.11" (sha256WithRSAEncryption)
const back = encodeOid("1.2.840.113549.1.1.11");
```
## Usage: Advanced Example
```ts
import { decodeOid, encodeOid } from "@suaveplan/x509/asn1";
// Round-trip every OID in a recognised set:
const OIDS = [
"2.5.29.17", // subjectAltName
"2.5.29.19", // basicConstraints
"1.3.101.112", // id-Ed25519
];
for (const o of OIDS) {
const body = encodeOid(o);
const back = decodeOid(body);
if (back !== o) throw new Error("round-trip mismatch");
}
```
## API Reference
- `decodeOid(body: Uint8Array): string` — OID body (without outer tag-length) into dotted-decimal string. Throws `X509Error(ASN1_MALFORMED)` on empty body, non-minimal sub-id, or truncation.
- `encodeOid(dotted: string): Uint8Array` — dotted-decimal into OID body. Throws on negative or non-decimal arcs, or invalid first two arcs.
## Implementation Notes
- Decoder uses `bigint` arithmetic for sub-identifiers — production OIDs occasionally exceed 32 bits (e.g. some experimental private-enterprise IDs).
- `arc1 ∈ {0, 1, 2}`; `arc2 < 40` when `arc1 < 2`. The encoder validates this constraint and folds correctly.
- Empty OIDs are rejected — every X.509 production that carries an OID requires at least two arcs.

View File

@@ -0,0 +1,48 @@
# `asn1/strings` — typed-string decoders
## Purpose
Decodes and encodes the typed ASN.1 string types used inside X.509 Names and SubjectAltName entries: `UTF8String`, `PrintableString`, `IA5String`, `TeletexString`, `BMPString`, `UniversalString`. The encoder picks the most-restrictive form that holds the input value per RFC 5280 paragraph 7. Decoder is strict about character-set membership — invalid `PrintableString` characters are rejected as `ASN1_MALFORMED`.
## Features
- UTF-8 decode via `TextDecoder` with `fatal: true`, so invalid UTF-8 is rejected (not replaced).
- `PrintableString` alphabet check per ITU-T X.520 — letters, digits, space, and the eleven punctuation characters allowed.
- `IA5String` 0x00-0x7F check.
- `BMPString` (UCS-2 big-endian) and `UniversalString` (UCS-4 big-endian) decode.
- `TeletexString` pragmatically decoded as Latin-1; sufficient for the rare legacy CAs that still emit it.
- Encoder picks `PrintableString` when valid, falling back to `UTF8String` — matches RFC 5280 paragraph 7.1 "most-restrictive" rule.
## Usage: Basic Example
```ts
import { decodeAsn1String, encodeAsn1String, UNIVERSAL_TAGS } from "@suaveplan/x509/asn1";
const s = decodeAsn1String(UNIVERSAL_TAGS.UTF8_STRING, new Uint8Array([0x68, 0x69])); // "hi"
const { tagNumber, body } = encodeAsn1String("Example CA");
// tagNumber === UNIVERSAL_TAGS.PRINTABLE_STRING
```
## Usage: Advanced Example
```ts
import { decodeAsn1, decodeAsn1String, UNIVERSAL_TAGS } from "@suaveplan/x509/asn1";
// Decode a Name attribute value where the tag depends on the CA's choice:
const attrNode = decodeAsn1(attrBytes);
if (!("content" in attrNode)) throw new Error("expected primitive");
const cn = decodeAsn1String(attrNode.tag.tagNumber, attrNode.content);
```
## API Reference
- `decodeAsn1String(tagNumber: number, body: Uint8Array): string` — dispatches on the universal tag; throws `X509Error(ASN1_MALFORMED)` on unsupported tag or character-set violation.
- `encodeAsn1String(value: string): { tagNumber, body }` — returns the most-restrictive choice that holds the value.
## Implementation Notes
- `PrintableString` regex covers ITU-T X.520 paragraph 6.1 — letters, digits, space, and the eleven allowed punctuation marks.
- `TeletexString` (T.61) is technically a 7/8-bit ISO 2022 escape system with national variants; decoding as Latin-1 is the de-facto industry practice (BoringSSL, OpenSSL).
- `BMPString` and `UniversalString` are byte-order strict — the BMP form is UCS-2 big-endian (2 bytes per char), Universal is UCS-4 big-endian (4 bytes per char).
- The encoder does not pick `IA5String`, `TeletexString`, `BMPString`, or `UniversalString` — these are accepted when reading legacy certs but every newly-encoded value defaults to `UTF8String` (the modern X.509 recommendation per RFC 5280 paragraph 4.1.2.4).

View File

@@ -0,0 +1,51 @@
# `asn1/time` — UTCTime and GeneralizedTime codec
## Purpose
Decodes and encodes the two time formats RFC 5280 paragraph 4.1.2.5 permits inside a certificate's `Validity` field: `UTCTime` (`YYMMDDHHMMSSZ`, two-digit-year 50/49 split per RFC 5280 paragraph 4.1.2.5.1) and `GeneralizedTime` (`YYYYMMDDHHMMSSZ`, mandatory `Z` per RFC 5280 paragraph 4.1.2.5.2). Returns and accepts epoch-ms so the validity-period check in `validate-chain` can compare against any injectable `Clock`. The encoder switches to GeneralizedTime when the timestamp is at or after `2050-01-01T00:00:00Z` per the RFC's hard cutoff.
## Features
- Strict shape regex per format — fractional seconds and non-`Z` zone designators rejected.
- 50/49 split — `YY < 50` resolves to `20YY`, `YY >= 50` resolves to `19YY`.
- Component-range validation — month 1-12, day 1-31, hour 0-23, minute 0-59, second 0-59.
- Encoder picks GeneralizedTime when timestamp lies outside the UTCTime-safe `[1950-01-01, 2050-01-01)` window.
- `decodeTime(tagNumber, body)` dispatcher consumes whichever choice the wire carries.
## Usage: Basic Example
```ts
import { decodeUtcTime, decodeGeneralizedTime, encodeTime } from "@suaveplan/x509/asn1";
const t = decodeUtcTime(new TextEncoder().encode("250101000000Z")); // 2025-01-01T00:00:00Z
const { tagNumber, body } = encodeTime(Date.UTC(2024, 0, 15)); // UTCTime
const { tagNumber: t2 } = encodeTime(Date.UTC(2055, 0, 15)); // GeneralizedTime
```
## Usage: Advanced Example
```ts
import { decodeAsn1, decodeTime } from "@suaveplan/x509/asn1";
// Inside a Validity SEQUENCE, choose between UTCTime and GeneralizedTime
// based on the observed tag:
const child = decodeAsn1(timeBytes);
if (!("content" in child)) throw new Error("expected primitive Time");
const notBefore = decodeTime(child.tag.tagNumber, child.content);
```
## API Reference
- `decodeUtcTime(body: Uint8Array): number` — strict UTCTime decode to epoch-ms.
- `decodeGeneralizedTime(body: Uint8Array): number` — strict GeneralizedTime decode to epoch-ms.
- `decodeTime(tagNumber: number, body: Uint8Array): number` — universal-tag dispatcher.
- `encodeTime(epochMs: number): { tagNumber, body }` — RFC 5280 paragraph 4.1.2.5 compliant encoder picking UTCTime or GeneralizedTime.
## Implementation Notes
- The 2050 boundary on `encodeTime` is the cutoff specified by RFC 5280 paragraph 4.1.2.5.1 — UTCTime cannot represent dates after 2050.
- Pre-1950 dates also switch to GeneralizedTime — UTCTime's 50/49 split cannot represent them.
- Fractional seconds in GeneralizedTime are rejected — RFC 5280 paragraph 4.1.2.5.2 forbids them.
- Non-`Z` zone designators are rejected — both RFC 5280 sections require UTC.
- Component-range checks happen after `Date.UTC` so leap-year arithmetic is consistent with JavaScript's `Date` implementation (e.g. February 29 on non-leap years).

View File

@@ -0,0 +1,50 @@
# `certificate/name` — Name codec + canonical comparison
## Purpose
Decodes RFC 5280 paragraph 4.1.2.4 `Name` (`RDNSequence`) structures into a typed shape carrying both the raw DER bytes (for byte-identical issuer/subject comparison when one CA always re-encodes identically) and a canonical byte-string suitable for RFC 5280 paragraph 7.1 case-folding / whitespace-normalising comparison. `namesEqual(a, b)` is the predicate `validate-chain` uses for the name-chaining check between an issuer's `subject` and a child's `issuer`.
## Features
- Strict ASN.1 shape check — outer SEQUENCE, each child SET, each grand-child SEQUENCE of (OID, value).
- Per-attribute value decoding via `decodeAsn1String`, supporting every typed-string variant RFC 5280 allows.
- Canonical byte-string built with NFKD normalisation, lowercase fold, whitespace collapse, and SET-order sort (RFC 5280 paragraph 7.1 reference implementation).
- `commonNameOf(name)` convenience for extracting the first CN attribute (used by `hasNameMatch`).
## Usage: Basic Example
```ts
import { decodeName, namesEqual, commonNameOf } from "@suaveplan/x509/certificate";
import { decodeAsn1 } from "@suaveplan/x509/asn1";
const issuer = decodeName(decodeAsn1(issuerBytes));
const subject = decodeName(decodeAsn1(subjectBytes));
if (namesEqual(issuer, subject)) {
console.log("self-issued");
}
console.log(commonNameOf(subject)); // "example.com"
```
## Usage: Advanced Example
```ts
import { parseCertificate } from "@suaveplan/x509/certificate";
import { namesEqual } from "@suaveplan/x509/certificate";
const child = parseCertificate(childBytes);
const parent = parseCertificate(parentBytes);
const chained = namesEqual(child.tbs.issuer, parent.tbs.subject);
```
## API Reference
- `decodeName(node: Asn1Node): Name` — throws `X509Error(CERTIFICATE_MALFORMED)` on shape violations.
- `namesEqual(a: Name, b: Name): boolean` — case-folded, whitespace-collapsed, sort-stable comparison.
- `commonNameOf(name: Name): string | undefined` — returns the first CN attribute value found in any RDN.
## Implementation Notes
- The canonical form is intentionally lossy — it discards attribute-encoding choice (PrintableString vs UTF8String) and SET ordering inside each RDN. This matches RFC 5280 paragraph 7.1 LDAP-style equality.
- Raw DER bytes are retained alongside the canonical form so callers needing byte-identical comparison (rare; matches only when both parties use the same encoder) can use `name.raw` directly.
- CN extraction uses OID `2.5.4.3`; the first CN in declaration order wins. This matches the heuristic browsers use when the SAN list is empty (legacy fallback).
- Canonicalisation prepends each RDN with a `0x01` separator and inserts `0x00` between OID and value — these are non-base64 control bytes so the byte-comparison is unambiguous against any encoded attribute payload.

View File

@@ -0,0 +1,54 @@
# `certificate/parse` — RFC 5280 paragraph 4.1 Certificate parser
## Purpose
Implements `parseCertificate(bytes)` decoding a DER- or PEM-encoded `Certificate` into a typed `X509Certificate` value object. Every TBSCertificate field per RFC 5280 paragraph 4.1.2 is decoded into a strongly-typed shape: `version` (mapped from the raw 0/1/2 to `"v1"/"v2"/"v3"`), `serialNumber` (length-bounded to 20 bytes per paragraph 4.1.2.2), `signature` algorithm identifier, `issuer` and `subject` `Name`s, `validity` window in epoch-ms, `subjectPublicKeyInfo`, optional unique IDs, and a `RawExtension[]` (typed extension decoders consume these via the registry). The result also carries `tbsCertificateBytes` (canonical DER of the inner TBSCertificate, used for re-hashing during signature verify) and `signatureValue` (the raw outer signature bytes).
## Features
- Accepts both raw DER and bare PEM (detected by `-----BEGIN` header probe in the first 64 bytes).
- Hard reject for serials longer than 20 bytes (`X509.CERTIFICATE_MALFORMED`).
- Hard reject for unsupported versions (`X509.UNSUPPORTED_VERSION` when the raw INTEGER value is outside {0, 1, 2}).
- v1/v2 certificates with extensions are rejected per RFC 5280 paragraph 4.1.2.9.
- Captures canonical TBS DER bytes by re-encoding the parsed structure (round-trip guaranteed by the ASN.1 codec).
- Records `x509.parse_duration_ms` and `x509.parse_total{outcome}` for every call; errors increment `x509.error_total{code}`.
- Extension OIDs increment `x509.extension_total{oid, critical}` with bounded cardinality (recognised set plus `"unknown"`).
## Usage: Basic Example
```ts
import { parseCertificate } from "@suaveplan/x509/certificate";
const cert = parseCertificate(derBytes);
console.log(cert.tbs.version); // "v3"
console.log(cert.tbs.subject.canonical); // Uint8Array
console.log(cert.tbs.subjectPublicKeyInfo.algorithm.oid);
```
## Usage: Advanced Example
```ts
import { parseCertificate } from "@suaveplan/x509/certificate";
// PEM input with leading whitespace and Windows line endings — both tolerated.
const pem = `-----BEGIN CERTIFICATE-----\r\n` +
`MII...\r\n` +
`-----END CERTIFICATE-----\r\n`;
const cert = parseCertificate(new TextEncoder().encode(pem));
// Re-hash the TBSCertificate for signature verify:
import { sha256 } from "@suaveplan/crypto-hash";
const digest = sha256(cert.tbsCertificateBytes);
```
## API Reference
- `parseCertificate(bytes: Uint8Array): X509Certificate` — synchronous decode. Throws `X509Error` with one of `CERTIFICATE_MALFORMED`, `UNSUPPORTED_VERSION`, `ASN1_MALFORMED`, or `PEM_MALFORMED`.
## Implementation Notes
- The parser re-encodes the TBSCertificate via `encodeAsn1` to obtain the canonical DER bytes used for signature verification. Re-encoding (rather than slicing the input) means a non-canonically-encoded input still verifies correctly when the issuing CA used a canonical encoding — but a forged input that exploits encoding ambiguity is detected because the re-encoded form differs from the input the issuer signed. This matches BoringSSL's recommended approach.
- Serial-number length bound is RFC 5280 paragraph 4.1.2.2 — 20 octets including the optional leading zero. The bound is checked on the raw INTEGER content, not the canonical bigint value.
- v1/v2 with extensions is a malformed certificate per RFC 5280 paragraph 4.1.2.9 — extensions are a v3 feature.
- Optional `issuerUniqueID` (context tag 1) and `subjectUniqueID` (context tag 2) are accepted on v2/v3 and ignored by `validateChain` (deprecated per RFC 5280 paragraph 4.1.2.8).
- PEM detection probes the first 64 bytes for `"-----BEGIN "`. This avoids decoding the entire payload through UTF-8 when the input is binary DER.

View File

@@ -0,0 +1,78 @@
# `errors/error-codes` — typed error registry
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Defines the frozen registry of typed error codes raised by `@suaveplan/x509`. Every public surface in the package throws `X509Error` (a subclass of `@suaveplan/error`'s `SuaveplanError<X509ErrorCode>`); consumers switch on `error.code` to map RFC 5280 §6.1.4 abort conditions, ASN.1/PEM parser failures, and internal-boundary refusals into actionable handling. The literal union is pinned by `expectTypeOf` in the test suite so accidental additions or removals fail typecheck before they ship.
## Features
- 20-code frozen registry constructed via `@suaveplan/error`'s `defineCodes("X509", [...])`.
- `X509ErrorCode` literal-union derived directly from the registry — no manual duplication.
- `X509Error` concrete subclass of `SuaveplanError<X509ErrorCode>` carrying optional `cause`, `context`, and `messageKey`.
- Every code is thrown by at least one production code path (verified by `error-codes.test.ts::"every code in the registry is thrown by at least one production path"`).
## Usage: Basic Example
```ts
import { X509_CODES, X509Error } from "@suaveplan/x509/errors";
try {
// ... call into @suaveplan/x509 ...
} catch (err) {
if (err instanceof X509Error) {
if (err.code === X509_CODES.CERT_EXPIRED) {
// present user-facing renewal prompt
}
}
}
```
## Usage: Advanced Example
```ts
import { X509_CODES, X509Error, type X509ErrorCode } from "@suaveplan/x509/errors";
const HUMAN_READABLE: Record<X509ErrorCode, string> = {
[X509_CODES.PEM_MALFORMED]: "The PEM block is malformed.",
[X509_CODES.ASN1_MALFORMED]: "The ASN.1 encoding is malformed.",
[X509_CODES.CERTIFICATE_MALFORMED]: "The certificate body is malformed.",
[X509_CODES.UNSUPPORTED_VERSION]: "Certificate version is unsupported.",
[X509_CODES.EXTENSION_MALFORMED]: "An extension value is malformed.",
[X509_CODES.CRITICAL_EXTENSION_UNRECOGNIZED]: "Critical extension is unrecognised.",
[X509_CODES.SIGNATURE_ALG_UNSUPPORTED]: "Signature algorithm is unsupported.",
[X509_CODES.SIGNATURE_INVALID]: "Signature did not verify.",
[X509_CODES.CERT_EXPIRED]: "Certificate has expired.",
[X509_CODES.CERT_NOT_YET_VALID]: "Certificate is not yet valid.",
[X509_CODES.NAME_CHAIN_BROKEN]: "Issuer/subject name chaining broken.",
[X509_CODES.NAME_CONSTRAINT_VIOLATION]: "Name constraint violated.",
[X509_CODES.ISSUER_NOT_CA]: "Issuer is not a CA.",
[X509_CODES.KEY_USAGE_INVALID]: "Issuer lacks keyCertSign.",
[X509_CODES.EXTENDED_KEY_USAGE_INVALID]: "Required EKU absent.",
[X509_CODES.CHAIN_TOO_DEEP]: "Chain depth exceeds bound.",
[X509_CODES.SELF_SIGNED_LEAF]: "Self-signed leaf without explicit trust.",
[X509_CODES.TRUST_ANCHOR_NOT_FOUND]: "No trust anchor matched.",
[X509_CODES.OCSP_REVOKED]: "Certificate is OCSP-revoked.",
[X509_CODES.ABORTED]: "Validation was aborted.",
};
throw new X509Error(X509_CODES.CERT_EXPIRED, "error.x509.cert-expired", {
context: { notAfter: 1700000000000, now: 1800000000000 },
});
```
## API Reference
- `X509_CODES` — frozen object mapping each code identifier to its `"X509.<NAME>"` literal string.
- `type X509ErrorCode` — discriminated literal union, exactly 20 members.
- `class X509Error extends SuaveplanError<X509ErrorCode>` — concrete error class.
- `constructor(code: X509ErrorCode, messageKey: string, options?: SuaveplanErrorOptions)` — `options` may carry `cause`, `context`, and i18n params per `@suaveplan/error`.
## Implementation Notes
- Codes were chosen to map 1:1 onto RFC 5280 §6.1.4 abort conditions plus parser-boundary failures. There is no aggregate "validation failed" code — every failure path names the precise condition so callers can route programmatically.
- The registry MUST stay frozen; adding a code is a breaking change because consumers may switch exhaustively on `X509ErrorCode`.
- `defineCodes` namespaces codes under `"X509"`; the raw string form is `"X509.<NAME>"` (e.g. `"X509.CERT_EXPIRED"`).
- `messageKey` is an i18n key; the human-readable mapping above is illustrative — production consumers wire it through `@suaveplan/i18n`.

View File

@@ -0,0 +1,56 @@
# `extensions/authority-key-id` — Authority Key Identifier
## Purpose
Decodes the `authority-key-id` extension per RFC 5280 paragraph 4.2. Every public field in the resulting type is captured from the DER value; OCTET STRING wrappers are stripped at the extension boundary so this module operates directly on the inner ASN.1 value. The decoded shape feeds `validateChain` (for critical-extension enforcement, key-usage gating on issuers, name-constraint matching, and trust-anchor SKI lookup) and is also exposed as the structured form sibling packages such as `@suaveplan/x509-crl` and `@suaveplan/webauthn` consume.
## Features
- Strict DER decode at the extension boundary.
- Discriminated-union return where the underlying ASN.1 CHOICE permits multiple shapes.
- Helpful `X509Error(EXTENSION_MALFORMED)` on every shape violation, with the failing reason captured in `error.context.reason` so callers can diagnose specific malformations.
- Zero allocations beyond the typed result; raw extension bytes are subarrayed, not copied through intermediate buffers.
## Usage: Basic Example
```ts
import { decodeauthority_key_id } from "@suaveplan/x509/extensions";
import { parseCertificate } from "@suaveplan/x509/certificate";
const cert = parseCertificate(derBytes);
const ext = cert.tbs.extensions.find((e) => e.oid === "<oid>");
if (ext !== undefined) {
const decoded = decodeauthority_key_id(ext.value);
// ... consume decoded ...
}
```
## Usage: Advanced Example
```ts
import { decodeExtension } from "@suaveplan/x509/extensions";
import { parseCertificate } from "@suaveplan/x509/certificate";
// Route every extension through the dispatch registry — the result is a
// discriminated union over every supported kind plus a "raw" fallback.
const cert = parseCertificate(derBytes);
for (const raw of cert.tbs.extensions) {
const decoded = decodeExtension(raw);
if (decoded.kind === "raw" && decoded.critical) {
throw new Error("unknown critical extension: " + decoded.oid);
}
}
```
## API Reference
- Concrete decoder `decodeauthority_key_id(value: Uint8Array)` exported from this module.
- OID string constant exported alongside the decoder.
- `decodeExtension(raw)` in `./registry.ts` dispatches by OID through this module.
## Implementation Notes
- The decoder operates on the OCTET STRING value of the extension as carried by `RawExtension.value` — callers pass exactly those bytes.
- Critical-extension semantics are NOT enforced at this layer; `validateChain` handles the §6.1.4(f) refusal on unknown critical extensions by inspecting `RawExtension.critical` directly.
- ASN.1 shape violations surface as `X509Error(EXTENSION_MALFORMED)` with the failing reason captured for diagnostics.
- The module emits no OpenTelemetry — that surface lives in `certificate/parse` (which records the per-extension counter) and `validate/validate-chain` (which records the per-validation histogram).

View File

@@ -0,0 +1,50 @@
# `extensions/basic-constraints` — RFC 5280 paragraph 4.2.1.9
## Purpose
Decodes the basic-constraints extension into `BasicConstraints { cA: boolean; pathLenConstraint?: number }`. During path validation, every non-leaf certificate in the chain SHALL have `cA: true`; the `pathLenConstraint` field bounds the number of non-self-issued intermediates beneath the issuer per RFC 5280 paragraph 6.1.4(h).
## Features
- Strict DER decode of the SEQUENCE wrapper.
- Default `cA: false` when the BOOLEAN field is absent (matches RFC 5280 paragraph 4.2.1.9 default).
- INTEGER decode for `pathLenConstraint` with bounded length (up to 4 bytes) and non-negative check.
- Rejects extra fields beyond `(cA, pathLenConstraint)` as `X509Error(EXTENSION_MALFORMED)`.
## Usage: Basic Example
```ts
import { decodeBasicConstraints } from "@suaveplan/x509/extensions";
const bc = decodeBasicConstraints(extensionValueBytes);
if (bc.cA && bc.pathLenConstraint !== undefined) {
console.log("CA with max", bc.pathLenConstraint, "non-self-issued intermediates");
}
```
## Usage: Advanced Example
```ts
import { decodeBasicConstraints } from "@suaveplan/x509/extensions";
import { parseCertificate } from "@suaveplan/x509/certificate";
const cert = parseCertificate(derBytes);
const ext = cert.tbs.extensions.find((e) => e.oid === "2.5.29.19");
if (ext !== undefined) {
const bc = decodeBasicConstraints(ext.value);
if (!bc.cA) {
throw new Error("issuer must have cA: true");
}
}
```
## API Reference
- `decodeBasicConstraints(value: Uint8Array): BasicConstraints` — decode an extension value (the OCTET STRING contents). Throws `X509Error(EXTENSION_MALFORMED)` on shape violation.
- `OID_BASIC_CONSTRAINTS` — string constant `"2.5.29.19"`.
## Implementation Notes
- `cA` defaults to `false` when the BOOLEAN field is absent; this is what RFC 5280 paragraph 4.2.1.9 mandates.
- The bound on `pathLenConstraint` length (4 bytes) is overgenerous — real-world values fit in a single byte — but matches the canonical DER encoding of small INTEGERs and rejects deliberately oversized values as malformed.
- The bound `n < 0` is also redundant since the bit-shift always produces non-negative values for content lengths up to 4 bytes; the check exists as defensive depth and is exercised by malformed-input tests.

View File

@@ -0,0 +1,56 @@
# `extensions/crl-distribution-points` — CRL Distribution Points parse-only
## Purpose
Decodes the `crl-distribution-points` extension per RFC 5280 paragraph 4.2. Every public field in the resulting type is captured from the DER value; OCTET STRING wrappers are stripped at the extension boundary so this module operates directly on the inner ASN.1 value. The decoded shape feeds `validateChain` (for critical-extension enforcement, key-usage gating on issuers, name-constraint matching, and trust-anchor SKI lookup) and is also exposed as the structured form sibling packages such as `@suaveplan/x509-crl` and `@suaveplan/webauthn` consume.
## Features
- Strict DER decode at the extension boundary.
- Discriminated-union return where the underlying ASN.1 CHOICE permits multiple shapes.
- Helpful `X509Error(EXTENSION_MALFORMED)` on every shape violation, with the failing reason captured in `error.context.reason` so callers can diagnose specific malformations.
- Zero allocations beyond the typed result; raw extension bytes are subarrayed, not copied through intermediate buffers.
## Usage: Basic Example
```ts
import { decodecrl_distribution_points } from "@suaveplan/x509/extensions";
import { parseCertificate } from "@suaveplan/x509/certificate";
const cert = parseCertificate(derBytes);
const ext = cert.tbs.extensions.find((e) => e.oid === "<oid>");
if (ext !== undefined) {
const decoded = decodecrl_distribution_points(ext.value);
// ... consume decoded ...
}
```
## Usage: Advanced Example
```ts
import { decodeExtension } from "@suaveplan/x509/extensions";
import { parseCertificate } from "@suaveplan/x509/certificate";
// Route every extension through the dispatch registry — the result is a
// discriminated union over every supported kind plus a "raw" fallback.
const cert = parseCertificate(derBytes);
for (const raw of cert.tbs.extensions) {
const decoded = decodeExtension(raw);
if (decoded.kind === "raw" && decoded.critical) {
throw new Error("unknown critical extension: " + decoded.oid);
}
}
```
## API Reference
- Concrete decoder `decodecrl_distribution_points(value: Uint8Array)` exported from this module.
- OID string constant exported alongside the decoder.
- `decodeExtension(raw)` in `./registry.ts` dispatches by OID through this module.
## Implementation Notes
- The decoder operates on the OCTET STRING value of the extension as carried by `RawExtension.value` — callers pass exactly those bytes.
- Critical-extension semantics are NOT enforced at this layer; `validateChain` handles the §6.1.4(f) refusal on unknown critical extensions by inspecting `RawExtension.critical` directly.
- ASN.1 shape violations surface as `X509Error(EXTENSION_MALFORMED)` with the failing reason captured for diagnostics.
- The module emits no OpenTelemetry — that surface lives in `certificate/parse` (which records the per-extension counter) and `validate/validate-chain` (which records the per-validation histogram).

View File

@@ -0,0 +1,56 @@
# `extensions/extended-key-usage` — Extended Key Usage purpose list
## Purpose
Decodes the `extended-key-usage` extension per RFC 5280 paragraph 4.2. Every public field in the resulting type is captured from the DER value; OCTET STRING wrappers are stripped at the extension boundary so this module operates directly on the inner ASN.1 value. The decoded shape feeds `validateChain` (for critical-extension enforcement, key-usage gating on issuers, name-constraint matching, and trust-anchor SKI lookup) and is also exposed as the structured form sibling packages such as `@suaveplan/x509-crl` and `@suaveplan/webauthn` consume.
## Features
- Strict DER decode at the extension boundary.
- Discriminated-union return where the underlying ASN.1 CHOICE permits multiple shapes.
- Helpful `X509Error(EXTENSION_MALFORMED)` on every shape violation, with the failing reason captured in `error.context.reason` so callers can diagnose specific malformations.
- Zero allocations beyond the typed result; raw extension bytes are subarrayed, not copied through intermediate buffers.
## Usage: Basic Example
```ts
import { decodeextended_key_usage } from "@suaveplan/x509/extensions";
import { parseCertificate } from "@suaveplan/x509/certificate";
const cert = parseCertificate(derBytes);
const ext = cert.tbs.extensions.find((e) => e.oid === "<oid>");
if (ext !== undefined) {
const decoded = decodeextended_key_usage(ext.value);
// ... consume decoded ...
}
```
## Usage: Advanced Example
```ts
import { decodeExtension } from "@suaveplan/x509/extensions";
import { parseCertificate } from "@suaveplan/x509/certificate";
// Route every extension through the dispatch registry — the result is a
// discriminated union over every supported kind plus a "raw" fallback.
const cert = parseCertificate(derBytes);
for (const raw of cert.tbs.extensions) {
const decoded = decodeExtension(raw);
if (decoded.kind === "raw" && decoded.critical) {
throw new Error("unknown critical extension: " + decoded.oid);
}
}
```
## API Reference
- Concrete decoder `decodeextended_key_usage(value: Uint8Array)` exported from this module.
- OID string constant exported alongside the decoder.
- `decodeExtension(raw)` in `./registry.ts` dispatches by OID through this module.
## Implementation Notes
- The decoder operates on the OCTET STRING value of the extension as carried by `RawExtension.value` — callers pass exactly those bytes.
- Critical-extension semantics are NOT enforced at this layer; `validateChain` handles the §6.1.4(f) refusal on unknown critical extensions by inspecting `RawExtension.critical` directly.
- ASN.1 shape violations surface as `X509Error(EXTENSION_MALFORMED)` with the failing reason captured for diagnostics.
- The module emits no OpenTelemetry — that surface lives in `certificate/parse` (which records the per-extension counter) and `validate/validate-chain` (which records the per-validation histogram).

View File

@@ -0,0 +1,56 @@
# `extensions/key-usage` — Key Usage flags bit-string
## Purpose
Decodes the `key-usage` extension per RFC 5280 paragraph 4.2. Every public field in the resulting type is captured from the DER value; OCTET STRING wrappers are stripped at the extension boundary so this module operates directly on the inner ASN.1 value. The decoded shape feeds `validateChain` (for critical-extension enforcement, key-usage gating on issuers, name-constraint matching, and trust-anchor SKI lookup) and is also exposed as the structured form sibling packages such as `@suaveplan/x509-crl` and `@suaveplan/webauthn` consume.
## Features
- Strict DER decode at the extension boundary.
- Discriminated-union return where the underlying ASN.1 CHOICE permits multiple shapes.
- Helpful `X509Error(EXTENSION_MALFORMED)` on every shape violation, with the failing reason captured in `error.context.reason` so callers can diagnose specific malformations.
- Zero allocations beyond the typed result; raw extension bytes are subarrayed, not copied through intermediate buffers.
## Usage: Basic Example
```ts
import { decodekey_usage } from "@suaveplan/x509/extensions";
import { parseCertificate } from "@suaveplan/x509/certificate";
const cert = parseCertificate(derBytes);
const ext = cert.tbs.extensions.find((e) => e.oid === "<oid>");
if (ext !== undefined) {
const decoded = decodekey_usage(ext.value);
// ... consume decoded ...
}
```
## Usage: Advanced Example
```ts
import { decodeExtension } from "@suaveplan/x509/extensions";
import { parseCertificate } from "@suaveplan/x509/certificate";
// Route every extension through the dispatch registry — the result is a
// discriminated union over every supported kind plus a "raw" fallback.
const cert = parseCertificate(derBytes);
for (const raw of cert.tbs.extensions) {
const decoded = decodeExtension(raw);
if (decoded.kind === "raw" && decoded.critical) {
throw new Error("unknown critical extension: " + decoded.oid);
}
}
```
## API Reference
- Concrete decoder `decodekey_usage(value: Uint8Array)` exported from this module.
- OID string constant exported alongside the decoder.
- `decodeExtension(raw)` in `./registry.ts` dispatches by OID through this module.
## Implementation Notes
- The decoder operates on the OCTET STRING value of the extension as carried by `RawExtension.value` — callers pass exactly those bytes.
- Critical-extension semantics are NOT enforced at this layer; `validateChain` handles the §6.1.4(f) refusal on unknown critical extensions by inspecting `RawExtension.critical` directly.
- ASN.1 shape violations surface as `X509Error(EXTENSION_MALFORMED)` with the failing reason captured for diagnostics.
- The module emits no OpenTelemetry — that surface lives in `certificate/parse` (which records the per-extension counter) and `validate/validate-chain` (which records the per-validation histogram).

View File

@@ -0,0 +1,56 @@
# `extensions/name-constraints` — Name Constraints permittedSubtrees/excludedSubtrees + DNS/IP matching helpers
## Purpose
Decodes the `name-constraints` extension per RFC 5280 paragraph 4.2. Every public field in the resulting type is captured from the DER value; OCTET STRING wrappers are stripped at the extension boundary so this module operates directly on the inner ASN.1 value. The decoded shape feeds `validateChain` (for critical-extension enforcement, key-usage gating on issuers, name-constraint matching, and trust-anchor SKI lookup) and is also exposed as the structured form sibling packages such as `@suaveplan/x509-crl` and `@suaveplan/webauthn` consume.
## Features
- Strict DER decode at the extension boundary.
- Discriminated-union return where the underlying ASN.1 CHOICE permits multiple shapes.
- Helpful `X509Error(EXTENSION_MALFORMED)` on every shape violation, with the failing reason captured in `error.context.reason` so callers can diagnose specific malformations.
- Zero allocations beyond the typed result; raw extension bytes are subarrayed, not copied through intermediate buffers.
## Usage: Basic Example
```ts
import { decodename_constraints } from "@suaveplan/x509/extensions";
import { parseCertificate } from "@suaveplan/x509/certificate";
const cert = parseCertificate(derBytes);
const ext = cert.tbs.extensions.find((e) => e.oid === "<oid>");
if (ext !== undefined) {
const decoded = decodename_constraints(ext.value);
// ... consume decoded ...
}
```
## Usage: Advanced Example
```ts
import { decodeExtension } from "@suaveplan/x509/extensions";
import { parseCertificate } from "@suaveplan/x509/certificate";
// Route every extension through the dispatch registry — the result is a
// discriminated union over every supported kind plus a "raw" fallback.
const cert = parseCertificate(derBytes);
for (const raw of cert.tbs.extensions) {
const decoded = decodeExtension(raw);
if (decoded.kind === "raw" && decoded.critical) {
throw new Error("unknown critical extension: " + decoded.oid);
}
}
```
## API Reference
- Concrete decoder `decodename_constraints(value: Uint8Array)` exported from this module.
- OID string constant exported alongside the decoder.
- `decodeExtension(raw)` in `./registry.ts` dispatches by OID through this module.
## Implementation Notes
- The decoder operates on the OCTET STRING value of the extension as carried by `RawExtension.value` — callers pass exactly those bytes.
- Critical-extension semantics are NOT enforced at this layer; `validateChain` handles the §6.1.4(f) refusal on unknown critical extensions by inspecting `RawExtension.critical` directly.
- ASN.1 shape violations surface as `X509Error(EXTENSION_MALFORMED)` with the failing reason captured for diagnostics.
- The module emits no OpenTelemetry — that surface lives in `certificate/parse` (which records the per-extension counter) and `validate/validate-chain` (which records the per-validation histogram).

View File

@@ -0,0 +1,56 @@
# `extensions/registry` — OID-keyed dispatch table
## Purpose
Decodes the `registry` extension per RFC 5280 paragraph 4.2. Every public field in the resulting type is captured from the DER value; OCTET STRING wrappers are stripped at the extension boundary so this module operates directly on the inner ASN.1 value. The decoded shape feeds `validateChain` (for critical-extension enforcement, key-usage gating on issuers, name-constraint matching, and trust-anchor SKI lookup) and is also exposed as the structured form sibling packages such as `@suaveplan/x509-crl` and `@suaveplan/webauthn` consume.
## Features
- Strict DER decode at the extension boundary.
- Discriminated-union return where the underlying ASN.1 CHOICE permits multiple shapes.
- Helpful `X509Error(EXTENSION_MALFORMED)` on every shape violation, with the failing reason captured in `error.context.reason` so callers can diagnose specific malformations.
- Zero allocations beyond the typed result; raw extension bytes are subarrayed, not copied through intermediate buffers.
## Usage: Basic Example
```ts
import { decoderegistry } from "@suaveplan/x509/extensions";
import { parseCertificate } from "@suaveplan/x509/certificate";
const cert = parseCertificate(derBytes);
const ext = cert.tbs.extensions.find((e) => e.oid === "<oid>");
if (ext !== undefined) {
const decoded = decoderegistry(ext.value);
// ... consume decoded ...
}
```
## Usage: Advanced Example
```ts
import { decodeExtension } from "@suaveplan/x509/extensions";
import { parseCertificate } from "@suaveplan/x509/certificate";
// Route every extension through the dispatch registry — the result is a
// discriminated union over every supported kind plus a "raw" fallback.
const cert = parseCertificate(derBytes);
for (const raw of cert.tbs.extensions) {
const decoded = decodeExtension(raw);
if (decoded.kind === "raw" && decoded.critical) {
throw new Error("unknown critical extension: " + decoded.oid);
}
}
```
## API Reference
- Concrete decoder `decoderegistry(value: Uint8Array)` exported from this module.
- OID string constant exported alongside the decoder.
- `decodeExtension(raw)` in `./registry.ts` dispatches by OID through this module.
## Implementation Notes
- The decoder operates on the OCTET STRING value of the extension as carried by `RawExtension.value` — callers pass exactly those bytes.
- Critical-extension semantics are NOT enforced at this layer; `validateChain` handles the §6.1.4(f) refusal on unknown critical extensions by inspecting `RawExtension.critical` directly.
- ASN.1 shape violations surface as `X509Error(EXTENSION_MALFORMED)` with the failing reason captured for diagnostics.
- The module emits no OpenTelemetry — that surface lives in `certificate/parse` (which records the per-extension counter) and `validate/validate-chain` (which records the per-validation histogram).

View File

@@ -0,0 +1,56 @@
# `extensions/subject-alt-name` — Subject Alternative Name discriminated union
## Purpose
Decodes the `subject-alt-name` extension per RFC 5280 paragraph 4.2. Every public field in the resulting type is captured from the DER value; OCTET STRING wrappers are stripped at the extension boundary so this module operates directly on the inner ASN.1 value. The decoded shape feeds `validateChain` (for critical-extension enforcement, key-usage gating on issuers, name-constraint matching, and trust-anchor SKI lookup) and is also exposed as the structured form sibling packages such as `@suaveplan/x509-crl` and `@suaveplan/webauthn` consume.
## Features
- Strict DER decode at the extension boundary.
- Discriminated-union return where the underlying ASN.1 CHOICE permits multiple shapes.
- Helpful `X509Error(EXTENSION_MALFORMED)` on every shape violation, with the failing reason captured in `error.context.reason` so callers can diagnose specific malformations.
- Zero allocations beyond the typed result; raw extension bytes are subarrayed, not copied through intermediate buffers.
## Usage: Basic Example
```ts
import { decodesubject_alt_name } from "@suaveplan/x509/extensions";
import { parseCertificate } from "@suaveplan/x509/certificate";
const cert = parseCertificate(derBytes);
const ext = cert.tbs.extensions.find((e) => e.oid === "<oid>");
if (ext !== undefined) {
const decoded = decodesubject_alt_name(ext.value);
// ... consume decoded ...
}
```
## Usage: Advanced Example
```ts
import { decodeExtension } from "@suaveplan/x509/extensions";
import { parseCertificate } from "@suaveplan/x509/certificate";
// Route every extension through the dispatch registry — the result is a
// discriminated union over every supported kind plus a "raw" fallback.
const cert = parseCertificate(derBytes);
for (const raw of cert.tbs.extensions) {
const decoded = decodeExtension(raw);
if (decoded.kind === "raw" && decoded.critical) {
throw new Error("unknown critical extension: " + decoded.oid);
}
}
```
## API Reference
- Concrete decoder `decodesubject_alt_name(value: Uint8Array)` exported from this module.
- OID string constant exported alongside the decoder.
- `decodeExtension(raw)` in `./registry.ts` dispatches by OID through this module.
## Implementation Notes
- The decoder operates on the OCTET STRING value of the extension as carried by `RawExtension.value` — callers pass exactly those bytes.
- Critical-extension semantics are NOT enforced at this layer; `validateChain` handles the §6.1.4(f) refusal on unknown critical extensions by inspecting `RawExtension.critical` directly.
- ASN.1 shape violations surface as `X509Error(EXTENSION_MALFORMED)` with the failing reason captured for diagnostics.
- The module emits no OpenTelemetry — that surface lives in `certificate/parse` (which records the per-extension counter) and `validate/validate-chain` (which records the per-validation histogram).

View File

@@ -0,0 +1,56 @@
# `extensions/subject-key-id` — Subject Key Identifier
## Purpose
Decodes the `subject-key-id` extension per RFC 5280 paragraph 4.2. Every public field in the resulting type is captured from the DER value; OCTET STRING wrappers are stripped at the extension boundary so this module operates directly on the inner ASN.1 value. The decoded shape feeds `validateChain` (for critical-extension enforcement, key-usage gating on issuers, name-constraint matching, and trust-anchor SKI lookup) and is also exposed as the structured form sibling packages such as `@suaveplan/x509-crl` and `@suaveplan/webauthn` consume.
## Features
- Strict DER decode at the extension boundary.
- Discriminated-union return where the underlying ASN.1 CHOICE permits multiple shapes.
- Helpful `X509Error(EXTENSION_MALFORMED)` on every shape violation, with the failing reason captured in `error.context.reason` so callers can diagnose specific malformations.
- Zero allocations beyond the typed result; raw extension bytes are subarrayed, not copied through intermediate buffers.
## Usage: Basic Example
```ts
import { decodesubject_key_id } from "@suaveplan/x509/extensions";
import { parseCertificate } from "@suaveplan/x509/certificate";
const cert = parseCertificate(derBytes);
const ext = cert.tbs.extensions.find((e) => e.oid === "<oid>");
if (ext !== undefined) {
const decoded = decodesubject_key_id(ext.value);
// ... consume decoded ...
}
```
## Usage: Advanced Example
```ts
import { decodeExtension } from "@suaveplan/x509/extensions";
import { parseCertificate } from "@suaveplan/x509/certificate";
// Route every extension through the dispatch registry — the result is a
// discriminated union over every supported kind plus a "raw" fallback.
const cert = parseCertificate(derBytes);
for (const raw of cert.tbs.extensions) {
const decoded = decodeExtension(raw);
if (decoded.kind === "raw" && decoded.critical) {
throw new Error("unknown critical extension: " + decoded.oid);
}
}
```
## API Reference
- Concrete decoder `decodesubject_key_id(value: Uint8Array)` exported from this module.
- OID string constant exported alongside the decoder.
- `decodeExtension(raw)` in `./registry.ts` dispatches by OID through this module.
## Implementation Notes
- The decoder operates on the OCTET STRING value of the extension as carried by `RawExtension.value` — callers pass exactly those bytes.
- Critical-extension semantics are NOT enforced at this layer; `validateChain` handles the §6.1.4(f) refusal on unknown critical extensions by inspecting `RawExtension.critical` directly.
- ASN.1 shape violations surface as `X509Error(EXTENSION_MALFORMED)` with the failing reason captured for diagnostics.
- The module emits no OpenTelemetry — that surface lives in `certificate/parse` (which records the per-extension counter) and `validate/validate-chain` (which records the per-validation histogram).

View File

@@ -0,0 +1,48 @@
# `helpers/chains-to` — SKI/AKI heuristic
## Purpose
The `helpers/chains-to` module exposes a pre-filtering / display helper around the parsed certificate data. It is independent of the path-validation algorithm: `validateChain` does not depend on it for correctness, and consumers can use it freely without invoking signature verify. The helper is convenient for UI display, candidate-issuer enumeration, and host-name matching outside the full path-validation path. The implementation operates exclusively on the parsed `X509Certificate` shape — no re-decoding, no cryptographic operations.
## Features
- Pure synchronous function, no I/O.
- No allocations beyond the boolean return.
- Idempotent — same input produces the same output.
- Compatible with the rest of the `@suaveplan/x509` public surface — the certificate, name, and extension types feed directly in.
- Defensive — missing fields (no SAN, no AKI/SKI) return `false` rather than throwing, so callers can drive their own branching logic.
## Usage: Basic Example
```ts
import { hasNameMatch, chainsTo, isIssuerOf } from "@suaveplan/x509/helpers";
import { parseCertificate } from "@suaveplan/x509/certificate";
const cert = parseCertificate(derBytes);
const anchor = parseCertificate(anchorBytes);
hasNameMatch(cert, "example.com");
chainsTo(cert, anchor);
isIssuerOf(cert, anchor);
```
## Usage: Advanced Example
```ts
import { chainsTo } from "@suaveplan/x509/helpers";
import { parseCertificate } from "@suaveplan/x509/certificate";
const candidates = await loadTrustAnchorBundle();
const candidate = candidates.find((a) => chainsTo(parsedLeaf, a));
```
## API Reference
- Helper function as named.
- Operates on `X509Certificate` values from `@suaveplan/x509/certificate`.
## Implementation Notes
- The helpers are intentionally heuristic. `hasNameMatch` follows RFC 6125 paragraph 6.4 — SAN dNSName entries take precedence; CN fallback only when SAN is absent. Wildcard matching honours the leftmost-label rule.
- `chainsTo` consults the AKI and SKI extensions; missing extensions yield `false` (the caller should fall back to a full subject/issuer-name match via `isIssuerOf` plus signature verify).
- `isIssuerOf` uses the canonical name comparison from `certificate/name`; the canonical form is whitespace-collapsed and case-folded so issuer/subject equality survives encoding choice (PrintableString vs UTF8String).
- None of the helpers replace `validateChain` — they are pre-filters and display helpers, not validators.

View File

@@ -0,0 +1,48 @@
# `helpers/has-name-match` — RFC 6125 host-name match
## Purpose
The `helpers/has-name-match` module exposes a pre-filtering / display helper around the parsed certificate data. It is independent of the path-validation algorithm: `validateChain` does not depend on it for correctness, and consumers can use it freely without invoking signature verify. The helper is convenient for UI display, candidate-issuer enumeration, and host-name matching outside the full path-validation path. The implementation operates exclusively on the parsed `X509Certificate` shape — no re-decoding, no cryptographic operations.
## Features
- Pure synchronous function, no I/O.
- No allocations beyond the boolean return.
- Idempotent — same input produces the same output.
- Compatible with the rest of the `@suaveplan/x509` public surface — the certificate, name, and extension types feed directly in.
- Defensive — missing fields (no SAN, no AKI/SKI) return `false` rather than throwing, so callers can drive their own branching logic.
## Usage: Basic Example
```ts
import { hasNameMatch, chainsTo, isIssuerOf } from "@suaveplan/x509/helpers";
import { parseCertificate } from "@suaveplan/x509/certificate";
const cert = parseCertificate(derBytes);
const anchor = parseCertificate(anchorBytes);
hasNameMatch(cert, "example.com");
chainsTo(cert, anchor);
isIssuerOf(cert, anchor);
```
## Usage: Advanced Example
```ts
import { chainsTo } from "@suaveplan/x509/helpers";
import { parseCertificate } from "@suaveplan/x509/certificate";
const candidates = await loadTrustAnchorBundle();
const candidate = candidates.find((a) => chainsTo(parsedLeaf, a));
```
## API Reference
- Helper function as named.
- Operates on `X509Certificate` values from `@suaveplan/x509/certificate`.
## Implementation Notes
- The helpers are intentionally heuristic. `hasNameMatch` follows RFC 6125 paragraph 6.4 — SAN dNSName entries take precedence; CN fallback only when SAN is absent. Wildcard matching honours the leftmost-label rule.
- `chainsTo` consults the AKI and SKI extensions; missing extensions yield `false` (the caller should fall back to a full subject/issuer-name match via `isIssuerOf` plus signature verify).
- `isIssuerOf` uses the canonical name comparison from `certificate/name`; the canonical form is whitespace-collapsed and case-folded so issuer/subject equality survives encoding choice (PrintableString vs UTF8String).
- None of the helpers replace `validateChain` — they are pre-filters and display helpers, not validators.

View File

@@ -0,0 +1,48 @@
# `helpers/is-issuer-of` — Canonical name equality
## Purpose
The `helpers/is-issuer-of` module exposes a pre-filtering / display helper around the parsed certificate data. It is independent of the path-validation algorithm: `validateChain` does not depend on it for correctness, and consumers can use it freely without invoking signature verify. The helper is convenient for UI display, candidate-issuer enumeration, and host-name matching outside the full path-validation path. The implementation operates exclusively on the parsed `X509Certificate` shape — no re-decoding, no cryptographic operations.
## Features
- Pure synchronous function, no I/O.
- No allocations beyond the boolean return.
- Idempotent — same input produces the same output.
- Compatible with the rest of the `@suaveplan/x509` public surface — the certificate, name, and extension types feed directly in.
- Defensive — missing fields (no SAN, no AKI/SKI) return `false` rather than throwing, so callers can drive their own branching logic.
## Usage: Basic Example
```ts
import { hasNameMatch, chainsTo, isIssuerOf } from "@suaveplan/x509/helpers";
import { parseCertificate } from "@suaveplan/x509/certificate";
const cert = parseCertificate(derBytes);
const anchor = parseCertificate(anchorBytes);
hasNameMatch(cert, "example.com");
chainsTo(cert, anchor);
isIssuerOf(cert, anchor);
```
## Usage: Advanced Example
```ts
import { chainsTo } from "@suaveplan/x509/helpers";
import { parseCertificate } from "@suaveplan/x509/certificate";
const candidates = await loadTrustAnchorBundle();
const candidate = candidates.find((a) => chainsTo(parsedLeaf, a));
```
## API Reference
- Helper function as named.
- Operates on `X509Certificate` values from `@suaveplan/x509/certificate`.
## Implementation Notes
- The helpers are intentionally heuristic. `hasNameMatch` follows RFC 6125 paragraph 6.4 — SAN dNSName entries take precedence; CN fallback only when SAN is absent. Wildcard matching honours the leftmost-label rule.
- `chainsTo` consults the AKI and SKI extensions; missing extensions yield `false` (the caller should fall back to a full subject/issuer-name match via `isIssuerOf` plus signature verify).
- `isIssuerOf` uses the canonical name comparison from `certificate/name`; the canonical form is whitespace-collapsed and case-folded so issuer/subject equality survives encoding choice (PrintableString vs UTF8String).
- None of the helpers replace `validateChain` — they are pre-filters and display helpers, not validators.

View File

@@ -0,0 +1,66 @@
# `observability/metrics` — OpenTelemetry surface
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Exposes the `@suaveplan/x509` OpenTelemetry surface: two histograms (`x509.parse_duration_ms`, `x509.validate_duration_ms`), a chain-length histogram (`x509.chain_length`), and four counters (`x509.parse_total`, `x509.validate_total`, `x509.extension_total`, `x509.error_total`). Production code paths in `certificate/parse`, `validate/validate-chain`, `extensions/registry`, and every catch-and-rethrow site call into this module so an embedded in-memory `MeterProvider` can assert end-to-end coverage of error and success paths.
## Features
- Lazy instrument creation — instruments resolve through `metrics.getMeter("@suaveplan/x509")` only on first use.
- Cached bag — every subsequent call reuses the resolved instruments.
- `resetMetricsForTesting()` clears the cache so the next call picks up a freshly-installed `MeterProvider`.
- `recordParseDuration`, `recordValidateDuration`, `recordExtension`, and `recordError` apply the documented attribute shape with bounded cardinality (`outcome`, `error_code`, `chain_length`, `oid`, `critical`).
- Unrecognised extension OIDs are bucketed under the literal `"unknown"` so the cardinality of `oid` stays bounded by the recognised-extension set plus one.
## Usage: Basic Example
```ts
import { recordParseDuration, nowMs } from "@suaveplan/x509/observability";
const start = nowMs();
// ... parse a certificate ...
recordParseDuration(nowMs() - start, "success");
```
## Usage: Advanced Example
```ts
import {
getInstrumentsForTesting,
resetMetricsForTesting,
recordParseDuration,
recordError,
} from "@suaveplan/x509/observability";
import { MeterProvider, InMemoryMetricExporter, PeriodicExportingMetricReader } from "@opentelemetry/sdk-metrics";
import { metrics } from "@opentelemetry/api";
resetMetricsForTesting();
const exporter = new InMemoryMetricExporter(0);
const reader = new PeriodicExportingMetricReader({ exporter, exportIntervalMillis: 1 });
const provider = new MeterProvider({ readers: [reader] });
metrics.setGlobalMeterProvider(provider);
// Trigger production code path.
recordParseDuration(12, "error", "PEM_MALFORMED");
recordError("PEM_MALFORMED");
// Assert via reader.collect() in tests.
```
## API Reference
- `recordParseDuration(durationMs, outcome, errorCode?)` — adds an observation to `x509.parse_duration_ms` and increments `x509.parse_total{outcome}`.
- `recordValidateDuration(durationMs, outcome, chainLength?, errorCode?)` — adds an observation to `x509.validate_duration_ms`, increments `x509.validate_total{outcome}`, and on success records the chain length into `x509.chain_length`.
- `recordExtension(oid, critical)` — increments `x509.extension_total{oid, critical}`. `oid` is bounded by the recognised set plus `"unknown"`.
- `recordError(code)` — increments `x509.error_total{code}` once per thrown `X509Error`.
- `nowMs()` — wall-clock reader (millisecond resolution); used internally for histogram timing.
- `resetMetricsForTesting()` / `getInstrumentsForTesting()` — test helpers.
## Implementation Notes
- The cached bag avoids re-resolving the meter on every operation while still honouring `resetMetricsForTesting()` between test cases.
- Counters and histograms record under attribute keys with bounded value sets — `outcome ∈ {success, error}`, `critical ∈ {true, false}`, `code ∈ X509ErrorCode`, `oid ∈ recognisedSet ∪ {"unknown"}`.
- `nowMs()` uses `Date.now()` for portability; this is wall-clock and may step backward across NTP corrections. Histogram durations are best-effort and not used for path validation correctness — only for observability.

View File

@@ -0,0 +1,58 @@
# `pem/pem` — RFC 7468 PEM framing
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Decodes PEM-encoded blobs per RFC 7468 paragraph 5 and re-encodes byte content into the canonical `-----BEGIN <LABEL>-----` / `-----END <LABEL>-----` framed form with 64-column-wrapped base64. Used by `parseCertificate` (accepts DER or PEM) and `TrustAnchorStore` (PEM-bundle constructor). Multi-cert bundles are returned in declaration order so a single PEM file holding root + intermediate(s) preserves the chain shape callers expect.
## Features
- Multi-block decode — every `BEGIN/END` pair in a single text input produces a `PemBlock` entry.
- `\r\n` and `\n` line-ending tolerance.
- Strict base64 alphabet check (`A-Za-z0-9+/=`) plus length-multiple-of-4 check.
- 64-column line wrapping on encode (RFC 7468 paragraph 4 default).
- Label whitelist on encode (`[A-Z0-9 ]+`).
- Empty input and missing END marker throw `X509Error(PEM_MALFORMED)`.
## Usage: Basic Example
```ts
import { decodePem, encodePem } from "@suaveplan/x509/pem";
const blocks = decodePem(`-----BEGIN CERTIFICATE-----
MIIBxjCCAW2gAwIBAgIQ...
-----END CERTIFICATE-----`);
// blocks[0].label === "CERTIFICATE"; blocks[0].bytes is a Uint8Array
const text = encodePem("CERTIFICATE", blocks[0].bytes);
```
## Usage: Advanced Example
```ts
import { decodePem } from "@suaveplan/x509/pem";
// Trust-anchor bundle with three roots concatenated:
const bundle = `${rootA}\n${rootB}\n${rootC}`;
const blocks = decodePem(bundle);
// blocks.length === 3, ordering preserved.
// Windows line endings tolerated:
const win = bundle.replace(/\n/g, "\r\n");
const sameBlocks = decodePem(win);
```
## API Reference
- `decodePem(text: string): PemBlock[]` — returns the array of blocks in declaration order. Throws `X509Error(PEM_MALFORMED)` on empty input, missing END marker, bad base64, or label/body mismatch.
- `encodePem(label: string, bytes: Uint8Array): string` — wraps the body at 64 columns, terminates with `\n`. Throws `X509Error(PEM_MALFORMED)` on labels outside the `[A-Z0-9 ]+` alphabet.
- `PemBlock { label: string; bytes: Uint8Array }`.
## Implementation Notes
- The decoder normalises `\r\n` to `\n` once at entry, then operates on the normalised text. This is simpler and faster than matching `\r?\n` in every position.
- `BEGIN`-pattern matching uses a non-greedy capture so labels with embedded spaces work (`X509 CRL`, `RSA PRIVATE KEY`).
- The label-on-encode whitelist is intentionally narrow — RFC 7468 paragraph 2 allows hyphens but every real-world usage stays inside `[A-Z0-9 ]+`. Callers needing exotic labels should escape upstream.
- `atob`/`btoa` are used when present (every modern browser and Node 16+); the package falls back to `Buffer.from` on legacy Node builds without `atob`.

View File

@@ -0,0 +1,56 @@
# `schemas/schemas` — Zod boundary schemas
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Defines Zod schemas for every public option-bundle and value-object type exposed by `@suaveplan/x509`. The most important boundary is `ValidateChainOptionsSchema`, which `validateChain` uses to refuse malformed caller input — passing `null` for `leaf`, an array of non-certificate values for `intermediates`, or a non-`AbortSignal` for `signal` is rejected before any work happens, with the original Zod issue list preserved as `cause` on the `X509Error(CERTIFICATE_MALFORMED)` it wraps with.
Schemas for the extension result types (`BasicConstraintsSchema`, `KeyUsageSchema`, `ExtendedKeyUsageSchema`, `GeneralNameSchema`, `NameConstraintsSchema`, `AuthorityKeyIdentifierSchema`, `SubjectKeyIdentifierSchema`, `CrlDistributionPointSchema`) pin the structural shape so tests can assert via `safeParse` on synthetic inputs and so callers serialising through JSON can validate before consuming.
## Features
- Strict objects (`.strict()`) on option types so unknown keys are rejected.
- Pass-through objects on value-object types where the runtime carries extra raw-bytes fields the schema doesn't enumerate.
- `instanceof Uint8Array` refinements on byte-typed fields.
- `instanceof AbortSignal` refinement on the optional cancellation signal.
- Reuse of the same `GeneralNameSchema` across every extension that references one.
## Usage: Basic Example
```ts
import { ValidateChainOptionsSchema } from "@suaveplan/x509";
const parsed = ValidateChainOptionsSchema.parse({
leaf: someCert,
intermediates: [],
trustAnchors: [root],
});
```
## Usage: Advanced Example
```ts
import { BasicConstraintsSchema } from "@suaveplan/x509";
const result = BasicConstraintsSchema.safeParse({
cA: true,
pathLenConstraint: 3,
});
if (!result.success) {
throw new Error("invariant violated");
}
```
## API Reference
- `ValidateChainOptionsSchema` — pins the `validateChain` boundary shape.
- `X509CertificateLikeSchema` — pass-through certificate shape carrying `tbs`, `signatureAlgorithm`, `signatureValue`, `tbsCertificateBytes`, `raw`.
- `BasicConstraintsSchema`, `KeyUsageSchema`, `ExtendedKeyUsageSchema`, `GeneralNameSchema`, `NameConstraintsSchema`, `AuthorityKeyIdentifierSchema`, `SubjectKeyIdentifierSchema`, `CrlDistributionPointSchema` — extension value shapes.
## Implementation Notes
- `validateChain` runs the options through Zod first; this is what produces the typed refusal of `{ leaf: null }` documented in the spec. The Zod issue is preserved as `cause` so consumers can inspect it programmatically.
- `X509CertificateLikeSchema` is intentionally loose — the schema doesn't reproduce the full TBSCertificate Zod hierarchy. The runtime validator inside `validateChain` does the real structural work; the schema is a guard against gross caller bugs.
- Schemas use `instanceof` refinements (`Uint8Array`, `AbortSignal`) so the runtime check matches the static type. A plain `{ buffer: ArrayBuffer }` would type-check but fail the `instanceof` predicate at runtime — exactly what the boundary is there to catch.

View File

@@ -0,0 +1,57 @@
# `signature/alg-map` — Signature-algorithm OID lookup table
## Purpose
The `signature/alg-map` module is part of the certificate signature-verify pipeline. Together with the rest of the `signature/` directory it converts an X.509 `signatureAlgorithm` OID into a concrete verifier dispatch and runs that verifier against the TBSCertificate bytes the certificate parser captured. RSA-SHA{256,384,512} and RSA-PSS verifies are delegated to `@suaveplan/crypto-rsa` (the just-shipped sibling package); ECDSA-P{256,384,521} and Ed25519 verifies are delegated to `@suaveplan/crypto-curve`'s `p256`, `p384`, `p521`, and `ed25519` namespaces.
## Features
- Pure OID-keyed table — no string parsing, no fallbacks, no implicit defaults.
- Strict OID lookup; unknown OIDs throw `X509Error(SIGNATURE_ALG_UNSUPPORTED)` with the offending OID in `error.context`.
- Async verify boundary so future engines (e.g. WebCrypto via `crypto-browser`) can plug in without API changes.
- Failed verify is converted to `X509Error(SIGNATURE_INVALID)` so consumers branch on the typed code rather than a boolean return.
- ECDSA signature is decoded once at the verify boundary; the compact `r || s` form is then handed to the curve primitive.
## Usage: Basic Example
```ts
import { resolveSignatureAlgorithm, verifyCertificateSignature } from "@suaveplan/x509/signature";
import { parseCertificate } from "@suaveplan/x509/certificate";
const child = parseCertificate(childBytes);
const parent = parseCertificate(parentBytes);
const ok = await verifyCertificateSignature(
child.tbsCertificateBytes,
child.signatureAlgorithm.oid,
child.signatureValue,
parent.tbs.subjectPublicKeyInfo
);
```
## Usage: Advanced Example
```ts
import {
resolveSignatureAlgorithm,
decodeEcdsaSignature,
} from "@suaveplan/x509/signature";
const alg = resolveSignatureAlgorithm("1.2.840.10045.4.3.2");
if (alg.family === "ecdsa") {
const compact = decodeEcdsaSignature(rawSignatureValue, 32);
// pass compact into the curve-specific verifier of your choice
}
```
## API Reference
- `resolveSignatureAlgorithm(oid: string): SignatureAlgorithm` — typed lookup.
- `verifyCertificateSignature(tbsBytes, sigAlgOid, signature, parentSpki): Promise<boolean>`.
- `decodeEcdsaSignature(bytes, fieldLen): Uint8Array` — DER SEQUENCE-of-INTEGERs to compact `r || s`.
## Implementation Notes
- The verify dispatcher catches non-`X509Error` throws from the underlying crypto engines and wraps them as `SIGNATURE_INVALID` with the original error as `cause` — this prevents bigint range errors or noble's internal exceptions from leaking to consumers.
- ECDSA INTEGER decoding strips a single leading `0x00` when followed by a byte with its high bit set (the canonical positive-INTEGER prefix per X.690 paragraph 8.3.2).
- RSA SPKI parsing is delegated to `@suaveplan/crypto-rsa`'s `parseRsaPublicKey({format: "spki"})` — re-implementing RSA-SPKI parse inline would duplicate code already at 100% coverage in that package.
- Ed448 is recognised by the OID map but the curve primitive is not yet available in `@suaveplan/crypto-curve`; verifying an Ed448-signed certificate throws `SIGNATURE_ALG_UNSUPPORTED` with the OID. This is honest behaviour — silently returning `false` would mask the missing primitive.

View File

@@ -0,0 +1,57 @@
# `signature/decode-ecdsa-sig` — ECDSA-Sig-Value DER decoder
## Purpose
The `signature/decode-ecdsa-sig` module is part of the certificate signature-verify pipeline. Together with the rest of the `signature/` directory it converts an X.509 `signatureAlgorithm` OID into a concrete verifier dispatch and runs that verifier against the TBSCertificate bytes the certificate parser captured. RSA-SHA{256,384,512} and RSA-PSS verifies are delegated to `@suaveplan/crypto-rsa` (the just-shipped sibling package); ECDSA-P{256,384,521} and Ed25519 verifies are delegated to `@suaveplan/crypto-curve`'s `p256`, `p384`, `p521`, and `ed25519` namespaces.
## Features
- Pure OID-keyed table — no string parsing, no fallbacks, no implicit defaults.
- Strict OID lookup; unknown OIDs throw `X509Error(SIGNATURE_ALG_UNSUPPORTED)` with the offending OID in `error.context`.
- Async verify boundary so future engines (e.g. WebCrypto via `crypto-browser`) can plug in without API changes.
- Failed verify is converted to `X509Error(SIGNATURE_INVALID)` so consumers branch on the typed code rather than a boolean return.
- ECDSA signature is decoded once at the verify boundary; the compact `r || s` form is then handed to the curve primitive.
## Usage: Basic Example
```ts
import { resolveSignatureAlgorithm, verifyCertificateSignature } from "@suaveplan/x509/signature";
import { parseCertificate } from "@suaveplan/x509/certificate";
const child = parseCertificate(childBytes);
const parent = parseCertificate(parentBytes);
const ok = await verifyCertificateSignature(
child.tbsCertificateBytes,
child.signatureAlgorithm.oid,
child.signatureValue,
parent.tbs.subjectPublicKeyInfo
);
```
## Usage: Advanced Example
```ts
import {
resolveSignatureAlgorithm,
decodeEcdsaSignature,
} from "@suaveplan/x509/signature";
const alg = resolveSignatureAlgorithm("1.2.840.10045.4.3.2");
if (alg.family === "ecdsa") {
const compact = decodeEcdsaSignature(rawSignatureValue, 32);
// pass compact into the curve-specific verifier of your choice
}
```
## API Reference
- `resolveSignatureAlgorithm(oid: string): SignatureAlgorithm` — typed lookup.
- `verifyCertificateSignature(tbsBytes, sigAlgOid, signature, parentSpki): Promise<boolean>`.
- `decodeEcdsaSignature(bytes, fieldLen): Uint8Array` — DER SEQUENCE-of-INTEGERs to compact `r || s`.
## Implementation Notes
- The verify dispatcher catches non-`X509Error` throws from the underlying crypto engines and wraps them as `SIGNATURE_INVALID` with the original error as `cause` — this prevents bigint range errors or noble's internal exceptions from leaking to consumers.
- ECDSA INTEGER decoding strips a single leading `0x00` when followed by a byte with its high bit set (the canonical positive-INTEGER prefix per X.690 paragraph 8.3.2).
- RSA SPKI parsing is delegated to `@suaveplan/crypto-rsa`'s `parseRsaPublicKey({format: "spki"})` — re-implementing RSA-SPKI parse inline would duplicate code already at 100% coverage in that package.
- Ed448 is recognised by the OID map but the curve primitive is not yet available in `@suaveplan/crypto-curve`; verifying an Ed448-signed certificate throws `SIGNATURE_ALG_UNSUPPORTED` with the OID. This is honest behaviour — silently returning `false` would mask the missing primitive.

View File

@@ -0,0 +1,57 @@
# `signature/verify` — Signature verify dispatch over crypto-rsa and crypto-curve
## Purpose
The `signature/verify` module is part of the certificate signature-verify pipeline. Together with the rest of the `signature/` directory it converts an X.509 `signatureAlgorithm` OID into a concrete verifier dispatch and runs that verifier against the TBSCertificate bytes the certificate parser captured. RSA-SHA{256,384,512} and RSA-PSS verifies are delegated to `@suaveplan/crypto-rsa` (the just-shipped sibling package); ECDSA-P{256,384,521} and Ed25519 verifies are delegated to `@suaveplan/crypto-curve`'s `p256`, `p384`, `p521`, and `ed25519` namespaces.
## Features
- Pure OID-keyed table — no string parsing, no fallbacks, no implicit defaults.
- Strict OID lookup; unknown OIDs throw `X509Error(SIGNATURE_ALG_UNSUPPORTED)` with the offending OID in `error.context`.
- Async verify boundary so future engines (e.g. WebCrypto via `crypto-browser`) can plug in without API changes.
- Failed verify is converted to `X509Error(SIGNATURE_INVALID)` so consumers branch on the typed code rather than a boolean return.
- ECDSA signature is decoded once at the verify boundary; the compact `r || s` form is then handed to the curve primitive.
## Usage: Basic Example
```ts
import { resolveSignatureAlgorithm, verifyCertificateSignature } from "@suaveplan/x509/signature";
import { parseCertificate } from "@suaveplan/x509/certificate";
const child = parseCertificate(childBytes);
const parent = parseCertificate(parentBytes);
const ok = await verifyCertificateSignature(
child.tbsCertificateBytes,
child.signatureAlgorithm.oid,
child.signatureValue,
parent.tbs.subjectPublicKeyInfo
);
```
## Usage: Advanced Example
```ts
import {
resolveSignatureAlgorithm,
decodeEcdsaSignature,
} from "@suaveplan/x509/signature";
const alg = resolveSignatureAlgorithm("1.2.840.10045.4.3.2");
if (alg.family === "ecdsa") {
const compact = decodeEcdsaSignature(rawSignatureValue, 32);
// pass compact into the curve-specific verifier of your choice
}
```
## API Reference
- `resolveSignatureAlgorithm(oid: string): SignatureAlgorithm` — typed lookup.
- `verifyCertificateSignature(tbsBytes, sigAlgOid, signature, parentSpki): Promise<boolean>`.
- `decodeEcdsaSignature(bytes, fieldLen): Uint8Array` — DER SEQUENCE-of-INTEGERs to compact `r || s`.
## Implementation Notes
- The verify dispatcher catches non-`X509Error` throws from the underlying crypto engines and wraps them as `SIGNATURE_INVALID` with the original error as `cause` — this prevents bigint range errors or noble's internal exceptions from leaking to consumers.
- ECDSA INTEGER decoding strips a single leading `0x00` when followed by a byte with its high bit set (the canonical positive-INTEGER prefix per X.690 paragraph 8.3.2).
- RSA SPKI parsing is delegated to `@suaveplan/crypto-rsa`'s `parseRsaPublicKey({format: "spki"})` — re-implementing RSA-SPKI parse inline would duplicate code already at 100% coverage in that package.
- Ed448 is recognised by the OID map but the curve primitive is not yet available in `@suaveplan/crypto-curve`; verifying an Ed448-signed certificate throws `SIGNATURE_ALG_UNSUPPORTED` with the OID. This is honest behaviour — silently returning `false` would mask the missing primitive.

View File

@@ -0,0 +1,63 @@
# `validate/trust-anchor` — SKI-indexed trust-anchor store
## Purpose
`validate/trust-anchor` is the core of the certificate path-validation pipeline. `validateChain` walks the certificate chain leaf-first up to the trust anchor, re-hashing each TBSCertificate, dispatching the signature verify through `@suaveplan/crypto-rsa` (for RSA-PKCS1v1.5 and RSA-PSS) or `@suaveplan/crypto-curve` (for ECDSA-P{256,384,521} and Ed25519), and checking validity, basic constraints, key usage, EKU, name chaining, name constraints, OCSP status (when an `OcspResponderLike` is injected), and critical-extension recognition per RFC 5280 paragraph 6.1.
## Features
- Full 12-step algorithm per RFC 5280 paragraph 6.1 with the spec-pinned set of failure codes.
- Injectable `Clock` so tests can probe expiry boundaries without time travel.
- Injectable `AbortSignal` so callers can interrupt slow chains (e.g. when an OCSP responder is unreachable).
- Bounded chain depth (default 16) with caller override.
- `TrustAnchorStore` value object with O(1) SKI lookup and PEM-bundle constructor.
- Configurable duplicate-SKI policy (`last-wins` default, `first-wins`, or `throw`).
- OpenTelemetry surface emitting `x509.validate_duration_ms`, `x509.validate_total`, `x509.chain_length`, and `x509.error_total` on every call.
## Usage: Basic Example
```ts
import { validateChain, TrustAnchorStore } from "@suaveplan/x509/validate";
import { parseCertificate } from "@suaveplan/x509/certificate";
const result = await validateChain({
leaf: parseCertificate(leafBytes),
intermediates: [parseCertificate(intermediateBytes)],
trustAnchors: [parseCertificate(rootBytes)],
});
console.log(result.trustedPath.length); // 3
```
## Usage: Advanced Example
```ts
import { validateChain, TrustAnchorStore } from "@suaveplan/x509/validate";
import { parseCertificate } from "@suaveplan/x509/certificate";
const store = new TrustAnchorStore(pemBundleString, { onDuplicate: "throw" });
const ac = new AbortController();
const result = await validateChain({
leaf: parseCertificate(leafBytes),
intermediates: intermediates.map(parseCertificate),
trustAnchors: store.toArray(),
clock: () => Date.UTC(2025, 0, 1),
signal: ac.signal,
maxChainDepth: 8,
ekuRequired: "serverAuth",
});
```
## API Reference
- `validateChain(opts: ValidateChainOptions): Promise<ValidateChainResult>` — async; throws `X509Error` on every failure path.
- `TrustAnchorStore` — `findBySki`, `findIssuer`, `contains`, `toArray`, `size`.
- `DEFAULT_MAX_CHAIN_DEPTH` — `16`.
- `OcspResponderLike` — `check(cert, issuer, signal?) => Promise<OcspStatus>`.
## Implementation Notes
- The chain is ordered leaf-first by walking from the leaf upward: at each step the next certificate is chosen by canonical-name equality between the current cert's issuer and a candidate's subject. The algorithm terminates when a trust anchor is reached.
- The OCSP boundary is interface-only — no built-in responder ships. Implementation lives in `@suaveplan/x509-ocsp` (follow-up).
- Policy mapping / policy constraints are out of scope; the validator does not enforce them and rejects them as unknown critical extensions per RFC 5280 paragraph 6.1.4(f).
- Self-signed leaf rejection happens before the chain walk; the leaf is accepted only when it is byte-identical to a trust anchor.
- Pre-aborted `AbortSignal` short-circuits before any work; aborts between per-cert iterations are honoured.

View File

@@ -0,0 +1,63 @@
# `validate/validate-chain` — RFC 5280 paragraph 6.1 path-validation algorithm
## Purpose
`validate/validate-chain` is the core of the certificate path-validation pipeline. `validateChain` walks the certificate chain leaf-first up to the trust anchor, re-hashing each TBSCertificate, dispatching the signature verify through `@suaveplan/crypto-rsa` (for RSA-PKCS1v1.5 and RSA-PSS) or `@suaveplan/crypto-curve` (for ECDSA-P{256,384,521} and Ed25519), and checking validity, basic constraints, key usage, EKU, name chaining, name constraints, OCSP status (when an `OcspResponderLike` is injected), and critical-extension recognition per RFC 5280 paragraph 6.1.
## Features
- Full 12-step algorithm per RFC 5280 paragraph 6.1 with the spec-pinned set of failure codes.
- Injectable `Clock` so tests can probe expiry boundaries without time travel.
- Injectable `AbortSignal` so callers can interrupt slow chains (e.g. when an OCSP responder is unreachable).
- Bounded chain depth (default 16) with caller override.
- `TrustAnchorStore` value object with O(1) SKI lookup and PEM-bundle constructor.
- Configurable duplicate-SKI policy (`last-wins` default, `first-wins`, or `throw`).
- OpenTelemetry surface emitting `x509.validate_duration_ms`, `x509.validate_total`, `x509.chain_length`, and `x509.error_total` on every call.
## Usage: Basic Example
```ts
import { validateChain, TrustAnchorStore } from "@suaveplan/x509/validate";
import { parseCertificate } from "@suaveplan/x509/certificate";
const result = await validateChain({
leaf: parseCertificate(leafBytes),
intermediates: [parseCertificate(intermediateBytes)],
trustAnchors: [parseCertificate(rootBytes)],
});
console.log(result.trustedPath.length); // 3
```
## Usage: Advanced Example
```ts
import { validateChain, TrustAnchorStore } from "@suaveplan/x509/validate";
import { parseCertificate } from "@suaveplan/x509/certificate";
const store = new TrustAnchorStore(pemBundleString, { onDuplicate: "throw" });
const ac = new AbortController();
const result = await validateChain({
leaf: parseCertificate(leafBytes),
intermediates: intermediates.map(parseCertificate),
trustAnchors: store.toArray(),
clock: () => Date.UTC(2025, 0, 1),
signal: ac.signal,
maxChainDepth: 8,
ekuRequired: "serverAuth",
});
```
## API Reference
- `validateChain(opts: ValidateChainOptions): Promise<ValidateChainResult>` — async; throws `X509Error` on every failure path.
- `TrustAnchorStore` — `findBySki`, `findIssuer`, `contains`, `toArray`, `size`.
- `DEFAULT_MAX_CHAIN_DEPTH` — `16`.
- `OcspResponderLike` — `check(cert, issuer, signal?) => Promise<OcspStatus>`.
## Implementation Notes
- The chain is ordered leaf-first by walking from the leaf upward: at each step the next certificate is chosen by canonical-name equality between the current cert's issuer and a candidate's subject. The algorithm terminates when a trust anchor is reached.
- The OCSP boundary is interface-only — no built-in responder ships. Implementation lives in `@suaveplan/x509-ocsp` (follow-up).
- Policy mapping / policy constraints are out of scope; the validator does not enforce them and rejects them as unknown critical extensions per RFC 5280 paragraph 6.1.4(f).
- Self-signed leaf rejection happens before the chain walk; the leaf is accepted only when it is byte-identical to a trust anchor.
- Pre-aborted `AbortSignal` short-circuits before any work; aborts between per-cert iterations are honoured.