Skip to main content

krabka_security/
jwks.rs

1//! JSON Web Key Set (RFC 7517) parsing + JWS signature verification.
2//!
3//! Pure logic, no I/O: this module parses a JWKS *string* and verifies a
4//! token's signature against an in-memory [`Jwks`]. The broker fetches the key
5//! set over the network and feeds it in through a [`JwksHandle`]. Supported key
6//! types / algorithms:
7//!
8//! - `RS256`: RSASSA-PKCS1-v1_5 + SHA-256 over an `RSA` key (`n`, `e`).
9//! - `ES256`: ECDSA P-256 + SHA-256 over an `EC` key (`crv:P-256`, `x`, `y`).
10//!
11//! These cover most OAuth 2.0 identity providers. This module delegates
12//! verification to `ring`, the same crypto backend that the rest of
13//! `crates/security` uses, and does not pull in a JWT crate. That keeps the
14//! dependency surface small and keeps the temporal checks (`exp` / `iat` /
15//! `nbf`) injectable for deterministic tests.
16
17use std::{
18    collections::HashMap,
19    sync::{
20        Arc,
21        atomic::{AtomicI64, Ordering},
22    },
23};
24
25use arc_swap::ArcSwap;
26use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD as B64URL};
27use ring::signature;
28use serde_json::Value;
29use tokio::sync::mpsc;
30
31use crate::AuthError;
32
33/// A single supported public key from a JWKS, keyed externally by `kid`.
34#[derive(Debug, Clone, PartialEq, Eq)]
35enum JwkKey {
36    /// RSA public key: big-endian modulus + exponent, as decoded from the JWK
37    /// `n` / `e` members. This module passes them verbatim to `ring`'s
38    /// `RsaPublicKeyComponents`.
39    Rsa { n: Vec<u8>, e: Vec<u8> },
40    /// EC P-256 public key: the SEC1 uncompressed point `0x04 ‖ x ‖ y`, ready
41    /// for `ring`'s `UnparsedPublicKey`.
42    EcP256 { point: Vec<u8> },
43}
44
45/// A parsed JWK key set.
46///
47/// Lookups are by `kid`. This module stores a key without a `kid` under the
48/// empty string, and uses that key when a token header omits `kid` and the set
49/// holds exactly one key.
50#[derive(Debug, Clone, Default, PartialEq, Eq)]
51pub struct Jwks {
52    keys: HashMap<String, JwkKey>,
53}
54
55impl Jwks {
56    /// An empty key set. Nothing validates until the broker's refresher fills
57    /// it.
58    #[must_use]
59    // cargo-mutants: trivial constructor.
60    #[cfg_attr(test, mutants::skip)]
61    pub fn empty() -> Self {
62        Self {
63            keys: HashMap::new(),
64        }
65    }
66
67    /// Whether the key set has no usable keys.
68    #[must_use]
69    pub fn is_empty(&self) -> bool {
70        self.keys.is_empty()
71    }
72
73    /// Number of usable keys. The count skips unsupported `kty` / `crv`.
74    #[must_use]
75    pub fn len(&self) -> usize {
76        self.keys.len()
77    }
78
79    /// Parse an RFC 7517 JWKS document.
80    ///
81    /// This function skips keys with an unsupported `kty` / `crv` / `alg`. A
82    /// skipped key is not an error, because identity providers publish
83    /// encryption keys and other algs next to the signing keys. A document that
84    /// is not valid JSON, or that has no `keys` array, is an error.
85    ///
86    /// When `ignore_key_use` is `false`, which is the default, this function
87    /// removes keys with `use=enc`. Encryption-only keys are unsuitable for
88    /// signature verification. When `ignore_key_use` is `true`, this function
89    /// keeps all keys regardless of `use`. Strimzi exposes the same toggle.
90    /// Some identity providers serve signing keys with `use:"enc"` by mistake,
91    /// and operators sometimes need to override.
92    ///
93    /// # Errors
94    ///
95    /// [`AuthError::MalformedMessage`] when the document is not valid JSON or
96    /// has no `keys` array.
97    // JWKS key-set load/reload. skip_all keeps the raw JWKS document `s` out of
98    // span fields (it is not secret, but is large); only the non-sensitive
99    // `ignore_key_use` toggle is recorded. `err` surfaces a parse failure.
100    #[tracing::instrument(level = "info", skip_all, fields(ignore_key_use = ignore_key_use), err)]
101    pub fn from_json(s: &str, ignore_key_use: bool) -> Result<Self, AuthError> {
102        let doc: Value = serde_json::from_str(s).map_err(|_| AuthError::MalformedMessage)?;
103        let arr = doc
104            .get("keys")
105            .and_then(Value::as_array)
106            .ok_or(AuthError::MalformedMessage)?;
107        let mut keys = HashMap::new();
108        for jwk in arr {
109            let Some((kid, key)) = parse_one_jwk(jwk, ignore_key_use) else {
110                continue;
111            };
112            keys.insert(kid, key);
113        }
114        Ok(Self { keys })
115    }
116
117    /// Whether this key set holds an entry for `kid`. Convenience for callers
118    /// that want to assert on individual keys without exposing the internal
119    /// map type.
120    #[must_use]
121    pub fn contains_kid(&self, kid: &str) -> bool {
122        self.keys.contains_key(kid)
123    }
124
125    /// Verify a JWS `signature` over `signing_input` with the key that `kid` /
126    /// `alg` select.
127    ///
128    /// `signing_input` is the ASCII `header_b64 "." payload_b64`.
129    ///
130    /// `kid` is the token header `kid` when the header has one. When `kid` is
131    /// `None`, the set must hold exactly one key. `alg` must be `RS256` or
132    /// `ES256` and must match the selected key's type.
133    ///
134    /// # Errors
135    ///
136    /// [`AuthError::InvalidToken`] for an unknown / ambiguous key, an
137    /// alg/key-type mismatch, or a bad signature.
138    // Per-token JWS signature verification. skip_all keeps `signing_input` +
139    // `signature` bytes out of span fields; only the non-sensitive `alg` and
140    // `kid` (a key identifier, not a secret) are recorded. `err` surfaces a
141    // verification failure (Debug).
142    #[tracing::instrument(
143        level = "debug",
144        skip_all,
145        fields(alg = %alg, kid = kid.unwrap_or("<none>")),
146        err
147    )]
148    pub fn verify(
149        &self,
150        kid: Option<&str>,
151        alg: &str,
152        signing_input: &[u8],
153        signature: &[u8],
154    ) -> Result<(), AuthError> {
155        let key = self.select_key(kid)?;
156        match (alg, key) {
157            ("RS256", JwkKey::Rsa { n, e }) => {
158                let pk = signature::RsaPublicKeyComponents { n, e };
159                pk.verify(
160                    &signature::RSA_PKCS1_2048_8192_SHA256,
161                    signing_input,
162                    signature,
163                )
164                .map_err(|_| AuthError::InvalidToken)
165            }
166            ("ES256", JwkKey::EcP256 { point }) => {
167                let pk =
168                    signature::UnparsedPublicKey::new(&signature::ECDSA_P256_SHA256_FIXED, point);
169                pk.verify(signing_input, signature)
170                    .map_err(|_| AuthError::InvalidToken)
171            }
172            // alg present but no key of the matching type, or unsupported alg.
173            _ => Err(AuthError::InvalidToken),
174        }
175    }
176
177    /// Pick the key for `kid`. With no `kid`, succeed only when the set holds a
178    /// single key. Any other count makes the selection ambiguous.
179    fn select_key(&self, kid: Option<&str>) -> Result<&JwkKey, AuthError> {
180        kid.map_or_else(
181            || {
182                if self.keys.len() == 1 {
183                    self.keys.values().next().ok_or(AuthError::InvalidToken)
184                } else {
185                    Err(AuthError::InvalidToken)
186                }
187            },
188            |kid| self.keys.get(kid).ok_or(AuthError::InvalidToken),
189        )
190    }
191}
192
193/// Parse a single JWK object into a `(kid, key)` pair.
194///
195/// This function returns `None` for an unsupported or malformed key. Such a key
196/// is skipped and is not fatal. The function honors `use` when the key has it: it
197/// skips a key marked for encryption with `use: "enc"` unless `ignore_key_use`
198/// is true.
199fn parse_one_jwk(jwk: &Value, ignore_key_use: bool) -> Option<(String, JwkKey)> {
200    if !ignore_key_use && jwk.get("use").and_then(Value::as_str) == Some("enc") {
201        return None;
202    }
203    let kid = jwk
204        .get("kid")
205        .and_then(Value::as_str)
206        .unwrap_or("")
207        .to_owned();
208    match jwk.get("kty").and_then(Value::as_str)? {
209        "RSA" => {
210            let n = b64url_field(jwk, "n")?;
211            let e = b64url_field(jwk, "e")?;
212            Some((kid, JwkKey::Rsa { n, e }))
213        }
214        "EC" => {
215            if jwk.get("crv").and_then(Value::as_str)? != "P-256" {
216                return None;
217            }
218            // P-256 coordinates are 32 bytes; JWK base64url may drop leading
219            // zero bytes, so left-pad before assembling the SEC1 point.
220            let x = left_pad_32(&b64url_field(jwk, "x")?)?;
221            let y = left_pad_32(&b64url_field(jwk, "y")?)?;
222            let mut point = Vec::with_capacity(65);
223            point.push(0x04);
224            point.extend_from_slice(&x);
225            point.extend_from_slice(&y);
226            Some((kid, JwkKey::EcP256 { point }))
227        }
228        _ => None,
229    }
230}
231
232/// base64url-decode a string-valued JWK member.
233fn b64url_field(jwk: &Value, key: &str) -> Option<Vec<u8>> {
234    let s = jwk.get(key).and_then(Value::as_str)?;
235    B64URL.decode(s).ok()
236}
237
238/// Left-pad a big-endian coordinate to exactly 32 bytes.
239///
240/// This function returns `None` if the input is already longer than 32 bytes,
241/// which is a malformed P-256 coordinate.
242fn left_pad_32(bytes: &[u8]) -> Option<[u8; 32]> {
243    let start = 32usize.checked_sub(bytes.len())?;
244    let mut out = [0u8; 32];
245    out.get_mut(start..)?.copy_from_slice(bytes);
246    Some(out)
247}
248
249/// A cheaply-clonable, atomically-swappable holder for the live [`Jwks`].
250///
251/// Mirrors `DynamicServerConfig`: the broker's background JWKS
252/// refresher [`store`](JwksHandle::store)s a freshly-fetched key set while
253/// validators [`load`](JwksHandle::load) the current one with no lock. A clone
254/// of a handle shares the same underlying cell.
255///
256/// A handle paired with a refresher also carries a shared
257/// `last_successful_fetch_ms` counter for the hard cache-expiry checks in the
258/// signed validator. It carries a `signal_tx` mpsc sender as well, for
259/// fire-and-forget on-demand refresh requests when a validator meets an
260/// unknown-kid or bad-signature token. A default-constructed handle carries no
261/// signal sender, and `signal_refresh()` is a silent no-op on those.
262#[derive(Debug, Clone)]
263pub struct JwksHandle {
264    keys: Arc<ArcSwap<Jwks>>,
265    /// Epoch ms of the last successful refresh. Validators
266    /// check this against `expiry_ms` to fail closed on a stale cache.
267    /// The `0` sentinel means never successfully fetched, the initial state.
268    last_successful_fetch_ms: Arc<AtomicI64>,
269    /// Fire-and-forget signal sender to the refresher.
270    /// The validator calls `signal_refresh()` on a verify failure, which is an
271    /// unknown kid or a bad signature. `None` when the validator is not paired
272    /// with a refresher, such as a default-constructed `JwksHandle` in a
273    /// non-signed validator or in the pre-`apply_to` state.
274    signal_tx: Option<mpsc::Sender<()>>,
275}
276
277impl JwksHandle {
278    /// Wrap an initial key set with NO refresher coordination.
279    ///
280    /// The initial key set is often [`Jwks::empty`] at startup.
281    /// Default-constructed handles use this constructor, as do tests that do
282    /// not need the signal channel. `signal_refresh()` on these is a silent
283    /// no-op.
284    #[must_use]
285    pub fn new(jwks: Jwks) -> Self {
286        Self {
287            keys: Arc::new(ArcSwap::from_pointee(jwks)),
288            last_successful_fetch_ms: Arc::new(AtomicI64::new(0)),
289            signal_tx: None,
290        }
291    }
292
293    /// Wrap an initial key set WITH the shared timestamp counter
294    /// and signal sender pre-wired.
295    ///
296    /// The refresher constructs its own `(signal_tx, signal_rx)` pair and
297    /// passes `signal_tx` here. The refresher keeps `signal_rx` and a clone of
298    /// the shared `Arc<AtomicI64>` for the timestamp updates.
299    #[must_use]
300    pub fn new_with_refresher_handles(
301        jwks: Jwks,
302        last_successful_fetch_ms: Arc<AtomicI64>,
303        signal_tx: mpsc::Sender<()>,
304    ) -> Self {
305        Self {
306            keys: Arc::new(ArcSwap::from_pointee(jwks)),
307            last_successful_fetch_ms,
308            signal_tx: Some(signal_tx),
309        }
310    }
311
312    /// Atomically replace the key set.
313    ///
314    /// The refresher calls this after a successful fetch. The swap is
315    /// lock-free. A concurrent `load` sees either the old set or the new set,
316    /// never a torn one.
317    pub fn store(&self, jwks: Jwks) {
318        self.keys.store(Arc::new(jwks));
319    }
320
321    /// Load the current key set. This costs one `Arc` clone.
322    #[must_use]
323    pub fn load(&self) -> Arc<Jwks> {
324        self.keys.load_full()
325    }
326
327    /// Epoch-ms timestamp of the last successful JWKS fetch.
328    ///
329    /// The value is `0` if no fetch has succeeded yet, the initial state.
330    /// Validators compare it against `now_ms - expiry_ms` to enforce hard cache
331    /// expiry.
332    #[must_use]
333    pub fn last_successful_fetch_ms(&self) -> i64 {
334        self.last_successful_fetch_ms.load(Ordering::Relaxed)
335    }
336
337    /// Fire-and-forget signal to the refresher that asks for an on-demand
338    /// refresh, for example after an unknown-kid token.
339    ///
340    /// This call does not block. It drops the signal silently if the channel is
341    /// full, because signals coalesce and one is enough. It is a no-op when
342    /// `signal_tx` is `None`, which is the case for default-constructed
343    /// handles.
344    pub fn signal_refresh(&self) {
345        if let Some(tx) = &self.signal_tx {
346            let _ = tx.try_send(());
347        }
348    }
349}
350
351impl Default for JwksHandle {
352    fn default() -> Self {
353        Self::new(Jwks::empty())
354    }
355}
356
357// Token-minting helpers the sibling `oauthbearer` tests reuse. Declared before
358// the test module so clippy's `items_after_test_module` stays quiet.
359#[cfg(test)]
360pub use tests::{mint_es256, mint_rs256, mint_rs256_with_header};
361
362#[cfg(test)]
363mod tests {
364    use assert2::check;
365    use ring::{
366        rand::SystemRandom,
367        signature::{EcdsaKeyPair, KeyPair, RsaKeyPair},
368    };
369
370    use super::*;
371
372    /// A static RSA-2048 PKCS#8 key, generated with `openssl genpkey`.
373    ///
374    /// `ring` cannot generate RSA keys, so tests mint RS256 tokens from this
375    /// fixed key. Production never sees a private key. It reads `n`/`e` from
376    /// JWKS.
377    const RSA_PKCS8_B64: &str = "MIIEvwIBADANBgkqhkiG9w0BAQEFAASCBKkwggSlAgEAAoIBAQC1Ekoc++7sSsH55QXBCq/aj71helk6ZCTkzYxfLRZXbox0FcV7vOkLNodetJLY7nAUekZLltQ7Q6FJ42geqGV+vgttF63Ue9OP24mPmn/OiFqVYhBaJDRI5BMBLCqZbUfpNBDh7ZOCczwlX8Z5FQS0QJBA4F26H9AKzFRvofwHFk1wxqiGdgwDyClgi+eDnhEGGhBEHuTl1edvTRif88rLDfPHKG1TRqKC6LMXCZQdNy7lrDEGPKHqfW4mb2mq7Vj6h2Jjv+1SpsSxdqX8Tsua4/LrAKvFIXfoZAnjzhACbhXqf1DdSdInZ0i1adY8JpgJQ+WtJ0i9aIOnnmDYwgMvAgMBAAECggEAHqBqUr62Kdd3Odpn/7/cAL7hTHSSVRMNPnoZ7RtGNSGothXcolJQpKnjebxXPkQORxhrfWuUmDWXOVUyjkTzbd2dNyWTLGaJYULD4LtENN3RXIUKuQR4p3+US1V6Gxtl12cMF/rEQYNWQAgUHPTWJ9rny2Fn2Qx6dukauwsOAvCU47fL873sm06SYgPJsLm7MKVeifl8dDudgpURxeC9z37cm9kjjE6n6aiBTNAuBEkMaAbcfgJ0RZfzaMo7IpsOeyOwp932JDlKROpQWKA+lz08YzhkU81qHJYOS/js2F0jxzFz31D9IN+OLu7vRCANFLJl/qnin1JEgVPh7gxSKQKBgQDfrQEsutvH1746ytfE+4jUXyv7Fuaz9MML8uaJbC4hMFdCJuMuLBY07bDE23+4byuWY7JHrgsLRaZ+qpNGWs3LH2x6xsHiK8Ivpuy8TVUJ6hgkPK1cr8yUJxaDcyV8tJAZ+mFmyyWx7wUdlgJFCa2MQF1HnrlBKZvSLWV4CjctZQKBgQDPPR2wLwyk6JlyapsVnCpNBGcXqbJxPh1TM7uPqlODxTzegUK+TMJDZ840u2aBNXf2D5WIJMl+/ohYefOOqK9z2OJUGObnJMgGusH04rdbBoDCdBwfwjiluU7vxbuQKBu8JNXzeb7HJhmgxtXWdJuFYcYbmGu8leFvmUxZTPRfAwKBgQCm6Gpf/m/SiGMjbAnmq+xGzV38V/J/hr2lRPRSx68EhRYX/vy3j55ikJu/yitcbViROIPoiS8kkizTiGWtskSuthw04ev74btd46n0OaCjbVPmdoDHEUgPpbtfC6WFkReWyweztRPD2yBuG2pGKhqe9cilkQOcZHgqNkXpdXYHIQKBgQCO0BQkdNfm0O/l3DdRdhPkjVMqCGSTC3YT/0OS5pK07PhccYF4ONdqsh91UWt7QUiRBf5LGubMoEV/i1LfjbmTQPP/dkWxJjS+Bndg9dfbX6jd2DwFWsfE1OXj8ESoPCuYxV23cr+Y59WjaUK1jhgam9106N3d0P/Q8zidFZ4V1wKBgQDFvIqMLnpaInWhb7kP+X6o0tPQSg+6odMWPnjhwnpSIiUjPUTZV4ijc/d1tPsUemFQxDe+ZreQXDMVGcAVldFnoEMyL8iAtMAHtsSmq2E80RNZfc6nUgy5esQ9rJeX2pH9aZCVvKv6iVTeUtAxS+ltjmEG9BSEI2WQI1WDzPbKiA==";
378
379    fn rsa_pkcs8() -> Vec<u8> {
380        use base64::engine::general_purpose::STANDARD;
381        STANDARD.decode(RSA_PKCS8_B64).unwrap()
382    }
383
384    /// Split the two big-endian INTEGERs, modulus and publicExponent, out of a
385    /// PKCS#1 `RSAPublicKey` DER.
386    ///
387    /// This helper is test-only. Production reads `n`/`e` straight from JWKS
388    /// JSON.
389    fn split_pkcs1_public(der: &[u8]) -> (Vec<u8>, Vec<u8>) {
390        // SEQUENCE { INTEGER n, INTEGER e }
391        let mut p = 0usize;
392        assert2::assert!(der[p] == 0x30);
393        p += 1;
394        let (_seq_len, adv) = read_der_len(&der[p..]);
395        p += adv;
396        let n = read_der_integer(der, &mut p);
397        let e = read_der_integer(der, &mut p);
398        (n, e)
399    }
400
401    fn read_der_len(b: &[u8]) -> (usize, usize) {
402        if b[0] & 0x80 == 0 {
403            (b[0] as usize, 1)
404        } else {
405            let nbytes = (b[0] & 0x7f) as usize;
406            let mut len = 0usize;
407            for i in 0..nbytes {
408                len = (len << 8) | b[1 + i] as usize;
409            }
410            (len, 1 + nbytes)
411        }
412    }
413
414    fn read_der_integer(der: &[u8], p: &mut usize) -> Vec<u8> {
415        assert2::assert!(der[*p] == 0x02);
416        *p += 1;
417        let (len, adv) = read_der_len(&der[*p..]);
418        *p += adv;
419        let mut bytes = der[*p..*p + len].to_vec();
420        *p += len;
421        // Strip the leading zero a DER INTEGER prepends to keep the high bit
422        // clear; JWK `n`/`e` are unsigned big-endian.
423        if bytes.first() == Some(&0) {
424            bytes.remove(0);
425        }
426        bytes
427    }
428
429    fn b64(b: &[u8]) -> String {
430        B64URL.encode(b)
431    }
432
433    /// Mint an RS256 token signed by the static RSA key and return
434    /// `(token, jwks_json)`.
435    ///
436    /// The JWKS advertises the matching public key under `kid`.
437    fn rs256(kid: &str, claims: &str) -> (String, String) {
438        let der = rsa_pkcs8();
439        let kp = RsaKeyPair::from_pkcs8(&der).unwrap();
440        let header = format!("{{\"alg\":\"RS256\",\"kid\":\"{kid}\"}}");
441        let signing_input = format!("{}.{}", b64(header.as_bytes()), b64(claims.as_bytes()));
442        let mut sig = vec![0u8; kp.public().modulus_len()];
443        kp.sign(
444            &signature::RSA_PKCS1_SHA256,
445            &SystemRandom::new(),
446            signing_input.as_bytes(),
447            &mut sig,
448        )
449        .unwrap();
450        let token = format!("{signing_input}.{}", b64(&sig));
451
452        let pkcs1 = kp.public().as_ref();
453        let (n, e) = split_pkcs1_public(pkcs1);
454        let jwks = format!(
455            "{{\"keys\":[{{\"kty\":\"RSA\",\"kid\":\"{kid}\",\"n\":\"{}\",\"e\":\"{}\"}}]}}",
456            b64(&n),
457            b64(&e),
458        );
459        (token, jwks)
460    }
461
462    /// Generate a fresh ES256 key pair and return `(key_pair, jwks_json)`.
463    fn es256_key(kid: &str) -> (EcdsaKeyPair, String) {
464        let rng = SystemRandom::new();
465        let pkcs8 = EcdsaKeyPair::generate_pkcs8(&signature::ECDSA_P256_SHA256_FIXED_SIGNING, &rng)
466            .unwrap();
467        let kp = EcdsaKeyPair::from_pkcs8(
468            &signature::ECDSA_P256_SHA256_FIXED_SIGNING,
469            pkcs8.as_ref(),
470            &rng,
471        )
472        .unwrap();
473        let point = kp.public_key().as_ref(); // 0x04 || x || y
474        let x = &point[1..33];
475        let y = &point[33..65];
476        let jwks = format!(
477            "{{\"keys\":[{{\"kty\":\"EC\",\"crv\":\"P-256\",\"kid\":\"{kid}\",\"x\":\"{}\",\"y\":\"{}\"}}]}}",
478            b64(x),
479            b64(y),
480        );
481        (kp, jwks)
482    }
483
484    fn es256_token(kp: &EcdsaKeyPair, kid: &str, claims: &str) -> String {
485        let header = format!("{{\"alg\":\"ES256\",\"kid\":\"{kid}\"}}");
486        let signing_input = format!("{}.{}", b64(header.as_bytes()), b64(claims.as_bytes()));
487        let sig = kp
488            .sign(&SystemRandom::new(), signing_input.as_bytes())
489            .unwrap();
490        format!("{signing_input}.{}", b64(sig.as_ref()))
491    }
492
493    /// Pull `(kid, alg, signing_input, sig)` out of a compact JWS for the
494    /// verify-level tests.
495    fn parts(token: &str) -> (Option<String>, String, Vec<u8>, Vec<u8>) {
496        let segs: Vec<&str> = token.split('.').collect();
497        let header: Value = serde_json::from_slice(&B64URL.decode(segs[0]).unwrap()).unwrap();
498        let kid = header.get("kid").and_then(Value::as_str).map(str::to_owned);
499        let alg = header
500            .get("alg")
501            .and_then(Value::as_str)
502            .unwrap()
503            .to_owned();
504        let signing_input = format!("{}.{}", segs[0], segs[1]).into_bytes();
505        let sig = B64URL.decode(segs[2]).unwrap();
506        (kid, alg, signing_input, sig)
507    }
508
509    #[test]
510    fn empty_jwks_has_no_keys() {
511        let jwks = Jwks::empty();
512        check!((jwks.is_empty(), jwks.len(), jwks.contains_kid("k1")) == (true, 0, false));
513    }
514
515    #[test]
516    fn jwks_with_key_is_not_empty() {
517        let (_token, jwks_json) = rs256("k1", "{\"sub\":\"alice\",\"exp\":9999999999}");
518        let jwks = Jwks::from_json(&jwks_json, false).expect("parse jwks");
519        check!((jwks.is_empty(), jwks.len(), jwks.contains_kid("k1")) == (false, 1, true));
520    }
521
522    #[test]
523    fn left_pad_32_accepts_short_and_exact_coordinates_only() {
524        assert2::assert!(left_pad_32(&[0xAA; 33]).is_none());
525
526        let short = left_pad_32(&[0x01, 0x02]).expect("short coordinate padded");
527        assert2::assert!(&short[..30] == &[0u8; 30][..]);
528        assert2::assert!(&short[30..] == &[0x01, 0x02][..]);
529
530        let exact = [0x7Fu8; 32];
531        assert2::assert!(left_pad_32(&exact).expect("exact coordinate") == exact);
532    }
533
534    #[test]
535    fn parses_mixed_rsa_and_ec_set() {
536        let (_kp, ec_jwks) = es256_key("ec1");
537        let (_t, rsa_jwks) = rs256("rsa1", "{\"sub\":\"a\",\"exp\":9999999999}");
538        // Merge the two single-key documents into one set.
539        let ec: Value = serde_json::from_str(&ec_jwks).unwrap();
540        let rsa: Value = serde_json::from_str(&rsa_jwks).unwrap();
541        let mut keys = ec["keys"].as_array().unwrap().clone();
542        keys.extend(rsa["keys"].as_array().unwrap().clone());
543        let merged = serde_json::json!({ "keys": keys }).to_string();
544        let jwks = Jwks::from_json(&merged, false).unwrap();
545        assert2::assert!(jwks.len() == 2);
546    }
547
548    #[test]
549    fn skips_unsupported_and_enc_keys() {
550        let jwks = Jwks::from_json(
551            r#"{"keys":[
552                {"kty":"oct","k":"AAAA","kid":"sym"},
553                {"kty":"EC","crv":"P-384","kid":"big"},
554                {"kty":"RSA","use":"enc","kid":"enc1","n":"AQAB","e":"AQAB"}
555            ]}"#,
556            false,
557        )
558        .unwrap();
559        assert2::assert!(jwks.is_empty());
560    }
561
562    #[test]
563    fn rejects_non_json_and_missing_keys_array() {
564        for (_name, input) in [("invalid JSON", "not json"), ("missing keys array", "{}")] {
565            assert2::assert!(Jwks::from_json(input, false) == Err(AuthError::MalformedMessage));
566        }
567    }
568
569    #[test]
570    fn verifies_valid_rs256() {
571        let (token, jwks_json) = rs256("rsa1", "{\"sub\":\"admin\",\"exp\":9999999999}");
572        let jwks = Jwks::from_json(&jwks_json, false).unwrap();
573        let (kid, alg, si, sig) = parts(&token);
574        assert2::assert!(jwks.verify(kid.as_deref(), &alg, &si, &sig).is_ok());
575    }
576
577    #[test]
578    fn verifies_valid_es256() {
579        let (kp, jwks_json) = es256_key("ec1");
580        let token = es256_token(&kp, "ec1", "{\"sub\":\"admin\",\"exp\":9999999999}");
581        let jwks = Jwks::from_json(&jwks_json, false).unwrap();
582        let (kid, alg, si, sig) = parts(&token);
583        assert2::assert!(jwks.verify(kid.as_deref(), &alg, &si, &sig).is_ok());
584    }
585
586    #[test]
587    fn rejects_tampered_rs256_signature() {
588        let (token, jwks_json) = rs256("rsa1", "{\"sub\":\"admin\",\"exp\":9999999999}");
589        let jwks = Jwks::from_json(&jwks_json, false).unwrap();
590        let (kid, alg, si, mut sig) = parts(&token);
591        sig[0] ^= 0xff;
592        assert2::assert!(
593            jwks.verify(kid.as_deref(), &alg, &si, &sig) == Err(AuthError::InvalidToken)
594        );
595    }
596
597    #[test]
598    fn rejects_unknown_kid() {
599        let (token, jwks_json) = rs256("rsa1", "{\"sub\":\"a\",\"exp\":9999999999}");
600        let jwks = Jwks::from_json(&jwks_json, false).unwrap();
601        let (_kid, alg, si, sig) = parts(&token);
602        assert2::assert!(
603            jwks.verify(Some("other"), &alg, &si, &sig) == Err(AuthError::InvalidToken)
604        );
605    }
606
607    #[test]
608    fn rejects_missing_kid_when_set_has_multiple_keys() {
609        let (kp, ec_jwks) = es256_key("ec1");
610        let (_t, rsa_jwks) = rs256("rsa1", "{\"sub\":\"a\",\"exp\":9999999999}");
611        let ec: Value = serde_json::from_str(&ec_jwks).unwrap();
612        let rsa: Value = serde_json::from_str(&rsa_jwks).unwrap();
613        let mut keys = ec["keys"].as_array().unwrap().clone();
614        keys.extend(rsa["keys"].as_array().unwrap().clone());
615        let merged = serde_json::json!({ "keys": keys }).to_string();
616        let jwks = Jwks::from_json(&merged, false).unwrap();
617        // Token minted without a kid in the header.
618        let header = b64(b"{\"alg\":\"ES256\"}");
619        let payload = b64(b"{\"sub\":\"a\",\"exp\":9999999999}");
620        let signing_input = format!("{header}.{payload}");
621        let sig = kp
622            .sign(&SystemRandom::new(), signing_input.as_bytes())
623            .unwrap();
624        assert2::assert!(
625            jwks.verify(None, "ES256", signing_input.as_bytes(), sig.as_ref())
626                == Err(AuthError::InvalidToken)
627        );
628    }
629
630    #[test]
631    fn accepts_missing_kid_when_set_has_single_key() {
632        let (token, jwks_json) =
633            mint_rs256_with_header("{\"alg\":\"RS256\"}", "{\"sub\":\"a\",\"exp\":9999999999}");
634        let jwks = Jwks::from_json(&jwks_json, false).unwrap();
635        let (kid, alg, si, sig) = parts(&token);
636        assert2::assert!(kid.is_none());
637        jwks.verify(kid.as_deref(), &alg, &si, &sig)
638            .expect("single-key JWKS should verify a token without kid");
639    }
640
641    #[test]
642    fn rejects_es256_signed_by_wrong_key() {
643        let (kp_a, _jwks_a) = es256_key("ec1");
644        let (_kp_b, jwks_b) = es256_key("ec1"); // same kid, different key
645        let token = es256_token(&kp_a, "ec1", "{\"sub\":\"a\",\"exp\":9999999999}");
646        let jwks = Jwks::from_json(&jwks_b, false).unwrap();
647        let (kid, alg, si, sig) = parts(&token);
648        assert2::assert!(
649            jwks.verify(kid.as_deref(), &alg, &si, &sig) == Err(AuthError::InvalidToken)
650        );
651    }
652
653    #[test]
654    fn rejects_alg_key_type_mismatch() {
655        // RSA key set, but ask it to verify with ES256.
656        let (token, jwks_json) = rs256("rsa1", "{\"sub\":\"a\",\"exp\":9999999999}");
657        let jwks = Jwks::from_json(&jwks_json, false).unwrap();
658        let (kid, _alg, si, sig) = parts(&token);
659        assert2::assert!(
660            jwks.verify(kid.as_deref(), "ES256", &si, &sig) == Err(AuthError::InvalidToken)
661        );
662    }
663
664    #[test]
665    fn handle_store_and_load_round_trips() {
666        let h = JwksHandle::default();
667        assert2::assert!(h.load().is_empty());
668        let (_t, jwks_json) = rs256("rsa1", "{\"sub\":\"a\",\"exp\":9999999999}");
669        h.store(Jwks::from_json(&jwks_json, false).unwrap());
670        assert2::assert!(h.load().len() == 1);
671    }
672
673    // ---- ignore_key_use filter + handle helpers ------------------
674
675    #[test]
676    fn parse_jwks_filters_use_enc_by_default() {
677        let json = r#"{
678            "keys": [
679                {"kty":"RSA","kid":"sig-key","use":"sig","n":"AQAB","e":"AQAB"},
680                {"kty":"RSA","kid":"enc-key","use":"enc","n":"AQAB","e":"AQAB"}
681            ]
682        }"#;
683        let jwks = Jwks::from_json(json, false).expect("parses");
684        assert2::assert!(
685            (jwks.contains_kid("sig-key"), jwks.contains_kid("enc-key")) == (true, false)
686        );
687    }
688
689    #[test]
690    fn parse_jwks_keeps_use_enc_when_ignore_key_use_true() {
691        let json = r#"{
692            "keys": [
693                {"kty":"RSA","kid":"sig-key","use":"sig","n":"AQAB","e":"AQAB"},
694                {"kty":"RSA","kid":"enc-key","use":"enc","n":"AQAB","e":"AQAB"}
695            ]
696        }"#;
697        let jwks = Jwks::from_json(json, true).expect("parses");
698        assert2::assert!(
699            (jwks.contains_kid("sig-key"), jwks.contains_kid("enc-key")) == (true, true)
700        );
701    }
702
703    #[test]
704    fn parse_jwks_keeps_keys_with_absent_use_field_regardless() {
705        let json = r#"{
706            "keys": [
707                {"kty":"RSA","kid":"no-use","n":"AQAB","e":"AQAB"}
708            ]
709        }"#;
710        assert2::assert!(Jwks::from_json(json, false).unwrap().contains_kid("no-use"));
711        assert2::assert!(Jwks::from_json(json, true).unwrap().contains_kid("no-use"));
712    }
713
714    #[test]
715    fn default_handle_signal_refresh_is_silent_noop() {
716        // A handle constructed without refresher wiring must not panic on
717        // signal_refresh(); validators may share the same code path whether
718        // or not a refresher is paired.
719        let h = JwksHandle::default();
720        h.signal_refresh();
721        assert2::assert!(h.last_successful_fetch_ms() == 0);
722    }
723
724    #[tokio::test(flavor = "current_thread")]
725    async fn paired_handle_signal_refresh_delivers_to_receiver() {
726        let (tx, mut rx) = mpsc::channel::<()>(1);
727        let ts = Arc::new(AtomicI64::new(0));
728        let h = JwksHandle::new_with_refresher_handles(Jwks::empty(), ts, tx);
729        h.signal_refresh();
730        assert2::assert!(rx.try_recv().is_ok());
731    }
732
733    // Expose minting helpers to the sibling `oauthbearer` tests.
734    pub fn mint_rs256(kid: &str, claims: &str) -> (String, String) {
735        rs256(kid, claims)
736    }
737
738    /// Mint an RS256 token with a fully-controlled header JSON, so the caller
739    /// can drive `typ` / `kid` / extra header fields.
740    ///
741    /// This helper returns `(token, jwks_json)` like the other minters.
742    pub fn mint_rs256_with_header(header_json: &str, claims: &str) -> (String, String) {
743        let der = rsa_pkcs8();
744        let kp = RsaKeyPair::from_pkcs8(&der).unwrap();
745        let signing_input = format!("{}.{}", b64(header_json.as_bytes()), b64(claims.as_bytes()));
746        let mut sig = vec![0u8; kp.public().modulus_len()];
747        kp.sign(
748            &signature::RSA_PKCS1_SHA256,
749            &SystemRandom::new(),
750            signing_input.as_bytes(),
751            &mut sig,
752        )
753        .unwrap();
754        let token = format!("{signing_input}.{}", b64(&sig));
755
756        // Echo the JWKS used by `rs256` so the validator can verify the
757        // signature. Default kid="k1" so tests don't have to thread it.
758        let pkcs1 = kp.public().as_ref();
759        let (n, e) = split_pkcs1_public(pkcs1);
760        let jwks = format!(
761            "{{\"keys\":[{{\"kty\":\"RSA\",\"kid\":\"k1\",\"n\":\"{}\",\"e\":\"{}\"}}]}}",
762            b64(&n),
763            b64(&e),
764        );
765        (token, jwks)
766    }
767
768    /// Mint an ES256 token under a *fresh* key and return `(token, jwks_json)`.
769    ///
770    /// Each call generates a new key pair, so two calls give independent keys.
771    /// That helps key-rotation tests, where RS256's fixed key cannot differ.
772    pub fn mint_es256(kid: &str, claims: &str) -> (String, String) {
773        let (kp, jwks) = es256_key(kid);
774        let token = es256_token(&kp, kid, claims);
775        (token, jwks)
776    }
777}