spiffe/bundle/jwt/
mod.rs

1//! JWT bundle types.
2
3use std::collections::HashMap;
4
5use jsonwebtoken::jwk::{Jwk, JwkSet};
6use thiserror::Error;
7
8use crate::bundle::{Bundle, BundleRefSource};
9use crate::spiffe_id::TrustDomain;
10
11/// This type contains a collection of trusted JWT authorities (Public keys) for a `TrustDomain`.
12#[derive(Debug, Clone, Eq, PartialEq)]
13pub struct JwtBundle {
14    trust_domain: TrustDomain,
15    jwt_authorities: HashMap<String, Jwk>,
16}
17
18impl Bundle for JwtBundle {}
19
20/// This type contains a set of [`JwtBundle`], keyed by [`TrustDomain`].
21#[derive(Debug, Clone, Eq, PartialEq)]
22pub struct JwtBundleSet {
23    bundles: HashMap<TrustDomain, JwtBundle>,
24}
25
26/// An error that can arise creating a new [`JwtBundle`].
27#[derive(Debug, Error)]
28#[non_exhaustive]
29pub enum JwtBundleError {
30    /// The JWT authority misses the key ID that identifies it.
31    #[error("missing key ID")]
32    MissingKeyId,
33    /// There was a problem deserializing bytes into a Json JWT keys set.
34    #[error("cannot deserialize json jwk set")]
35    Deserialize(#[from] serde_json::Error),
36}
37
38impl JwtBundle {
39    /// Creates an empty `JwtBundle` for the given `TrustDomain`.
40    pub fn new(trust_domain: TrustDomain) -> Self {
41        Self {
42            trust_domain,
43            jwt_authorities: HashMap::new(),
44        }
45    }
46
47    /// Parses a `JwtBundle` from bytes representing a set of  JWT authorities. The data must be
48    /// a standard RFC 7517 JWTK document.
49    ///
50    /// # Arguments
51    ///
52    /// * `trust_domain` -  A [`TrustDomain`] to associate to the bundle.
53    /// * `jwt_authorities` -  A slice of bytes representing a set of JWT authorities in a standard RFC 7517 JWKS document.
54    ///
55    /// # Errors
56    ///
57    /// If the function cannot parse the bytes into a JSON WebKey Set, a [`JwtBundleError`] variant will be returned.
58    ///
59    /// # Examples
60    ///
61    /// ```
62    /// use spiffe::{JwtBundle, TrustDomain};
63    ///
64    /// let jwt_authorities = r#"{
65    ///     "keys": [
66    ///         {
67    ///             "kty": "EC",
68    ///             "kid": "C6vs25welZOx6WksNYfbMfiw9l96pMnD",
69    ///             "crv": "P-256",
70    ///             "x": "ngLYQnlfF6GsojUwqtcEE3WgTNG2RUlsGhK73RNEl5k",
71    ///             "y": "tKbiDSUSsQ3F1P7wteeHNXIcU-cx6CgSbroeQrQHTLM"
72    ///         }
73    ///     ]
74    ///  }"#
75    /// .as_bytes();
76    /// let trust_domain = TrustDomain::new("example.org").unwrap();
77    /// let jwt_bundle = JwtBundle::from_jwt_authorities(trust_domain, jwt_authorities).unwrap();
78    ///
79    /// assert!(jwt_bundle
80    ///     .find_jwt_authority("C6vs25welZOx6WksNYfbMfiw9l96pMnD")
81    ///     .is_some());
82    /// ```
83    pub fn from_jwt_authorities(
84        trust_domain: TrustDomain,
85        jwt_authorities: &[u8],
86    ) -> Result<Self, JwtBundleError> {
87        let mut authorities = HashMap::new();
88        let jwk_set: JwkSet = serde_json::from_slice(jwt_authorities)?;
89
90        for key in jwk_set.keys.into_iter() {
91            let key_id = match &key.common.key_id {
92                Some(k) => k,
93                None => return Err(JwtBundleError::MissingKeyId),
94            };
95            authorities.insert(key_id.to_owned(), key);
96        }
97
98        Ok(Self {
99            trust_domain,
100            jwt_authorities: authorities,
101        })
102    }
103    /// Returns the [`JwtAuthority`] with the given key ID.
104    pub fn find_jwt_authority(&self, key_id: &str) -> Option<&Jwk> {
105        self.jwt_authorities.get(key_id)
106    }
107
108    /// Adds a [`JwtAuthority`] to the bundle.
109    pub fn add_jwt_authority(&mut self, authority: Jwk) -> Result<(), JwtBundleError> {
110        let key_id = match &authority.common.key_id {
111            Some(k) => k.to_owned(),
112            None => return Err(JwtBundleError::MissingKeyId),
113        };
114
115        self.jwt_authorities.insert(key_id, authority);
116        Ok(())
117    }
118
119    /// Returns the [`TrustDomain`] associated to the bundle.
120    pub fn trust_domain(&self) -> &TrustDomain {
121        &self.trust_domain
122    }
123}
124
125impl JwtBundleSet {
126    /// Creates an empty JWT bundle set.
127    pub fn new() -> Self {
128        Self {
129            bundles: HashMap::new(),
130        }
131    }
132
133    /// Adds a new [`JwtBundle`] into the set. If a bundle already exists for the
134    /// trust domain, the existing bundle is replaced.
135    pub fn add_bundle(&mut self, bundle: JwtBundle) {
136        self.bundles.insert(bundle.trust_domain().clone(), bundle);
137    }
138
139    /// Returns the [`JwtBundle`] associated to the given [`TrustDomain`].
140    pub fn get_bundle(&self, trust_domain: &TrustDomain) -> Option<&JwtBundle> {
141        self.bundles.get(trust_domain)
142    }
143}
144
145impl Default for JwtBundleSet {
146    fn default() -> Self {
147        Self::new()
148    }
149}
150
151impl BundleRefSource for JwtBundleSet {
152    type Item = JwtBundle;
153
154    /// Returns the [`JwtBundle`] associated to the given [`TrustDomain`].
155    fn get_bundle_for_trust_domain(
156        &self,
157        trust_domain: &TrustDomain,
158    ) -> Result<Option<&Self::Item>, Box<dyn std::error::Error + Send + Sync + 'static>> {
159        Ok(self.bundles.get(trust_domain))
160    }
161}
162
163#[cfg(test)]
164mod jwt_bundle_test {
165
166    use super::*;
167
168    #[test]
169    fn test_parse_bundle_from_json_single_authority() {
170        let bundle_bytes = r#"{
171        "keys": [
172        {
173            "kty": "EC",
174            "kid": "C6vs25welZOx6WksNYfbMfiw9l96pMnD",
175            "crv": "P-256",
176            "x": "ngLYQnlfF6GsojUwqtcEE3WgTNG2RUlsGhK73RNEl5k",
177            "y": "tKbiDSUSsQ3F1P7wteeHNXIcU-cx6CgSbroeQrQHTLM"
178        }
179    ]
180}"#
181        .as_bytes();
182        let trust_domain = TrustDomain::new("example.org").unwrap();
183        let jwt_bundle = JwtBundle::from_jwt_authorities(trust_domain, bundle_bytes).unwrap();
184        assert!(jwt_bundle
185            .find_jwt_authority("C6vs25welZOx6WksNYfbMfiw9l96pMnD")
186            .is_some());
187    }
188
189    #[test]
190    fn test_parse_bundle_from_json_multiple_authorities() {
191        let bundle_bytes = r#"{
192  "keys": [
193    {
194      "kty": "EC",
195      "kid": "C6vs25welZOx6WksNYfbMfiw9l96pMnD",
196      "crv": "P-256",
197      "x": "ngLYQnlfF6GsojUwqtcEE3WgTNG2RUlsGhK73RNEl5k",
198      "y": "tKbiDSUSsQ3F1P7wteeHNXIcU-cx6CgSbroeQrQHTLM"
199    },
200    {
201      "kty": "EC",
202      "kid": "gHTCunJbefYtnZnTctd84xeRWyMrEsWD",
203      "crv": "P-256",
204      "x": "7MGOl06DP9df2u8oHY6lqYFIoQWzCj9UYlp-MFeEYeY",
205      "y": "PSLLy5Pg0_kNGFFXq_eeq9kYcGDM3MPHJ6ncteNOr6w"
206    }
207  ]
208}"#
209        .as_bytes();
210
211        let trust_domain = TrustDomain::new("example.org").unwrap();
212        let jwt_bundle = JwtBundle::from_jwt_authorities(trust_domain, bundle_bytes).unwrap();
213        assert!(jwt_bundle
214            .find_jwt_authority("C6vs25welZOx6WksNYfbMfiw9l96pMnD")
215            .is_some());
216        assert!(jwt_bundle
217            .find_jwt_authority("gHTCunJbefYtnZnTctd84xeRWyMrEsWD")
218            .is_some());
219    }
220
221    #[test]
222    fn test_parse_bundle_from_authority_missing_key_id() {
223        let bundle_bytes = r#"{{
224    "keys": [
225        {
226            "kty": "EC",
227            "kid": "C6vs25welZOx6WksNYfbMfiw9l96pMnD",
228            "crv": "P-256",
229            "x": "ngLYQnlfF6GsojUwqtcEE3WgTNG2RUlsGhK73RNEl5k",
230            "y": "tKbiDSUSsQ3F1P7wteeHNXIcU-cx6CgSbroeQrQHTLM"
231        },
232        {
233            "kty": "EC",
234            "crv": "P-256",
235            "x": "7MGOl06DP9df2u8oHY6lqYFIoQWzCj9UYlp-MFeEYeY",
236            "y": "PSLLy5Pg0_kNGFFXq_eeq9kYcGDM3MPHJ6ncteNOr6w"
237        }
238    ]
239}"#
240        .as_bytes();
241
242        let trust_domain = TrustDomain::new("example.org").unwrap();
243        let result = JwtBundle::from_jwt_authorities(trust_domain, bundle_bytes);
244
245        assert!(matches!(
246            result.unwrap_err(),
247            JwtBundleError::Deserialize(..)
248        ));
249    }
250
251    #[test]
252    fn test_parse_jwks_with_empty_keys_array() {
253        let bundle_bytes = r#"{"keys": []}"#.as_bytes();
254        let trust_domain = TrustDomain::new("domain.test").unwrap();
255        let jwt_bundle = JwtBundle::from_jwt_authorities(trust_domain, bundle_bytes)
256            .expect("Failed to parse JWKS with empty keys array");
257
258        assert!(
259            jwt_bundle.jwt_authorities.is_empty(),
260            "JWT authorities should be empty"
261        );
262    }
263}