Merge pull request #386 from AgentEnder/codex/fix-jwt-provider-panic

fix(jwt): unify jsonwebtoken crypto backend
This commit is contained in:
Mick Grove 2026-05-22 12:05:30 -04:00 committed by GitHub
commit 5d8bc3e88c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 61 additions and 11 deletions

14
Cargo.lock generated
View file

@ -377,7 +377,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ec2f1fc3ec205783a5da9a7e6c1509cc69dedf09a1949e412c1e18469326d00"
dependencies = [
"aws-lc-sys",
"untrusted 0.7.1",
"zeroize",
]
@ -4930,7 +4929,6 @@ version = "10.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eba32bfb4ffdeaca3e34431072faf01745c9b26d25504aa7a6cf5684334fc4fc"
dependencies = [
"aws-lc-rs",
"base64",
"ed25519-dalek",
"getrandom 0.2.17",
@ -5020,6 +5018,7 @@ dependencies = [
"indenter",
"indicatif",
"ipnet",
"jsonwebtoken",
"kingfisher-core",
"kingfisher-rules",
"kingfisher-scanner",
@ -5047,6 +5046,7 @@ dependencies = [
"regex",
"reqwest 0.12.28",
"roaring",
"rsa",
"rusqlite",
"rustc-hash",
"rustls",
@ -7161,7 +7161,7 @@ dependencies = [
"cfg-if",
"getrandom 0.2.17",
"libc",
"untrusted 0.9.0",
"untrusted",
"windows-sys 0.52.0",
]
@ -7356,7 +7356,7 @@ dependencies = [
"aws-lc-rs",
"ring",
"rustls-pki-types",
"untrusted 0.9.0",
"untrusted",
]
[[package]]
@ -9052,12 +9052,6 @@ version = "0.2.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861"
[[package]]
name = "untrusted"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a"
[[package]]
name = "untrusted"
version = "0.9.0"

View file

@ -220,7 +220,7 @@ aws-sdk-ssm = { version = "1.102.0", default-features = false, features = ["defa
gcloud-storage = { version = "1.1.1", default-features = false, features = [
"rustls-tls",
"auth",
"jwt-aws-lc-rs",
"jwt-rust-crypto",
] }
tokei = "14.0.0"
crc32fast = "1.5.0"
@ -252,6 +252,10 @@ testcontainers = "0.27.2"
predicates = "3.1.3"
assert_cmd = "2.1.1"
proptest = "1.9.0"
jsonwebtoken = { version = "10.4.0", default-features = false, features = ["rust_crypto"] }
# Test-only: generate an ephemeral RSA keypair for the RS256 JWT regression test.
# `getrandom` enables `rsa::rand_core::OsRng`; `pem` is on by default for PEM export.
rsa = { version = "0.9.10", features = ["getrandom"] }
[profile.release]
debug = false

52
tests/int_jwt_provider.rs Normal file
View file

@ -0,0 +1,52 @@
use jsonwebtoken::{Algorithm, DecodingKey, EncodingKey, Header, encode};
use kingfisher_scanner::validation::jwt::{ValidateOptions, validate_jwt_with};
use rsa::RsaPrivateKey;
use rsa::pkcs1::{EncodeRsaPrivateKey, LineEnding};
use rsa::pkcs8::EncodePublicKey;
use rsa::rand_core::OsRng;
/// Regression test for the `jsonwebtoken` CryptoProvider panic (see issue #385).
///
/// It exercises the asymmetric (RS256) verification path through `validate_jwt_with`
/// via the fallback decoding key. A throwaway RSA keypair is generated at runtime and
/// the token is signed from readable claims, so no opaque token blobs or key material
/// are committed to the repository.
#[tokio::test]
async fn validate_jwt_with_fallback_key_handles_rs256_without_panicking() {
// Generate an ephemeral RSA keypair for this test run only.
let mut rng = OsRng;
let private_key = RsaPrivateKey::new(&mut rng, 2048).expect("generate RSA key");
let private_pem = private_key.to_pkcs1_pem(LineEnding::LF).expect("encode private key");
let public_pem =
private_key.to_public_key().to_public_key_pem(LineEnding::LF).expect("encode public key");
// Omitting `iss` routes validation through the fallback-key path (the one that panicked).
let claims = serde_json::json!({
"sub": "mock-subject",
"nbf": 0,
"exp": 4_102_444_800_i64, // year 2100, so the token never expires during CI
});
let token = encode(
&Header::new(Algorithm::RS256),
&claims,
&EncodingKey::from_rsa_pem(private_pem.as_bytes()).expect("valid encoding key"),
)
.expect("sign RS256 token");
let opts = ValidateOptions {
allow_alg_none: false,
fallback_decoding_key: Some(
DecodingKey::from_rsa_pem(public_pem.as_bytes()).expect("valid RSA key"),
),
};
let (ok, message) = validate_jwt_with(&token, &opts, false, false)
.await
.expect("RS256 validation should not panic or error");
assert!(ok, "expected JWT signature verification to succeed: {message}");
assert!(
message.contains("JWT valid via fallback key"),
"expected the fallback-key verification path: {message}"
);
}