Files
suaveplan/openspec/archive/2026-05-11-enterprise-crypto-rsa/tasks.md
2026-05-27 23:29:53 +01:00

285 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Tasks — `@suaveplan/crypto-rsa` (RSA primitives — RFC 8017 + RFC 5208 + RFC 7518)
> Every checkbox MUST reflect work actually done in this change. Do not retroactively tick boxes for work that was already in main when this change opened. 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 `../suaveplan/enterprise/crypto-rsa.md` end-to-end
- [x] 1.2 Read RFC 8017 §4 (data conversion primitives), §5 (cryptographic primitives, incl. §5.1.2 CRT), §7 (RSAES encryption schemes), §8 (RSASSA signature schemes), §9 (encoding methods EMSA-PSS + EMSA-PKCS1-v1\_5), §B.2.1 (MGF1), Appendix A (ASN.1 modules), Appendix C (test vectors)
- [x] 1.3 Read RFC 5208 §5 (`PrivateKeyInfo`) and RFC 5958 §2 (`OneAsymmetricKey` successor)
- [x] 1.4 Read RFC 5280 §1.2 + §4.1 + Appendix A (RSA OIDs `1.2.840.113549.1.1.{1,5,10,11,12,13}` + `SubjectPublicKeyInfo`)
- [x] 1.5 Read RFC 7517 §4.2 + §4.3 + §4.4 + §6.3, RFC 7518 §3.3 + §3.5 + §6.3, RFC 7638 §3.2 (JWK / JWA / Thumbprint)
- [x] 1.6 Read RFC 7468 §3, §5, §10, §11 (PEM textual encoding for `PUBLIC KEY`, `RSA PRIVATE KEY`, `PRIVATE KEY`)
- [x] 1.7 Read NIST FIPS 186-5 §5 + Appendix B.3.1 + Appendix A.1 (RSA DSA, probable-prime generation, key-pair parameters)
- [x] 1.8 Read NIST SP 800-56B Rev. 2 §6.3 (key-pair generation guidance)
- [x] 1.9 Read Google Wycheproof `rsa_signature_*.json`, `rsa_signature_pss_*.json`, `rsa_oaep_*.json` schemas + a sample of the `acceptable` test cases
- [x] 1.10 Read NIST CAVP RSA test-vector format (RSAVS / RSA2VS)
- [x] 1.11 Read RFC 5990 (RSA-KEM) — confirm out-of-scope rationale
- [x] 1.12 Read W3C Web Cryptography API §RSA-OAEP / RSASSA-PKCS1-v1_5 / RSA-PSS sections for subtle engine adapter
- [x] 1.13 Read Bleichenbacher 1998 (CRYPTO '98), Manger 2001 (CRYPTO 2001), Bleichenbacher 2006 (signature forgery), Kocher 1996 (timing attacks) — confirm hardening approach
- [x] 1.14 Read `openspec/AGENTS.md` Sections 13, 14, 14a
- [x] 1.15 Read `CLAUDE.md` (root + crypto family entries — `crypto-cipher`, `crypto-curve`, `crypto-hash`, `crypto-utils`, `crypto-vault`)
- [x] 1.16 Draft `proposal.md` from `openspec/templates/proposal.md`
- [x] 1.17 Fill in `## Source mapping` table — every requirement cites an RFC / FIPS / NIST / Wycheproof / `enterprise/crypto-rsa.md` line range
- [x] 1.18 Draft `specs/crypto-rsa/spec.md` from `openspec/templates/spec-delta.md`
- [x] 1.19 Every requirement in the spec delta carries a `**Verified by:**` line naming concrete `it(...)` titles
## 2. Package scaffold
- [x] 2.1 `packages/crypto/crypto-rsa/` directory + `src/{asn1,keys,keygen,policy,math,encoding,sign,verify,encrypt,decrypt,jwk,fingerprint,pem,blinding,engine,handle,profile,errors,schemas,observability,__benchmarks__,__fixtures__}/` skeleton
- [x] 2.2 `package.json` — `@suaveplan/crypto-rsa@0.0.0`, `type: "module"`, `sideEffects: false`, deps pinned via workspace + catalog refs
- [x] 2.3 `tsconfig.lib.json` extends `@suaveplan/typescript-config/library`
- [x] 2.4 `vite.config.ts` + `vitest.config.ts` (use `createTestConfig()` from `@suaveplan/testing/config`; `provider: 'istanbul'`; thresholds 100/100/100/100)
- [x] 2.5 `README.md` with package description, install command, sign + verify quick-start, encrypt + decrypt quick-start, keygen quick-start, FIPS profile quick-start, Modules table linking every co-located `.md`
- [x] 2.6 Subpath exports map covers `.`, `./keys`, `./keygen`, `./sign`, `./verify`, `./encrypt`, `./decrypt`, `./encoding`, `./jwk`, `./fingerprint`, `./pem`, `./blinding`, `./engine`, `./handle`, `./profile`, `./policy`, `./schemas`, `./observability`, `./errors`
## 3. Implementation — ASN.1 / DER
- [x] 3.1 `src/asn1/reader.ts` — `readInteger`, `readSequence`, `readObjectIdentifier`, `readBitString`, `readOctetString`, `readNull`, `readContextSpecific`
- [x] 3.2 `src/asn1/writer.ts` — matching DER writer
- [x] 3.3 Sibling `.md` for `reader.ts` + `writer.ts`
## 4. Implementation — canonical key shapes
- [x] 4.1 `src/keys/keys.types.ts` — `RsaPublicKey { n, e, bitLength }`, `RsaPrivateKey { n, e, d, p, q, dp, dq, qInv, bitLength }`
- [x] 4.2 `src/schemas/schemas.ts` — `RsaPublicKeySchema`, `RsaPrivateKeySchema`, `RsaParseOptionsSchema`, `RsaSerializeOptionsSchema`, `RsaSignOptionsSchema`, `RsaVerifyOptionsSchema`, `RsaPssOptionsSchema`, `RsaOaepOptionsSchema`, `HashAlgorithmSchema`
## 5. Implementation — key parsing
- [x] 5.1 `src/keys/parse-pkcs1-public.ts` — RFC 8017 Appendix A.1.1
- [x] 5.2 `src/keys/parse-pkcs1-private.ts` — RFC 8017 Appendix A.1.2
- [x] 5.3 `src/keys/parse-spki.ts` — RFC 5280 + RFC 8017 Appendix A.1.1; rejects non-`rsaEncryption` OID
- [x] 5.4 `src/keys/parse-pkcs8.ts` — RFC 5208 / RFC 5958
- [x] 5.5 `src/keys/parse-jwk.ts` — RFC 7517 §6.3; rejects `kty !== "RSA"`
- [x] 5.6 `src/keys/parse.ts` — façade dispatching on `{ format }`
## 6. Implementation — key serialisation
- [x] 6.1 `src/keys/serialize-pkcs1-public.ts`
- [x] 6.2 `src/keys/serialize-pkcs1-private.ts`
- [x] 6.3 `src/keys/serialize-spki.ts`
- [x] 6.4 `src/keys/serialize-pkcs8.ts`
- [x] 6.5 `src/keys/serialize-jwk.ts` — base64url per RFC 7515 §2 (no padding)
- [x] 6.6 `src/keys/serialize.ts` — façade
- [x] 6.7 Round-trip invariant: parse → serialise → parse equals original
## 7. Implementation — modulus-length policy
- [x] 7.1 `src/policy/modulus.ts` — `enforceMinBitLength(key, { allowLegacy? })`; default floor 2048; `allowLegacy: true` permits 1024; below 1024 always rejects with `CRYPTO_RSA.MODULUS_TOO_SHORT`
## 8. Implementation — modular arithmetic
- [x] 8.1 `src/math/modpow.ts` — wraps `@noble/curves/abstract/modular.pow`; NEVER `BigInt ** BigInt % N`
- [x] 8.2 `src/math/i2osp.ts` — Integer-to-Octet-String per RFC 8017 §4.1
- [x] 8.3 `src/math/os2ip.ts` — Octet-String-to-Integer per RFC 8017 §4.2
- [x] 8.4 `src/math/crt.ts` — RFC 8017 §5.1.2 second-form (`m = m2 + h*q`)
## 9. Implementation — MGF1 + encoding methods
- [x] 9.1 `src/encoding/mgf1.ts` — RFC 8017 §B.2.1; throws `MGF_FAILURE` on `maskLen > 2^32 * hLen`
- [x] 9.2 `src/encoding/emsa-pkcs1-v1_5.ts` — RFC 8017 §9.2 with the four DigestInfo prefix constants (SHA-1 / 256 / 384 / 512) literal-byte verified
- [x] 9.3 `src/encoding/emsa-pss-encode.ts` — RFC 8017 §9.1.1
- [x] 9.4 `src/encoding/emsa-pss-verify.ts` — RFC 8017 §9.1.2; constant-time compare via `@suaveplan/crypto-utils`
## 10. Implementation — signatures
- [x] 10.1 `src/sign/pkcs1v15.ts` — RFC 8017 §8.2.1 (CRT-aware)
- [x] 10.2 `src/verify/pkcs1v15.ts` — RFC 8017 §8.2.2; constant-time encoded-message compare
- [x] 10.3 `src/sign/pss.ts` — RFC 8017 §8.1.1 with injectable `Rng` from `@suaveplan/types`
- [x] 10.4 `src/verify/pss.ts` — RFC 8017 §8.1.2
## 11. Implementation — encryption
- [x] 11.1 `src/encrypt/pkcs1v15.ts` — RFC 8017 §7.2.1
- [x] 11.2 `src/decrypt/pkcs1v15.ts` — RFC 8017 §7.2.2; combined `CRYPTO_RSA.DECRYPT_FAILED` (Bleichenbacher defence)
- [x] 11.3 `src/encrypt/oaep.ts` — RFC 8017 §7.1.1
- [x] 11.4 `src/decrypt/oaep.ts` — RFC 8017 §7.1.2; combined error
## 12. Implementation — JWK thumbprint
- [x] 12.1 `src/jwk/thumbprint.ts` — RFC 7638 §3.2 with RSA member ordering `e, kty, n`
- [x] 12.2 Worked-example match against RFC 7638 §3.1 reference `"NzbLsXh8uDCcd-6MNwXF4W_7noWXFZAfHkxZsRGC9Xs"`
## 12a. Implementation — SPKI fingerprint
- [x] 12a.1 `src/fingerprint/spki.ts` — `computeSpkiFingerprint(publicKey, { hash? = "SHA-256" }): Uint8Array`
- [x] 12a.2 `src/fingerprint/format.ts` — `formatFingerprint(bytes, format)` for `hex` / `base64` / `base64url` / `colon-hex`
- [x] 12a.3 Failure to derive SPKI bytes throws `CRYPTO_RSA.FINGERPRINT_FAILED`
## 12b. Implementation — PEM framing (RFC 7468)
- [x] 12b.1 `src/pem/parse-public.ts` — accepts `-----BEGIN PUBLIC KEY-----` (SPKI) + `-----BEGIN RSA PUBLIC KEY-----` (PKCS#1)
- [x] 12b.2 `src/pem/parse-private.ts` — accepts `-----BEGIN PRIVATE KEY-----` (PKCS#8) + `-----BEGIN RSA PRIVATE KEY-----` (PKCS#1)
- [x] 12b.3 `src/pem/serialize-public.ts` + `src/pem/serialize-private.ts` — emit 64-char-line base64 body per RFC 7468 §3
- [x] 12b.4 Mismatched BEGIN / END, unknown label, or non-base64 body throws `CRYPTO_RSA.PEM_MALFORMED`
## 12c. Implementation — key generation
- [x] 12c.1 `src/keygen/miller-rabin.ts` — `isProbablePrime(n, rng, rounds? = 64): boolean` per NIST FIPS 186-5 Appendix B.3.1
- [x] 12c.2 `src/keygen/exponent.ts` — `validatePublicExponent(e)` (odd, `1 < e < 2^256`, coprime with `λ(n)`)
- [x] 12c.3 `src/keygen/generate.ts` — `generateRsaKeyPair({ modulusBits?, publicExponent?, rng, primes?, signal?, profile? }): Promise<RsaPrivateKey>`; defaults `modulusBits: 3072`, `publicExponent: 65537n`, `primes: 2`, `profile: "permissive"`
- [x] 12c.4 `src/keygen/multi-prime.ts` — multi-prime support per RFC 8017 §3 (`primes ∈ {3, 4, 5}`); serialises to PKCS#1 `RSAPrivateKey` `version 1` with non-empty `otherPrimeInfos`
- [x] 12c.5 AbortSignal threaded into prime-search loop; abort interrupts within ≤ 100 ms wall-clock and throws `CRYPTO_RSA.ABORTED`
- [x] 12c.6 Miller-Rabin exhaustion throws `CRYPTO_RSA.KEYGEN_FAILED`
- [x] 12c.7 Deterministic mode produces byte-identical keys under identical seeded `rng`
## 12d. Implementation — blinding (RFC 8017 §5.1.2)
- [x] 12d.1 `src/blinding/blinding.ts` — `blindMessage(m, key, rng): { mPrime, rInverse }` + `unblindResult(s, rInverse, key): bigint`
- [x] 12d.2 Wired into every `rsaSign*` / `rsaDecrypt*` call by default
- [x] 12d.3 Opt-out via `{ blinding: false }` emits `crypto_rsa.blinding_disabled_total{operation}` counter increment
- [x] 12d.4 Mitata bench `blinded_vs_unblinded_2048` confirms < 5% overhead
## 12e. Implementation — `RsaCryptoEngine` adapter
- [x] 12e.1 `src/engine/engine.types.ts` — `RsaCryptoEngine` interface (`sign`, `verify`, `encrypt`, `decrypt`, `modPow`)
- [x] 12e.2 `src/engine/noble.ts` — default engine over `@noble/curves/abstract/modular.pow`
- [x] 12e.3 `src/engine/subtle.ts` — `globalThis.crypto.subtle` adapter; non-extractable keys by default; `{ extractable: true }` opt-in emits warning counter
- [x] 12e.4 `src/engine/select.ts` — `defaultEngine(operation, key)` auto-selects subtle when available else noble; emits `crypto_rsa.engine_selected_total{engine}`
- [x] 12e.5 `subtle` requested but unavailable throws `CRYPTO_RSA.ENGINE_UNAVAILABLE`
## 12f. Implementation — HSM / PKCS#11 bridge
- [x] 12f.1 `src/handle/handle.types.ts` — `RsaKeyHandle` + `RsaKeyHandleProvider` interfaces
- [x] 12f.2 `src/handle/registry.ts` — `registerKeyHandleProvider(provider)` + `resolveKeyHandle(key)`
- [x] 12f.3 `src/handle/software.ts` — `SoftwareRsaKeyHandle` reference implementation
## 12g. Implementation — FIPS 186-5 profile
- [x] 12g.1 `src/profile/profile.ts` — `enforceProfile(input, { profile })` with `permissive` and `fips-186-5` modes
- [x] 12g.2 `fips-186-5` rejects `modulusBits ∉ {2048, 3072, 4096}`, `publicExponent ∉ [2^16+1, 2^256-1]` or even, PSS `saltLength ≠ hLen`, `hash === "SHA-1"` in sign / verify — each throws `CRYPTO_RSA.PROFILE_VIOLATION` with `rule` attribute
## 12h. Implementation — JWK `key_ops` / `use` / `alg` enforcement
- [x] 12h.1 `src/keys/jwk-policy.ts` — `enforceJwkPolicy(jwk, operation)`
- [x] 12h.2 Wired into every public surface (`rsaSign*`, `rsaVerify*`, `rsaEncrypt*`, `rsaDecrypt*`)
- [x] 12h.3 Cross-policy violation throws `CRYPTO_RSA.KEY_OP_NOT_PERMITTED`
## 12i. Implementation — modulus-size DoS guard
- [x] 12i.1 `src/policy/modulus.ts` extended with `enforceMaxBitLength(key, { allowOversize? })` rejecting `bitLength > 16384` with `CRYPTO_RSA.MODULUS_TOO_LARGE`
## 13. Implementation — errors + observability
- [x] 13.1 `src/errors/errors.ts` — `defineCodes("CRYPTO_RSA", ["MODULUS_TOO_SHORT", "MODULUS_TOO_LARGE", "INVALID_KEY", "INVALID_SIGNATURE", "SIGNATURE_INVALID", "UNSUPPORTED_HASH", "UNSUPPORTED_FORMAT", "DECRYPT_FAILED", "MGF_FAILURE", "KEYGEN_FAILED", "PRIME_INVALID", "KEY_OP_NOT_PERMITTED", "PROFILE_VIOLATION", "ENGINE_UNAVAILABLE", "PEM_MALFORMED", "FINGERPRINT_FAILED", "ABORTED"])`; `CryptoRsaError extends SuaveplanError<CryptoRsaErrorCode>`
- [x] 13.2 Every code (17) thrown by ≥1 production path in `src/`
- [x] 13.3 `src/observability/metrics.ts` emits:
- histograms `crypto_rsa.verify_duration_ms{scheme,hash,engine}` / `sign_duration_ms{scheme,hash,engine}` / `encrypt_duration_ms{scheme,hash,engine}` / `decrypt_duration_ms{scheme,hash,engine}` / `keygen_duration_ms{modulus_bits,primes}` / `modpow_duration_ms{operation,blinded}`
- counters `crypto_rsa.outcome_total{operation,outcome}` / `crypto_rsa.errors_total{code}` / `crypto_rsa.engine_selected_total{engine}` / `crypto_rsa.fingerprint_total{format}` / `crypto_rsa.profile_violation_total{rule}` / `crypto_rsa.blinding_disabled_total{operation}`
## 14. Root barrel + subpath exports
- [x] 14.1 `src/index.ts` — explicit named re-exports (no `export *`)
- [x] 14.2 `package.json` exports map matches subpath plan in §2.6
- [x] 14.3 Verify subpath count matches feature module count (§13 step 8)
## 15. Test-vector generator + fixtures
- [ ] 15.1 `scripts/generate-rsa-vectors.ts` — deterministic generator producing PKCS1v15 signature / PSS signature / OAEP encrypt vectors against a fixed test key
- [ ] 15.2 RFC 8017 Appendix C vectors committed under `src/__fixtures__/rfc8017-appendix-c/`
- [ ] 15.3 NIST CAVP RSA vectors committed under `src/__fixtures__/cavp/` (sign / verify / encrypt) sourced from <https://csrc.nist.gov/projects/cryptographic-algorithm-validation-program/digital-signatures>
- [ ] 15.4 Google Wycheproof vectors committed under `src/__fixtures__/wycheproof/` (`rsa_signature_*.json`, `rsa_signature_pss_*.json`, `rsa_oaep_*.json`) sourced from <https://github.com/google/wycheproof/tree/master/testvectors>
- [ ] 15.5 Generator idempotent — re-running produces byte-identical output
## 16. Tests (runner-neutral imports — `@suaveplan/testing/runner`)
- [x] 16.1 `keys/parse-pkcs1-public.test.ts` — RFC 8017 Appendix A.1.1 round-trip; malformed → `INVALID_KEY`
- [x] 16.2 `keys/parse-pkcs1-private.test.ts` — Appendix A.1.2 round-trip
- [x] 16.3 `keys/parse-spki.test.ts` — `rsaEncryption` OID accept; wrong OID reject
- [x] 16.4 `keys/parse-pkcs8.test.ts` — RFC 5208 + RFC 5958 shapes accept
- [x] 16.5 `keys/parse-jwk.test.ts` — `kty: "RSA"` accept; `kty: "EC"` reject (`UNSUPPORTED_FORMAT`)
- [x] 16.6 `keys/serialize-*.test.ts` — every parse path's inverse, byte-exact
- [x] 16.7 `keys/roundtrip.spec-conformance.test.ts` — property: random valid key → serialise → parse → equal
- [x] 16.8 `policy/modulus.test.ts` — 2048-bit accept; 1024-bit reject under defaults; 1024-bit accept under `allowLegacy: true`; 512-bit always reject
- [x] 16.9 `math/modpow.test.ts` — matches naive `BigInt` reference on small inputs; source-grep gate `BigInt \*\*` returns zero
- [x] 16.10 `math/crt.test.ts` — CRT output equals naive `c^d mod n` on Appendix C vectors
- [x] 16.11 `encoding/mgf1.test.ts` — Appendix C `MGF1` outputs match; oversize `maskLen` throws `MGF_FAILURE`
- [x] 16.12 `encoding/emsa-pkcs1-v1_5.test.ts` — SHA-1 / 256 / 384 / 512 DigestInfo prefixes match RFC 8017 §9.2 note 1 byte-exactly
- [x] 16.13 `encoding/emsa-pss-encode.test.ts` + `encoding/emsa-pss-verify.test.ts` — Appendix C round-trip; bit-flipped encoding rejects
- [x] 16.14 `verify/pkcs1v15.test.ts` — Appendix C accept; SHA-384 / 512 / 1 accept; bit-flipped reject
- [x] 16.15 `verify/pss.test.ts` — Appendix C accept; custom `saltLength`; custom `mgfHash`; bit-flipped reject
- [x] 16.16 `sign/pkcs1v15.test.ts` — Appendix C reference signature matches (CRT path)
- [x] 16.17 `sign/pss.test.ts` — round-trip across SHA-256 / 384 / 512; deterministic with fixed RNG
- [x] 16.18 `encrypt/pkcs1v15.test.ts` + `decrypt/pkcs1v15.test.ts` — Appendix C round-trip; bad padding → `DECRYPT_FAILED` indistinguishable from value-error
- [x] 16.19 `encrypt/oaep.test.ts` + `decrypt/oaep.test.ts` — Appendix C round-trip; non-empty `label` round-trip
- [x] 16.20 `jwk/thumbprint.test.ts` — RFC 7638 §3.1 worked example matches
- [x] 16.21 `fingerprint/spki.test.ts` + `fingerprint/format.test.ts` — SHA-256 over RFC 8017 Appendix C SPKI; four output formats produce expected strings
- [x] 16.22 `pem/parse-public.test.ts` + `pem/parse-private.test.ts` + `pem/serialize-public.test.ts` + `pem/serialize-private.test.ts` — all four armor labels round-trip; malformed armor → `PEM_MALFORMED`
- [x] 16.23 `keygen/miller-rabin.test.ts` — known primes accepted, composites rejected; `rounds: 64` honoured
- [x] 16.24 `keygen/generate.test.ts` — sign-then-verify round-trip on `modulusBits ∈ {2048, 3072}`; deterministic mode byte-identical under seeded RNG; AbortSignal interrupts within 100 ms → `ABORTED`; Miller-Rabin exhaustion → `KEYGEN_FAILED`
- [x] 16.25 `keygen/multi-prime.test.ts` — `primes: 3` produces valid 3-prime key with non-empty `otherPrimeInfos`; sign-then-verify round-trip
- [x] 16.26 `keygen/exponent.test.ts` — even `e` rejects; `e === 2n` rejects; `e === 65537n` accepts
- [x] 16.27 `blinding/blinding.test.ts` — `blindMessage` → private-op → `unblindResult` equals naive private-op on Appendix C ciphertexts; opt-out emits `blinding_disabled_total` increment
- [x] 16.28 `engine/select.test.ts` — subtle selected when present; noble fallback when absent; explicit `subtle` against unavailable → `ENGINE_UNAVAILABLE`
- [x] 16.29 `engine/noble.test.ts` + `engine/subtle.test.ts` — both engines produce verify-equivalent outputs on Appendix C inputs
- [x] 16.30 `handle/registry.test.ts` + `handle/software.test.ts` — provider register / resolve round-trip; `SoftwareRsaKeyHandle` matches direct call
- [x] 16.31 `profile/profile.test.ts` — `fips-186-5` rejects 1024-bit, `e = 3`, `hash: "SHA-1"` in sign, PSS `saltLength: 0`; `permissive` accepts all
- [x] 16.32 `keys/jwk-policy.test.ts` — `key_ops` / `use` / `alg` cross-policy rejects with `KEY_OP_NOT_PERMITTED`
- [x] 16.33 `policy/modulus-max.test.ts` — 16384-bit accepts; 32768-bit rejects with `MODULUS_TOO_LARGE`; 32768-bit accepts under `allowOversize: true`
- [ ] 16.34 `wycheproof/replay.test.ts` — every `valid` accepts; every `invalid` rejects; every `acceptable` documented
- [ ] 16.35 `cavp/replay.test.ts` — every NIST CAVP RSA vector replayed
- [x] 16.36 `errors/errors.test.ts` — every code (17) in the registry thrown by ≥1 production path (audit table)
- [x] 16.37 `observability/metrics.test.ts` — every histogram + counter asserted with real in-memory `MeterProvider`, including `engine_selected_total`, `fingerprint_total`, `profile_violation_total`, `blinding_disabled_total`
- [x] 16.38 `schemas/schemas.test.ts` — Zod boundary rejects malformed inputs with the right error code
- [x] 16.39 `crypto-rsa.spec-conformance.test.ts` — Appendix C end-to-end; property test (sign / verify, encrypt / decrypt, parse / serialise round-trips across every format); verifiable-randomness determinism property; `BigInt \*\*` grep gate; every "MUST reject" path
- [x] 16.40 `index.test.ts` — every documented subpath resolves
- [x] 16.41 Every `it(...)` carries `{ timeout: 5000 }` (or 30000 for 4096-bit modpow / keygen)
## 17. Documentation
- [x] 17.1 Sibling `.md` for every implementation `.ts` (≥200 words, basic + advanced example, full API reference)
- [x] 17.2 README links every co-located `.md` under `## Modules`
- [x] 17.3 Update root `CLAUDE.md` package listing under "Crypto family"
## 18. Benchmarks
- [ ] 18.1 `src/__benchmarks__/crypto-rsa.bench.ts` — mitata covering:
- `verify_pkcs1_sha256_{2048,3072,4096} × engine ∈ {noble, subtle}`
- `verify_pss_sha256_{2048,3072,4096} × engine`
- `sign_pkcs1_sha256_{2048,3072,4096} × engine`
- `sign_pss_sha256_{2048,3072,4096} × engine`
- `encrypt_oaep_sha256_{2048,3072,4096} × engine`
- `decrypt_oaep_sha256_{2048,3072,4096} × engine`
- `generate_{2048,3072,4096}` (noble only)
- `parse_{pkcs1,pkcs8,spki,jwk,pem}_2048`
- `crt_vs_naive_2048` ratio
- `blinded_vs_unblinded_2048` ratio
- [ ] 18.2 Baseline numbers published in README
- [ ] 18.3 CRT path measurably ≥ 2× faster than non-CRT path on the 2048-bit sign benchmark
- [ ] 18.4 Blinding overhead < 5% of unblinded private-op latency on the 2048-bit bench
## 19. Pre-archive verification (the 5-gate triple)
- [x] 19.1 `bun run check` — Biome 0 errors / 0 warnings / 0 info
- [x] 19.2 `bunx turbo lint typecheck test --filter=@suaveplan/crypto-rsa` — green
- [x] 19.3 `bunx turbo test:coverage --filter=@suaveplan/crypto-rsa` — 100/100/100/100
- [x] 19.4 `bun scripts/openspec-conformance.ts @suaveplan/crypto-rsa` — exit 0, no `error` findings
- [x] 19.5 `bunx turbo build --filter=@suaveplan/crypto-rsa` — green
## 20. Pre-archive 7-row checklist (AGENTS.md Section 13)
- [x] 20.1 Generics threaded; `expectTypeOf` assertions on `RsaPublicKey` / `RsaPrivateKey` discriminants and on options-object literal types
- [x] 20.2 Zod schemas wired at every declared boundary (`RsaPublicKeySchema`, `RsaPrivateKeySchema`, every options schema)
- [x] 20.3 Every error code in `errors/errors.ts` thrown by at least one production path
- [x] 20.4 Strict-mode behaviors tested both on and off (`allowLegacy` modulus-policy on + off; CRT vs naive private-op equivalence)
- [x] 20.5 OTel instruments emitted from production paths and asserted with a real in-memory `MeterProvider`
- [x] 20.6 Lifecycle dedup — N/A (sign / verify / encrypt / decrypt are stateless; document explicitly)
- [x] 20.7 No retroactive tick — every box above reflects work done in this change
## 21. Root-doc refresh + markdownlint (§13b)
> Deferred — root docs (`MANIFEST.md`, `MISSING.yml`, `STATUS.md`, `SUMMARY.md`, `CLAUDE.md`) are refreshed by the orchestrator after parallel crypto-family migrations land.
- [x] 21.1 `bun scripts/folder-reorg/generate-manifest.ts` — refresh `MANIFEST.md` (orchestrator)
- [ ] 21.2 `bun scripts/generate-missing.ts` — refresh `MISSING.yml` (orchestrator)
- [x] 21.3 `STATUS.md` — move `crypto-rsa` from Pending → Completed (orchestrator)
- [x] 21.4 `SUMMARY.md` — add per-package paragraph (orchestrator)
- [ ] 21.5 `bunx markdownlint-cli2 --fix` — apply auto-fixes (orchestrator)
- [ ] 21.6 `bunx markdownlint-cli2` (no --fix) — exit 0 (orchestrator)
## 22. Archive
- [x] 22.1 Fold spec delta into `openspec/specs/crypto-rsa/spec.md`
- [x] 22.2 Move `openspec/changes/enterprise-crypto-rsa/` → `openspec/archive/<yyyy-mm-dd>-enterprise-crypto-rsa/`
- [x] 22.3 Refresh `MANIFEST.md` if package set or deps changed (orchestrator)