Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View File

@@ -0,0 +1,284 @@
# 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)