186 lines
15 KiB
Markdown
186 lines
15 KiB
Markdown
# Tasks — `@suaveplan/x509` (RFC 5280 X.509 v3 parser + path validator)
|
||
|
||
> Every checkbox below MUST reflect work actually done in this change. Do not retroactively tick boxes. Do not tick a box for a test that doesn't exist or doesn't pin the requirement it claims to verify.
|
||
|
||
## 1. Spec authoring
|
||
|
||
- [x] 1.1 Read `enterprise/x509.md` end-to-end
|
||
- [x] 1.2 Read RFC 5280 (Internet X.509 PKI), RFC 6818 (RFC 5280 updates), RFC 7468 (PEM framing), RFC 6125 (DNS name matching), RFC 8410 (Ed25519/Ed448 OIDs), RFC 6960 + RFC 8954 (OCSP for the interface only)
|
||
- [x] 1.3 Read `openspec/AGENTS.md` Sections 13, 14, 14a
|
||
- [x] 1.4 Confirm `proposal.md` covers every public-surface item, every subpath, every dependency, and a complete Parity Checklist
|
||
- [x] 1.5 Confirm `specs/x509/spec.md` carries one requirement per logical capability with ≥2 scenarios each
|
||
- [x] 1.6 Confirm every requirement has a `**Verified by:**` line naming concrete `it(...)` titles in concrete test file paths
|
||
- [x] 1.7 Source-mapping table cites every RFC 5280 section consumed (§4.1, §4.2.1.1–§4.2.1.13, §4.2.2.6, §5, §6.1, §7, Appendix A)
|
||
|
||
## 2. Package scaffold
|
||
|
||
- [x] 2.1 Create `packages/security/x509/` with `src/{asn1,pem,certificate,extensions,signature,validate,helpers,errors,observability,schemas,__benchmarks__}/`
|
||
- [x] 2.2 `package.json` — `@suaveplan/x509@0.0.0`, `type: module`, `sideEffects: false`, deps `@suaveplan/crypto-curve`, `@suaveplan/crypto-hash`, `@suaveplan/crypto-utils`, `@suaveplan/error`, `@suaveplan/types`, `@opentelemetry/api`, `tslib`, `zod` via catalog refs only (no bare versions)
|
||
- [x] 2.3 `package.json` `exports` map declares `.`, `./asn1`, `./pem`, `./certificate`, `./extensions`, `./signature`, `./validate`, `./helpers`, `./errors` (9 subpaths) — each mapped to its built `dist/<feature>/<feature>.js` + `.d.ts` pair
|
||
- [x] 2.4 `tsconfig.lib.json` extends `@suaveplan/typescript-config/library.json`
|
||
- [x] 2.5 `vite.config.ts` with multi-entry build (one entry per subpath) + `vitest.config.ts` calling `createTestConfig()` from `@suaveplan/testing/config`
|
||
- [x] 2.6 `README.md` with package name, description, `bun add` install, quick-start example (parse + validate a 3-cert chain), Modules table linking every co-located `.md`
|
||
|
||
## 3. Implementation — ASN.1 codec
|
||
|
||
- [x] 3.1 `src/asn1/decoder.ts` — recursive TLV decoder for X.509 subset (RFC 5280 Appendix A.1 + A.2; X.690 §8); DER strict, BER permissive
|
||
- [x] 3.2 `src/asn1/encoder.ts` — canonical DER encoder; round-trip with decoder byte-identical
|
||
- [x] 3.3 `src/asn1/oid.ts` — `decodeOid` / `encodeOid` per X.690 §8.19
|
||
- [x] 3.4 `src/asn1/strings.ts` — `UTF8String`, `PrintableString`, `IA5String`, `TeletexString`, `BMPString`, `UniversalString` decoders; encoder picks most-restrictive form per RFC 5280 §7
|
||
- [x] 3.5 `src/asn1/time.ts` — `UTCTime` (50/49 split) + `GeneralizedTime` (mandatory `Z`) decode + encode
|
||
- [x] 3.6 `src/asn1/asn1.types.ts` — `Asn1Tag`, `Asn1Class`, `Asn1Node` discriminated union, `Asn1DecodeMode`
|
||
|
||
## 4. Implementation — PEM framing
|
||
|
||
- [x] 4.1 `src/pem/pem.ts` — `decodePem(text): PemBlock[]` + `encodePem(label, bytes): string` per RFC 7468; multi-cert bundles
|
||
- [x] 4.2 `src/pem/pem.types.ts` — `PemBlock`
|
||
- [x] 4.3 `\r\n` and `\n` line endings tolerated; label whitelist (`CERTIFICATE`, others rejected at boundary)
|
||
|
||
## 5. Implementation — certificate parser
|
||
|
||
- [x] 5.1 `src/certificate/parse.ts` — `parseCertificate(bytes): X509Certificate` for full TBSCertificate (RFC 5280 §4.1); accepts DER and bare PEM (via `decodePem`); captures canonical TBS DER bytes for signature verify
|
||
- [x] 5.2 `src/certificate/name.ts` — RFC 5280 §4.1.2.4 `Name` decode + canonical encode for name-chaining equality
|
||
- [x] 5.3 `src/certificate/certificate.types.ts` — `X509Certificate`, `TbsCertificate`, `Name`, `RelativeDistinguishedName`, `AttributeTypeAndValue`, `Validity`, `SubjectPublicKeyInfo`, `AlgorithmIdentifier`, `Extension`, `RawExtension`
|
||
|
||
## 6. Implementation — extensions
|
||
|
||
- [x] 6.1 `src/extensions/basic-constraints.ts` (RFC 5280 §4.2.1.9)
|
||
- [x] 6.2 `src/extensions/key-usage.ts` (§4.2.1.3, 9-bit `BIT STRING`)
|
||
- [x] 6.3 `src/extensions/extended-key-usage.ts` (§4.2.1.12)
|
||
- [x] 6.4 `src/extensions/subject-alt-name.ts` (§4.2.1.6 + §4.2.2.6 — full `GeneralName` discriminated union)
|
||
- [x] 6.5 `src/extensions/name-constraints.ts` (§4.2.1.10 — DNS suffix / RFC 822 / URI / IP prefix / directory-name matching)
|
||
- [x] 6.6 `src/extensions/authority-key-id.ts` (§4.2.1.1)
|
||
- [x] 6.7 `src/extensions/subject-key-id.ts` (§4.2.1.2)
|
||
- [x] 6.8 `src/extensions/crl-distribution-points.ts` (§4.2.1.13 — parse only)
|
||
- [x] 6.9 `src/extensions/registry.ts` — OID-keyed dispatch; unknown → `RawExtension`; unknown critical preserved for `validateChain` failure
|
||
|
||
## 7. Implementation — signature verify
|
||
|
||
- [x] 7.1 `src/signature/alg-map.ts` — OID → `@suaveplan/crypto-curve` algorithm ID for RSA-SHA{256,384,512}, ECDSA-SHA{256,384,512}, Ed25519, Ed448 (RFC 8410)
|
||
- [x] 7.2 `src/signature/verify.ts` — `verifyCertificateSignature(cert, parentSpki)` re-encoding TBS to DER, decoding ECDSA `SEQUENCE{r,s}`, dispatching to crypto-curve
|
||
|
||
## 8. Implementation — path validation
|
||
|
||
- [x] 8.1 `src/validate/validate-chain.ts` — full RFC 5280 §6.1 12-step algorithm
|
||
- [x] 8.2 `src/validate/validate-chain.types.ts` — `ValidateChainOptions`, `ValidateChainResult`
|
||
- [x] 8.3 `src/validate/trust-anchor.ts` — `TrustAnchorStore` with O(1) `findBySki` + `findIssuer`; PEM-bundle constructor
|
||
- [x] 8.4 `src/validate/ocsp.types.ts` — `OcspResponderLike` interface only (no impl)
|
||
- [x] 8.5 Chain depth bound default 16; configurable; exceed → `X509.CHAIN_TOO_DEEP`
|
||
- [x] 8.6 Self-signed leaf rejected unless explicitly in trust anchors → `X509.SELF_SIGNED_LEAF`
|
||
- [x] 8.7 `AbortSignal` honoured — pre-aborted → `X509.ABORTED`
|
||
|
||
## 9. Implementation — helpers
|
||
|
||
- [x] 9.1 `src/helpers/has-name-match.ts` — CN + SAN dNSName match per RFC 6125
|
||
- [x] 9.2 `src/helpers/chains-to.ts` — SKI/AKI heuristic
|
||
- [x] 9.3 `src/helpers/is-issuer-of.ts` — canonical-name equality
|
||
|
||
## 10. Implementation — errors, schemas, observability
|
||
|
||
- [x] 10.1 `src/errors/error-codes.ts` — `defineCodes("X509", [...])` registry: `PEM_MALFORMED`, `ASN1_MALFORMED`, `CERTIFICATE_MALFORMED`, `UNSUPPORTED_VERSION`, `EXTENSION_MALFORMED`, `CRITICAL_EXTENSION_UNRECOGNIZED`, `SIGNATURE_ALG_UNSUPPORTED`, `SIGNATURE_INVALID`, `CERT_EXPIRED`, `CERT_NOT_YET_VALID`, `NAME_CHAIN_BROKEN`, `NAME_CONSTRAINT_VIOLATION`, `ISSUER_NOT_CA`, `KEY_USAGE_INVALID`, `EXTENDED_KEY_USAGE_INVALID`, `CHAIN_TOO_DEEP`, `SELF_SIGNED_LEAF`, `TRUST_ANCHOR_NOT_FOUND`, `OCSP_REVOKED`, `ABORTED`; `X509Error extends SuaveplanError<X509ErrorCode>`
|
||
- [x] 10.2 `src/schemas/schemas.ts` — Zod schemas for `ParseCertificateOptions`, `ValidateChainOptions`, `X509Certificate`, `GeneralName`, `KeyUsage`, `ExtendedKeyUsage`, `BasicConstraints`, `NameConstraints`, `CrlDistributionPoint`, `AuthorityKeyIdentifier`, `SubjectKeyIdentifier`
|
||
- [x] 10.3 `src/observability/metrics.ts` — `x509.parse_duration_ms` + `x509.validate_duration_ms` histograms; `x509.parse_total{outcome}`, `x509.validate_total{outcome}`, `x509.extension_total{oid,critical}`, `x509.error_total{code}` counters; `x509.chain_length` histogram
|
||
- [x] 10.4 Every code in the registry thrown by ≥1 production code path; verified by `error-codes.test.ts`
|
||
|
||
## 11. Root barrel + subpath barrels
|
||
|
||
- [x] 11.1 `src/index.ts` — explicit named re-exports (no `export *`); namespace re-exports allowed for sub-namespaces (per CLAUDE.md §10)
|
||
- [x] 11.2 Every `<feature>/index.ts` uses explicit named re-exports per AGENTS.md §6a
|
||
- [x] 11.3 `package.json` `exports` map matches the 9 subpaths declared in §2.3
|
||
|
||
## 12. Tests (runner-neutral imports — `@suaveplan/testing/runner` only)
|
||
|
||
- [x] 12.1 `src/asn1/decoder.test.ts` — every X.509 production; DER vs BER indefinite-length handling
|
||
- [x] 12.2 `src/asn1/encoder.test.ts` — round-trip with decoder byte-identical on every primitive + constructed shape
|
||
- [x] 12.3 `src/asn1/oid.test.ts` — IANA OID round-trips
|
||
- [x] 12.4 `src/asn1/strings.test.ts` — every typed string; encoder picks most-restrictive form
|
||
- [x] 12.5 `src/asn1/time.test.ts` — `UTCTime` 50/49 split; `GeneralizedTime` mandatory `Z`
|
||
- [x] 12.6 `src/asn1/asn1.spec-conformance.test.ts` — RFC 5280 Appendix B modules parse without error
|
||
- [x] 12.7 `src/pem/pem.test.ts` — single block + multi-cert bundle + line-ending tolerance + malformed → `PEM_MALFORMED`
|
||
- [x] 12.8 `src/certificate/parse.test.ts` — Mozilla NSS / pkijs corpus certificates parse to expected typed fields
|
||
- [x] 12.9 `src/certificate/parse.spec-conformance.test.ts` — fast-check round-trip: parse → DER encode → parse byte-identical
|
||
- [x] 12.10 `src/certificate/name.test.ts` — canonical `Name` encode/decode; equality predicate
|
||
- [x] 12.11 `src/extensions/basic-constraints.test.ts` — `cA: true` with `pathLenConstraint`; end-entity rejected as issuer
|
||
- [x] 12.12 `src/extensions/key-usage.test.ts` — every bit position; `keyCertSign` enforced on issuers
|
||
- [x] 12.13 `src/extensions/extended-key-usage.test.ts` — every recognised KeyPurposeId; `anyExtendedKeyUsage` permissive
|
||
- [x] 12.14 `src/extensions/subject-alt-name.test.ts` — every `GeneralName` variant; IPv4 + IPv6
|
||
- [x] 12.15 `src/extensions/name-constraints.test.ts` — DNS suffix match; IP-prefix match; excluded-subtree rejection
|
||
- [x] 12.16 `src/extensions/authority-key-id.test.ts` — `keyIdentifier` decode; matches issuer SKI
|
||
- [x] 12.17 `src/extensions/subject-key-id.test.ts` — `keyIdentifier` decode
|
||
- [x] 12.18 `src/extensions/crl-distribution-points.test.ts` — `fullName` URIs decoded
|
||
- [x] 12.19 `src/extensions/registry.test.ts` — dispatch + `RawExtension` + critical preservation
|
||
- [x] 12.20 `src/signature/alg-map.test.ts` — every supported OID maps; unknown → `SIGNATURE_ALG_UNSUPPORTED`
|
||
- [x] 12.21 `src/signature/verify.test.ts` — RSA-SHA256, ECDSA-P256-SHA256, Ed25519 verify; tampered TBS → `SIGNATURE_INVALID`
|
||
- [x] 12.22 `src/validate/validate-chain.test.ts` — every error path: expired, not-yet-valid, name-chain-broken, depth-exceeded, issuer-not-CA, key-usage-invalid, name-constraint-violation, critical-extension-unrecognized, signature-invalid, trust-anchor-not-found
|
||
- [x] 12.23 `src/validate/validate-chain.spec-conformance.test.ts` — real WebAuthn TPM attestation chain + real Apple WebAuthn attestation chain validate
|
||
- [x] 12.24 `src/validate/trust-anchor.test.ts` — SKI lookup; PEM-bundle constructor; duplicate-SKI
|
||
- [x] 12.25 `src/helpers/has-name-match.test.ts` — CN + SAN; wildcard per RFC 6125
|
||
- [x] 12.26 `src/helpers/chains-to.test.ts` — SKI/AKI heuristic; AKI-absent → false
|
||
- [x] 12.27 `src/helpers/is-issuer-of.test.ts` — canonical-name equality across encoding variants
|
||
- [x] 12.28 `src/errors/error-codes.test.ts` — every code thrown; `expectTypeOf` pins literal union
|
||
- [x] 12.29 `src/observability/metrics.test.ts` — histograms + counters under in-memory `MeterProvider`
|
||
- [x] 12.30 `src/schemas/schemas.test.ts` — every Zod schema rejects malformed input
|
||
- [x] 12.31 `src/index.test.ts` — every documented subpath resolves
|
||
- [x] 12.32 Every `it(...)` carries `{ timeout: 5000 }` (or `{ timeout: 30000 }` for property + chain tests)
|
||
- [x] 12.33 Runner-neutral imports — `grep -rnE "from ['\"](vitest|bun:test)['\"]" packages/security/x509/src/` returns zero output
|
||
|
||
## 13. Documentation
|
||
|
||
- [x] 13.1 Sibling `.md` for every implementation `.ts` (≥200 words, basic + advanced examples, full API reference) — covers asn1/decoder, asn1/encoder, asn1/oid, asn1/strings, asn1/time, pem/pem, certificate/parse, certificate/name, every extension decoder, signature/alg-map, signature/verify, validate/validate-chain, validate/trust-anchor, helpers/has-name-match, helpers/chains-to, helpers/is-issuer-of, errors/error-codes, observability/metrics, schemas/schemas
|
||
- [x] 13.2 README.md links every co-located `.md` under `## Modules`
|
||
- [x] 13.3 Update root `CLAUDE.md` package listing — add the `x509` paragraph under the existing security plugin grouping (after `macos-keychain`)
|
||
|
||
## 14. Benchmarks
|
||
|
||
> **Skipped at archive.** Mitata benchmarks were not authored in this change;
|
||
> the package ships without baseline numbers. Re-open if perf regressions
|
||
> are observed.
|
||
|
||
- [ ] 14.1 `src/__benchmarks__/parse.bench.ts` — parse a 1 KiB EE cert, parse a 3 KiB root CA, parse a 10-cert PEM bundle
|
||
- [ ] 14.2 `src/__benchmarks__/validate.bench.ts` — validate 3-chain, 10-chain
|
||
- [ ] 14.3 `src/__benchmarks__/pem.bench.ts` — `decodePem` over 10-cert bundle
|
||
- [ ] 14.4 README.md publishes baseline mitata numbers
|
||
|
||
## 15. Pre-archive verification (the 5-gate triple)
|
||
|
||
- [x] 15.1 `bun run check` — Biome 0 errors / 0 warnings / 0 info across the monorepo
|
||
- [x] 15.2 `bunx turbo lint typecheck test --filter=@suaveplan/x509` — green
|
||
- [x] 15.3 `bunx turbo test:coverage --filter=@suaveplan/x509` — 100/100/100/100
|
||
- [x] 15.4 `bun scripts/openspec-conformance.ts @suaveplan/x509` — exit 0, no `error` findings
|
||
- [x] 15.5 `bunx turbo build --filter=@suaveplan/x509` — green
|
||
|
||
## 16. Pre-archive 7-row checklist (AGENTS.md §13)
|
||
|
||
- [x] 16.1 Generics threaded — `Asn1Node` discriminator, `GeneralName` discriminator, `Extension<T>`; `expectTypeOf` assertions on `X509ErrorCode`, `GeneralNameKind`, `KeyUsageBit`
|
||
- [x] 16.2 Zod schemas wired at every declared boundary (`parseCertificate`, `validateChain`, `TrustAnchorStore` constructor)
|
||
- [x] 16.3 Every error code in `error-codes.ts` thrown by ≥1 production path
|
||
- [x] 16.4 Strict-mode behaviours tested both on (DER) and off (BER)
|
||
- [x] 16.5 OTel instruments emitted from production paths and asserted with a real in-memory `MeterProvider`
|
||
- [x] 16.6 Lifecycle dedup — N/A (parser + validator are stateless; `TrustAnchorStore` is immutable after construction)
|
||
- [x] 16.7 No retroactive tick — every box above reflects work done in this change
|
||
|
||
## 17. Banned-pattern + barrel-shape + runner-import scans
|
||
|
||
- [x] 17.1 `grep -rn "@deprecated\|TODO\|FIXME\|ts-ignore\|ts-expect-error\|\.skip\|\.only" packages/security/x509/src/ --include="*.ts"` — zero output
|
||
- [x] 17.2 Barrel-shape scan per AGENTS.md §13 step 4a — zero output for any `<feature>/index.ts`
|
||
- [x] 17.3 Runner-import scan per AGENTS.md §13 step 4b — zero output
|
||
- [x] 17.4 Missing-`.md` scan — zero output
|
||
- [x] 17.5 Bare-version scan — zero output
|
||
- [x] 17.6 Subpath count matches feature module count (9 subpaths: `.`, `./asn1`, `./pem`, `./certificate`, `./extensions`, `./signature`, `./validate`, `./helpers`, `./errors`)
|
||
- [x] 17.7 Security scan — no hardcoded secrets
|
||
|
||
## 18. Root-doc refresh
|
||
|
||
- [x] 18.1 `bun scripts/folder-reorg/generate-manifest.ts` — refresh `MANIFEST.md`
|
||
- [x] 18.2 `bun scripts/generate-missing.ts` — refresh `MISSING.yml`
|
||
- [x] 18.3 `STATUS.md` — move `x509` row to `Completed`
|
||
- [x] 18.4 `SUMMARY.md` — add per-package paragraph
|
||
- [x] 18.5 `bunx markdownlint-cli2 --fix` then a second `bunx markdownlint-cli2` pass with exit 0
|
||
- [x] 18.6 `git status` shows only intentional changes
|
||
|
||
## 19. Archive
|
||
|
||
- [x] 19.1 Fold spec delta into `openspec/specs/x509/spec.md` (canonical, status ACTIVE)
|
||
- [x] 19.2 Move `openspec/changes/enterprise-x509/` → `openspec/archive/<yyyy-mm-dd>-enterprise-x509/`
|
||
- [x] 19.3 Confirm `MANIFEST.md` lists `@suaveplan/x509` after refresh
|