1use serde_json::Value;
41
42use crate::cose::SigningAlgorithm;
43use crate::trust::ArtifactVerifier;
44
45#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
47pub enum JwkError {
48 #[error("the JWK carries a private key component")]
50 NotPublic,
51 #[error("the JWK has no `{0}`")]
53 Missing(&'static str),
54 #[error("`{0}` is not unpadded base64url")]
56 NotBase64Url(&'static str),
57 #[error("{0}")]
59 WrongLength(String),
60 #[error("unsupported key: {0}")]
62 Unsupported(String),
63 #[error("JWK `alg` `{declared}` does not match key material algorithm `{actual}`")]
65 AlgorithmDisagreement {
66 declared: String,
68 actual: &'static str,
70 },
71}
72
73#[derive(Debug, Clone, Copy, PartialEq, Eq)]
75pub struct ExpectedAlgorithms {
76 pub jose: &'static str,
78 pub cose: Option<SigningAlgorithm>,
80}
81
82pub fn expected_algorithms_for_jwk(jwk: &Value) -> Result<ExpectedAlgorithms, JwkError> {
88 let key_type = member(jwk, "kty").ok_or(JwkError::Missing("kty"))?;
89 let curve = member(jwk, "crv").unwrap_or_default();
90
91 let expected = match (key_type, curve) {
92 ("OKP", "Ed25519") => ExpectedAlgorithms {
93 jose: "EdDSA",
94 cose: Some(SigningAlgorithm::EdDSA),
95 },
96 ("EC", "P-256") => ExpectedAlgorithms {
97 jose: "ES256",
98 cose: Some(SigningAlgorithm::ES256),
99 },
100 ("EC", "P-384") => ExpectedAlgorithms {
101 jose: "ES384",
102 cose: Some(SigningAlgorithm::ES384),
103 },
104 (other, curve) => {
105 return Err(JwkError::Unsupported(format!(
106 "`kty` `{other}` with `crv` `{curve}`"
107 )));
108 }
109 };
110
111 if let Some(declared) = member(jwk, "alg")
112 && declared != expected.jose
113 {
114 return Err(JwkError::AlgorithmDisagreement {
115 declared: declared.to_owned(),
116 actual: expected.jose,
117 });
118 }
119
120 Ok(expected)
121}
122
123pub struct JwkVerifier {
127 inner: Key,
128 expected: ExpectedAlgorithms,
129}
130
131impl std::fmt::Debug for JwkVerifier {
132 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
133 formatter.write_str("JwkVerifier")
134 }
135}
136
137enum Key {
139 #[cfg(feature = "ed25519")]
140 Ed25519(Box<ed25519_dalek::VerifyingKey>),
141 #[cfg(feature = "p256")]
142 P256(Box<p256::ecdsa::VerifyingKey>),
143 #[cfg(feature = "p384")]
144 P384(Box<p384::ecdsa::VerifyingKey>),
145}
146
147impl ArtifactVerifier for JwkVerifier {
148 #[cfg_attr(
149 not(any(feature = "ed25519", feature = "p256", feature = "p384")),
150 allow(unused_variables)
151 )]
152 fn verify(&self, data: &[u8], signature: &[u8]) -> bool {
153 match &self.inner {
154 #[cfg(feature = "ed25519")]
155 Key::Ed25519(key) => {
156 use ed25519_dalek::Verifier;
157
158 ed25519_dalek::Signature::from_slice(signature)
159 .is_ok_and(|signature| key.verify(data, &signature).is_ok())
160 }
161 #[cfg(feature = "p256")]
162 Key::P256(key) => {
163 use p256::ecdsa::signature::Verifier;
164
165 p256::ecdsa::Signature::from_slice(signature)
166 .is_ok_and(|signature| key.verify(data, &signature).is_ok())
167 }
168 #[cfg(feature = "p384")]
169 Key::P384(key) => {
170 use p384::ecdsa::signature::Verifier;
171
172 p384::ecdsa::Signature::from_slice(signature)
173 .is_ok_and(|signature| key.verify(data, &signature).is_ok())
174 }
175 #[allow(unreachable_patterns)]
177 _ => false,
178 }
179 }
180
181 fn expected_jws_algorithm(&self) -> Option<&'static str> {
182 Some(self.expected.jose)
183 }
184
185 fn expected_cose_algorithm(&self) -> Option<SigningAlgorithm> {
186 self.expected.cose
187 }
188}
189
190pub fn public_key_from_jwk(jwk: &Value) -> Result<JwkVerifier, JwkError> {
196 if jwk.get("d").is_some() {
197 return Err(JwkError::NotPublic);
198 }
199
200 let expected = expected_algorithms_for_jwk(jwk)?;
203 let x = coordinate(jwk, "x")?;
204
205 match expected.jose {
206 "EdDSA" => ed25519_from_x(x, expected),
207 "ES256" => p256_from_coordinates(x, coordinate(jwk, "y")?, expected),
208 "ES384" => p384_from_coordinates(x, coordinate(jwk, "y")?, expected),
209 other => Err(JwkError::Unsupported(other.to_owned())),
210 }
211}
212
213#[cfg(feature = "ed25519")]
218fn ed25519_from_x(x: Vec<u8>, expected: ExpectedAlgorithms) -> Result<JwkVerifier, JwkError> {
219 let bytes: [u8; 32] = x
220 .try_into()
221 .map_err(|_| JwkError::WrongLength("an Ed25519 `x` is not 32 bytes".to_owned()))?;
222 let key = ed25519_dalek::VerifyingKey::from_bytes(&bytes)
223 .map_err(|error| JwkError::WrongLength(error.to_string()))?;
224
225 Ok(JwkVerifier {
226 inner: Key::Ed25519(Box::new(key)),
227 expected,
228 })
229}
230
231#[cfg(not(feature = "ed25519"))]
232fn ed25519_from_x(_x: Vec<u8>, _expected: ExpectedAlgorithms) -> Result<JwkVerifier, JwkError> {
233 Err(JwkError::Unsupported(
234 "Ed25519: enable the `ed25519` feature".to_owned(),
235 ))
236}
237
238#[cfg(feature = "p256")]
239fn p256_from_coordinates(
240 x: Vec<u8>,
241 y: Vec<u8>,
242 expected: ExpectedAlgorithms,
243) -> Result<JwkVerifier, JwkError> {
244 let point = sec1_point(&x, &y, 32, "P-256")?;
245 let key = p256::ecdsa::VerifyingKey::from_sec1_bytes(&point)
246 .map_err(|error| JwkError::WrongLength(error.to_string()))?;
247
248 Ok(JwkVerifier {
249 inner: Key::P256(Box::new(key)),
250 expected,
251 })
252}
253
254#[cfg(not(feature = "p256"))]
255fn p256_from_coordinates(
256 _x: Vec<u8>,
257 _y: Vec<u8>,
258 _expected: ExpectedAlgorithms,
259) -> Result<JwkVerifier, JwkError> {
260 Err(JwkError::Unsupported(
261 "P-256: enable the `p256` feature".to_owned(),
262 ))
263}
264
265#[cfg(feature = "p384")]
266fn p384_from_coordinates(
267 x: Vec<u8>,
268 y: Vec<u8>,
269 expected: ExpectedAlgorithms,
270) -> Result<JwkVerifier, JwkError> {
271 let point = sec1_point(&x, &y, 48, "P-384")?;
272 let key = p384::ecdsa::VerifyingKey::from_sec1_bytes(&point)
273 .map_err(|error| JwkError::WrongLength(error.to_string()))?;
274
275 Ok(JwkVerifier {
276 inner: Key::P384(Box::new(key)),
277 expected,
278 })
279}
280
281#[cfg(not(feature = "p384"))]
282fn p384_from_coordinates(
283 _x: Vec<u8>,
284 _y: Vec<u8>,
285 _expected: ExpectedAlgorithms,
286) -> Result<JwkVerifier, JwkError> {
287 Err(JwkError::Unsupported(
288 "P-384: enable the `p384` feature".to_owned(),
289 ))
290}
291
292#[cfg_attr(
294 not(any(feature = "p256", feature = "p384")),
295 allow(dead_code, unused_variables)
296)]
297fn sec1_point(x: &[u8], y: &[u8], width: usize, curve: &str) -> Result<Vec<u8>, JwkError> {
298 if x.len() != width || y.len() != width {
299 return Err(JwkError::WrongLength(format!(
300 "a {curve} coordinate is not {width} bytes"
301 )));
302 }
303
304 let mut point = Vec::with_capacity(1 + width * 2);
305 point.push(0x04);
306 point.extend_from_slice(x);
307 point.extend_from_slice(y);
308
309 Ok(point)
310}
311
312fn member<'a>(jwk: &'a Value, name: &str) -> Option<&'a str> {
313 jwk.get(name)?.as_str()
314}
315
316fn coordinate(jwk: &Value, name: &'static str) -> Result<Vec<u8>, JwkError> {
317 use base64::Engine;
318
319 let encoded = member(jwk, name).ok_or(JwkError::Missing(name))?;
320 base64::engine::general_purpose::URL_SAFE_NO_PAD
321 .decode(encoded)
322 .map_err(|_| JwkError::NotBase64Url(name))
323}
324
325#[cfg(test)]
326mod tests {
327 use super::*;
328 use serde_json::json;
329
330 #[test]
331 fn algorithms_come_from_key_material_not_from_what_the_jwk_claims() {
332 let ed25519 = json!({"kty": "OKP", "crv": "Ed25519", "x": "AA"});
333 assert_eq!(
334 expected_algorithms_for_jwk(&ed25519).unwrap(),
335 ExpectedAlgorithms {
336 jose: "EdDSA",
337 cose: Some(SigningAlgorithm::EdDSA),
338 }
339 );
340
341 let p256 = json!({"kty": "EC", "crv": "P-256", "x": "AA", "y": "AA"});
342 assert_eq!(expected_algorithms_for_jwk(&p256).unwrap().jose, "ES256");
343
344 let p384 = json!({"kty": "EC", "crv": "P-384", "x": "AA", "y": "AA"});
345 assert_eq!(expected_algorithms_for_jwk(&p384).unwrap().jose, "ES384");
346
347 let lying = json!({"kty": "OKP", "crv": "Ed25519", "alg": "ES256", "x": "AA"});
350 assert_eq!(
351 expected_algorithms_for_jwk(&lying).unwrap_err(),
352 JwkError::AlgorithmDisagreement {
353 declared: "ES256".to_owned(),
354 actual: "EdDSA",
355 }
356 );
357
358 let rsa = json!({"kty": "RSA", "n": "AA", "e": "AQAB"});
360 assert!(matches!(
361 expected_algorithms_for_jwk(&rsa).unwrap_err(),
362 JwkError::Unsupported(_)
363 ));
364 }
365
366 #[test]
367 fn a_jwk_carrying_private_material_is_refused() {
368 let private = json!({"kty": "OKP", "crv": "Ed25519", "x": "AA", "d": "secret"});
369 assert_eq!(
370 public_key_from_jwk(&private).unwrap_err(),
371 JwkError::NotPublic
372 );
373 }
374
375 #[test]
376 fn a_malformed_key_is_refused_rather_than_truncated() {
377 let short = json!({"kty": "OKP", "crv": "Ed25519", "x": "AAAA"});
378 assert!(matches!(
379 public_key_from_jwk(&short).unwrap_err(),
380 JwkError::WrongLength(_)
381 ));
382
383 let not_base64 = json!({"kty": "OKP", "crv": "Ed25519", "x": "not base64!"});
384 assert_eq!(
385 public_key_from_jwk(¬_base64).unwrap_err(),
386 JwkError::NotBase64Url("x")
387 );
388
389 let no_y = json!({"kty": "EC", "crv": "P-256", "x": "AA"});
390 assert_eq!(
391 public_key_from_jwk(&no_y).unwrap_err(),
392 JwkError::Missing("y")
393 );
394 }
395
396 #[cfg(feature = "ed25519")]
397 #[test]
398 fn an_ed25519_jwk_verifies_what_its_key_signed() {
399 use base64::Engine;
400 use ed25519_dalek::Signer;
401
402 let signing = ed25519_dalek::SigningKey::from_bytes(&[0x42; 32]);
403 let jwk = json!({
404 "kty": "OKP",
405 "crv": "Ed25519",
406 "alg": "EdDSA",
407 "x": base64::engine::general_purpose::URL_SAFE_NO_PAD
408 .encode(signing.verifying_key().as_bytes()),
409 });
410
411 let verifier = public_key_from_jwk(&jwk).expect("the JWK reads");
412 let signature = signing.sign(b"artifact bytes");
413 assert!(verifier.verify(b"artifact bytes", &signature.to_bytes()));
414 assert!(!verifier.verify(b"other bytes", &signature.to_bytes()));
415 }
416
417 #[cfg(feature = "p256")]
418 #[test]
419 fn a_p256_jwk_verifies_what_its_key_signed() {
420 use base64::Engine;
421 use p256::ecdsa::signature::Signer;
422
423 let signing = p256::ecdsa::SigningKey::from_slice(&[0x11; 32]).expect("a signing key");
424 let point = signing.verifying_key().to_encoded_point(false);
425 let encode = |bytes: &[u8]| base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(bytes);
426 let jwk = json!({
427 "kty": "EC",
428 "crv": "P-256",
429 "alg": "ES256",
430 "x": encode(point.x().expect("x")),
431 "y": encode(point.y().expect("y")),
432 });
433
434 let verifier = public_key_from_jwk(&jwk).expect("the JWK reads");
435 let signature: p256::ecdsa::Signature = signing.sign(b"artifact bytes");
436 assert!(verifier.verify(b"artifact bytes", &signature.to_bytes()));
437 assert!(!verifier.verify(b"other bytes", &signature.to_bytes()));
438 }
439}