1use 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#[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#[derive(Debug, Clone, Eq, PartialEq)]
22pub struct JwtBundleSet {
23 bundles: HashMap<TrustDomain, JwtBundle>,
24}
25
26#[derive(Debug, Error)]
28#[non_exhaustive]
29pub enum JwtBundleError {
30 #[error("missing key ID")]
32 MissingKeyId,
33 #[error("cannot deserialize json jwk set")]
35 Deserialize(#[from] serde_json::Error),
36}
37
38impl JwtBundle {
39 pub fn new(trust_domain: TrustDomain) -> Self {
41 Self {
42 trust_domain,
43 jwt_authorities: HashMap::new(),
44 }
45 }
46
47 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 pub fn find_jwt_authority(&self, key_id: &str) -> Option<&Jwk> {
107 self.jwt_authorities.get(key_id)
108 }
109
110 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 pub fn trust_domain(&self) -> &TrustDomain {
123 &self.trust_domain
124 }
125}
126
127impl JwtBundleSet {
128 pub fn new() -> Self {
130 Self {
131 bundles: HashMap::new(),
132 }
133 }
134
135 pub fn add_bundle(&mut self, bundle: JwtBundle) {
138 self.bundles.insert(bundle.trust_domain().clone(), bundle);
139 }
140
141 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 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}