Skip to content

fix(agent): build rsa signing key from components, not ssh-key TryFrom#1

Merged
timqi merged 4 commits into
timqi:mainfrom
scyehan:rsa-fix
Jul 7, 2026
Merged

fix(agent): build rsa signing key from components, not ssh-key TryFrom#1
timqi merged 4 commits into
timqi:mainfrom
scyehan:rsa-fix

Conversation

@scyehan

@scyehan scyehan commented Jul 7, 2026

Copy link
Copy Markdown

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.

silas and others added 2 commits July 7, 2026 16:23
`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 timqi left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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² ≠ nvalidate() returns InvalidModulusError::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_bytes strips the leading 0x00 sign byte → BigUint::from_bytes_be gets 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_components drops the old >= MIN_KEY_SIZE guard the buggy TryFrom had. 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 + the rsa/sha1 deps are cfg(target_os = "macos")), so it rides on the author's ssh-add -T verification.

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.

timqi and others added 2 commits July 7, 2026 16:40
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>
@timqi
timqi merged commit e18f70c into timqi:main Jul 7, 2026
3 checks passed
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.

2 participants