1use anyhow::{anyhow, bail, Context as _, Result};
17use base64::engine::general_purpose::URL_SAFE_NO_PAD;
18use base64::Engine;
19use p256::ecdsa::signature::{Signer as _, Verifier as _};
20use p256::ecdsa::{Signature, SigningKey as EcdsaSigningKey, VerifyingKey};
21use serde_json::Value;
22
23use super::keys::SigningKey;
24
25const ALG: &str = "ES256";
27
28fn signing_input(header: &Value, claims: &Value) -> Result<String> {
30 let header = serde_json::to_vec(header).context("serializing the JWS header")?;
31 let claims = serde_json::to_vec(claims).context("serializing the JWS claims")?;
32 Ok(format!(
33 "{}.{}",
34 URL_SAFE_NO_PAD.encode(header),
35 URL_SAFE_NO_PAD.encode(claims)
36 ))
37}
38
39pub fn sign(key: &SigningKey, header: &Value, claims: &Value) -> Result<String> {
49 let mut header = header.clone();
54 header
55 .as_object_mut()
56 .ok_or_else(|| anyhow!("JWS header must be a JSON object"))?
57 .insert("alg".into(), Value::String(ALG.into()));
58
59 let input = signing_input(&header, claims)?;
60 let signer = EcdsaSigningKey::from(key.secret());
61 let signature: Signature = signer.sign(input.as_bytes());
62 Ok(format!(
63 "{input}.{}",
64 URL_SAFE_NO_PAD.encode(signature.to_bytes())
65 ))
66}
67
68pub fn verify(key: &SigningKey, jws: &str) -> Result<()> {
73 let parts: Vec<&str> = jws.split('.').collect();
74 if parts.len() != 3 {
75 bail!("malformed JWS: expected 3 segments, got {}", parts.len());
76 }
77 let header: Value = serde_json::from_slice(
82 &URL_SAFE_NO_PAD
83 .decode(parts[0])
84 .context("header is not valid base64url")?,
85 )
86 .context("JWS header is not valid JSON")?;
87 match header.get("alg").and_then(Value::as_str) {
88 Some(ALG) => {}
89 other => bail!("unsupported JWS alg {other:?}; only {ALG} is accepted"),
90 }
91
92 let raw = URL_SAFE_NO_PAD
93 .decode(parts[2])
94 .context("signature is not valid base64url")?;
95 let signature =
96 Signature::from_slice(&raw).map_err(|err| anyhow!("not a valid P-256 signature: {err}"))?;
97
98 let input = format!("{}.{}", parts[0], parts[1]);
99 let verifier = VerifyingKey::from(key.secret().public_key());
100 verifier
101 .verify(input.as_bytes(), &signature)
102 .map_err(|_| anyhow!("JWS signature does not verify"))
103}
104
105#[cfg(test)]
106mod tests {
107 use super::*;
108 use serde_json::json;
109
110 const KID: &str = "featherreader-oauth-1";
111
112 fn decode_part(part: &str) -> Value {
113 serde_json::from_slice(&URL_SAFE_NO_PAD.decode(part).unwrap()).unwrap()
114 }
115
116 #[test]
128 fn a_valid_jws_with_a_trailing_segment_is_refused() {
129 let key = SigningKey::generate(KID);
130 let jws = sign(&key, &json!({"typ": "JWT"}), &json!({"iss": "x"})).unwrap();
131 verify(&key, &jws).expect("the unmodified JWS must verify");
132
133 let extended = format!("{jws}.AAAA");
134 let err =
135 verify(&key, &extended).expect_err("a JWS with a trailing segment must not verify");
136 assert!(
137 format!("{err:#}").contains("expected 3 segments"),
138 "refused, but not by the segment count — the relaxed check is what this \
139 test exists to catch: {err:#}",
140 );
141 }
142
143 #[test]
157 fn a_signature_valid_under_a_forged_alg_header_is_refused() {
158 let key = SigningKey::generate(KID);
159 let header = json!({"alg": "RS256", "typ": "JWT", "kid": KID});
160 let claims = json!({"iss": "https://x.example"});
161
162 let input = signing_input(&header, &claims).unwrap();
163 let signer = EcdsaSigningKey::from(key.secret());
164 let signature: Signature = signer.sign(input.as_bytes());
165 let forged = format!("{input}.{}", URL_SAFE_NO_PAD.encode(signature.to_bytes()));
166
167 let honest_input =
170 signing_input(&json!({"alg": "ES256", "typ": "JWT", "kid": KID}), &claims).unwrap();
171 let honest_sig: Signature = signer.sign(honest_input.as_bytes());
172 verify(
173 &key,
174 &format!(
175 "{honest_input}.{}",
176 URL_SAFE_NO_PAD.encode(honest_sig.to_bytes())
177 ),
178 )
179 .expect("the same construction with an honest alg must verify");
180
181 let err = verify(&key, &forged).expect_err("a forged alg header must not verify");
182 assert!(
183 format!("{err:#}").contains("unsupported JWS alg"),
184 "refused, but not by the alg check — if the signature merely failed, the \
185 guard could be deleted and this test would still pass: {err:#}",
186 );
187 }
188
189 #[test]
190 fn a_signed_jws_has_three_base64url_segments() {
191 let key = SigningKey::generate(KID);
192 let jws = sign(&key, &json!({"alg": "ES256"}), &json!({"iss": "x"})).unwrap();
193 let parts: Vec<&str> = jws.split('.').collect();
194 assert_eq!(parts.len(), 3);
195 for p in &parts {
196 assert!(!p.is_empty());
197 assert!(!p.contains('='), "base64url in JOSE is unpadded: {p}");
198 assert!(!p.contains('+') && !p.contains('/'), "not url-safe: {p}");
199 }
200 }
201
202 #[test]
203 fn the_header_and_payload_round_trip_verbatim() {
204 let key = SigningKey::generate(KID);
205 let header = json!({"alg": "ES256", "typ": "JWT", "kid": KID});
206 let claims = json!({"iss": "https://x.example", "jti": "abc", "iat": 1_700_000_000});
207 let jws = sign(&key, &header, &claims).unwrap();
208 let parts: Vec<&str> = jws.split('.').collect();
209 assert_eq!(decode_part(parts[0]), header);
210 assert_eq!(decode_part(parts[1]), claims);
211 }
212
213 #[test]
221 fn the_signature_is_fixed_width_r_s_not_der() {
222 for _ in 0..16 {
223 let key = SigningKey::generate(KID);
224 let jws = sign(&key, &json!({}), &json!({"n": 1})).unwrap();
225 let sig = URL_SAFE_NO_PAD
226 .decode(jws.split('.').nth(2).unwrap())
227 .unwrap();
228 assert_eq!(sig.len(), 64, "not a fixed-width P-256 JOSE signature");
229 }
230 }
231
232 #[test]
237 fn sign_owns_the_alg_header_and_overrides_the_caller() {
238 let key = SigningKey::generate(KID);
239 for given in [json!({}), json!({"alg": "RS256"}), json!({"alg": "none"})] {
240 let jws = sign(&key, &given, &json!({"x": 1})).unwrap();
241 assert_eq!(decode_part(jws.split('.').next().unwrap())["alg"], "ES256");
242 assert!(verify(&key, &jws).is_ok());
243 }
244 }
245
246 #[test]
247 fn sign_preserves_the_callers_other_header_members() {
248 let key = SigningKey::generate(KID);
249 let jws = sign(&key, &json!({"typ": "dpop+jwt", "kid": KID}), &json!({})).unwrap();
250 let header = decode_part(jws.split('.').next().unwrap());
251 assert_eq!(header["typ"], "dpop+jwt");
252 assert_eq!(header["kid"], KID);
253 assert_eq!(header["alg"], "ES256");
254 }
255
256 #[test]
260 fn verify_rejects_a_header_advertising_another_algorithm() {
261 let key = SigningKey::generate(KID);
262 let jws = sign(&key, &json!({}), &json!({"x": 1})).unwrap();
263 let seg: Vec<&str> = jws.split('.').collect();
264
265 for forged_header in [json!({"alg": "RS256"}), json!({"alg": "none"}), json!({})] {
266 let swapped = format!(
267 "{}.{}.{}",
268 URL_SAFE_NO_PAD.encode(serde_json::to_vec(&forged_header).unwrap()),
269 seg[1],
270 seg[2]
271 );
272 assert!(verify(&key, &swapped).is_err(), "accepted {forged_header}");
273 }
274 }
275
276 #[test]
277 fn verify_rejects_a_non_json_header() {
278 let key = SigningKey::generate(KID);
279 let jws = sign(&key, &json!({}), &json!({"x": 1})).unwrap();
280 let seg: Vec<&str> = jws.split('.').collect();
281 let bad = format!(
282 "{}.{}.{}",
283 URL_SAFE_NO_PAD.encode(b"not json"),
284 seg[1],
285 seg[2]
286 );
287 assert!(verify(&key, &bad).is_err());
288 }
289
290 #[test]
291 fn a_signature_verifies_against_the_signing_keys_public_half() {
292 let key = SigningKey::generate(KID);
293 let jws = sign(&key, &json!({"alg": "ES256"}), &json!({"sub": "did:plc:x"})).unwrap();
294 assert!(verify(&key, &jws).is_ok());
295 }
296
297 #[test]
298 fn a_signature_from_a_different_key_does_not_verify() {
299 let a = SigningKey::generate(KID);
300 let b = SigningKey::generate(KID);
301 let jws = sign(&a, &json!({"alg": "ES256"}), &json!({"sub": "x"})).unwrap();
302 assert!(verify(&b, &jws).is_err());
303 }
304
305 #[test]
306 fn tampering_with_the_payload_invalidates_the_signature() {
307 let key = SigningKey::generate(KID);
308 let jws = sign(&key, &json!({"alg": "ES256"}), &json!({"amount": 1})).unwrap();
309 let parts: Vec<&str> = jws.split('.').collect();
310 let forged = URL_SAFE_NO_PAD.encode(br#"{"amount":1000000}"#);
311 let tampered = format!("{}.{}.{}", parts[0], forged, parts[2]);
312 assert!(verify(&key, &tampered).is_err());
313 }
314
315 #[test]
316 fn a_malformed_jws_is_rejected_rather_than_panicking() {
317 let key = SigningKey::generate(KID);
318 for bad in [
319 "",
320 "onlyonepart",
321 "two.parts",
322 "a.b.c.d",
323 "...",
324 "!!!.@@@.###",
325 "a.b.",
326 ] {
327 assert!(verify(&key, bad).is_err(), "should reject {bad:?}");
328 }
329 }
330
331 #[test]
336 fn signing_is_deterministic_rfc6979() {
337 let key = SigningKey::generate(KID);
338 let a = sign(&key, &json!({"alg": "ES256"}), &json!({"x": 1})).unwrap();
339 let b = sign(&key, &json!({"alg": "ES256"}), &json!({"x": 1})).unwrap();
340 assert_eq!(a, b, "p256 signs deterministically per RFC 6979");
341 }
342}