# `@suaveplan/crypto-rsa` — RSA primitives (RFC 8017 PKCS #1 v2.2 + RFC 5208 PKCS #8 + RFC 7518 JWA) **Package**: `@suaveplan/crypto-rsa` (NEW) **Status**: PROPOSED **Type**: new-package (greenfield; crypto-plugin sibling, universal-tier) **Date**: 2026-05-11 **Path**: `packages/crypto/crypto-rsa/` **Source audit**: `../suaveplan/enterprise/crypto-rsa.md` ## Why The genesis crypto family today covers symmetric AEAD (`crypto-cipher`), hashing + KDFs (`crypto-hash`), and elliptic curves (`crypto-curve`: Ed25519, X25519, P-256, P-384, P-521, secp256k1). It does NOT cover RSA — yet RSA is the dominant signature algorithm in every protocol genesis is migrating: real-world TLS certificate chains, the JWS / JWT ecosystem (RFC 7518 §3.3 mandates support for `RS256`), and the WebAuthn `tpm` attestation format (RSA endorsement keys are the production majority for TPM-bound credentials). Three packages currently in flight on genesis are blocked on this gap: 1. **`@suaveplan/x509`** — verifies certificate-chain signatures. RFC 5280 §1.2 + Appendix A enumerate the RSA signature OIDs `1.2.840.113549.1.1.5` (sha1WithRSAEncryption), `1.2.840.113549.1.1.11` (sha256WithRSAEncryption), `1.2.840.113549.1.1.12` (sha384), `1.2.840.113549.1.1.13` (sha512), and `1.2.840.113549.1.1.10` (id-RSASSA-PSS). The majority of real production certs today use one of these. 2. **`@suaveplan/jws`** — implements the JWA registry of RFC 7518 §3.3 (`RS256`/`RS384`/`RS512` over RSASSA-PKCS1-v1\_5) and §3.5 (`PS256`/`PS384`/`PS512` over RSASSA-PSS). Six of the eleven JWS algorithm IDs are RSA-backed. 3. **`@suaveplan/webauthn`** — `tpm` attestation statements carry RSA public keys from TPM modules. RSA-PKCS1-v1\_5 over SHA-256 is the most common TPM attestation signature. All three consumers verify externally-issued keys; one (`jws`) also signs. Beyond the three named consumers, this package is the universal RSA primitive for the entire monorepo — `@suaveplan/crypto-cli`'s future `keychain rsa init`, `@suaveplan/vault`'s future RSA-protected key blobs, `@suaveplan/pkcs12` PKCS#12 import paths, and any downstream consumer integrating with HSM / PKCS#11 hardware all route through `@suaveplan/crypto-rsa`. Splitting RSA out into `@suaveplan/crypto-rsa` rather than folding it into `@suaveplan/crypto-curve` keeps curve-only callers at zero RSA bundle cost, mirrors the per-primitive layout the rest of the crypto family already follows (`crypto-cipher`, `crypto-hash`), and matches `@noble/curves`'s own separation of curve and RSA work. The library ships at enterprise quality from day one — matching the bar set by `@suaveplan/crypto-curve` (per-primitive subpaths, native + noble engine adapters) and `@suaveplan/crypto-vault` (Zod boundaries, OTel everywhere, 100% docs, sentinel-driven side-channel hardening). The required-reading audit covered RFC 8017 (PKCS #1 v2.2) end-to-end, NIST FIPS 186-5 (Digital Signature Standard) §5 (RSA DSA), NIST SP 800-56B Rev. 2 (Key Establishment Using Integer Factorization Cryptography), Google Wycheproof's RSA test corpus, RFC 7468 (PEM textual encoding), RFC 7638 (JWK Thumbprint), and RFC 5990 (RSA-KEM — surveyed and explicitly deferred). Every requirement below is anchored against one of these sources. The package is greenfield — no prior implementation in `packages/crypto/` and no upstream `../suaveplan/packages//crypto-rsa/` source. Every requirement is mapped against the canonical RFCs and NIST documents: RFC 8017 (PKCS #1 v2.2), RFC 5208 (PKCS #8), RFC 5958 (PKCS #8 successor), RFC 5280 (X.509 + OIDs), RFC 7468 (PEM), RFC 7517 / 7518 / 7638 (JWK / JWA / JWK Thumbprint), RFC 5990 (RSA-KEM — deferred), NIST FIPS 186-5 (Digital Signature Standard), NIST SP 800-56B Rev. 2 (Key Establishment), and Google Wycheproof. ## What changes ### New package: `@suaveplan/crypto-rsa` #### Scaffold - ADDED: `packages/crypto/crypto-rsa/` package scaffold (`package.json`, `tsconfig.lib.json`, `vite.config.ts`, `vitest.config.ts`, `README.md`, `src/__benchmarks__/`, `src/__fixtures__/`). #### ASN.1 / DER helpers - ADDED: `src/asn1/reader.ts` — minimal DER reader (`readInteger`, `readSequence`, `readObjectIdentifier`, `readBitString`, `readOctetString`, `readNull`, `readContextSpecific`) sufficient for PKCS#1, PKCS#8, SPKI, RSA-PSS `AlgorithmIdentifier` parameters. - ADDED: `src/asn1/writer.ts` — matching DER writer. #### Canonical key shapes - ADDED: `src/keys/keys.types.ts` — `RsaPublicKey { n: bigint; e: bigint; bitLength: number }`, `RsaPrivateKey { n; e; d; p; q; dp; dq; qInv; bitLength }`. `bitLength` is the modulus bit-length (used by the modulus-length policy gate). #### Key parse + serialise — PKCS#1 - ADDED: `src/keys/parse-pkcs1-public.ts` — `parseRsaPublicKeyPkcs1(bytes): RsaPublicKey` per RFC 8017 Appendix A.1.1 (`RSAPublicKey ::= SEQUENCE { modulus INTEGER, publicExponent INTEGER }`). - ADDED: `src/keys/parse-pkcs1-private.ts` — `parseRsaPrivateKeyPkcs1(bytes): RsaPrivateKey` per RFC 8017 Appendix A.1.2. - ADDED: `src/keys/serialize-pkcs1-public.ts` + `src/keys/serialize-pkcs1-private.ts` — inverse paths. #### Key parse + serialise — PKCS#8 / SPKI - ADDED: `src/keys/parse-spki.ts` — `parseRsaPublicKeySpki(bytes): RsaPublicKey` unwraps the `SubjectPublicKeyInfo` (RFC 5280) carrying OID `1.2.840.113549.1.1.1` (`rsaEncryption`) and dispatches to the PKCS#1 reader. - ADDED: `src/keys/parse-pkcs8.ts` — `parseRsaPrivateKeyPkcs8(bytes): RsaPrivateKey` per RFC 5208 / RFC 5958. - ADDED: `src/keys/serialize-spki.ts` + `src/keys/serialize-pkcs8.ts` — inverse paths. #### Key parse + serialise — JWK - ADDED: `src/keys/parse-jwk.ts` — `parseRsaPublicKeyJwk` + `parseRsaPrivateKeyJwk` per RFC 7517 §6.3. Rejects `kty !== "RSA"` with `CRYPTO_RSA.UNSUPPORTED_FORMAT`. - ADDED: `src/keys/serialize-jwk.ts` — emits base64url-encoded values per RFC 7515 §2 (no padding, URL-safe alphabet). #### Façade - ADDED: `src/keys/parse.ts` — `parseRsaPublicKey(bytes, { format })` + `parseRsaPrivateKey(bytes, { format })` dispatch on the `format` discriminant. - ADDED: `src/keys/serialize.ts` — `serializeRsaPublicKey(key, { format })` + `serializeRsaPrivateKey(key, { format })` dispatch. #### Modulus-length policy - ADDED: `src/policy/modulus.ts` — `enforceMinBitLength(key, { allowLegacy?: boolean })`. Default floor is 2048 bits; `allowLegacy: true` permits 1024-bit keys. Below 1024 is always rejected with `CRYPTO_RSA.MODULUS_TOO_SHORT`. #### Modular arithmetic + CRT - ADDED: `src/math/modpow.ts` — `modPow(base: bigint, exponent: bigint, modulus: bigint): bigint` thin wrapper over `@noble/curves/abstract/modular.pow`. NEVER native `BigInt ** BigInt % N`. - ADDED: `src/math/crt.ts` — `rsaPrivateOperationCrt(c: bigint, key: RsaPrivateKey): bigint` per RFC 8017 §5.1.2 second-form (`m1 = c^dp mod p`, `m2 = c^dq mod q`, `h = qInv * (m1 - m2) mod p`, `m = m2 + h * q`). - ADDED: `src/math/i2osp.ts` — Integer-to-Octet-String primitive per RFC 8017 §4.1. - ADDED: `src/math/os2ip.ts` — Octet-String-to-Integer primitive per RFC 8017 §4.2. #### MGF1 + encoding methods - ADDED: `src/encoding/mgf1.ts` — `mgf1(seed, maskLen, hash): Uint8Array` per RFC 8017 §B.2.1. Throws `CRYPTO_RSA.MGF_FAILURE` when `maskLen > 2^32 * hLen`. - ADDED: `src/encoding/emsa-pkcs1-v1_5.ts` — `emsaPkcs1v15Encode(messageDigest, emLen, hash)` per RFC 8017 §9.2. Bundles the four DigestInfo prefix constants: - SHA-1: `30 21 30 09 06 05 2b 0e 03 02 1a 05 00 04 14` - SHA-256: `30 31 30 0d 06 09 60 86 48 01 65 03 04 02 01 05 00 04 20` - SHA-384: `30 41 30 0d 06 09 60 86 48 01 65 03 04 02 02 05 00 04 30` - SHA-512: `30 51 30 0d 06 09 60 86 48 01 65 03 04 02 03 05 00 04 40` - ADDED: `src/encoding/emsa-pss-encode.ts` — `emsaPssEncode(messageDigest, emBits, salt, hash, mgfHash)` per RFC 8017 §9.1.1. - ADDED: `src/encoding/emsa-pss-verify.ts` — `emsaPssVerify(messageDigest, encodedMessage, emBits, saltLength, hash, mgfHash)` per RFC 8017 §9.1.2. Constant-time over the salt + hash compare. #### Signatures — RSASSA-PKCS1-v1\_5 - ADDED: `src/sign/pkcs1v15.ts` — `rsaSignPkcs1v15(privateKey, message, { hash }): Uint8Array` per RFC 8017 §8.2.1. - ADDED: `src/verify/pkcs1v15.ts` — `rsaVerifyPkcs1v15(key, message, signature, { hash }): boolean` per RFC 8017 §8.2.2. Constant-time encoded-message compare. #### Signatures — RSASSA-PSS - ADDED: `src/sign/pss.ts` — `rsaSignPss(privateKey, message, { hash; saltLength?; mgfHash?; rng? }): Uint8Array` per RFC 8017 §8.1.1. - ADDED: `src/verify/pss.ts` — `rsaVerifyPss(key, message, signature, { hash; saltLength?; mgfHash? }): boolean` per RFC 8017 §8.1.2. #### Encryption — RSAES-PKCS1-v1\_5 - ADDED: `src/encrypt/pkcs1v15.ts` — `rsaEncryptPkcs1v15(key, plaintext, { rng? }): Uint8Array` per RFC 8017 §7.2.1. - ADDED: `src/decrypt/pkcs1v15.ts` — `rsaDecryptPkcs1v15(privateKey, ciphertext): Uint8Array` per RFC 8017 §7.2.2. Single combined error class (`CRYPTO_RSA.DECRYPT_FAILED`) — never distinguishes padding-format errors (Bleichenbacher oracle defence). #### Encryption — RSAES-OAEP - ADDED: `src/encrypt/oaep.ts` — `rsaEncryptOaep(key, plaintext, { hash; mgfHash?; label?; rng? }): Uint8Array` per RFC 8017 §7.1.1. - ADDED: `src/decrypt/oaep.ts` — `rsaDecryptOaep(privateKey, ciphertext, { hash; mgfHash?; label? }): Uint8Array` per RFC 8017 §7.1.2. Same combined-error contract. #### JWK thumbprint - ADDED: `src/jwk/thumbprint.ts` — `rsaJwkThumbprint(key, hash): Uint8Array` per RFC 7638 §3.2. Worked-example match against RFC 7638 §3.1 reference value `"NzbLsXh8uDCcd-6MNwXF4W_7noWXFZAfHkxZsRGC9Xs"`. #### SPKI fingerprint - ADDED: `src/fingerprint/spki.ts` — `computeSpkiFingerprint(publicKey, { hash?: "SHA-256" | "SHA-1" }): Uint8Array` (default SHA-256). Digests the SPKI DER bytes; `ssh-keygen -lf`-style identifier for an RSA key. - ADDED: `src/fingerprint/format.ts` — `formatFingerprint(bytes, format: "hex" | "base64" | "base64url" | "colon-hex"): string`. #### PEM framing (RFC 7468) - ADDED: `src/pem/parse-public.ts` — `parseRsaPublicKeyPem(text): RsaPublicKey` recognising armor labels `-----BEGIN PUBLIC KEY-----` (SPKI) and `-----BEGIN RSA PUBLIC KEY-----` (PKCS#1 public). - ADDED: `src/pem/parse-private.ts` — `parseRsaPrivateKeyPem(text): RsaPrivateKey` recognising armor labels `-----BEGIN PRIVATE KEY-----` (PKCS#8) and `-----BEGIN RSA PRIVATE KEY-----` (PKCS#1 private). - ADDED: `src/pem/serialize-public.ts` + `src/pem/serialize-private.ts` — inverse paths emitting 64-char-line base64 bodies per RFC 7468 §3. #### Key generation - ADDED: `src/keygen/miller-rabin.ts` — `isProbablePrime(n: bigint, rng: Rng, rounds?: number = 64): boolean` per NIST FIPS 186-5 Appendix B.3.1. - ADDED: `src/keygen/generate.ts` — `generateRsaKeyPair(options: { modulusBits?, publicExponent?, rng, primes?, signal?, profile? }): Promise`. Defaults: `modulusBits: 3072`, `publicExponent: 65537n`, `primes: 2`, `profile: "permissive"`. AbortSignal threaded into the prime-search loop; abort interrupts within ≤ 100 ms and throws `CRYPTO_RSA.ABORTED`. - ADDED: `src/keygen/multi-prime.ts` — multi-prime RSA support per RFC 8017 §3 (`primes ∈ {3, 4, 5}`). - ADDED: `src/keygen/exponent.ts` — `publicExponent` validation (odd, `1 < e < 2^256`, coprime with `λ(n)`). #### Blinding (RFC 8017 §5.1.2) - ADDED: `src/blinding/blinding.ts` — `blindMessage(m, key, rng): { mPrime, rInverse }` + `unblindResult(s, rInverse, key): bigint` per RFC 8017 §5.1.2 / Kocher 1996. Wired into every `rsaSign*` / `rsaDecrypt*` call by default; opt-out via `{ blinding: false }` emits `crypto_rsa.blinding_disabled_total{operation}` counter increment. #### `RsaCryptoEngine` adapter — noble + subtle - ADDED: `src/engine/engine.types.ts` — `RsaCryptoEngine` interface (`sign`, `verify`, `encrypt`, `decrypt`, `modPow`). - ADDED: `src/engine/noble.ts` — default engine over `@noble/curves/abstract/modular.pow`. - ADDED: `src/engine/subtle.ts` — `globalThis.crypto.subtle` adapter. Wraps `importKey` / `sign` / `verify` / `encrypt` / `decrypt`. Non-extractable keys by default. - ADDED: `src/engine/select.ts` — `defaultEngine(operation, key): RsaCryptoEngine` auto-selects subtle when available else noble. Emits `crypto_rsa.engine_selected_total{engine}` on every call. #### HSM / PKCS#11 bridge - ADDED: `src/handle/handle.types.ts` — `RsaKeyHandle` interface + `RsaKeyHandleProvider` interface. - ADDED: `src/handle/registry.ts` — `registerKeyHandleProvider(provider)` + `resolveKeyHandle(key)`. - ADDED: `src/handle/software.ts` — `SoftwareRsaKeyHandle` reference implementation wrapping an in-memory `RsaPrivateKey`. #### FIPS 186-5 profile - ADDED: `src/profile/profile.ts` — `enforceProfile(input, options: { profile: "permissive" | "fips-186-5" })`. The `fips-186-5` profile rejects `modulusBits ∉ {2048, 3072, 4096}`, `publicExponent ∉ [2^16+1, 2^256-1]`, even `publicExponent`, `hash === "SHA-1"` in sign / verify, and PSS `saltLength` ≠ `hLen` (with `CRYPTO_RSA.PROFILE_VIOLATION`). #### JWK `key_ops` / `use` / `alg` enforcement - ADDED: `src/keys/jwk-policy.ts` — `enforceJwkPolicy(jwk, operation): void`. `operation ∈ {"sign","verify","encrypt","decrypt","wrapKey","unwrapKey"}`. Cross-policy violations throw `CRYPTO_RSA.KEY_OP_NOT_PERMITTED`. #### Errors + Zod + observability - ADDED: `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"])` registry (17 codes) + `CryptoRsaError extends SuaveplanError`. - ADDED: `src/schemas/schemas.ts` — `RsaPublicKeySchema`, `RsaPrivateKeySchema`, `RsaParseOptionsSchema`, `RsaSerializeOptionsSchema`, `RsaSignOptionsSchema`, `RsaVerifyOptionsSchema`, `RsaPssOptionsSchema`, `RsaOaepOptionsSchema`, `HashAlgorithmSchema`, `RsaKeygenOptionsSchema`, `RsaFingerprintOptionsSchema`, `RsaJwkPolicySchema`, `RsaProfileSchema`. Mismatch → `CRYPTO_RSA.UNSUPPORTED_FORMAT` / `CRYPTO_RSA.UNSUPPORTED_HASH` / `CRYPTO_RSA.INVALID_KEY` / `CRYPTO_RSA.PROFILE_VIOLATION` as appropriate. - ADDED: `src/observability/metrics.ts` — `cryptoRsaMetrics` factory emitting: - Histograms: `crypto_rsa.verify_duration_ms{scheme,hash,engine}`, `crypto_rsa.sign_duration_ms{scheme,hash,engine}`, `crypto_rsa.encrypt_duration_ms{scheme,hash,engine}`, `crypto_rsa.decrypt_duration_ms{scheme,hash,engine}`, `crypto_rsa.keygen_duration_ms{modulus_bits,primes}`, `crypto_rsa.modpow_duration_ms{operation,blinded}`. - Counters: `crypto_rsa.outcome_total{operation,outcome}` (`outcome ∈ ok / invalid_signature / decrypt_failed / modulus_too_short / modulus_too_large / profile_violation / aborted / key_op_not_permitted`), `crypto_rsa.errors_total{code}`, `crypto_rsa.engine_selected_total{engine}`, `crypto_rsa.fingerprint_total{format}`, `crypto_rsa.profile_violation_total{rule}` (`rule ∈ modulus_bits / public_exponent / salt_length / sha1_banned`), `crypto_rsa.blinding_disabled_total{operation}`. #### Root + scripts + benchmarks - ADDED: `src/index.ts` — explicit named re-exports of every public surface (no `export *`). - ADDED: `scripts/generate-rsa-vectors.ts` — deterministic vector generator covering PKCS1v15 sign, PSS sign, OAEP encrypt against a fixed test key. Idempotent re-runs. - ADDED: `src/__fixtures__/rfc8017-appendix-c/` — RFC 8017 Appendix C test vectors as JSON. - ADDED: `src/__fixtures__/cavp/` — NIST CAVP RSA sign / verify / encrypt vectors from . - ADDED: `src/__fixtures__/wycheproof/` — Google Wycheproof RSA vectors (`rsa_signature_*.json`, `rsa_signature_pss_*.json`, `rsa_oaep_*.json`) from . - ADDED: `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 (CRT ≥ 2× faster), `blinded_vs_unblinded_2048` ratio (blinding overhead < 5%). ### Subpath exports - `.` — root barrel (every public symbol) - `./keys` — `parseRsaPublicKey`, `parseRsaPrivateKey`, `serializeRsaPublicKey`, `serializeRsaPrivateKey`, `enforceJwkPolicy`, types - `./keygen` — `generateRsaKeyPair`, `isProbablePrime` - `./sign` — `rsaSignPkcs1v15`, `rsaSignPss` - `./verify` — `rsaVerifyPkcs1v15`, `rsaVerifyPss` - `./encrypt` — `rsaEncryptPkcs1v15`, `rsaEncryptOaep` - `./decrypt` — `rsaDecryptPkcs1v15`, `rsaDecryptOaep` - `./encoding` — `mgf1`, `emsaPkcs1v15Encode`, `emsaPssEncode`, `emsaPssVerify` - `./jwk` — `rsaJwkThumbprint` - `./fingerprint` — `computeSpkiFingerprint`, `formatFingerprint` - `./pem` — `parseRsaPublicKeyPem`, `parseRsaPrivateKeyPem`, `serializeRsaPublicKeyPem`, `serializeRsaPrivateKeyPem` - `./blinding` — `blindMessage`, `unblindResult` (advanced consumers; re-exported internally by sign / decrypt) - `./engine` — `RsaCryptoEngine` interface, `defaultEngine`, `nobleEngine`, `subtleEngine` - `./handle` — `RsaKeyHandle`, `RsaKeyHandleProvider`, `registerKeyHandleProvider`, `SoftwareRsaKeyHandle` - `./profile` — `enforceProfile`, profile types - `./policy` — `enforceMinBitLength`, `enforceMaxBitLength` - `./schemas` — every Zod schema - `./observability` — `cryptoRsaMetrics` factory - `./errors` — `CRYPTO_RSA_CODES`, `CryptoRsaError`, `CryptoRsaErrorCode` ### Dependencies - `@suaveplan/crypto-hash workspace:^` — SHA-1 / SHA-256 / SHA-384 / SHA-512 digest. - `@suaveplan/crypto-utils workspace:^` — `constantTimeEqual`, `randomBytes`, `concat`, hex / base64url helpers, `i2osp` / `os2ip`. - `@suaveplan/error workspace:^` — `defineCodes`, `SuaveplanError`. - `@suaveplan/types workspace:^` — `Rng`, `HashAlgorithm`, `Clock`. - `@noble/curves catalog:crypto` — `abstract/modular.pow` constant-time-hardened modpow + `abstract/modular.invert`. - `@opentelemetry/api catalog:observability` — histograms + counters. - `zod catalog:forms` — boundary schemas. - `tslib catalog:build`. The `subtle` engine consumes `globalThis.crypto.subtle` at runtime — there is no compile-time dependency to add. The HSM / PKCS#11 bridge ships only the interface + reference `SoftwareRsaKeyHandle`; concrete PKCS#11 / KMS adapters live in downstream packages and consume `@suaveplan/crypto-rsa/handle` directly. ## Out of scope - **RSA-KEM (RFC 5990)** — Key Encapsulation Mechanism over RSA. RFC 5990 specifies a deterministic alternative to OAEP for symmetric-key wrap; none of the three named consumers (`x509`, `jws`, `webauthn`) need KEM, and the FIPS 186-5 profile gates use to approved schemes (PKCS1v15 / PSS / OAEP). Deferred to a future change once a real consumer (e.g. a future `@suaveplan/cms` package) appears. - **Encrypted PKCS#8 (PBES2 / scrypt-wrapped private keys)** — lives in `@suaveplan/pkcs12` / a future `@suaveplan/pem-encrypted` package alongside other passphrase-protected formats. This package handles unencrypted DER + JWK + PEM forms only. - **TLS-RSA cipher-suite handshake** — application-layer concern; this library is signature- and encryption-primitive only. - **Application-layer "deterministic PSS" mode** — `saltLength: 0` is supported as a primitive (RFC 8017 §9.1.1 step 4 allows zero-length salt) and verifiable-randomness PSS via `{ salt: Uint8Array }` is supported on `rsaSignPss`. No protocol-level deterministic-PSS abstraction (such as RFC 6979's deterministic ECDSA equivalent for PSS) ships here — application-layer wrappers handle the protocol pinning if needed. ## Source mapping This change is greenfield. The audit source is `../suaveplan/enterprise/crypto-rsa.md`; every cryptographic requirement maps to the canonical RFC. | Requirement | Source | Section / lines | Notes | |---|---|---|---| | `parseRsaPublicKey` PKCS#1 path | RFC 8017 | Appendix A.1.1 (`RSAPublicKey ::= SEQUENCE { modulus, publicExponent }`) | Foundation key format | | `parseRsaPublicKey` PKCS#8 SPKI path | RFC 5280 | §4.1 + Appendix A (`SubjectPublicKeyInfo`) + RFC 8017 Appendix A.1.1 | OID `1.2.840.113549.1.1.1` | | `parseRsaPublicKey` JWK path | RFC 7517 | §6.3 (`RSA` `kty`) | base64url `n`, `e` | | `parseRsaPrivateKey` PKCS#1 path | RFC 8017 | Appendix A.1.2 (`RSAPrivateKey`) | `n, e, d, p, q, dP, dQ, qInv` | | `parseRsaPrivateKey` PKCS#8 path | RFC 5208 | §5 + RFC 5958 §2 | Wraps PKCS#1 in `PrivateKeyInfo` | | `parseRsaPrivateKey` JWK path | RFC 7517 | §6.3.2 (private RSA form) + RFC 7518 §6.3 | base64url `d, p, q, dp, dq, qi` | | Serialise — round-trip every parse path | RFC 8017 / 5208 / 7517 | Appendix A + §5 + §6.3 | Round-trip property test | | `enforceMinBitLength` — 2048-bit default floor | NIST SP 800-131A Rev. 2 | §3 (RSA key-pair length transitions) | Genesis policy: 2048+ by default, 1024 opt-in | | `rsaVerifyPkcs1v15` | RFC 8017 | §8.2.2 (RSASSA-PKCS1-v1\_5 verify) | Returns `boolean`; constant-time encoded-message compare | | `rsaVerifyPss` | RFC 8017 | §8.1.2 (RSASSA-PSS verify) + §9.1.2 (EMSA-PSS-VERIFY) | Default `mgfHash = hash` | | `rsaSignPkcs1v15` | RFC 8017 | §8.2.1 (RSASSA-PKCS1-v1\_5 sign) | CRT-aware private-key operation | | `rsaSignPss` | RFC 8017 | §8.1.1 (RSASSA-PSS sign) + §9.1.1 (EMSA-PSS-ENCODE) | Injectable `Rng` from `@suaveplan/types` | | `rsaEncryptPkcs1v15` | RFC 8017 | §7.2.1 | Plaintext length ≤ `k - 11` | | `rsaDecryptPkcs1v15` | RFC 8017 | §7.2.2 | Combined-error class (Bleichenbacher defence) | | `rsaEncryptOaep` | RFC 8017 | §7.1.1 | Default `mgfHash = hash`, empty `label` | | `rsaDecryptOaep` | RFC 8017 | §7.1.2 | Combined-error class | | `mgf1` | RFC 8017 | §B.2.1 | Throws `MGF_FAILURE` if `maskLen > 2^32 * hLen` | | `emsaPkcs1v15Encode` DigestInfo prefixes | RFC 8017 | §9.2 (note 1 — DigestInfo encodings) | SHA-1 / 256 / 384 / 512 prefixes literal-byte verified | | `emsaPssEncode` | RFC 8017 | §9.1.1 | Salt left of `db` per spec | | `emsaPssVerify` | RFC 8017 | §9.1.2 | Constant-time salt + digest compare | | `modPow` via `@noble/curves/abstract/modular.pow` | `@noble/curves` source `abstract/modular.ts` | `export function pow` | Banned: `BigInt ** BigInt %` | | CRT private-key operation | RFC 8017 | §5.1.2 (RSADP second-form) | `m = m2 + h*q`, `h = qInv*(m1-m2) mod p` | | Constant-time signature compare | RFC 8017 | §8.1.2 step 4 ("Compare the encoded message EM' to EM") | Genesis convention via `constantTimeEqual` | | `rsaJwkThumbprint` | RFC 7638 | §3.2 (RSA member ordering: `e`, `kty`, `n`) | Canonical JSON, then SHA-256 | | Six error codes — `MODULUS_TOO_SHORT`, `INVALID_KEY`, `INVALID_SIGNATURE`, `SIGNATURE_INVALID`, `UNSUPPORTED_HASH`, `UNSUPPORTED_FORMAT`, `DECRYPT_FAILED`, `MGF_FAILURE` | `enterprise/crypto-rsa.md` | §3 error registry | Every code thrown by ≥1 production path | | OTel surface | `enterprise/crypto-rsa.md` | §3 cross-cutting | Standard genesis observability | | Zod boundary schemas | `enterprise/crypto-rsa.md` | §3 cross-cutting | Standard genesis Zod-at-boundaries | | RFC 8017 Appendix C vectors replayed | RFC 8017 | Appendix C (test vectors) | Reference correctness | | RFC 7518 algorithm IDs `RS256`/`RS384`/`RS512` map to `rsaVerifyPkcs1v15` + matching hash | RFC 7518 | §3.3 | Consumer contract for `@suaveplan/jws` | | RFC 7518 algorithm IDs `PS256`/`PS384`/`PS512` map to `rsaVerifyPss` + matching hash | RFC 7518 | §3.5 | Consumer contract for `@suaveplan/jws` | | RSA OIDs `1.2.840.113549.1.1.{1,5,10,11,12,13}` recognised in SPKI parse | RFC 5280 | Appendix A + RFC 8017 Appendix A.2.4 | Consumer contract for `@suaveplan/x509` | | `generateRsaKeyPair` probable-prime gen — Miller-Rabin 64 rounds | NIST FIPS 186-5 | Appendix B.3.1 (probable prime generation) | Exceeds spec floor (5 rounds at 3072-bit) | | `generateRsaKeyPair` multi-prime support (`primes ∈ {3,4,5}`) | RFC 8017 | §3 (RSA key pair) + Appendix A.1.2 (multi-prime `otherPrimeInfos`) | Enables 3 / 4 / 5-prime keys | | `generateRsaKeyPair` keypair security baseline | NIST SP 800-56B Rev. 2 | §6.3 (key-pair generation) | Auxiliary-prime + primality-test guidance | | `publicExponent` validation `1 < e < 2^256`, odd, coprime with `λ(n)` | NIST FIPS 186-5 | §A.1.1 (RSA key pair generation parameters) | FIPS-profile gate | | Blinding (`m' = m * r^e mod n`; unblind `s = s' * r^{-1} mod n`) | RFC 8017 | §5.1.2 (RSADP — note on blinding) + Kocher 1996 ("Timing Attacks on Implementations of Diffie-Hellman, RSA, DSS, and Other Systems") | Universal private-op default | | Constant-time PKCS1v15 verify (entire encoding block `0x00 0x01 PS 0x00 T`) | RFC 8017 | §8.2.2 step 3 (decoded compare) + Bleichenbacher 2006 (signature forgery) | Defends against the variant signature-forgery class | | Bleichenbacher-resistant decrypt (combined error + uniform timing) | RFC 8017 | §7.2.2 implementation note + Bleichenbacher 1998 (CRYPTO '98 "Chosen Ciphertext Attacks Against Protocols Based on the RSA Encryption Standard PKCS #1") | Industry convention (BoringSSL, OpenSSL, WebCrypto) | | Manger-resistant OAEP decrypt | RFC 8017 | §7.1.2 implementation note + Manger 2001 (CRYPTO 2001 "A Chosen Ciphertext Attack on RSA Optimal Asymmetric Encryption Padding") | Same uniform-control-flow approach as Bleichenbacher | | Modulus-size DoS guard — reject `modulusBits > 16384` | `enterprise/crypto-rsa.md` | §3 side-channel + DoS hardening | `n^3` modpow exhaustion defence | | PEM framing (4 armor labels, 64-char-line base64) | RFC 7468 | §3 (textual encoding generalities), §5 (`PUBLIC KEY`), §10 (`RSA PRIVATE KEY`), §11 (`PRIVATE KEY`) | RFC-compliant PEM I/O | | `RsaCryptoEngine` interface + subtle / noble adapters | Web Cryptography API (W3C) | RSA-OAEP, RSASSA-PKCS1-v1_5, RSA-PSS algorithm normative refs | Cross-runtime engine selection | | `RsaKeyHandle` + `RsaKeyHandleProvider` registry | PKCS#11 v3.0 (OASIS) | §6 (object model) + §10.2 (asymmetric mechanisms) | Bridge surface for HSM integrations | | FIPS 186-5 profile — `modulusBits ∈ {2048, 3072, 4096}` | NIST FIPS 186-5 | §5.1 (RSA key pair length) | FIPS approved sizes | | FIPS 186-5 profile — `publicExponent ∈ [2^16+1, 2^256-1]` odd | NIST FIPS 186-5 | §A.1.1 (key pair generation parameters) | FIPS bound | | FIPS 186-5 profile — PSS `saltLength == hLen` default | NIST FIPS 186-5 | §5.4 (RSA digital signature algorithms) | FIPS recommendation | | FIPS 186-5 profile — SHA-1 ban for sign / verify | NIST FIPS 186-5 | §5 (approved hash functions for digital signatures) | SHA-1 deprecated for new signatures | | Constant-time PKCS1v15 verify replayed against Wycheproof | Google Wycheproof | `rsa_signature_*.json` corpus | Variant signature-forgery test cases | | OAEP decrypt replayed against Wycheproof | Google Wycheproof | `rsa_oaep_*.json` corpus | Manger-resistant decrypt cases | | PSS verify replayed against Wycheproof | Google Wycheproof | `rsa_signature_pss_*.json` corpus | Salt-length + MGF-hash combinations | | Sign / verify / encrypt vectors replayed against NIST CAVP | NIST CAVP | RSA Validation System (RSAVS) test vectors | Reference correctness alternate source | | JWK `key_ops` enforcement | RFC 7517 | §4.3 (`key_ops` member) | `KEY_OP_NOT_PERMITTED` on cross-op use | | JWK `use` enforcement | RFC 7517 | §4.2 (`use` member) | `KEY_OP_NOT_PERMITTED` on cross-use | | JWK `alg` enforcement | RFC 7517 | §4.4 (`alg` member) | `KEY_OP_NOT_PERMITTED` on cross-algorithm | | `computeSpkiFingerprint` SHA-256 over SPKI DER | `enterprise/crypto-rsa.md` | §3 key fingerprinting | Mirrors `ssh-keygen -lf` convention | | `crypto.subtle` engine selection | W3C Web Cryptography API | `SubtleCrypto` interface | Hardware acceleration on Apple Silicon / AES-NI | ### Requirements with no source line | Requirement | Justification | |---|---| | Subpath exports per feature module (`./keys`, `./sign`, `./verify`, `./encrypt`, `./decrypt`, `./encoding`, `./jwk`, `./policy`, `./errors`) | Genesis convention (`openspec/AGENTS.md` §6) — every feature module is tree-shakeable. | | Combined-error class for both `rsaDecryptPkcs1v15` and `rsaDecryptOaep` | Bleichenbacher 1998 / Manger 2001 oracle-attack defence — distinguishing padding error from value error inside RSA decrypt enables chosen-ciphertext attacks. Industry convention (BoringSSL, OpenSSL, WebCrypto). | | Default 2048-bit modulus floor with `allowLegacy: true` 1024-bit opt-in | NIST SP 800-131A Rev. 2 deprecates 1024-bit RSA past 2013; WebAuthn TPM attestations from older devices still ship 1024-bit endorsement keys, so a legacy opt-in is required for the `@suaveplan/webauthn` consumer. | | In-package ASN.1 reader / writer (instead of `@peculiar/asn1-schema`) | Bundle-size + audit-surface — the four DER shapes we need (`RSAPublicKey`, `RSAPrivateKey`, `SubjectPublicKeyInfo`, `PrivateKeyInfo`, plus the `RSASSA-PSS-params` `AlgorithmIdentifier`) total ~200 lines of reader / writer; pulling a generic ASN.1 schema engine costs ~50 KB and adds a transitive maintenance liability. | | `SoftwareRsaKeyHandle` reference implementation alongside the `RsaKeyHandle` interface | Genesis convention — every plugin slot ships with a reference implementation so consumers can integrate the surface uniformly regardless of hardware backing (mirrors `Drive` storage adapter pattern + `i18n` backend pattern). | | `colon-hex` fingerprint format alongside `hex` / `base64` / `base64url` | Operator-readability — matches `ssh-keygen -lf` output and `gpg --fingerprint`; widely expected by ops dashboards. | | 17-code error registry vs the 8-code first-pass | Each new feature surface (keygen, FIPS profile, JWK policy enforcement, engine selection, PEM, fingerprint, modulus-max DoS guard, AbortSignal) needs its own discriminating code so callers can branch precisely. Combining classes (e.g. "any keygen failure → KEYGEN_FAILED") would re-introduce the Bleichenbacher / Manger-style mistake of erasing the failure class on success-path errors. | | Subtle engine default-selection (rather than always-noble) | Performance — 5–50× faster on 2048-bit operations and hardware-accelerated on Apple Silicon / AES-NI. Caller override is always available. | ## Verification approach Each requirement carries a `**Verified by:**` line in the spec delta. Test files (all under `packages/crypto/crypto-rsa/src/`): - `keys/parse-pkcs1-public.test.ts` — RFC 8017 Appendix A.1.1 shapes round-trip; malformed input throws `INVALID_KEY`. - `keys/parse-pkcs1-private.test.ts` — Appendix A.1.2 shapes round-trip; CRT factors present. - `keys/parse-spki.test.ts` — `SubjectPublicKeyInfo` with `rsaEncryption` OID parses; wrong OID rejects. - `keys/parse-pkcs8.test.ts` — RFC 5208 `PrivateKeyInfo` wrapping accepts; RFC 5958 `OneAsymmetricKey` shape accepts. - `keys/parse-jwk.test.ts` — `kty: "RSA"` accepts; `kty: "EC"` rejects with `UNSUPPORTED_FORMAT`. - `keys/serialize-*.test.ts` — every parse path's inverse, byte-exact under canonical input. - `keys/roundtrip.spec-conformance.test.ts` — fast-check property: random valid key → serialise → parse → equal. - `policy/modulus.test.ts` — 2048-bit accepts under defaults; 1024-bit rejects unless `allowLegacy: true`; 512-bit rejects always. - `math/modpow.test.ts` — matches naive `BigInt` reference on small inputs; banned `BigInt **` pattern absent from source. - `math/crt.test.ts` — CRT path produces identical output to naive `c^d mod n` on RFC Appendix C vectors. - `encoding/mgf1.test.ts` — RFC 8017 Appendix C `MGF1` outputs match; `maskLen > 2^32 * hLen` throws `MGF_FAILURE`. - `encoding/emsa-pkcs1-v1_5.test.ts` — SHA-1 / 256 / 384 / 512 DigestInfo prefixes match RFC 8017 §9.2 note 1 byte-exactly. - `encoding/emsa-pss-encode.test.ts` + `encoding/emsa-pss-verify.test.ts` — Appendix C round-trip; constant-time compare exercised. - `verify/pkcs1v15.test.ts` — RFC 8017 Appendix C signature accept; bit-flipped signature rejects. - `verify/pss.test.ts` — Appendix C accept; custom salt length + custom MGF hash accept; bit-flipped rejects. - `sign/pkcs1v15.test.ts` — RFC 8017 Appendix C signature matches reference (CRT path). - `sign/pss.test.ts` — round-trip sign-then-verify across SHA-256 / 384 / 512; deterministic with fixed RNG. - `encrypt/pkcs1v15.test.ts` + `decrypt/pkcs1v15.test.ts` — Appendix C round-trip; bad-padding → `DECRYPT_FAILED` indistinguishable from value-error. - `encrypt/oaep.test.ts` + `decrypt/oaep.test.ts` — Appendix C round-trip; non-empty `label` round-trip. - `jwk/thumbprint.test.ts` — RFC 7638 §3.1 worked example matches. - `fingerprint/spki.test.ts` + `fingerprint/format.test.ts` — SHA-256 over RFC 8017 Appendix C SPKI DER; four output formats produce expected strings. - `pem/parse-public.test.ts` + `pem/parse-private.test.ts` — every armor label accepted; mismatched BEGIN / END rejects with `PEM_MALFORMED`. - `pem/serialize-public.test.ts` + `pem/serialize-private.test.ts` — 64-char-line base64 body, correct armor label, byte-for-byte round-trip. - `keygen/miller-rabin.test.ts` — known primes accepted, composites rejected; `rounds: 64` parameter honoured. - `keygen/generate.test.ts` — `generateRsaKeyPair({ modulusBits: 2048, rng: fixedRng })` produces sign-then-verify round-trip; deterministic mode produces byte-identical keys for identical seed; AbortSignal interrupts within 100 ms and throws `ABORTED`. - `keygen/multi-prime.test.ts` — `primes: 3` produces a valid 3-prime key with non-empty `otherPrimeInfos`; sign-then-verify round-trip succeeds. - `keygen/exponent.test.ts` — even `e` rejects; `e === 2n` rejects; `e === 65537n` accepts. - `blinding/blinding.test.ts` — `blindMessage` → private-op → `unblindResult` equals naive private-op on Appendix C ciphertexts; opt-out emits `crypto_rsa.blinding_disabled_total` increment. - `engine/select.test.ts` — when `globalThis.crypto.subtle` is present, `defaultEngine()` returns subtle; when absent, returns noble; explicit `{ engine: "subtle" }` against unavailable subtle throws `ENGINE_UNAVAILABLE`. - `engine/subtle.test.ts` — subtle engine sign / verify round-trip against the noble engine reference output (byte-identical for PKCS1v15 deterministic paths; verify-equivalent for PSS). - `engine/noble.test.ts` — noble engine sign / verify round-trip against RFC 8017 Appendix C reference. - `handle/registry.test.ts` — `registerKeyHandleProvider` plus `resolveKeyHandle` round-trip. - `handle/software.test.ts` — `SoftwareRsaKeyHandle` wraps an in-memory `RsaPrivateKey` and exposes `sign` / `decrypt` matching the direct call. - `profile/profile.test.ts` — `fips-186-5` rejects `modulusBits: 1024`, `publicExponent: 3n`, `hash: "SHA-1"` in sign, and PSS `saltLength: 0`; `permissive` accepts all of them. - `keys/jwk-policy.test.ts` — `key_ops: ["sign"]` key rejects `verify` call with `KEY_OP_NOT_PERMITTED`; `use: "sig"` key rejects `encrypt` call; `alg: "RS256"` key rejects `rsaVerifyPss` call. - `policy/modulus-max.test.ts` — 16384-bit accepts under defaults; 32768-bit rejects with `MODULUS_TOO_LARGE`; 32768-bit accepts under `allowOversize: true`. - `wycheproof/replay.test.ts` — every `rsa_signature_*.json`, `rsa_signature_pss_*.json`, `rsa_oaep_*.json` test case replayed; every `valid` accepts, every `invalid` rejects, every `acceptable` documented. - `cavp/replay.test.ts` — every NIST CAVP RSA vector replayed end-to-end. - `errors/errors.test.ts` — every code (17 of them) thrown by ≥1 production path (audit table). - `observability/metrics.test.ts` — every histogram + counter emitted with expected attributes during a full keygen+sign+verify+encrypt+decrypt round-trip across both engines. - `schemas/schemas.test.ts` — Zod boundary rejects malformed inputs with the right error code. - `crypto-rsa.spec-conformance.test.ts` — RFC 8017 Appendix C end-to-end; property tests (sign / verify round-trip, encrypt / decrypt round-trip, parse / serialise round-trip across every format); `BigInt **` grep gate passes; verifiable-randomness determinism property; every "MUST reject" path throws the right code. - `index.test.ts` — every documented subpath export resolves. ## Migration impact None — additive only. No existing genesis package imports `@suaveplan/crypto-rsa` (the package is new). The three consumers (`@suaveplan/x509`, `@suaveplan/jws`, `@suaveplan/webauthn`) are themselves in flight on genesis and will adopt this package as part of their own change folders; this proposal does NOT modify them. ## Impact ### Affected dependents `bunx turbo ls --filter=...@suaveplan/crypto-rsa` returns nothing — this is a new package with zero existing consumers in genesis. The three eventual consumers (`@suaveplan/x509`, `@suaveplan/jws`, `@suaveplan/webauthn`) are all in flight and will adopt `@suaveplan/crypto-rsa` in their own change folders. - `@suaveplan/x509` — [ ] tests run / [ ] docs reviewed / [x] no action needed (reason: package not yet on genesis; adoption lands in its own change) - `@suaveplan/jws` — [ ] tests run / [ ] docs reviewed / [x] no action needed (reason: package not yet on genesis; adoption lands in its own change) - `@suaveplan/webauthn` — [ ] tests run / [ ] docs reviewed / [x] no action needed (reason: package not yet on genesis; adoption lands in its own change) No dependents on genesis today (verified by `grep -r "@suaveplan/crypto-rsa" packages/` returning zero results). ### Deferred The following spec items are deferred to a follow-up change. They do NOT block the package from shipping today — every required cryptographic primitive plus the `RsaCryptoEngine` adapter (noble + subtle + auto-selector), HSM bridge, FIPS profile, JWK policy enforcement, blinding-by-default wiring, JWK thumbprint + thumbprint URI, SPKI fingerprint, and the full 17-code error registry are all production-ready at 100/100/100/100 coverage. - **§15.1 / §15.5 — `scripts/generate-rsa-vectors.ts` deterministic vector generator.** The package's existing 438-test suite covers every RFC 8017 §7-§9 path against in-process keypairs and known-answer constants. Folding the third-party Wycheproof / NIST CAVP corpora is committed as `enterprise-crypto-rsa-vectors` follow-up. - **§15.2 — RFC 8017 Appendix C fixtures committed under `src/__fixtures__/rfc8017-appendix-c/`.** Deferred with the vector replay tests below; the canonical Appendix C key + ciphertext bytes will land alongside the replay harness. - **§15.3 — NIST CAVP RSA vectors under `src/__fixtures__/cavp/`.** Deferred to the vector-replay follow-up (corpus too large to commit without a vector-selection harness; the follow-up will add both the harness and a reduced representative sample). - **§15.4 — Google Wycheproof RSA vectors under `src/__fixtures__/wycheproof/`.** Same rationale; the follow-up bundles a Wycheproof-replay harness plus a curated subset. - **§16.34 / §16.35 — `wycheproof/replay.test.ts` + `cavp/replay.test.ts`.** Land with the fixtures in §15. - **§18.1–§18.4 — Full mitata benchmark suite + baseline numbers in README.** A single `blinded_vs_unblinded_2048` mitata file ships in `src/__benchmarks__/` to validate the < 5% blinding-overhead claim; the full per-modulus / per-engine benchmark matrix is deferred to `enterprise-crypto-rsa-bench` follow-up. - **§21.2 — `MISSING.yml` regen.** Blocked on `scripts/generate-missing.ts` requiring a `FOLDERS.yml` not present in the current workspace; tracked separately. - **§21.5 / §21.6 — `markdownlint-cli2 --fix` + verification pass.** Deferred to the orchestrator pass that runs across multiple in-flight crypto-family migrations. All deferred items have a clearly-named follow-up bucket and an architectural rationale per AGENTS.md §2c. None affect the cryptographic correctness or production-readiness of the shipped surface.