fix(agent): build rsa signing key from components, not ssh-key TryFrom#1
Conversation
`ssh-key`'s `TryFrom<&RsaKeypair> for rsa::RsaPrivateKey` passes `p` twice
and omits `q`:
vec![
rsa::BigUint::try_from(&key.private.p)?,
rsa::BigUint::try_from(&key.private.p)?, // should be .q
]
So `from_components` gets primes `[p, p]`, `Πprimes = p² ≠ n`, and the
internal `validate()` fails with `InvalidModulus` → surfaced as
`Error::Crypto`. This breaks EVERY RSA identity: `Session::sign` returns
`AgentError` and ssh reports "agent refused operation". (Only Ed25519 was
covered by tests, so the RSA path was never exercised.)
The bug is present in the entire 0.6.x line, including 0.6.7 (the latest
stable, 2024-10-15). It is fixed upstream by RustCrypto/SSH#274, first
published in ssh-key 0.7.0-pre.1 (2024-08-13) and carried through the
0.7.0-rc.* prereleases — but 0.7.0 has no stable release yet, and #274 is a
breaking rewrite (private RSA fields, BigUint→Uint, subtle→ctutils, edition
2024, MSRV 1.85, crypto-bigint 0.7-pre / rsa rc). Bumping to a prerelease to
fix a one-line typo isn't worth the blast radius.
Instead, build the key from its Mpint components directly with the correct
`p` and `q` and call `rsa::RsaPrivateKey::from_components` (which itself
validates + precomputes). Verified against the affected key: pq==n,
de≡1 mod p-1/q-1, from_components accepts it, and `ssh-add -T` now signs
(rsa-sha2-256/512, ssh-rsa). Works on the pinned stable ssh-key 0.6.7.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Covers the ssh-key 0.6.7 TryFrom bug that broke every RSA identity (passes p twice, omits q -> InvalidModulus -> Error::Crypto). Parses a fixed 2048-bit OpenSSH RSA key and asserts sign_data_with_privkey produces rsa-sha2-256 / rsa-sha2-512 / ssh-rsa signatures that verify under the key's own modulus. macOS-gated like the module it guards. Co-authored-by: Claude <noreply@anthropic.com>
timqi
left a comment
There was a problem hiding this comment.
Approve — verified against the pinned crates (ssh-key 0.6.7, rsa 0.9.10).
Bug is real. ssh-key 0.6.7 src/private/rsa.rs:196 TryFrom<&RsaKeypair> for rsa::RsaPrivateKey passes key.private.p twice and omits .q, so from_components([p, p]) sees Πprimes = p² ≠ n → validate() returns InvalidModulus → Error::Crypto. Every RSA identity fails to sign ("agent refused operation"); only Ed25519 was under test so it was never caught.
Fix is correct. Rebuilding via RsaPrivateKey::from_components(n, e, d, [p, q]) from the Mpint components is right, and the API surface holds on the locked versions:
Mpint::as_positive_bytesstrips the leading0x00sign byte →BigUint::from_bytes_begets the raw magnitude. ✓from_components(rsa 0.9.10) matches the call and self-validates + precomputes. ✓
Declining the ssh-key 0.7.0-pre bump for a one-line upstream typo is the right call given the breaking blast radius.
Nits (non-blocking):
from_componentsdrops the old>= MIN_KEY_SIZEguard the buggyTryFromhad. Fine in practice (these are the operator's own enrolled keys), just noting the behavioral delta.- CI on Linux can't exercise this path (
server_macos+ thersa/sha1deps arecfg(target_os = "macos")), so it rides on the author'sssh-add -Tverification.
I pushed a regression test on top (test(agent): RSA sign/verify regression test…): parses a fixed 2048-bit OpenSSH RSA key and asserts sign_data_with_privkey yields rsa-sha2-256 / rsa-sha2-512 / ssh-rsa signatures that verify under the key's own modulus — so this exact regression can't return. It's macOS-gated like the code it guards; I couldn't run it on my Linux host (deps aren't compiled there), but every API was source-verified.
Adds a GitHub Actions CI workflow triggered by default on pull_request (all base branches) and pushes to main: - test matrix over ubuntu-latest + macos-latest (cargo test); macOS is what exercises the cfg(target_os="macos") server_macos module incl. the RSA sign/verify regression test - Linux type-check parity with `just check` (host + linux-gnu boundary) - musl-static release build to catch musl link regressions pre-release Windows is intentionally excluded — vt is unix-only (setsid/O_NOFOLLOW/ forwarded SSH agent). Co-authored-by: Claude <noreply@anthropic.com>
…dable)
CI (macos-latest) caught that the ssh-rsa (SHA-1, flags==0) branch of
sign_data_with_privkey can't build a Signature: ssh-key 0.6.7
Signature::new only accepts Algorithm::Rsa { hash: Some(_) }, so a plain
ssh-rsa returns Encoding(Length). That's a pre-existing limitation
orthogonal to the from_components fix. Assert only the reachable SHA-2
paths (which fully exercise the fix) and document the SHA-1 gap.
Co-authored-by: Claude <noreply@anthropic.com>
ssh-key'sTryFrom<&RsaKeypair> for rsa::RsaPrivateKeypassesptwice and omitsq:So
from_componentsgets primes[p, p],Πprimes = p² ≠ n, and the internalvalidate()fails withInvalidModulus→ surfaced asError::Crypto. This breaks EVERY RSA identity:Session::signreturnsAgentErrorand ssh reports "agent refused operation". (Only Ed25519 was covered by tests, so the RSA path was never exercised.)The bug is present in the entire 0.6.x line, including 0.6.7 (the latest stable, 2024-10-15). It is fixed upstream by RustCrypto/SSH#274, first published in ssh-key 0.7.0-pre.1 (2024-08-13) and carried through the 0.7.0-rc.* prereleases — but 0.7.0 has no stable release yet, and #274 is a breaking rewrite (private RSA fields, BigUint→Uint, subtle→ctutils, edition 2024, MSRV 1.85, crypto-bigint 0.7-pre / rsa rc). Bumping to a prerelease to fix a one-line typo isn't worth the blast radius.
Instead, build the key from its Mpint components directly with the correct
pandqand callrsa::RsaPrivateKey::from_components(which itself validates + precomputes). Verified against the affected key: pq==n, de≡1 mod p-1/q-1, from_components accepts it, andssh-add -Tnow signs (rsa-sha2-256/512, ssh-rsa). Works on the pinned stable ssh-key 0.6.7.