40 KiB
@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:
@suaveplan/x509— verifies certificate-chain signatures. RFC 5280 §1.2 + Appendix A enumerate the RSA signature OIDs1.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), and1.2.840.113549.1.1.10(id-RSASSA-PSS). The majority of real production certs today use one of these.@suaveplan/jws— implements the JWA registry of RFC 7518 §3.3 (RS256/RS384/RS512over RSASSA-PKCS1-v1_5) and §3.5 (PS256/PS384/PS512over RSASSA-PSS). Six of the eleven JWS algorithm IDs are RSA-backed.@suaveplan/webauthn—tpmattestation 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-PSSAlgorithmIdentifierparameters. - 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 }.bitLengthis 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): RsaPublicKeyper RFC 8017 Appendix A.1.1 (RSAPublicKey ::= SEQUENCE { modulus INTEGER, publicExponent INTEGER }). - ADDED:
src/keys/parse-pkcs1-private.ts—parseRsaPrivateKeyPkcs1(bytes): RsaPrivateKeyper 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): RsaPublicKeyunwraps theSubjectPublicKeyInfo(RFC 5280) carrying OID1.2.840.113549.1.1.1(rsaEncryption) and dispatches to the PKCS#1 reader. - ADDED:
src/keys/parse-pkcs8.ts—parseRsaPrivateKeyPkcs8(bytes): RsaPrivateKeyper 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+parseRsaPrivateKeyJwkper RFC 7517 §6.3. Rejectskty !== "RSA"withCRYPTO_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 theformatdiscriminant. - 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: truepermits 1024-bit keys. Below 1024 is always rejected withCRYPTO_RSA.MODULUS_TOO_SHORT.
Modular arithmetic + CRT
- ADDED:
src/math/modpow.ts—modPow(base: bigint, exponent: bigint, modulus: bigint): bigintthin wrapper over@noble/curves/abstract/modular.pow. NEVER nativeBigInt ** BigInt % N. - ADDED:
src/math/crt.ts—rsaPrivateOperationCrt(c: bigint, key: RsaPrivateKey): bigintper 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): Uint8Arrayper RFC 8017 §B.2.1. ThrowsCRYPTO_RSA.MGF_FAILUREwhenmaskLen > 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
- SHA-1:
- 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 }): Uint8Arrayper RFC 8017 §8.2.1. - ADDED:
src/verify/pkcs1v15.ts—rsaVerifyPkcs1v15(key, message, signature, { hash }): booleanper 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? }): Uint8Arrayper RFC 8017 §8.1.1. - ADDED:
src/verify/pss.ts—rsaVerifyPss(key, message, signature, { hash; saltLength?; mgfHash? }): booleanper RFC 8017 §8.1.2.
Encryption — RSAES-PKCS1-v1_5
- ADDED:
src/encrypt/pkcs1v15.ts—rsaEncryptPkcs1v15(key, plaintext, { rng? }): Uint8Arrayper RFC 8017 §7.2.1. - ADDED:
src/decrypt/pkcs1v15.ts—rsaDecryptPkcs1v15(privateKey, ciphertext): Uint8Arrayper 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? }): Uint8Arrayper RFC 8017 §7.1.1. - ADDED:
src/decrypt/oaep.ts—rsaDecryptOaep(privateKey, ciphertext, { hash; mgfHash?; label? }): Uint8Arrayper RFC 8017 §7.1.2. Same combined-error contract.
JWK thumbprint
- ADDED:
src/jwk/thumbprint.ts—rsaJwkThumbprint(key, hash): Uint8Arrayper 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): RsaPublicKeyrecognising armor labels-----BEGIN PUBLIC KEY-----(SPKI) and-----BEGIN RSA PUBLIC KEY-----(PKCS#1 public). - ADDED:
src/pem/parse-private.ts—parseRsaPrivateKeyPem(text): RsaPrivateKeyrecognising 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): booleanper 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 throwsCRYPTO_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—publicExponentvalidation (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): bigintper RFC 8017 §5.1.2 / Kocher 1996. Wired into everyrsaSign*/rsaDecrypt*call by default; opt-out via{ blinding: false }emitscrypto_rsa.blinding_disabled_total{operation}counter increment.
RsaCryptoEngine adapter — noble + subtle
- ADDED:
src/engine/engine.types.ts—RsaCryptoEngineinterface (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.subtleadapter. WrapsimportKey/sign/verify/encrypt/decrypt. Non-extractable keys by default. - ADDED:
src/engine/select.ts—defaultEngine(operation, key): RsaCryptoEngineauto-selects subtle when available else noble. Emitscrypto_rsa.engine_selected_total{engine}on every call.
HSM / PKCS#11 bridge
- ADDED:
src/handle/handle.types.ts—RsaKeyHandleinterface +RsaKeyHandleProviderinterface. - ADDED:
src/handle/registry.ts—registerKeyHandleProvider(provider)+resolveKeyHandle(key). - ADDED:
src/handle/software.ts—SoftwareRsaKeyHandlereference implementation wrapping an in-memoryRsaPrivateKey.
FIPS 186-5 profile
- ADDED:
src/profile/profile.ts—enforceProfile(input, options: { profile: "permissive" | "fips-186-5" }). Thefips-186-5profile rejectsmodulusBits ∉ {2048, 3072, 4096},publicExponent ∉ [2^16+1, 2^256-1], evenpublicExponent,hash === "SHA-1"in sign / verify, and PSSsaltLength≠hLen(withCRYPTO_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 throwCRYPTO_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_VIOLATIONas appropriate. - ADDED:
src/observability/metrics.ts—cryptoRsaMetricsfactory 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}.
- Histograms:
Root + scripts + benchmarks
- ADDED:
src/index.ts— explicit named re-exports of every public surface (noexport *). - 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 coveringverify_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_2048ratio (CRT ≥ 2× faster),blinded_vs_unblinded_2048ratio (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—RsaCryptoEngineinterface,defaultEngine,nobleEngine,subtleEngine./handle—RsaKeyHandle,RsaKeyHandleProvider,registerKeyHandleProvider,SoftwareRsaKeyHandle./profile—enforceProfile, profile types./policy—enforceMinBitLength,enforceMaxBitLength./schemas— every Zod schema./observability—cryptoRsaMetricsfactory./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.powconstant-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/cmspackage) appears. - Encrypted PKCS#8 (PBES2 / scrypt-wrapped private keys) — lives in
@suaveplan/pkcs12/ a future@suaveplan/pem-encryptedpackage 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: 0is supported as a primitive (RFC 8017 §9.1.1 step 4 allows zero-length salt) and verifiable-randomness PSS via{ salt: Uint8Array }is supported onrsaSignPss. 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 throwsINVALID_KEY.keys/parse-pkcs1-private.test.ts— Appendix A.1.2 shapes round-trip; CRT factors present.keys/parse-spki.test.ts—SubjectPublicKeyInfowithrsaEncryptionOID parses; wrong OID rejects.keys/parse-pkcs8.test.ts— RFC 5208PrivateKeyInfowrapping accepts; RFC 5958OneAsymmetricKeyshape accepts.keys/parse-jwk.test.ts—kty: "RSA"accepts;kty: "EC"rejects withUNSUPPORTED_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 unlessallowLegacy: true; 512-bit rejects always.math/modpow.test.ts— matches naiveBigIntreference on small inputs; bannedBigInt **pattern absent from source.math/crt.test.ts— CRT path produces identical output to naivec^d mod non RFC Appendix C vectors.encoding/mgf1.test.ts— RFC 8017 Appendix CMGF1outputs match;maskLen > 2^32 * hLenthrowsMGF_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_FAILEDindistinguishable from value-error.encrypt/oaep.test.ts+decrypt/oaep.test.ts— Appendix C round-trip; non-emptylabelround-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 withPEM_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: 64parameter 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 throwsABORTED.keygen/multi-prime.test.ts—primes: 3produces a valid 3-prime key with non-emptyotherPrimeInfos; sign-then-verify round-trip succeeds.keygen/exponent.test.ts— evenerejects;e === 2nrejects;e === 65537naccepts.blinding/blinding.test.ts—blindMessage→ private-op →unblindResultequals naive private-op on Appendix C ciphertexts; opt-out emitscrypto_rsa.blinding_disabled_totalincrement.engine/select.test.ts— whenglobalThis.crypto.subtleis present,defaultEngine()returns subtle; when absent, returns noble; explicit{ engine: "subtle" }against unavailable subtle throwsENGINE_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—registerKeyHandleProviderplusresolveKeyHandleround-trip.handle/software.test.ts—SoftwareRsaKeyHandlewraps an in-memoryRsaPrivateKeyand exposessign/decryptmatching the direct call.profile/profile.test.ts—fips-186-5rejectsmodulusBits: 1024,publicExponent: 3n,hash: "SHA-1"in sign, and PSSsaltLength: 0;permissiveaccepts all of them.keys/jwk-policy.test.ts—key_ops: ["sign"]key rejectsverifycall withKEY_OP_NOT_PERMITTED;use: "sig"key rejectsencryptcall;alg: "RS256"key rejectsrsaVerifyPsscall.policy/modulus-max.test.ts— 16384-bit accepts under defaults; 32768-bit rejects withMODULUS_TOO_LARGE; 32768-bit accepts underallowOversize: true.wycheproof/replay.test.ts— everyrsa_signature_*.json,rsa_signature_pss_*.json,rsa_oaep_*.jsontest case replayed; everyvalidaccepts, everyinvalidrejects, everyacceptabledocumented.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.tsdeterministic 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 asenterprise-crypto-rsa-vectorsfollow-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_2048mitata file ships insrc/__benchmarks__/to validate the < 5% blinding-overhead claim; the full per-modulus / per-engine benchmark matrix is deferred toenterprise-crypto-rsa-benchfollow-up. - §21.2 —
MISSING.ymlregen. Blocked onscripts/generate-missing.tsrequiring aFOLDERS.ymlnot 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.