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}
129
130impl std::fmt::Debug for JwkVerifier {
131 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
132 formatter.write_str("JwkVerifier")
133 }
134}
135
136enum Key {
138 #[cfg(feature = "ed25519")]
139 Ed25519(Box<ed25519_dalek::VerifyingKey>),
140 #[cfg(feature = "p256")]
141 P256(Box<p256::ecdsa::VerifyingKey>),
142 #[cfg(feature = "p384")]
143 P384(Box<p384::ecdsa::VerifyingKey>),
144}
145
146impl ArtifactVerifier for JwkVerifier {
147 #[cfg_attr(
148 not(any(feature = "ed25519", feature = "p256", feature = "p384")),
149 allow(unused_variables)
150 )]
151 fn verify(&self, data: &[u8], signature: &[u8]) -> bool {
152 match &self.inner {
153 #[cfg(feature = "ed25519")]
154 Key::Ed25519(key) => {
155 use ed25519_dalek::Verifier;
156
157 ed25519_dalek::Signature::from_slice(signature)
158 .is_ok_and(|signature| key.verify(data, &signature).is_ok())
159 }
160 #[cfg(feature = "p256")]
161 Key::P256(key) => {
162 use p256::ecdsa::signature::Verifier;
163
164 p256::ecdsa::Signature::from_slice(signature)
165 .is_ok_and(|signature| key.verify(data, &signature).is_ok())
166 }
167 #[cfg(feature = "p384")]
168 Key::P384(key) => {
169 use p384::ecdsa::signature::Verifier;
170
171 p384::ecdsa::Signature::from_slice(signature)
172 .is_ok_and(|signature| key.verify(data, &signature).is_ok())
173 }
174 #[allow(unreachable_patterns)]
176 _ => false,
177 }
178 }
179}
180
181pub fn public_key_from_jwk(jwk: &Value) -> Result<JwkVerifier, JwkError> {
187 if jwk.get("d").is_some() {
188 return Err(JwkError::NotPublic);
189 }
190
191 let expected = expected_algorithms_for_jwk(jwk)?;
194 let x = coordinate(jwk, "x")?;
195
196 match expected.jose {
197 "EdDSA" => ed25519_from_x(x),
198 "ES256" => p256_from_coordinates(x, coordinate(jwk, "y")?),
199 "ES384" => p384_from_coordinates(x, coordinate(jwk, "y")?),
200 other => Err(JwkError::Unsupported(other.to_owned())),
201 }
202}
203
204#[cfg(feature = "ed25519")]
209fn ed25519_from_x(x: Vec<u8>) -> Result<JwkVerifier, JwkError> {
210 let bytes: [u8; 32] = x
211 .try_into()
212 .map_err(|_| JwkError::WrongLength("an Ed25519 `x` is not 32 bytes".to_owned()))?;
213 let key = ed25519_dalek::VerifyingKey::from_bytes(&bytes)
214 .map_err(|error| JwkError::WrongLength(error.to_string()))?;
215
216 Ok(JwkVerifier {
217 inner: Key::Ed25519(Box::new(key)),
218 })
219}
220
221#[cfg(not(feature = "ed25519"))]
222fn ed25519_from_x(_x: Vec<u8>) -> Result<JwkVerifier, JwkError> {
223 Err(JwkError::Unsupported(
224 "Ed25519: enable the `ed25519` feature".to_owned(),
225 ))
226}
227
228#[cfg(feature = "p256")]
229fn p256_from_coordinates(x: Vec<u8>, y: Vec<u8>) -> Result<JwkVerifier, JwkError> {
230 let point = sec1_point(&x, &y, 32, "P-256")?;
231 let key = p256::ecdsa::VerifyingKey::from_sec1_bytes(&point)
232 .map_err(|error| JwkError::WrongLength(error.to_string()))?;
233
234 Ok(JwkVerifier {
235 inner: Key::P256(Box::new(key)),
236 })
237}
238
239#[cfg(not(feature = "p256"))]
240fn p256_from_coordinates(_x: Vec<u8>, _y: Vec<u8>) -> Result<JwkVerifier, JwkError> {
241 Err(JwkError::Unsupported(
242 "P-256: enable the `p256` feature".to_owned(),
243 ))
244}
245
246#[cfg(feature = "p384")]
247fn p384_from_coordinates(x: Vec<u8>, y: Vec<u8>) -> Result<JwkVerifier, JwkError> {
248 let point = sec1_point(&x, &y, 48, "P-384")?;
249 let key = p384::ecdsa::VerifyingKey::from_sec1_bytes(&point)
250 .map_err(|error| JwkError::WrongLength(error.to_string()))?;
251
252 Ok(JwkVerifier {
253 inner: Key::P384(Box::new(key)),
254 })
255}
256
257#[cfg(not(feature = "p384"))]
258fn p384_from_coordinates(_x: Vec<u8>, _y: Vec<u8>) -> Result<JwkVerifier, JwkError> {
259 Err(JwkError::Unsupported(
260 "P-384: enable the `p384` feature".to_owned(),
261 ))
262}
263
264#[cfg_attr(
266 not(any(feature = "p256", feature = "p384")),
267 allow(dead_code, unused_variables)
268)]
269fn sec1_point(x: &[u8], y: &[u8], width: usize, curve: &str) -> Result<Vec<u8>, JwkError> {
270 if x.len() != width || y.len() != width {
271 return Err(JwkError::WrongLength(format!(
272 "a {curve} coordinate is not {width} bytes"
273 )));
274 }
275
276 let mut point = Vec::with_capacity(1 + width * 2);
277 point.push(0x04);
278 point.extend_from_slice(x);
279 point.extend_from_slice(y);
280
281 Ok(point)
282}
283
284fn member<'a>(jwk: &'a Value, name: &str) -> Option<&'a str> {
285 jwk.get(name)?.as_str()
286}
287
288fn coordinate(jwk: &Value, name: &'static str) -> Result<Vec<u8>, JwkError> {
289 use base64::Engine;
290
291 let encoded = member(jwk, name).ok_or(JwkError::Missing(name))?;
292 base64::engine::general_purpose::URL_SAFE_NO_PAD
293 .decode(encoded)
294 .map_err(|_| JwkError::NotBase64Url(name))
295}
296
297#[cfg(test)]
298mod tests {
299 use super::*;
300 use serde_json::json;
301
302 #[test]
303 fn algorithms_come_from_key_material_not_from_what_the_jwk_claims() {
304 let ed25519 = json!({"kty": "OKP", "crv": "Ed25519", "x": "AA"});
305 assert_eq!(
306 expected_algorithms_for_jwk(&ed25519).unwrap(),
307 ExpectedAlgorithms {
308 jose: "EdDSA",
309 cose: Some(SigningAlgorithm::EdDSA),
310 }
311 );
312
313 let p256 = json!({"kty": "EC", "crv": "P-256", "x": "AA", "y": "AA"});
314 assert_eq!(expected_algorithms_for_jwk(&p256).unwrap().jose, "ES256");
315
316 let p384 = json!({"kty": "EC", "crv": "P-384", "x": "AA", "y": "AA"});
317 assert_eq!(expected_algorithms_for_jwk(&p384).unwrap().jose, "ES384");
318
319 let lying = json!({"kty": "OKP", "crv": "Ed25519", "alg": "ES256", "x": "AA"});
322 assert_eq!(
323 expected_algorithms_for_jwk(&lying).unwrap_err(),
324 JwkError::AlgorithmDisagreement {
325 declared: "ES256".to_owned(),
326 actual: "EdDSA",
327 }
328 );
329
330 let rsa = json!({"kty": "RSA", "n": "AA", "e": "AQAB"});
332 assert!(matches!(
333 expected_algorithms_for_jwk(&rsa).unwrap_err(),
334 JwkError::Unsupported(_)
335 ));
336 }
337
338 #[test]
339 fn a_jwk_carrying_private_material_is_refused() {
340 let private = json!({"kty": "OKP", "crv": "Ed25519", "x": "AA", "d": "secret"});
341 assert_eq!(
342 public_key_from_jwk(&private).unwrap_err(),
343 JwkError::NotPublic
344 );
345 }
346
347 #[test]
348 fn a_malformed_key_is_refused_rather_than_truncated() {
349 let short = json!({"kty": "OKP", "crv": "Ed25519", "x": "AAAA"});
350 assert!(matches!(
351 public_key_from_jwk(&short).unwrap_err(),
352 JwkError::WrongLength(_)
353 ));
354
355 let not_base64 = json!({"kty": "OKP", "crv": "Ed25519", "x": "not base64!"});
356 assert_eq!(
357 public_key_from_jwk(¬_base64).unwrap_err(),
358 JwkError::NotBase64Url("x")
359 );
360
361 let no_y = json!({"kty": "EC", "crv": "P-256", "x": "AA"});
362 assert_eq!(
363 public_key_from_jwk(&no_y).unwrap_err(),
364 JwkError::Missing("y")
365 );
366 }
367
368 #[cfg(feature = "ed25519")]
369 #[test]
370 fn an_ed25519_jwk_verifies_what_its_key_signed() {
371 use base64::Engine;
372 use ed25519_dalek::Signer;
373
374 let signing = ed25519_dalek::SigningKey::from_bytes(&[0x42; 32]);
375 let jwk = json!({
376 "kty": "OKP",
377 "crv": "Ed25519",
378 "alg": "EdDSA",
379 "x": base64::engine::general_purpose::URL_SAFE_NO_PAD
380 .encode(signing.verifying_key().as_bytes()),
381 });
382
383 let verifier = public_key_from_jwk(&jwk).expect("the JWK reads");
384 let signature = signing.sign(b"artifact bytes");
385 assert!(verifier.verify(b"artifact bytes", &signature.to_bytes()));
386 assert!(!verifier.verify(b"other bytes", &signature.to_bytes()));
387 }
388
389 #[cfg(feature = "p256")]
390 #[test]
391 fn a_p256_jwk_verifies_what_its_key_signed() {
392 use base64::Engine;
393 use p256::ecdsa::signature::Signer;
394
395 let signing = p256::ecdsa::SigningKey::from_slice(&[0x11; 32]).expect("a signing key");
396 let point = signing.verifying_key().to_encoded_point(false);
397 let encode = |bytes: &[u8]| base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(bytes);
398 let jwk = json!({
399 "kty": "EC",
400 "crv": "P-256",
401 "alg": "ES256",
402 "x": encode(point.x().expect("x")),
403 "y": encode(point.y().expect("y")),
404 });
405
406 let verifier = public_key_from_jwk(&jwk).expect("the JWK reads");
407 let signature: p256::ecdsa::Signature = signing.sign(b"artifact bytes");
408 assert!(verifier.verify(b"artifact bytes", &signature.to_bytes()));
409 assert!(!verifier.verify(b"other bytes", &signature.to_bytes()));
410 }
411}