1use crate::DpopError;
2use base64::engine::general_purpose::URL_SAFE_NO_PAD as B64;
3use base64::Engine;
4#[cfg(feature = "eddsa")]
5use ed25519_dalek::VerifyingKey as Ed25519VerifyingKey;
6use p256::ecdsa::VerifyingKey;
7use p256::{EncodedPoint, FieldBytes};
8use sha2::{Digest, Sha256};
9use std::collections::BTreeMap;
10
11const P256_COORDINATE_LENGTH: usize = 32;
13#[cfg(feature = "eddsa")]
14const ED25519_KEY_LENGTH: usize = 32;
15
16pub fn verifying_key_from_p256_xy(x_b64: &str, y_b64: &str) -> Result<VerifyingKey, DpopError> {
18 let x_coordinate = B64
19 .decode(x_b64)
20 .map_err(|_| DpopError::BadJwk("bad jwk.x"))?;
21 let y_coordinate = B64
22 .decode(y_b64)
23 .map_err(|_| DpopError::BadJwk("bad jwk.y"))?;
24
25 if x_coordinate.len() != P256_COORDINATE_LENGTH || y_coordinate.len() != P256_COORDINATE_LENGTH
26 {
27 return Err(DpopError::BadJwk("jwk x/y must be 32 bytes"));
28 }
29
30 let encoded_point = EncodedPoint::from_affine_coordinates(
31 FieldBytes::from_slice(&x_coordinate),
32 FieldBytes::from_slice(&y_coordinate),
33 false,
34 );
35
36 VerifyingKey::from_encoded_point(&encoded_point)
37 .map_err(|_| DpopError::BadJwk("invalid EC point"))
38}
39
40pub fn thumbprint_ec_p256(x_b64: &str, y_b64: &str) -> Result<String, DpopError> {
41 let mut canonical_jwk_map = BTreeMap::new();
42 canonical_jwk_map.insert("crv", "P-256");
43 canonical_jwk_map.insert("kty", "EC");
44 canonical_jwk_map.insert("x", x_b64);
45 canonical_jwk_map.insert("y", y_b64);
46 let canonical_json =
47 serde_json::to_string(&canonical_jwk_map).map_err(|_| DpopError::BadJwk("canonicalize"))?;
48 Ok(B64.encode(Sha256::digest(canonical_json.as_bytes())))
49}
50
51#[cfg(feature = "eddsa")]
52pub fn verifying_key_from_okp_ed25519(
53 x_b64: &str,
54) -> Result<Ed25519VerifyingKey, crate::DpopError> {
55 let key_bytes_vec = B64
56 .decode(x_b64)
57 .map_err(|_| crate::DpopError::BadJwk("bad jwk.x"))?;
58
59 if key_bytes_vec.len() != ED25519_KEY_LENGTH {
60 return Err(crate::DpopError::BadJwk("jwk x must be 32 bytes"));
61 }
62
63 let key_bytes: [u8; ED25519_KEY_LENGTH] = key_bytes_vec
64 .try_into()
65 .map_err(|_| crate::DpopError::BadJwk("invalid key length"))?;
66
67 Ed25519VerifyingKey::from_bytes(&key_bytes)
68 .map_err(|_| crate::DpopError::BadJwk("invalid Ed25519 key"))
69}
70
71#[cfg(feature = "eddsa")]
72pub fn thumbprint_okp_ed25519(x_b64: &str) -> Result<String, crate::DpopError> {
73 let mut canonical_jwk_map = BTreeMap::new();
75 canonical_jwk_map.insert("crv", "Ed25519");
76 canonical_jwk_map.insert("kty", "OKP");
77 canonical_jwk_map.insert("x", x_b64);
78 let canonical_json = serde_json::to_string(&canonical_jwk_map)
79 .map_err(|_| crate::DpopError::BadJwk("canonicalize"))?;
80 Ok(B64.encode(Sha256::digest(canonical_json.as_bytes())))
81}
82
83#[cfg(test)]
84mod tests {
85 use super::*;
86 use p256::ecdsa::SigningKey;
87 use rand_core::OsRng;
88
89 #[test]
92 fn vk_from_xy_roundtrip_valid() {
93 let signing_key = SigningKey::random(&mut OsRng);
95 let verifying_key = VerifyingKey::from(&signing_key);
96 let encoded_point = verifying_key.to_encoded_point(false);
97 let x_b64 = B64.encode(encoded_point.x().expect("x"));
98 let y_b64 = B64.encode(encoded_point.y().expect("y"));
99
100 let reconstructed = verifying_key_from_p256_xy(&x_b64, &y_b64).expect("verifying key");
101 assert_eq!(reconstructed.to_encoded_point(false), encoded_point);
102 }
103
104 #[test]
105 fn vk_rejects_bad_base64_inputs() {
106 let good32 = B64.encode([0u8; 32]);
108 assert!(
109 verifying_key_from_p256_xy("AA==", &good32).is_err(),
110 "padded x should fail"
111 );
112 assert!(
113 verifying_key_from_p256_xy(&good32, "A*").is_err(),
114 "invalid char in y should fail"
115 );
116 }
117
118 #[test]
119 fn vk_rejects_wrong_lengths() {
120 let x31 = B64.encode([0u8; 31]);
121 let x32 = B64.encode([0u8; 32]);
122 let y33 = B64.encode([0u8; 33]);
123 assert!(
124 verifying_key_from_p256_xy(&x31, &x32).is_err(),
125 "31-byte x must fail"
126 );
127 assert!(
128 verifying_key_from_p256_xy(&x32, &y33).is_err(),
129 "33-byte y must fail"
130 );
131 }
132
133 #[test]
134 fn vk_rejects_invalid_point() {
135 let zeros32 = B64.encode([0u8; 32]);
137 let err = verifying_key_from_p256_xy(&zeros32, &zeros32).unwrap_err();
138 matches!(err, DpopError::BadJwk(_));
139 }
140
141 #[test]
144 fn thumbprint_has_length_43_and_no_padding() {
145 let x = B64.encode([0u8; 32]);
147 let y = B64.encode([1u8; 32]);
148 let t = thumbprint_ec_p256(&x, &y).expect("thumbprint");
149 assert_eq!(t.len(), 43);
150 assert!(!t.contains('='));
151 }
152
153 #[test]
154 fn thumbprint_is_deterministic() {
155 let x = B64.encode([42u8; 32]);
156 let y = B64.encode([99u8; 32]);
157 let t1 = thumbprint_ec_p256(&x, &y).unwrap();
158 let t2 = thumbprint_ec_p256(&x, &y).unwrap();
159 assert_eq!(t1, t2);
160 }
161
162 #[test]
163 fn thumbprint_changes_when_xy_changes() {
164 let x = B64.encode([7u8; 32]);
165 let mut x2_bytes = [7u8; 32];
166 x2_bytes[0] ^= 0x01; let x2 = B64.encode(x2_bytes);
168 let y = B64.encode([9u8; 32]);
169
170 let t1 = thumbprint_ec_p256(&x, &y).unwrap();
171 let t2 = thumbprint_ec_p256(&x2, &y).unwrap();
172 assert_ne!(t1, t2);
173 }
174
175 #[test]
176 fn thumbprint_canonicalization_order_is_fixed() {
177 let x_coordinate = B64.encode([0xAB; 32]);
180 let y_coordinate = B64.encode([0xCD; 32]);
181
182 let thumbprint_from_func = thumbprint_ec_p256(&x_coordinate, &y_coordinate).unwrap();
184
185 let mut canonical_jwk_map = BTreeMap::new();
187 canonical_jwk_map.insert("crv", "P-256");
188 canonical_jwk_map.insert("kty", "EC");
189 canonical_jwk_map.insert("x", x_coordinate.as_str());
190 canonical_jwk_map.insert("y", y_coordinate.as_str());
191 let canonical_json = serde_json::to_string(&canonical_jwk_map).unwrap();
192 let thumbprint_manual = B64.encode(Sha256::digest(canonical_json.as_bytes()));
193 assert_eq!(thumbprint_from_func, thumbprint_manual);
194 }
195}