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!(
80    ///     jwt_bundle
81    ///         .find_jwt_authority("C6vs25welZOx6WksNYfbMfiw9l96pMnD")
82    ///         .is_some()
83    /// );
84    /// ```
85    pub fn from_jwt_authorities(
86        trust_domain: TrustDomain,
87        jwt_authorities: &[u8],
88    ) -> Result<Self, JwtBundleError> {
89        let mut authorities = HashMap::new();
90        let jwk_set: JwkSet = serde_json::from_slice(jwt_authorities)?;
91
92        for key in jwk_set.keys.into_iter() {
93            let key_id = match &key.common.key_id {
94                Some(k) => k,
95                None => return Err(JwtBundleError::MissingKeyId),
96            };
97            authorities.insert(key_id.to_owned(), key);
98        }
99
100        Ok(Self {
101            trust_domain,
102            jwt_authorities: authorities,
103        })
104    }
105    /// Returns the [`JwtAuthority`] with the given key ID.
106    pub fn find_jwt_authority(&self, key_id: &str) -> Option<&Jwk> {
107        self.jwt_authorities.get(key_id)
108    }
109
110    /// Adds a [`JwtAuthority`] to the bundle.
111    pub fn add_jwt_authority(&mut self, authority: Jwk) -> Result<(), JwtBundleError> {
112        let key_id = match &authority.common.key_id {
113            Some(k) => k.to_owned(),
114            None => return Err(JwtBundleError::MissingKeyId),
115        };
116
117        self.jwt_authorities.insert(key_id, authority);
118        Ok(())
119    }
120
121    /// Returns the [`TrustDomain`] associated to the bundle.
122    pub fn trust_domain(&self) -> &TrustDomain {
123        &self.trust_domain
124    }
125}
126
127impl JwtBundleSet {
128    /// Creates an empty JWT bundle set.
129    pub fn new() -> Self {
130        Self {
131            bundles: HashMap::new(),
132        }
133    }
134
135    /// Adds a new [`JwtBundle`] into the set. If a bundle already exists for the
136    /// trust domain, the existing bundle is replaced.
137    pub fn add_bundle(&mut self, bundle: JwtBundle) {
138        self.bundles.insert(bundle.trust_domain().clone(), bundle);
139    }
140
141    /// Returns the [`JwtBundle`] associated to the given [`TrustDomain`].
142    pub fn get_bundle(&self, trust_domain: &TrustDomain) -> Option<&JwtBundle> {
143        self.bundles.get(trust_domain)
144    }
145}
146
147impl Default for JwtBundleSet {
148    fn default() -> Self {
149        Self::new()
150    }
151}
152
153impl BundleRefSource for JwtBundleSet {
154    type Item = JwtBundle;
155
156    /// Returns the [`JwtBundle`] associated to the given [`TrustDomain`].
157    fn get_bundle_for_trust_domain(
158        &self,
159        trust_domain: &TrustDomain,
160    ) -> Result<Option<&Self::Item>, Box<dyn std::error::Error + Send + Sync + 'static>> {
161        Ok(self.bundles.get(trust_domain))
162    }
163}
164
165#[cfg(test)]
166mod jwt_bundle_test {
167
168    use super::*;
169
170    #[test]
171    fn test_parse_bundle_from_json_single_authority() {
172        let bundle_bytes = r#"{
173        "keys": [
174        {
175            "kty": "EC",
176            "kid": "C6vs25welZOx6WksNYfbMfiw9l96pMnD",
177            "crv": "P-256",
178            "x": "ngLYQnlfF6GsojUwqtcEE3WgTNG2RUlsGhK73RNEl5k",
179            "y": "tKbiDSUSsQ3F1P7wteeHNXIcU-cx6CgSbroeQrQHTLM"
180        }
181    ]
182}"#
183        .as_bytes();
184        let trust_domain = TrustDomain::new("example.org").unwrap();
185        let jwt_bundle = JwtBundle::from_jwt_authorities(trust_domain, bundle_bytes).unwrap();
186        assert!(jwt_bundle
187            .find_jwt_authority("C6vs25welZOx6WksNYfbMfiw9l96pMnD")
188            .is_some());
189    }
190
191    #[test]
192    fn test_parse_bundle_from_json_multiple_authorities() {
193        let bundle_bytes = r#"{
194  "keys": [
195    {
196      "kty": "EC",
197      "kid": "C6vs25welZOx6WksNYfbMfiw9l96pMnD",
198      "crv": "P-256",
199      "x": "ngLYQnlfF6GsojUwqtcEE3WgTNG2RUlsGhK73RNEl5k",
200      "y": "tKbiDSUSsQ3F1P7wteeHNXIcU-cx6CgSbroeQrQHTLM"
201    },
202    {
203      "kty": "EC",
204      "kid": "gHTCunJbefYtnZnTctd84xeRWyMrEsWD",
205      "crv": "P-256",
206      "x": "7MGOl06DP9df2u8oHY6lqYFIoQWzCj9UYlp-MFeEYeY",
207      "y": "PSLLy5Pg0_kNGFFXq_eeq9kYcGDM3MPHJ6ncteNOr6w"
208    }
209  ]
210}"#
211        .as_bytes();
212
213        let trust_domain = TrustDomain::new("example.org").unwrap();
214        let jwt_bundle = JwtBundle::from_jwt_authorities(trust_domain, bundle_bytes).unwrap();
215        assert!(jwt_bundle
216            .find_jwt_authority("C6vs25welZOx6WksNYfbMfiw9l96pMnD")
217            .is_some());
218        assert!(jwt_bundle
219            .find_jwt_authority("gHTCunJbefYtnZnTctd84xeRWyMrEsWD")
220            .is_some());
221    }
222
223    #[test]
224    fn test_parse_bundle_from_authority_missing_key_id() {
225        let bundle_bytes = r#"{{
226    "keys": [
227        {
228            "kty": "EC",
229            "kid": "C6vs25welZOx6WksNYfbMfiw9l96pMnD",
230            "crv": "P-256",
231            "x": "ngLYQnlfF6GsojUwqtcEE3WgTNG2RUlsGhK73RNEl5k",
232            "y": "tKbiDSUSsQ3F1P7wteeHNXIcU-cx6CgSbroeQrQHTLM"
233        },
234        {
235            "kty": "EC",
236            "crv": "P-256",
237            "x": "7MGOl06DP9df2u8oHY6lqYFIoQWzCj9UYlp-MFeEYeY",
238            "y": "PSLLy5Pg0_kNGFFXq_eeq9kYcGDM3MPHJ6ncteNOr6w"
239        }
240    ]
241}"#
242        .as_bytes();
243
244        let trust_domain = TrustDomain::new("example.org").unwrap();
245        let result = JwtBundle::from_jwt_authorities(trust_domain, bundle_bytes);
246
247        assert!(matches!(
248            result.unwrap_err(),
249            JwtBundleError::Deserialize(..)
250        ));
251    }
252
253    #[test]
254    fn test_parse_jwks_with_empty_keys_array() {
255        let bundle_bytes = r#"{"keys": []}"#.as_bytes();
256        let trust_domain = TrustDomain::new("domain.test").unwrap();
257        let jwt_bundle = JwtBundle::from_jwt_authorities(trust_domain, bundle_bytes)
258            .expect("Failed to parse JWKS with empty keys array");
259
260        assert!(
261            jwt_bundle.jwt_authorities.is_empty(),
262            "JWT authorities should be empty"
263        );
264    }
265}