diff --git a/backend/windmill-common/src/guest_jwt.rs b/backend/windmill-common/src/guest_jwt.rs index c1d8f0e225..ce5b113be8 100644 --- a/backend/windmill-common/src/guest_jwt.rs +++ b/backend/windmill-common/src/guest_jwt.rs @@ -9,7 +9,7 @@ use std::sync::Arc; use std::time::{Duration, Instant}; use jsonwebtoken::{ - jwk::{AlgorithmParameters, Jwk, JwkSet, PublicKeyUse}, + jwk::{AlgorithmParameters, Jwk, PublicKeyUse}, Algorithm, DecodingKey, Validation, }; use quick_cache::sync::Cache; @@ -249,11 +249,23 @@ pub async fn fetch_jwks(url: &str) -> Result> { } body.extend_from_slice(&chunk); } - let set: JwkSet = serde_json::from_slice(&body) - .map_err(|e| Error::BadRequest(format!("JWKS is not a JSON Web Key Set: {e}")))?; - let keys: HashMap = set - .keys - .into_iter() + parse_jwks_keys(&body) +} + +/// The usable signing keys in a JWKS body, by `kid`. Each key is parsed on its own and +/// one that does not model as a JWT key is skipped, not fatal: a set may legitimately +/// carry an encryption key (say `alg: "RSA-OAEP"`, which is not in jsonwebtoken's signing +/// `Algorithm` enum and would fail whole-set deserialization) beside its signing keys. +fn parse_jwks_keys(body: &[u8]) -> Result> { + let set: serde_json::Value = serde_json::from_slice(body) + .map_err(|e| Error::BadRequest(format!("JWKS is not JSON: {e}")))?; + let entries = set + .get("keys") + .and_then(|k| k.as_array()) + .ok_or_else(|| Error::BadRequest("JWKS has no `keys` array".to_string()))?; + let keys: HashMap = entries + .iter() + .filter_map(|entry| serde_json::from_value::(entry.clone()).ok()) .filter(|jwk| jwk_algorithms(jwk).is_some()) .filter_map(|jwk| jwk.common.key_id.clone().map(|kid| (kid, jwk))) .collect(); @@ -444,6 +456,22 @@ mod tests { assert_eq!(jwk_algorithms(&k), None); } + #[test] + fn a_mixed_use_jwks_keeps_only_the_signing_keys() { + // An encryption key (RSA-OAEP is not in jsonwebtoken's signing Algorithm enum) + // beside a signing key must not fail the whole set. + let body = serde_json::json!({ + "keys": [ + {"kty":"RSA","alg":"RSA-OAEP","kid":"enc","use":"enc","n":"aa","e":"AQAB"}, + {"kty":"EC","crv":"P-256","kid":"sig","x":"aa","y":"bb"} + ] + }) + .to_string(); + let keys = parse_jwks_keys(body.as_bytes()).expect("the signing key survives"); + assert!(keys.contains_key("sig")); + assert!(!keys.contains_key("enc")); + } + #[test] fn key_ops_without_verify_is_refused() { let enc = jwk(serde_json::json!({"kty":"RSA","key_ops":["encrypt"],"n":"aa","e":"AQAB"}));