Skip to content

fix(server): remove crypto-bigint 0.5 for RUSTSEC-2026-0387 - #1151

Open
ragnorc wants to merge 1 commit into
ModernRelay:mainfrom
ragnorc:fix/remove-crypto-bigint-0-5-advisory
Open

ragnorc wants to merge 1 commit into
ModernRelay:mainfrom
ragnorc:fix/remove-crypto-bigint-0-5-advisory

Conversation

@ragnorc

@ragnorc ragnorc commented Oct 24, 2026

Copy link
Copy Markdown
Contributor

RUSTSEC-2026-0387 reports timing side channels in crypto-bigint 0.5.5. The Dependency Guard now fails on main and on every pull request, so nothing can pass the merge queue. The crate came in twice through elliptic-curve 0.13: the server's own p256 0.13 dependency, and jsonwebtoken's rust_crypto backend through p256 and p384 0.13. The patched line, 0.7.6, needs elliptic-curve 0.14. jsonwebtoken 11.1.0 still pins p256 0.13, so a version bump alone cannot clear it.

The server's p256 moves to 0.14. Its ES256 data-token verification compiles unchanged against it: VerifyingKey::from_public_key_pem, to_public_key_der, Signature::from_slice and Verifier::verify. jsonwebtoken switches from its rust_crypto backend to aws_lc_rs, which was already in the lock through rustls, rustls-webpki and object_store. OIDC verification accepts RS256 only (oidc_identity.rs), and aws-lc-rs verifies RS256. The lock now holds only crypto-bigint 0.7.6, and the rust_crypto chain leaves with it: p384, ed25519-dalek, curve25519-dalek and fiat-crypto. No deny.toml exemption is added.

The data-token and OIDC library tests pass on this branch. They sign with test keys and verify through the production paths: key rotation, strict JOSE header and signature checks, signature headers that try to bypass the published keys, and deadlines that never slide. cargo deny reports advisories, bans, licenses and sources ok.

For review: the change to challenge is the backend switch. Every key and token the server accepted before must still verify. The tests above pin RS256 OIDC tokens and ES256 data tokens. Nothing in production signs with these crates; signing appears only in tests.

Move the server's p256 to 0.14 and verify JSON Web Tokens through
jsonwebtoken's aws_lc_rs backend instead of rust_crypto, so the lock holds
only crypto-bigint 0.7.6. aws-lc-rs was already in the lock through rustls,
rustls-webpki and object_store.

ragnorc commented Oct 24, 2026

Copy link
Copy Markdown
Contributor Author

Recommendation

Approve. This pull request removes the cause of RUSTSEC-2026-0387. I found no blocking issue.

Why

Dependency Guard fails on main at 7711e4a3 because crypto-bigint 0.5.5 is in the lock. RUSTSEC-2026-0387 reports timing side channels in subtraction, zero counting, division, Montgomery multiplication, UintRef equality, and BoxedMontyForm::invert. The patched release is 0.7.6. elliptic-curve 0.13 requires 0.5, so cargo update -p crypto-bigint cannot select the patched crate.

The server had two edges into that crate. Data-token verification depends on p256 0.13. jsonwebtoken with rust_crypto depends on p256 0.13 and p384 0.13. Both edges meet at elliptic-curve 0.13.8. An ignore row in deny.toml would leave the vulnerable crate in the shipped graph.

What

omnigraph-server now depends on p256 0.14 with the same ecdsa and pem features. jsonwebtoken 10 stays, and its features change from rust_crypto to aws_lc_rs. deny.toml is unchanged. The lock contains one crypto-bigint, version 0.7.6.

The server source that verifies tokens is unchanged. ES256 data tokens use p256 directly. OIDC tokens use jsonwebtoken, and the server accepts only RS256 on that path.

p256 = { version = "0.14", features = ["ecdsa", "pem"] }
jsonwebtoken = { version = "10", default-features = false, features = ["aws_lc_rs", "use_pem"] }

How this holds the contract

Invariant 10 says the HTTP boundary resolves bearer tokens to actors. The client does not supply a trusted actor. This change does not add a second verifier, a new API, or a shadow source of truth. It selects a different implementation of the same two checks.

The server still accepts two algorithms.

  • ES256, fixed-width JOSE r||s (64 bytes), through VerifyingKey::from_public_key_pem, Signature::from_slice, and Verifier::verify.
  • RS256, through DecodingKey::from_rsa_components and Validation::new(Algorithm::RS256). The modulus must be 256 to 512 bytes with the high bit set. The exponent must be 65537 or 3.

pub fn verify_authenticated_at(&self, token: &str, now: u64) -> Option<AuthenticatedActor> {
if token.len() > MAX_TOKEN_BYTES {
return None;
}
let mut parts = token.split('.');
let header_part = parts.next()?;
let claims_part = parts.next()?;
let signature_part = parts.next()?;
if parts.next().is_some() {
return None;
}
let header_bytes = URL_SAFE_NO_PAD.decode(header_part).ok()?;
let header: DataTokenHeader = serde_json::from_slice(&header_bytes).ok()?;
if header.typ != "JWT" || header.alg != "ES256" || !valid_kid(&header.kid) {
return None;
}
let (_, key) = self.keys.iter().find(|(kid, _)| *kid == header.kid)?;
let signature_bytes = URL_SAFE_NO_PAD.decode(signature_part).ok()?;
let signature = Signature::from_slice(&signature_bytes).ok()?;
key.verify(
format!("{header_part}.{claims_part}").as_bytes(),
&signature,
)

let mut keys = BTreeMap::new();
for key in doc.keys {
let modulus = URL_SAFE_NO_PAD.decode(&key.n)?;
let exponent = URL_SAFE_NO_PAD.decode(&key.e)?;
if !bounded(&key.kid, 128)
|| key.kty != "RSA"
|| key.alg != "RS256"
|| key.usage != "sig"
|| !(256..=512).contains(&modulus.len())
|| modulus.first().is_none_or(|b| b & 0x80 == 0)
|| !(exponent == [1, 0, 1] || exponent == [3])
|| keys.contains_key(&key.kid)
{
bail!("invalid or duplicate OIDC RSA signing key");
}
keys.insert(key.kid, DecodingKey::from_rsa_components(&key.n, &key.e)?);
}

pub(crate) fn verify_at(&self, token: &str, now: i64) -> Option<AuthenticatedActor> {
if token.is_empty() || token.len() > MAX_TOKEN_BYTES {
return None;
}
let snapshot = self.current.load();
if now < 0
|| snapshot.expires_at <= now
|| snapshot.generated_at > now.saturating_add(CLOCK_SKEW_SECONDS)
{
return None;
}
let header = decode_header(token).ok()?;
if header.alg != Algorithm::RS256
|| header.crit.is_some()
|| header.enc.is_some()
|| header.zip.is_some()
|| header
.typ
.as_deref()
.is_some_and(|t| !matches!(t, "JWT" | "at+jwt"))
{
return None;
}
let key = snapshot.keys.get(header.kid.as_deref()?)?;
let mut validation = Validation::new(Algorithm::RS256);
validation.leeway = 0;
// Explicit clock makes expiry tests deterministic and shares one instant
// with the snapshot check; signature/issuer/audience still use the library.
validation.validate_exp = false;
validation.validate_nbf = false;
validation.set_required_spec_claims(&["iss", "sub", "aud", "iat", "exp"]);
validation.set_issuer(&[&snapshot.binding.issuer]);
validation.set_audience(&[&snapshot.binding.audience]);

jsonwebtoken 10.4.0 still implements the full algorithm enum on the aws_lc_rs backend, including ES256, ES384, PS256, and EdDSA. The server does not call those verifiers. Header checks still reject none, HS256, crit, and a typ other than JWT or at+jwt. No accepted algorithm is removed.

p256 0.14 sets NORMALIZE_S to false. ecdsa 0.17 rejects a high s only when that flag is true. Signature::from_slice still accepts s in the range 1..n. A high-S data token therefore still verifies. Data tokens do not use the aws-lc ECDSA_P256_SHA256_FIXED verifier. That verifier is the low-S path inside jsonwebtoken, and the server does not use it for ES256.

jsonwebtoken 11.1.0 still has a rust_crypto feature that enables p256 ^0.13.2 and p384 ^0.13.0. A version bump of jsonwebtoken alone does not remove crypto-bigint 0.5 from that backend. aws-lc-rs 1.18.1 and aws-lc-sys 0.45.0 were already in the lock through rustls and object_store. The OIDC path now uses that stack. Its RS256 verifier is RSA_PKCS1_2048_8192_SHA256. The server modulus range is 2048 to 4096 bits. aws-lc accepts an odd public exponent greater than 1 and at most 33 bits. That range includes 3 and 65537.

Impact and liability

Query rows, counts, column types, and errors do not change. A GQT case does not apply. The DST storage-cost table does not apply. No user document names p256 or rust_crypto. The changelog note is the right user-visible record.

This removes a vulnerable bigint from every workspace feature and target. cargo tree --locked --all-features --target all --edges all -i crypto-bigint shows only 0.7.6, and the only server edge is p256 0.14. p384 is absent. The lock has one crypto-bigint package, so no target can select 0.5 without a lock change. rust_crypto is not a feature of any workspace crate.

The new p256 0.14 line adds second majors beside the rsa 0.9 line that reqsign still requires:

  • signature 2.2.0 and 3.0.0
  • der 0.7.10 and 0.8.2
  • pkcs8 0.10.2 and 0.11.0
  • spki 0.7.3 and 0.8.1
  • pem-rfc7468 0.7.0 and 1.0.0
  • untrusted 0.7.1 and 0.9.0

untrusted 0.7.1 arrives because jsonwebtoken enables the default features of aws-lc-rs (ring-io and ring-sig-verify). Those features add ring-compatible helpers. They do not change PublicKey::from_der acceptance. deny.toml sets multiple-versions to allow. cargo deny reports no new advisory, no yanked crate, and no warning. The same two ignore rows remain.

Six unused crates leave: p384, ed25519, ed25519-dalek, curve25519-dalek, curve25519-dalek-derive, and fiat-crypto. Four crates enter with p256 0.14: cpubits, primefield, serdect, and wnaf. The lock gains four version rows.

Five similar advisory bumps would add more majors only while rsa 0.9 stays. That duplicate line already exists. This pull request does not create a new pattern of ignore entries.

Other lock pull requests

The open pull requests that change Cargo.lock are this one, #1130 at bbd0cd8, and #1001 at b3ceada. git merge-tree --write-tree of 8c592db with each head is clean. Both merged lock texts contain only crypto-bigint 0.7.6 and elliptic-curve 0.14.1. #1001 also edits omnigraph-server/Cargo.toml. The merged file keeps p256 0.14, aws_lc_rs, and the OpenTelemetry dependencies. A rebase still re-resolves the lock. Those diffs do not add elliptic-curve 0.13.

Findings

Blocking: none.

Optional: none. The duplicate majors in the liability section are an accepted cost of this fix. I do not ask for an ignore entry or a further abstraction.

Validation

Before, on main 7711e4a3def5c179cf95ec2525a6263819155916:

  • Local cargo-deny 0.20.2, the same version as the CI action, command cargo-deny --locked check: advisories FAILED on RUSTSEC-2026-0387. The graph was crypto-bigint 0.5.5, elliptic-curve 0.13.8, p256 0.13.2, p384 0.13.1, and jsonwebtoken 10.4.0. Bans, licenses, and sources passed. JSON summary: 1 error, 0 warnings, 4 notes.
  • Nightly Dependency Guard run 40060793527 on that same commit failed with the same advisory.
  • cargo test -p omnigraph-server --lib --locked -- data_tokens:: oidc_identity result: 15 passed, 0 failed, 0 ignored.

After, on head 8c592db3edd0d7bb0b9c61a8b27bc3dfcdfa5e13:

  • The same cargo-deny --locked check result: advisories ok, bans ok, licenses ok, sources ok. JSON summary: 0 errors, 0 warnings, 4 notes.
  • Exact-commit CI job Dependency Guard (cargo deny) succeeded.
  • The same server test command, with this commit's Cargo.toml and Cargo.lock and the same test source, result: 15 passed, 0 failed, 0 ignored.
  • A local cross-version harness, outside the repository. It signed with p256 0.13.2 and jsonwebtoken 10.4.0 rust_crypto (crypto-bigint 0.5.5). It verified with p256 0.14.0, ecdsa 0.17.0, elliptic-curve 0.14.1, crypto-bigint 0.7.6, jsonwebtoken 10.4.0 aws_lc_rs, aws-lc-rs 1.18.1, and aws-lc-sys 0.45.0. Low-S ES256 verified. High-S ES256 verified (same r, s replaced by n-s). RS256 with the fixture exponent 65537 verified. RS256 with a 2048-bit exponent-3 key verified. Both RSA checks used from_rsa_components and Validation::new(RS256) with the server clock flags (leeway 0, validate_exp false, validate_nbf false). A tampered payload was rejected in both RSA cases.
  • cargo tree --locked --all-features --target all --edges all -i crypto-bigint shows only 0.7.6. The same is true for the server default features and for --features aws. p384 matches no package. git diff 7711e4a3 -- deny.toml is empty.

Exact-commit CI on this head also passed Test Workspace, Test omnigraph-server --features aws, clippy, rustfmt, the GQT jobs, DST, RustFS, Azurite, the V5 to V10 fence, and Storage Upgrade Compatibility. Devin Review skipped a full review because the trial has no credits. That check is not a review of this commit.

Limitations

I did not rerun the integration test signed_data_requires_cedar_and_rejects_forgery_on_every_protected_route locally. CI Test Workspace on this head ran the server suite. cargo deny rejects a repeated --target flag, so I did not run one deny process per target. The lock contains no crypto-bigint 0.5 package, and cargo tree --target all shows only 0.7.6. I did not measure performance. The cross-version harness is local evidence. It is not a committed regression test. A GQT test cannot express this dependency contract.

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