Skip to content

rsa: RSASSA-PKCS1-v1_5, RSASSA-PSS and PSS-SHAKE across RSA-1024 through 8192 - #146

Open
dghgit wants to merge 45 commits into
release/0.1.4alphafrom
feature/rsa-sig
Open

dghgit wants to merge 45 commits into
release/0.1.4alphafrom
feature/rsa-sig

Conversation

@dghgit

@dghgit dghgit commented Sep 20, 2026 •

Copy link
Copy Markdown
Contributor

Summary

RSA digital signatures, plus the RSASVE KEM, across a limited range of modulus sizes up to the maximum originally requested, 8192 bits: RSASSA-PKCS1-v1_5 and RSASSA-PSS (RFC 8017 §8), the latter with its parameters as a type, the conventional MGF1 pairing, PSS-SHAKE128/256 (RFC 8702 §3.2.1), another MGF1 hash or salt length (§8.1, Appendix A.2.3) and the zero salt (§9.1 note 4, deterministic), over RSA-1024/1536 (verify-only) through RSA-2048/3072/4096/8192 (sign+verify), on bouncycastle_core's key roles. The size is a marker type (RSA2048), a scheme is one generic marker over the size, the hash and its parameters (RSASSA_PSS<RSA2048, SHA256>, RSASSA_PSS<RSA2048, SHAKE128>, RSASSA_PSS<RSA2048, SHA512, Mgf1<SHA256, 32>>), and a key is a key for one of them: RsaPrivateKey<A> implements SigningKey or DeterministicSigningKey by what the scheme draws and RsaPublicKey<A> VerifyingKey, handing out SignatureSigner / SignatureVerifier workers, with the material behind them as RsaPrivateComponents<K> / RsaPublicComponents<K>. RSASVE (SP 800-56B Rev. 2 §7.2.1) is the KEM roles EncapsulatingKey / DecapsulatingKey over () on KEM-only key types at 2048/3072/4096/8192, and RSASVE_HKDF<K, H> the same roles over a KdfParams<HkdfDescription> for a derived secret. Key pair generation is FIPS 186-5 Appendix A.1.3, from the size marker for one scheme (RSA2048::generate_pss_signature_key_pair::<SHA256>(&mut rng)). A new crate, bouncycastle-rsa, with its CLI subcommands, criterion benches and stack-usage harness.

Description

  • Reuses bouncycastle_ec's constant-time fixed-width limb arithmetic (bouncycastle_ec::nat, bouncycastle_ec::montgomery) rather than a dedicated BigInteger type, for the same constant-time-on-secret-exponent reasoning that motivates that crate's own custom-curve arithmetic: a modular exponentiation over a secret exponent has to run in the same time regardless of the exponent's value, which needs fixed-width operands. modexp is Montgomery exponentiation over a runtime modulus (f7f2f12); keys holds the CRT key types and rsa_core the CRT-based RSASP1/RSAVP1 (182eb3c).
  • 2048-8192 bit moduli support both signing and verification; 1024- and 1536-bit moduli are verification-only, enforced at compile time: the size markers implement size::RsaSize, and only those from 2048 up size::RsaSigningSize, on which the private key's constructors and encoder, the signing roles, RSASP1, key generation and the KEM private key are all written, so no private key value and no private-key arithmetic exists at the two legacy sizes (a compile_fail doctest in rsa_1024.rs pins it).
  • API shape: the EMSA/RSASSA layers take the message hash rather than the message (2c0166e), so the streaming workers share one encoding path; the crate is written once per scheme over the size marker (eb4aefd), the key types carry their algorithm (010cf1a) and RSASSA-PSS its parameters (5c31875). RSASSA_PKCS1_v1_5<K, H> and RSASSA_PSS<K, H, P = Conventional> (spec spelling per QUALITY_AND_STYLE) are the markers; P is rsassa_pss::PssParams<H>, the mask and the salt length: Conventional reads the hash's conventional pairing off PssHash (MGF1 over the hash with a salt one output long for SHA-2 and SHA-3, the XOF itself as the mask for SHAKE), Mgf1<M, SALT_LEN> spells another, NoSalt<M> is the zero salt and puts the key on the deterministic role. A key is RsaPrivateKey<A> / RsaPublicKey<A> over the components, so sk.sign(..) and pk.verify(..) resolve from the key, a PSS key cannot sign PKCS#1 v1.5 (SP 800-57 Part 1 Rev. 5's one scheme per key pair), and the components become a key for a scheme through key::<A>(); the roles are implemented once in rsassa over three algorithm traits, with three workers (RsaSigner with set_salt, RsaDeterministicSigner, RsaVerifier) and the *_from_hash entry points. Per-size alias modules (rsa_2048::RSASSA_PSS_SHA256, SIG_LEN) serve code at one size. The schemes were first wired as Signer / SignatureVerifier types (a996164), the per-size free functions dropped once the traits covered them (c84e7a6), and the whole ported onto the key role traits when those landed on the release (c6ea90d).
  • Key pair generation (db9c026): FIPS 186-5 Appendix A.1.3 (probable primes: trial division against the primes below 2^10, then Appendix B.3.1 Miller-Rabin with Table B.1's round counts; e = 65537; |p - q| and d > 2^(nlen/2) criteria from A.1.1), structured after BC Java's RSAKeyPairGenerator, on the crate's own limb arithmetic, no bignum dependency. Exposed on the signing sizes as size::SignatureKeyPair (RSA2048::generate_pkcs1_v1_5_signature_key_pair::<SHA256>(&mut rng), generate_pss_signature_key_pair::<H>, generate_pss_signature_key_pair_with::<H, P>, a pair for one algorithm, the hash spelled and never defaulted) and size::KemKeyPair, with RsaSigningSize::keygen giving the components; the RNG must be at least the modulus's strength. The private-key paths scrub what they derive from the primes (57c5c09).
  • RSASVE (b539612, e075ea6, c3129a3): the scheme BC FIPS Java 2.0 exposes as FipsRSA.KTS_SVE (JCA "RSA-KAS-KEM") and OpenSSL 3 as its "RSASVE" KEM operation. b539612 is a behaviour-preserving refactor that splits the exponentiation out of rsavp1/rsasp1 into crate-private public_exp/crt_exp, so RSASVE can reuse it under SP 800-56B's 1 < x < n - 1 range checks instead of RFC 8017's. The KEM-only key types RsaKEMPublicKey / RsaKEMPrivateKey keep key establishment and signing apart (§6.1 item 5) and add the §6.2.1 checks (e >= 65537, n of exactly nlen bits). After the squash of PR KEM encapsulation and decapsulation move onto the key objects (EncapsulatingKey / DecapsulatingKey, ML-KEM ported, KeyType::SharedSecret) #192 they implement the KEM roles over (), naming RSASVE as the algorithm, with RsasveEncapsulator::encaps_out as RSASVE.GENERATE and RsasveDecapsulator::decaps_out as RSASVE.RECOVER into the caller's buffers; the ciphertext is a [u8; CT_LEN], so a wrong length is a type error. The shared secret is the raw Z at the modulus's full length, typed CryptographicRandom at the modulus's strength, for the caller's KDF; RSASVE_HKDF<K, H> (rsasve_kdf, in eb4aefd) gives the same key types the KEM roles over KdfParams<HkdfDescription<H>>, SP 800-56B Rev. 2 Section 5.5's derivation of the secret from Z, the parameters being the library's HSM-readable description.
  • CLI subcommands rsa-<size> and rsasve-<size>, which stream stdin through the workers in this CLI's usual 1 KB chunks; criterion benches (including keygen at 2048/3072); a stack-usage harness.

Scope and Risk

New crate only (bouncycastle-rsa) plus its CLI wiring and mem_usage_benches harness, based on release/0.1.4alpha. Two changes outside those: utils::const_str::ConstStr::decimal, the digits of a number for an algorithm name (-MGF1-SHA256-32), with its tests; and core-test-framework's KEM suite (dce5490): its fixed seed was a 256-byte counting pattern, so two consecutive draws of 256, 512 or 1024 bytes, RSASVE's secret at 2048, 4096 and 8192 bits, were identical and the "two encapsulations differ" check failed; it is a 4096-byte hashed pattern now. The branch has taken each release tip by merge, most recently f213c9e (the ECDSA-over-Curve and KDF-description squashes).

Validation

  • cargo test --workspace passes at the head, all targets build, rustfmt is clean, and the rsa-* / rsasve-* CLI tests pass. For the crate: Wycheproof PKCS1-v1.5/PSS/PSS-SHAKE vectors per size and hash, its rsa_pss_2048_sha512_mgf1sha256_32_params and rsa_pss_2048_sha256_mgf1_0 files for the spelled parameters (178 and 103 vectors), BC Java cross-check vectors at 8192, core-test-framework's signature conformance suite for every pairing at every size, and its KEM conformance and key suites for RSASVE at every size.
  • RSASVE (tests/rsasve_tests.rs, vectors in bc-test-data/crypto/rsa): NIST ACVP-Server KAS-IFC-SSC-Sp800-56Br2, the KAS1 group (2048, CRT) and the KAS2 group (3072, CRT, both directions), decaps recovering Z from C and an encapsulation fed the vector's Z reproducing its C, with the failChangedZ cases not matching; ACVP-Server RSA-DecryptionPrimitive-Sp800-56Br2, CRT groups at 2048/3072/4096 including the out-of-range ciphertexts; OpenSSL 3.0's "RSASVE" KEM operation at every size, 8192 included.
  • ./dev_scripts/quality_stats.sh ./crypto/rsa at the head: 4383 core lines, 1290 docstring lines, test ratio 0.95, 1 unwrap and 36 Err() sites in core code.
  • cargo mutants, as reported in the comments below (the hashes there belong to the pre-rebase history): the full-crate run of the pre-trait-wiring state, 646 mutants, 446 caught, 6 missed (documented OR/XOR and boundary equivalents), 3 timeouts, 191 unviable; the scoped run over the 14 trait-wiring and keygen files, 751 mutants, 476 caught, 0 missed, 180 unviable, 95 timeouts, of which the --iterate --timeout 7200 re-run left 14 genuine infinite loops, all itemised; RSASVE's rsasve.rs and the KEM code of the four size modules, 168 mutants, 72 caught, 0 missed, 95 unviable, one real hang (attempts += 1 to *= 1 removes the draw cap). Since then, each squash carries its own scoped run in its message: the size markers and RSASVE-HKDF (eb4aefd), 1168 mutants, 639 caught, 362 unviable, 25 timeouts, 142 missed and itemised, keygen.rs's slow nondeterministic ones among them; the scheme keys (010cf1a), 505 mutants, 162 caught, 330 unviable, 2 timeouts, 11 missed; the PSS parameters (5c31875), 481 mutants, 187 caught, 264 unviable, no timeouts, 27 missed after the targeted re-runs, plus utils const_str.rs, 39 mutants, 19 caught, 19 unviable, 1 build-time hang. Each squash message itemises its misses.

AI Usage Statement

  • Yes, non-trivial code changes were generated by AI

Assisted-by: Claude:claude-sonnet-5
Assisted-by: Claude:claude-fable-5-1
Assisted-by: Claude:claude-opus-5-5
Assisted-by: Claude:claude-mythos-5-1

🤖 Generated with Claude Code

@dghgit

dghgit commented Sep 21, 2026

Copy link
Copy Markdown
Contributor Author

cargo mutants -- full-crate confirmation run, pre-trait-wiring state (61aa840)

Completed on separate hardware (mirroring PR #143's approach): 646/646 mutants, 446 caught, 6 missed, 3 timeout, 191 unviable.

All 9 non-caught findings are already-documented equivalents/artifacts, consistent with the two-pass investigation described in the PR body:

Missed (6), all documented OR/XOR or boundary equivalents:

  • emsa_pkcs1_v1_5.rs:124:18, :151:28, :192:20 (decode_digest_info/emsa_pkcs1_v1_5_verify) -- boundary-condition and short-circuit equivalents already covered by the crate's own vector-driven tests.
  • modexp.rs:109:54, :185:36, :193:54 (redc_normalize/ct_shift_in_bit_mod) -- disjoint-bit-range OR/XOR equivalences (the two operands never overlap by construction, so | and ^ agree).

Timeout (3), genuine infinite loops, not coverage gaps:

  • emsa_pkcs1_v1_5.rs:182:11 (emsa_pkcs1_v1_5_verify) -- a +=→*= mutation stalls a length-tracking loop forever.
  • mgf1.rs:24:18, :34:16 -- similarly stall MGF1's output-generation loop.

This confirms the pre-trait-wiring baseline (everything through commit 61aa840) is clean: no silent gaps, every non-caught mutant independently verified via code reasoning to be either a strict semantic equivalent or a loud (non-hanging-forever-undetected) infinite loop.

The later trait-wiring and key-generation commits (f1a87e2..1efdd64) have their own scoped mutants run in progress, covering the 14 files those commits touch; that result will follow as a separate comment once complete.

@dghgit

dghgit commented Sep 22, 2026

Copy link
Copy Markdown
Contributor Author

cargo mutants -- scoped run over the trait-wiring + keygen commits (f1a87e2..1efdd64)

Scoped to the 14 files those commits touch (emsa_pkcs1_v1_5.rs, emsa_pss.rs, emsa_pss_shake.rs, keygen.rs, keys.rs, rsa_1024/1536/2048/3072/4096/8192.rs, rsassa_pkcs1_v1_5.rs, rsassa_pss.rs, rsassa_pss_shake.rs), --jobs 3 --timeout 3000, single-threaded tests.

Result: 751/751 mutants -- 476 caught, 0 missed, 180 unviable, 95 timeout. Every timeout was individually investigated (not just left as "presumably fine"); none is a silent gap. They break down as:

  • 2 build timeouts -- cold scratch-copy compile time exceeding the auto-set cap, unrelated to the mutation.
  • ~14 genuine infinite loops -- real hangs, i.e. legitimate kills, just slow ones. All in keygen.rs's retry/gate logic (see below) or has_small_factor's prime-sieve loop.
  • ~79 equivalent-at-cap -- the mutated code runs to completion correctly (confirmed by watching many full test binaries pass in the log) but the single-threaded suite's wall-clock is close enough to the 3000s cap that the run gets cut off deep in the 8192-bit tests before finishing everything. Not equivalents in the strict sense for all of these -- see the structural notes below -- but none change the outcome the tests check.

Why so many timeouts turn out harmless

  • gcd/div/shl/shr/trailing_zeros/mask_to_bits/inverse_of_e_mod in keygen.rs feed only the FIPS 186-5 criterion-3 "d > 2^(nlen/2)" gate check, never the actual signing exponents (dP/dQ/qInv, computed independently via Fermat's theorem). Breaking these helpers can make the gate wrongly retry (a real infinite loop, ~14 of the above) or wrongly skip a redundant margin check (harmless, since the key is still correct) -- but it can never produce a broken key.
  • has_small_factor's prime-batching loop has three distinct ways its index bookkeeping can be broken into a genuine, deterministic infinite loop (confirmed by tracing the fixed batching structure by hand, not just observing the hang).
  • is_probable_prime's parity and small-value fast paths are redundant shortcuts whose fallback (full trial division / Miller-Rabin) independently reaches the same correct answer, so breaking the shortcut is invisible.
  • candidate()'s odd-forcing bit (out[0] |= 1 mutated to &=) degrades the low-order entropy of generated primes without ever producing an invalid one -- rejected-if-even candidates are simply retried. Functionally harmless, but a real (if narrow) reduction in randomness quality that no functional test could ever catch.

Four things worth your attention (none are silent mutants -- all found by manually reasoning through the timeout set, not something cargo-mutants itself flagged as missed)

  1. primes_too_close's rejection mechanism is never tested against a deliberately close p/q pair. keygen_tests.rs independently re-derives |p-q| > 2^(nlen/2-100) on whatever real key came out, rather than checking the rejection fired -- and two random ~1024-bit primes landing within that (astronomically small) distance essentially never happens by chance, so the check's disablement is invisible either way.
  2. keygen_from_rng's RNG-security-strength gate is only ever tested strictly above the required strength, never exactly at it. The suite's two RNG scenarios are None (must fail) and _256bit (must succeed) -- nothing tests an RNG at exactly the strength a given modulus size requires, which is the ordinary real-world case, not an edge case.
  3. Highest-leverage one: core-test-framework's test_boundary_conditions (used by every RSA size's test_keys call) never asserts a real PK::from_bytes(&pk.encode()) == pk round trip -- only that the encoded array has the declared (compile-time-fixed) length and that malformed-length buffers get rejected. Confirmed exhaustively: encode/encode_out are untested for every RSA size and both key-type flavors except RSA-2048 (which alone has an extra, independent round-trip test in keys_tests.rs). A single added assertion in the shared helper would close this for every algorithm using it, not just RSA.
  4. rsassa_pss's and rsassa_pss_shake's public sign_from_hash functions appear to be dead code. The actual Signer::sign_final path calls sign_from_hash_with_salt directly; a crate-wide grep turns up no call site for the plain sign_from_hash anywhere but its own definition and doc comments. (PKCS#1-v1.5's equivalent sign_from_hash is called from its own sign_final -- this is PSS/PSS-SHAKE specific.) Flagging as an open question rather than a bug: could be intentional public API for one-shot, non-streaming callers, or a refactor leftover that should be wired in or removed.

A --iterate --timeout 7200 --build-timeout 900 re-run over just the 95 timeout mutants is in progress to positively confirm the genuine-infinite-loop ones don't terminate even given much more time; a follow-up comment will report that once it completes.

AI Usage Statement

  • Yes, this analysis was performed by AI

Assisted-by: Claude:claude-sonnet-5

🤖 Generated with Claude Code

@dghgit

dghgit commented Sep 23, 2026

Copy link
Copy Markdown
Contributor Author

Follow-up: --iterate --timeout 7200 re-confirmation of the 95 timeout mutants

Re-ran just the 95 timeout mutants from the scoped trait-wiring/keygen run above with a much longer cap (7200s test / 900s build) to positively confirm the genuine-infinite-loop classifications rather than relying on "didn't finish in 3000s."

Final result: 95/95 -- 76 missed, 5 caught, 14 timeout. All 14 remaining timeouts are the genuine infinite loops already itemized above, and every one of them ran the full 7200s (or hit the 900s build-timeout, for the one const-eval compile-time hang) without resolving -- confirming non-termination rather than a slow pass.

One worth flagging precisely because it's the interesting case: keygen.rs:162:5 (lt stubbed to always return true) was originally observed hanging under the 3000s cap and was provisionally grouped with the other infinite loops, but flagged at the time as behaving differently from truly deterministic hangs like is_zero's -- its corruption causes a wrapping-underflow subtraction that's genuinely input-dependent, since gcd() is fed a fresh random p/q on every run. This time it resolved to caught instead: a fast, clean panic (mod_pow needs base < modulus) within a second, on different random primes than the earlier run drew. That's not a contradiction -- it's the input-dependent behavior playing out the other way this time, and either outcome (hang or fast panic) is a legitimate kill, never a silent pass. So the final count is 14 confirmed genuine infinite loops rather than 15, with 162:5 moving to the caught column.

This closes out the mutation-testing investigation for f1a87e2..1efdd64.

@dghgit
dghgit force-pushed the feature/rsa-sig branch 2 times, most recently from 007ec12 to 1d4ea0b Compare September 28, 2026 10:11
@hubot
hubot deleted the branch release/0.1.4alpha September 28, 2026 15:21
@hubot
hubot deleted the feature/rsa-sig branch September 28, 2026 15:21
@hubot
hubot deleted the branch feature/ecdsa November 3, 2026 13:16
@hubot
hubot restored the feature/rsa-sig branch November 3, 2026 14:53
@dghgit

dghgit commented Nov 4, 2026

Copy link
Copy Markdown
Contributor Author

Merged feature/ecdsa at a42ce74 (which carries release/0.1.3alpha 029a17b, the squash of PR #190) into this branch as b5e25d7. The RSA and RSA-SVE CLI commands moved onto the release's input rule from #190: private keys through the zeroized secret readers, public keys, signatures and ciphertexts as raw bytes or hex under the global --hex-input; the RSA CLI tests write raw key files. Workspace suite passes (2408 tests).

@dghgit

dghgit commented Nov 4, 2026

Copy link
Copy Markdown
Contributor Author

Replayed onto feature/ecdsa as rebased onto release/0.1.4alpha (69df164): feature/rsa-sig is now 7bb63e9, 26 commits on that tip, replayed from b5e25d7 with the ecdsa merges dropped and the tree restored to the old tip plus the 0.1.4 line's setup (version bump and notes file). The base stays feature/ecdsa. Workspace suite passes (2408 tests).

@hubot
hubot deleted the branch feature/ecdsa November 4, 2026 22:43
@hubot
hubot deleted the feature/rsa-sig branch November 4, 2026 22:43
@hubot
hubot restored the feature/rsa-sig branch November 5, 2026 00:20
@hubot
hubot force-pushed the feature/rsa-sig branch 2 times, most recently from a582209 to de882ed Compare November 5, 2026 16:32
dghgit and others added 2 commits November 6, 2026 20:51
Empty crate for RSASSA-PKCS1-v1_5 and RSASSA-PSS (RFC 8017), depending on
bouncycastle-ec for its constant-time limb arithmetic (nat/montgomery) rather
than a dedicated bignum type. No algorithms yet -- this is step 1 of the
RSA signature plan on this branch.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds modexp.rs: a Montgomery-based mod_pow generic over limb count L, built
on bouncycastle-ec's nat/montgomery primitives. Unlike every curve in that
crate, RSA's modulus arrives at runtime rather than as a compile-time
constant, so this derives its own Montgomery setup (n' by Newton's method,
R mod n and R^2 mod n by repeated doubling-and-reduction) instead of using
precomputed per-curve constants -- both operate on the modulus alone, which
is public in RSA, so they run in variable time. mod_pow itself is constant
time in the exponent via Joye & Yen's Montgomery powering ladder (CHES
2002): one multiply and one square per modulus bit, moved into place with a
branch-free conditional swap.

Validated against known-answer vectors computed with Python's arbitrary-
precision pow() (not from recall) at L=1, 2, 4, and 32 (2048-bit, the actual
RSA-2048 limb count), plus a genuine 128-bit toy RSA keypair's sign/verify
round trip and the classic textbook n=3233 example.

cargo mutants -p bouncycastle-rsa --jobs 3: 26 mutants, 23 caught, 2
unviable, 1 missed (an OR/XOR equivalence in redc_normalize's subtraction
mask -- documented inline, not reachable given montgomery::redc's own <2n
postcondition).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…nd SignatureVerifier methods

60634ad on feature/ecdsa renamed the streaming halves of the signature
traits to do_sign_init / do_sign_update / do_sign_final /
do_sign_final_out and do_verify_init / do_verify_update /
do_verify_final, and moved do_hazardous_operations from
bouncycastle_core::key_material to bouncycastle_core::hazmat. The
preceding merge brought those changes in, but the RSA crate, its tests
and the rsa CLI command still used the old names, so the workspace did
not build. This is the mechanical rename and import fix, nothing else.

Assisted-by: Claude:claude-mythos-5-1
RSASSA-PKCS1-v1_5, RSASSA-PSS and RSASSA-PSS with SHAKE lose their
`Signer`/`SignatureVerifier` impls and the `sign_randomized`,
`set_signer_salt` and `do_sign_*` surface that came with them; each
(scheme, hash, size) type is now an algorithm marker (`Algorithm` +
`SignatureAlgorithm`, named "RSASSA-PKCS1-v1_5", "RSASSA-PSS" and
"RSASSA-PSS-SHAKE", strength from the modulus length) that
`bouncycastle_core::signature_key`'s role traits are implemented over.
`RsaPrivateKey` implements `SigningKey` for the PSS markers, whose
`create_signer` draws the salt from the caller's RNG, and
`DeterministicSigningKey` for PKCS#1 v1.5, which draws nothing;
`RsaPublicKey` implements `VerifyingKey` for all three. The workers
(`RsaPssSigner`, `RsaPssShakeSigner`, `RsaPkcs1v15Signer` and their
verifiers) borrow the key, and the PSS signers keep `set_salt` for a
known-answer test. The role impls are bounded on the key's
`SignaturePrivateKey`/`SignaturePublicKey` impl, so a 1024- or 1536-bit
key still has no signing role, as the `compile_fail` doctest pins. Since
one key type serves every marker, a call spells it:
`SigningKey::<RSASSA_PSS_SHA256, 256>::sign(&sk, &mut rng, msg, None)`.

A signature of the wrong length is a `LengthError`, as the other
signature crates report it, where it was `SignatureVerificationFailed`.
The no-argument `keygen()` goes and `keygen_from_rng` becomes
`keygen(rng)` in each size module and in RSASVE, the rule the rest of
the library follows; the CLI's keygen draws from
`HashDRBG_SHA512::new_from_os()`. The EMSA encoders take `HashOneShot`
and call `H::hash_out`. Tests, benches, the stack harness and the CLI
move onto the key traits; the state-confusion test of the old
`do_sign_init` went with the method it exercised.

Assisted-by: Claude:claude-mythos-5-1
…ried over the rebase onto its release-based history

This branch took feature/key-rework-memory by merge, and the merge commits carried the
adaptations to what arrived. The rebase onto the branch's new history (key-rework-memory
rebased onto release/0.1.3alpha, which carries feature/key-rework as one squash commit)
dropped those merges; this commit restores the affected files to the branch's pre-rebase
tip, so the tree is identical to it.

Assisted-by: Claude:claude-mythos-5-1
…er the rebase onto feature/ecdsa's release/0.1.4alpha history

This branch took feature/ecdsa (which carried release/0.1.3alpha 029a17b, the squash merge of
PR #190) by merge, and that merge commit carried the adaptations to what arrived. The rebase onto
feature/ecdsa as replayed onto release/0.1.4alpha dropped the merge; this commit restores the
affected files to the branch's pre-rebase tip, so the tree is identical to it apart from the
0.1.4 line's own version bump and notes file.

Assisted-by: Claude:claude-fable-5-1
An audit against QUALITY_AND_STYLE's rule on secret-holding locals.
The key material itself was already in `Secret` (the CRT quintuple
in the key, `p`, `q`, `d` and the quintuple in key generation, `Z`
and `z` in RSASVE); what the rule found was the plain locals around
it.

`crt_exp` left every intermediate of the CRT on the stack: the two
half-signatures (either one factors `n`, through `gcd(s - s1, n)`),
`m mod p`, `m mod q`, `h` and the recombination temporaries, plus two
`MontgomeryContext`s each holding a prime. The nine are zeroized
before `s` is returned, and the contexts are built inside `Secret`:
`MontgomeryContext` gains `Copy` and a `ZeroizablePrimitive` impl for
that. `mod_pow` left its second ladder register and the per-bit
products; both are now declared before the ladder and zeroized with
`r0` after it, and `from_montgomery` and `mul_mod` zeroize their own
temporaries. The products inside `mont_mul` and `mont_square` are the
callee intermediates the rule does not reach, and are left.

`RsaPrivateKey::from_bytes_raw` decoded through five byte-array
copies and five limb arrays; the byte arrays are now borrowed from
the caller's buffer and the limb arrays zeroized once the key holds
them. In key generation, `is_probable_prime` holds `w - 1`, `m`, the
context over `w` and the Miller-Rabin `z` in `Secret` (the function
has several exits), `primes_too_close` zeroizes the differences it
takes, and `keygen_from_rng`'s two blocks zeroize `g`, `(q-1)/g`,
`q mod p` and `p - 2` and hold the context over `p` in `Secret`.

`primes_too_close` gains the unit test its reject path and boundary
need, which no key generation reaches (two random primes land within
`2^(nlen/2 - 100)` with probability about `2^-100`): the private
known-answer exception, with the cases either side of `2^28` at
`N = 2` and in both argument orders.

No behavioural change. cargo test -p bouncycastle-rsa 245 passed, 4 ignored; clippy
locations identical.

Mutants (apollo, `--in-diff`): 47 tested, 36 caught, 11 unviable, 0
missed. Caught: keygen 18 (the `primes_too_close` restructure and
its new tests among them), keys 10, modexp 6, rsa_core 2; the
unviable ones are whole-function replacements of types without
`Default`. A removed `zeroize` call is not a mutant cargo mutants
generates.

Assisted-by: Claude:claude-mythos-5-1
@dghgit
dghgit changed the base branch from feature/ecdsa to release/0.1.4alpha November 6, 2026 10:00
…feature/rsa-sig: no conflicts

Assisted-by: Claude:claude-opus-5-5
…llia and SM4; fix/block-cipher-key; fix/shared-bitslice) into feature/rsa-sig: no conflicts; the workspace manifest, the umbrella crate, cli/src/main.rs and the mem_usage_benches manifest and lib keep both sides' entries

Assisted-by: Claude:claude-mythos-5-1
@hubot
hubot deleted the feature/rsa-sig branch November 7, 2026 20:03
@hubot
hubot deleted the branch release/0.1.4alpha November 7, 2026 20:03
…ature worker traits are dyn-compatible) into feature/rsa-sig: no conflicts

Assisted-by: Claude:claude-opus-5-5
@dghgit dghgit added this to the 0.1.4alpha milestone Nov 8, 2026
@dghgit
dghgit requested a review from laruizlo November 8, 2026 02:41
dghgit added 10 commits November 8, 2026 15:28
…nd decapsulation on the key objects with encaps_out / decaps_out, KdfParams<K, LEN> with KeyLen, the KDF trait reshaped, SHA-3 and SHAKE no longer KDFs, SuspendableKeyed gone) into feature/rsa-sig: no conflicts
… the largest KEM and does not repeat

The seed was a 256-byte counting pattern, so two consecutive draws of 256, 512 or 1024 bytes,
RSASVE's secret at 2048, 4096 and 8192 bits, were identical and the "two encapsulations
differ" check failed for every size but 3072; a 4096-byte hashed pattern now.

Assisted-by: Claude:claude-mythos-5-1
The squash of PR #192 (f3fdb8b) replaced the static KEM traits: the KEM-only key types
implement `EncapsulatingKey<()>` / `DecapsulatingKey<()>` in each size module, naming `RSASVE`
as the `KEMAlgorithm`, and hand out `RsasveEncapsulator` / `RsasveDecapsulator` workers whose
`encaps_out` is RSASVE.GENERATE and `decaps_out` RSASVE.RECOVER, writing into the caller's
buffers and leaving them zeroized on error; `encaps`, `encaps_rng` and `decaps` go, and a
wrong-length ciphertext is a type error, so its test and `KEMError::LengthError` go with them.
The tests, benches and crate docs call the roles.

Assisted-by: Claude:claude-mythos-5-1
…em's ciphertext reader

Assisted-by: Claude:claude-mythos-5-1
Assisted-by: Claude:claude-mythos-5-1
…ier types the port replaced

Assisted-by: Claude:claude-mythos-5-1
…ment and ECC CDH with the X9.63 and concatenation KDFs; PR #180 squash: BLAKE2b-512, BLAKE2s-256 and BLAKE3; the post-#193 cleanups) into feature/rsa-sig

No conflicts. One adaptation, as on feature/tdes: the CLI's main thread stack goes from 8 MiB to
32 MiB, since in a debug build the startup frame for the full subcommand set passes 8 MiB once
the rsa commands join the release's and the debug binary aborted on every invocation (the
release-profile binary was unaffected). That bound is the release's to set once. The rsa and
cli suites, the blake and eccdh CLI shell suites and the all-targets build pass.
…generator per curve in bouncycastle-ec's hazmat, which the ecdsa and eccdh keygens and derive_pks now wrap) into feature/rsa-sig
…markers and the key pair extension traits; fix/ec-curve-trait squash: curve::Curve in bouncycastle-ec; fix/eccdh-generic squash: ECC CDH once over Curve, utils ConstStr, the ec re-export; fix/ecdsa-generic squash: ECDSA once over Curve and the hash, and KdfParams over a KdfDescription an HSM can read) into feature/rsa-sig

No conflicts and no adaptation: the rsa crate, its CLI commands and its stack harness are
untouched by the four squashes. RSASVE implements the KEM roles over `()`, so the change of
KdfParams to borrow a KdfDescription does not reach it; a KDF-paired RSASVE would be written
over the descriptions. bouncycastle-utils' ConstStr, which the size-marker refactor on
fix/rsa-size-markers cherry-picked ahead of this merge, now comes from the release.

Assisted-by: Claude:claude-mythos-5-1
…h: the hkdf description clone test and the KDF mutants numbers in the release notes) into feature/rsa-sig

No conflicts and no adaptation.

Assisted-by: Claude:claude-mythos-5-1
…eme over a modulus-size marker and gives RSASVE a KDF pairing: the six per-size modules each expanded the key types, the three signature schemes at every hash and RSASVE through macros over up to twelve const parameters, so a scheme was six copies and a signature of it unreadable. The size is now a marker type, RSA1024, RSA1536, RSA2048, RSA3072, RSA4096 and RSA8192 at the crate root, implementing size::RsaSize (the lengths as consts, the limb and byte arrays as associated types through a small Array<T> trait, and forwarders to the public-key arithmetic at that width) and, from 2048 up, size::RsaSigningSize (FIPS 186-5 Table B.1's Miller-Rabin rounds, keygen, and the private key's arithmetic: the CRT exponentiation, the prime-width codecs and comparison, and the modulus from its primes), the latter the compile-time form of the crate's rule that RSA-1024 and RSA-1536 verify only: the private key's constructors and encoder, the signing roles, rsasp1, RSASVE's key generation and the KEM private key are all on impl<K: RsaSigningSize>, so no private key value and no private-key arithmetic exists at the two legacy sizes. The keys are RsaPublicKey<K> and RsaPrivateKey<K>, the schemes RSASSA_PKCS1_v1_5<K, H>, RSASSA_PSS<K, H> and RSASSA_PSS_SHAKE<K, X>, named by scheme and hash through utils' ConstStr ("RSASSA-PSS-SHA256") at the lesser of the modulus's and the hash's strength, and the KEM RSASVE<K> ("RSASVE-2048") over RsaKEMPublicKey<K> and RsaKEMPrivateKey<K>; the role traits that name a [u8; N] are implemented once each over an equality bound on the marker's array type, as the ECDSA and ML-DSA crates do, so a call needs no annotation beyond the algorithm; size::SignatureKeyPair and size::KemKeyPair hang the two generators on a signing size's marker. RSASP1 and RSAVP1 are rsasp1::<K> and rsavp1::<K>; MGF1 and the EMSA encoders take the digest as a slice and the modulus length as the one const they need; the PSS signers hold the salt in a buffer sized for the largest hash. The per-size modules stay as alias modules (rsa_2048::RSASSA_PSS_SHA256 is RSASSA_PSS<RSA2048, SHA256>, RSA2048PrivateKey is RsaPrivateKey<RSA2048>, SIG_LEN is RSA2048::K_LEN), so the CLI, the benches, the stack harness and every known-answer suite are unchanged in what they exercise. One behaviour is dropped: a private key built from CRT components of the wrong width was a decoding error at run time and is now a type error. RSASVE gains the pairing the KDF description was made for: SP 800-56B Rev. 2 passes the secret value Z through a key-derivation method (its Section 5.5) before any use, and rsasve_kdf.rs gives the key-establishment keys the KEM roles a second time over KdfParams<'p, HkdfDescription<'s, H>, SS_LEN>, the shape the ML-KEM pairings have: RSASVE_HKDF<K, H>, named "RSASVE-<bits>-HKDF-<hash>" at the lesser of the size's and the hash's strength, for any modulus size and any HMAC hash since the parameter type is the library's description and nothing foreign can be paired; the workers run RSASVE.GENERATE or RSASVE.RECOVER for Z into one local zeroized when it drops, build the HKDF engine from the description, derive the key of the length the parameters name (one byte to 255 hash outputs, checked at compile time where a worker is made) and cap its label at the algorithm's strength, with a ciphertext for a key the caller never got cleared and the caller's buffer left zeroized on every error; the description's salt and the info are what a token key reads to hand its device the same derivation. No CLI subcommand yet: the rsasve-* commands hand out the raw Z as before. Tests: the RSASVE suite's macros take the marker and the direct generic calls name it; rsasve_hkdf_tests runs the core framework KEM suite over RSA-2048 with HKDF-SHA256 and HKDF-SHA512, the derivation against HKDF by hand over the raw Z (the crate's example key, so no key generation), another info and the absent salt, the prefix and maximum lengths, the refused ciphertext, and the names and strengths at three sizes; keys_tests checks the CRT range checks at every signing size, the RSA-1024 and RSA-1536 suites the out-of-range signature representative through their own range check, and rsassa_worker_tests the three schemes' workers naming their algorithm and PSS's random-salt sign_from_hash drawing a fresh salt that verifies. Verified: the rsa suite (the 4096 and 8192 keygens still #[ignore]d), the rsa doctests, the cli suite, the all-targets build, nightly rustfmt, clippy adding nothing on the changed lines, rustdoc with warnings denied on the crate apart from modexp.rs's pre-existing private-item links. Mutants on apollo, scoped to the crate's sources against the subset of its suites the earlier runs validated: 1168 (639 caught, 362 unviable, 25 timeouts, 142 missed: keygen.rs's 65, where bounds were the only change and the file's mutants are the slow, nondeterministic ones documented before; the check_lengths and len_check const assertions; emsa_pkcs1_v1_5's checks noted as equivalent at their sites and MAX_T_LEN's arithmetic; the PSS encoders' step 3 length check that no modulus here reaches and their 8 * K_LEN arithmetic; mgf1's block count; modexp's disjoint-bit ORs; the defensive length checks in the KDF derivations; rsasve's kem_error arms, covered by the keygen test the scoped run skips; and the forwarders, worker names and random-salt entry points the final commit's tests close).

Assisted-by: Claude:claude-mythos-5-1
…eme and hash: a key served every scheme, so sk.sign(..) was ambiguous and every call spelled SigningKey::<RSASSA_PSS<RSA2048, SHA256>, 256>::sign(&sk, ..), and nothing stopped one key pair signing under two schemes, which SP 800-57 Part 1 Rev. 5 asks a key pair not to do. The key types now carry their algorithm, as the ECDSA keys carry their curve and hash: what RsaPublicKey<K> and RsaPrivateKey<K> were is keys::RsaPublicComponents<K> and RsaPrivateComponents<K>, the material with its validation and raw encoding and no role, and RsaPublicKey<A> and RsaPrivateKey<A> wrap them for the algorithm A their type names, so that sk.sign(&mut rng, msg, None), sk.sign_deterministic(msg, None) and pk.verify(msg, None, &sig) resolve from the key, a PSS key has no sign_deterministic and a PKCS#1 v1.5 key no sign; the components turn into a key for a scheme through key::<A>() / into_key::<A>(), the one deliberate step by which the same material serves two schemes, and the bytes carry no scheme, so a key file is decoded under the key type it is for. The roles are written once, in the new rsassa module, over three traits the markers implement: RsaSignatureAlgorithm (the size, the hash and the scheme's EMSA verification), RsaDeterministicSignatureAlgorithm (PKCS#1 v1.5, an encoding of the hash alone) and RsaRandomizedSignatureAlgorithm (PSS, an encoding of the hash and a salt), split by what the signer draws because two blanket role impls distinguished only by a bound on A overlap under coherence; three workers, RsaSigner with set_salt, RsaDeterministicSigner and RsaVerifier, replace the nine per-scheme ones, the scheme modules keep only their markers, and the pre-hash entry points are rsassa::sign_from_hash, sign_from_hash_with_salt, sign_from_hash_deterministic and verify_from_hash over <A, K_LEN>. The signing-size markers generate a pair for one algorithm, RSA2048::generate_pkcs1_v1_5_signature_key_pair::<SHA256>(&mut rng) and generate_pss_signature_key_pair::<SHA256>, the hash spelled and never defaulted as on the ECDSA markers, and the plain generate_signature_key_pair goes; RsaSigningSize::keygen returns the components. The size modules lose RSA2048PublicKey, RSA2048PrivateKey and keygen, a key having no meaning without a scheme, and keep the scheme aliases and lengths; the key-establishment keys wrap the components as before, with their own Display. The CLI's generic rsa_pss_cmd, rsa_pkcs1_cmd and do_verify are over the algorithm alone and each size's command passes the marker's generator for the pairing it dispatches, subcommands, flags and outputs unchanged; the stack harness names its keys with their scheme. Tests: every suite builds the components once and types a key per pairing, the encoding tests under one scheme since the bytes are the same under any, and the rsa CLI tests type the documented key with the scheme each case checks. Verified: the rsa suite (the 4096 and 8192 keygens still #[ignore]d), the doctests, the cli suite, the all-targets build, nightly rustfmt, clippy adding nothing on the changed lines, rustdoc with warnings denied on the crate apart from modexp.rs's pre-existing private-item links. Mutants on apollo, scoped to keys.rs, rsassa.rs, the three scheme files, size.rs and rsasve.rs against the signature and RSASVE suites at 2048 and the verify-only sizes: 505 (162 caught, 330 unviable, 2 timeouts, both rsasve's encaps_out attempt counter made to loop, and 11 missed: the typed keys' e accessor, Debug and PartialEq, which the next squash's keys test closes; rsasve's kem_error arms, covered by the keygen test the scoped run skips; and size.rs's check_lengths const assertion and is_less at the two verify-only sizes, as before).

Assisted-by: Claude:claude-mythos-5-1
…a type and makes SHAKE a hash like any other: RFC 8017 §9.1 parameterises EMSA-PSS by the hash, the mask generation function and the salt length, and the crate fixed MGF1 over the hash and sLen = hLen, with a second marker, RSASSA_PSS_SHAKE<K, X>, for RFC 8702's XOF-as-mask. The marker is now RSASSA_PSS<K, H, P> over rsassa_pss::PssParams<H> (the salt length, the mask, and what the name says after RSASSA-PSS-<hash>), P defaulting to Conventional, which reads the hash's conventional pairing off PssHash, a trait implemented once per hash family: MGF1 over the hash itself with a salt one output long for SHA256Internal, SHA512Internal and SHA3Internal (§8.1's recommendation, §9.1 note 4's typical length), the XOF as the mask with its output length as the salt for SHAKEInternal (RFC 8702 §3.2.1), so RSASSA_PSS<RSA2048, SHAKE128> is RSASSA-PSS-SHAKE128 and a SHAKE pair comes from generate_pss_signature_key_pair::<SHAKE128> like a SHA-2 one. Spelled parameters are the other two types: Mgf1<M, SALT_LEN>, MGF1 over another hash and any salt from 1 to 64 bytes, which §8.1 allows and Appendix A.2.3's RSASSA-PSS-params carry, named -MGF1-<hash>-<sLen>; and NoSalt<M>, §9.1 note 4's zero salt, which makes the encoding a function of the key and message alone and so puts the key on the deterministic role (sign_deterministic, create_signer_deterministic) rather than the salted one, the SaltedPss / UnsaltedPss markers deciding which of the two rsassa algorithm traits a marker implements. A key is for one (H, P), so a verifier never guesses the mask or the salt length, and a conventional key rejects a zero-salt or short-salt signature over the same material. emsa_pss is written once over H and P; emsa_pss_shake, rsassa_pss_shake, the RSASSA_PSS_SHAKE alias and generate_pss_shake_signature_key_pair are gone, generate_pss_signature_key_pair_with::<H, P> added for the spelled forms, and the signer's set_salt takes [u8; A::SALT_LEN]. The strength stays min(size, H): the mask's hash does not enter the proof (§8.1). utils gains ConstStr::decimal, the digits of a number for a name, with its tests. The CLI's only change is the generator name; the per-size aliases and every command are as before. Tests: tests/rsa_2048_pss_params_tests.rs runs Wycheproof's rsa_pss_2048_sha512_mgf1sha256_32_params_test.json (178 vectors, 132 valid and 46 invalid) under RSASSA_PSS<RSA2048, SHA512, Mgf1<SHA256, 32>> and rsa_pss_2048_sha256_mgf1_0_test.json (103 vectors, 61 valid and 42 invalid) under RSASSA_PSS<RSA2048, SHA256, NoSalt<SHA256>>, the names, the strengths against the size's and the hash's, each family's conventional mask and salt length, no-salt signing deterministic and reproduced through the streaming signer, a 16-byte salt pinned through set_salt, the parameters not interchangeable, and the generator with spelled parameters; keys_tests checks the typed keys' e, Debug and PartialEq, the misses of the previous squash's run; the SHAKE suites and rsassa_worker_tests use the one marker. Verified: the rsa suite, the utils suite, the rsa doctests, the rsa CLI tests, the all-targets build, nightly rustfmt, clippy adding nothing on the changed lines, rustdoc with warnings denied apart from modexp.rs's pre-existing private-item links. Mutants on apollo, scoped to keys.rs, rsassa_pss.rs, emsa_pss.rs, rsassa.rs and size.rs against the signature suites at 2048 and the verify-only sizes alone (the KEM suites left out, which is what makes the run 19 minutes rather than 32): 481 (187 caught, 264 unviable, no timeouts, 30 missed, of which min_strength's comparison and the SHA-512 and SHA-3 masks emptied were then caught by the two test commits in a targeted re-run, leaving 27: emsa_pss's buffer-size arithmetic over MAX_HASH_LEN and its step 3 length check that no modulus here reaches, as before; size.rs's check_lengths const assertion and is_less at the two verify-only sizes, as before; and keys.rs's modulus_strength and size.rs's is_less and half_limbs_from_be_bytes at 3072, 4096 and 8192, which only the RSASVE suites reach and which the previous squash's run, with those suites, caught); utils const_str.rs 39 (19 caught, 19 unviable, 1 timeout: the copy loop's increment made a no-op, so const evaluation never finishes and the build never completes, a kill in effect).

Assisted-by: Claude:claude-mythos-5-1

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant