Skip to main content

identity_kit/resolver/
jwks.rs

1//! `JwksResolver`: the keys of an HTTPS issuer, from its JWT VC issuer metadata.
2
3use core::fmt;
4use std::collections::HashMap;
5use std::sync::{Arc, PoisonError, RwLock};
6use std::time::{Duration, Instant};
7
8use serde::Deserialize;
9use serde_json::Value;
10use url::Url;
11
12use super::{KeyReference, KeyResolutionError, KeyResolver, KeyRole};
13use crate::jose::Jwk;
14use crate::methods::web::{HttpClient, HttpError};
15
16/// Resolves a [`KeyReference::Jwks`] through the JWT VC issuer metadata of the issuer (SD-JWT VC
17/// §3.5); every other reference is [`Unsupported`](KeyResolutionError::Unsupported).
18///
19/// For an issuer `https://host[:port][/path]` it fetches
20/// `https://host[:port]/.well-known/jwt-vc-issuer[/path]` (a trailing `/` of the path dropped),
21/// requires the metadata's `issuer` to be the issuer, takes its `jwks`, or fetches its
22/// `jwks_uri`, and returns the one key whose `kid` is the reference's. Issuer and `jwks_uri`
23/// must be `https`, and the issuer has no query, fragment or credentials. The key set of an
24/// issuer is cached for a time to live ([`DEFAULT_TTL`](Self::DEFAULT_TTL) unless set with
25/// [`with_ttl`](Self::with_ttl)), so a rotated key is found once its entry expires.
26///
27/// The issuer comes from the token, so the client fetches whatever a token names: `client` must
28/// enforce the protections `ReqwestClient` (feature `reqwest`) does (HTTPS only, no redirects,
29/// no non-public addresses after name resolution, a body limit and a timeout), or resolution
30/// becomes a request forgery primitive. Resolving says nothing about whether the issuer is
31/// trusted; that stays the application's policy.
32///
33/// # Examples
34///
35/// ```
36/// use identity_kit::methods::web::{HttpClient, HttpError, HttpResponse};
37/// use identity_kit::resolver::{JwksResolver, KeyReference, KeyResolver as _, KeyRole};
38/// use url::Url;
39///
40/// /// Serves the metadata of `https://example.com/issuer`.
41/// struct Served;
42///
43/// #[async_trait::async_trait]
44/// impl HttpClient for Served {
45///     async fn get(&self, url: &Url) -> Result<HttpResponse, HttpError> {
46///         assert_eq!(url.as_str(), "https://example.com/.well-known/jwt-vc-issuer/issuer");
47///         Ok(HttpResponse::new(200, r#"{
48///           "issuer": "https://example.com/issuer",
49///           "jwks": { "keys": [{
50///             "kid": "key-1", "kty": "OKP", "crv": "Ed25519",
51///             "x": "11qYAYKxCrfVS_7TyWQHOg7hcvPapiMlrwIaaPcHURo"
52///           }]}
53///         }"#))
54///     }
55/// }
56///
57/// # tokio::runtime::Builder::new_current_thread().build()?.block_on(async {
58/// let resolver = JwksResolver::new(Served);
59/// let reference = KeyReference::Jwks {
60///     issuer: Url::parse("https://example.com/issuer")?,
61///     kid: "key-1".to_owned(),
62/// };
63/// let key = resolver.resolve_key(&reference, KeyRole::Issuer).await?;
64/// assert_eq!(key.crv(), Some("Ed25519"));
65/// # Ok::<(), Box<dyn std::error::Error>>(())
66/// # })?;
67/// # Ok::<(), Box<dyn std::error::Error>>(())
68/// ```
69pub struct JwksResolver<C> {
70    client: C,
71    ttl: Duration,
72    capacity: usize,
73    cache: RwLock<HashMap<Url, Entry>>,
74}
75
76/// The key set of one issuer and when it expires.
77struct Entry {
78    keys: Arc<[Value]>,
79    expires_at: Option<Instant>,
80}
81
82impl Entry {
83    fn is_live(&self, now: Instant) -> bool {
84        self.expires_at.is_none_or(|expires_at| expires_at > now)
85    }
86}
87
88/// The members of JWT VC issuer metadata this resolver reads.
89#[derive(Deserialize)]
90struct Metadata {
91    issuer: String,
92    #[serde(default)]
93    jwks: Option<KeySet>,
94    #[serde(default)]
95    jwks_uri: Option<String>,
96}
97
98/// A JWK set (RFC 7517 §5), its keys left as JSON so that a key of a type this crate does not
99/// read does not spoil the others.
100#[derive(Deserialize)]
101struct KeySet {
102    keys: Vec<Value>,
103}
104
105impl<C> JwksResolver<C> {
106    /// How long [`new`](Self::new) keeps an issuer's key set: five minutes.
107    pub const DEFAULT_TTL: Duration = Duration::from_secs(300);
108
109    /// How many issuers [`new`](Self::new) keeps key sets of.
110    pub const DEFAULT_CAPACITY: usize = 256;
111
112    /// A resolver fetching with `client`, keeping key sets for
113    /// [`DEFAULT_TTL`](Self::DEFAULT_TTL).
114    pub fn new(client: C) -> Self {
115        Self {
116            client,
117            ttl: Self::DEFAULT_TTL,
118            capacity: Self::DEFAULT_CAPACITY,
119            cache: RwLock::new(HashMap::new()),
120        }
121    }
122
123    /// This resolver keeping key sets for `ttl`; zero keeps none.
124    #[must_use]
125    pub fn with_ttl(mut self, ttl: Duration) -> Self {
126        self.ttl = ttl;
127        self
128    }
129
130    /// This resolver keeping the key sets of at most `capacity` issuers (at least one).
131    #[must_use]
132    pub fn with_capacity(mut self, capacity: usize) -> Self {
133        self.capacity = capacity.max(1);
134        self
135    }
136
137    /// How long a key set is kept.
138    pub fn ttl(&self) -> Duration {
139        self.ttl
140    }
141
142    /// The HTTP client.
143    pub fn client(&self) -> &C {
144        &self.client
145    }
146
147    /// The cached key set of `issuer`, if live.
148    fn cached(&self, issuer: &Url) -> Option<Arc<[Value]>> {
149        let cache = self.cache.read().unwrap_or_else(PoisonError::into_inner);
150        cache
151            .get(issuer)
152            .filter(|entry| entry.is_live(Instant::now()))
153            .map(|entry| Arc::clone(&entry.keys))
154    }
155
156    /// Keeps `keys` as the key set of `issuer`, evicting expired entries and then the one that
157    /// expires soonest when full.
158    fn store(&self, issuer: &Url, keys: Arc<[Value]>) {
159        if self.ttl.is_zero() {
160            return;
161        }
162        let now = Instant::now();
163        let mut cache = self.cache.write().unwrap_or_else(PoisonError::into_inner);
164        if !cache.contains_key(issuer) && cache.len() >= self.capacity {
165            cache.retain(|_, entry| entry.is_live(now));
166            if cache.len() >= self.capacity
167                && let Some(soonest) = cache
168                    .iter()
169                    .min_by_key(|(_, entry)| entry.expires_at)
170                    .map(|(issuer, _)| issuer.clone())
171            {
172                cache.remove(&soonest);
173            }
174        }
175        cache.insert(
176            issuer.clone(),
177            Entry {
178                keys,
179                expires_at: now.checked_add(self.ttl),
180            },
181        );
182    }
183}
184
185impl<C: HttpClient> JwksResolver<C> {
186    /// The key set of `issuer`, from the cache or fetched.
187    async fn key_set(&self, issuer: &Url) -> Result<Arc<[Value]>, JwksError> {
188        if let Some(keys) = self.cached(issuer) {
189            return Ok(keys);
190        }
191        let url = metadata_url(issuer)?;
192        let metadata: Metadata = self.fetch_json(&url).await?;
193        if Url::parse(&metadata.issuer).ok().as_ref() != Some(issuer) {
194            return Err(JwksError::IssuerMismatch {
195                expected: issuer.clone(),
196                found: metadata.issuer,
197            });
198        }
199        let keys = match (metadata.jwks, metadata.jwks_uri) {
200            (Some(jwks), None) => jwks.keys,
201            (None, Some(jwks_uri)) => {
202                let jwks_uri = Url::parse(&jwks_uri).map_err(|e| JwksError::InvalidMetadata {
203                    url: url.clone(),
204                    reason: format!("`jwks_uri` {jwks_uri:?}: {e}"),
205                })?;
206                if jwks_uri.scheme() != "https" {
207                    return Err(JwksError::NotHttps(jwks_uri));
208                }
209                self.fetch_json::<KeySet>(&jwks_uri).await?.keys
210            }
211            (Some(_), Some(_)) => {
212                return Err(JwksError::InvalidMetadata {
213                    url,
214                    reason: "both `jwks` and `jwks_uri` are present".to_owned(),
215                });
216            }
217            (None, None) => {
218                return Err(JwksError::InvalidMetadata {
219                    url,
220                    reason: "neither `jwks` nor `jwks_uri` is present".to_owned(),
221                });
222            }
223        };
224        let keys: Arc<[Value]> = keys.into();
225        self.store(issuer, Arc::clone(&keys));
226        Ok(keys)
227    }
228
229    /// The JSON body at `url`, parsed.
230    async fn fetch_json<T: serde::de::DeserializeOwned>(&self, url: &Url) -> Result<T, JwksError> {
231        let response = self
232            .client
233            .get(url)
234            .await
235            .map_err(|source| JwksError::Http {
236                url: url.clone(),
237                source,
238            })?;
239        if !(200..300).contains(&response.status) {
240            return Err(JwksError::HttpStatus {
241                url: url.clone(),
242                status: response.status,
243            });
244        }
245        serde_json::from_slice(&response.body).map_err(|e| JwksError::InvalidMetadata {
246            url: url.clone(),
247            reason: e.to_string(),
248        })
249    }
250}
251
252/// `https://host[:port]/.well-known/jwt-vc-issuer[/path]` for `issuer`.
253fn metadata_url(issuer: &Url) -> Result<Url, JwksError> {
254    if issuer.scheme() != "https" {
255        return Err(JwksError::NotHttps(issuer.clone()));
256    }
257    if issuer.query().is_some()
258        || issuer.fragment().is_some()
259        || issuer.host().is_none()
260        || !issuer.username().is_empty()
261        || issuer.password().is_some()
262    {
263        return Err(JwksError::InvalidIssuer(issuer.clone()));
264    }
265    let path = issuer.path().trim_end_matches('/');
266    let mut url = issuer.clone();
267    url.set_path(&format!("/.well-known/jwt-vc-issuer{path}"));
268    Ok(url)
269}
270
271/// The one key of `keys` whose `kid` is `kid`, as a public JWK.
272fn select(keys: &[Value], issuer: &Url, kid: &str) -> Result<Jwk, JwksError> {
273    let mut matching = keys
274        .iter()
275        .filter(|key| key.get("kid").and_then(Value::as_str) == Some(kid));
276    let key = matching.next().ok_or_else(|| JwksError::UnknownKid {
277        issuer: issuer.clone(),
278        kid: kid.to_owned(),
279    })?;
280    if matching.next().is_some() {
281        return Err(JwksError::DuplicateKid {
282            issuer: issuer.clone(),
283            kid: kid.to_owned(),
284        });
285    }
286    let jwk: Jwk = serde_json::from_value(key.clone()).map_err(|e| JwksError::InvalidKey {
287        kid: kid.to_owned(),
288        reason: e.to_string(),
289    })?;
290    if jwk.is_private() {
291        return Err(JwksError::InvalidKey {
292            kid: kid.to_owned(),
293            reason: "the key set publishes private material".to_owned(),
294        });
295    }
296    Ok(jwk)
297}
298
299impl<C> fmt::Debug for JwksResolver<C>
300where
301    C: fmt::Debug,
302{
303    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
304        f.debug_struct("JwksResolver")
305            .field("client", &self.client)
306            .field("ttl", &self.ttl)
307            .field("capacity", &self.capacity)
308            .finish_non_exhaustive()
309    }
310}
311
312#[async_trait::async_trait]
313impl<C: HttpClient> KeyResolver for JwksResolver<C> {
314    async fn resolve_key(&self, key: &KeyReference, _: KeyRole) -> Result<Jwk, KeyResolutionError> {
315        let KeyReference::Jwks { issuer, kid } = key else {
316            return Err(KeyResolutionError::unsupported(key));
317        };
318        let keys = self.key_set(issuer).await?;
319        Ok(select(&keys, issuer, kid)?)
320    }
321}
322
323/// Why a [`JwksResolver`] found no key.
324#[derive(Debug, thiserror::Error)]
325#[non_exhaustive]
326pub enum JwksError {
327    /// The issuer or `jwks_uri` is not an `https` URL.
328    #[error("`{0}` is not an HTTPS URL")]
329    NotHttps(Url),
330    /// The issuer has a query, a fragment or credentials, or no host.
331    #[error(
332        "`{0}` is not an issuer identifier: it has a query, a fragment or credentials, or no host"
333    )]
334    InvalidIssuer(Url),
335    /// The [`HttpClient`] got no response.
336    #[error("fetching `{url}` failed: {source}")]
337    Http {
338        /// The URL fetched.
339        url: Url,
340        /// Why there was no response.
341        #[source]
342        source: HttpError,
343    },
344    /// The server answered with a status other than `2xx`.
345    #[error("fetching `{url}` returned HTTP {status}")]
346    HttpStatus {
347        /// The URL fetched.
348        url: Url,
349        /// The status code.
350        status: u16,
351    },
352    /// The metadata or key set is not what SD-JWT VC §3.5 and RFC 7517 §5 describe.
353    #[error("`{url}`: {reason}")]
354    InvalidMetadata {
355        /// The URL fetched.
356        url: Url,
357        /// What is wrong with the body.
358        reason: String,
359    },
360    /// The metadata names another issuer.
361    #[error("the metadata of `{expected}` names the issuer {found:?}")]
362    IssuerMismatch {
363        /// The issuer whose metadata was fetched.
364        expected: Url,
365        /// The `issuer` the metadata names.
366        found: String,
367    },
368    /// No key of the issuer has the `kid`.
369    #[error("issuer `{issuer}` has no key `{kid}`")]
370    UnknownKid {
371        /// The issuer.
372        issuer: Url,
373        /// The `kid`.
374        kid: String,
375    },
376    /// Several keys of the issuer have the `kid`, so it names none of them.
377    #[error("issuer `{issuer}` has several keys `{kid}`")]
378    DuplicateKid {
379        /// The issuer.
380        issuer: Url,
381        /// The `kid`.
382        kid: String,
383    },
384    /// The key is not a public JWK this crate reads.
385    #[error("key `{kid}`: {reason}")]
386    InvalidKey {
387        /// The `kid`.
388        kid: String,
389        /// What is wrong with it.
390        reason: String,
391    },
392}
393
394impl From<JwksError> for KeyResolutionError {
395    fn from(error: JwksError) -> Self {
396        Self::new(error)
397    }
398}