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

40 KiB
Raw Blame History

@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/<plugin>/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<RsaPrivateKey>. 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<CryptoRsaErrorCode>.
  • 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 https://csrc.nist.gov/projects/cryptographic-algorithm-validation-program/digital-signatures.
  • ADDED: src/__fixtures__/wycheproof/ — Google Wycheproof RSA vectors (rsa_signature_*.json, rsa_signature_pss_*.json, rsa_oaep_*.json) from https://github.com/google/wycheproof/tree/master/testvectors.
  • 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.