1use base64::prelude::*;
39use serde::Deserialize;
40use tracing::warn;
41
42use crate::error::Problem;
43use crate::jws::Jwk;
44use crate::jws::signature::{SignatureError, verify_jwk_signature_and_get_der};
45
46pub type KeyChangeJws = crate::jws::AcmeJwsRequest;
49
50#[derive(Debug, Deserialize)]
53pub struct InnerHeader {
54 pub alg: String,
55 pub jwk: Jwk,
56 pub url: String,
57}
58
59#[derive(Debug, Deserialize)]
61pub struct InnerPayload {
62 pub account: String,
63 #[serde(rename = "oldKey")]
64 pub old_key: Jwk,
65}
66
67#[derive(Debug)]
74pub enum KeyChangeError {
75 Malformed(&'static str),
76 BadSignature,
77 Internal(&'static str),
78}
79
80impl From<SignatureError> for KeyChangeError {
81 fn from(error: SignatureError) -> Self {
82 match error {
83 SignatureError::Malformed(detail) => KeyChangeError::Malformed(detail),
84 SignatureError::BadAlgorithm(detail) => KeyChangeError::Malformed(detail),
90 SignatureError::BadSignature(_) => KeyChangeError::BadSignature,
91 SignatureError::Encoding(detail) => KeyChangeError::Internal(detail),
92 }
93 }
94}
95
96pub fn parse_header(
105 inner: &KeyChangeJws,
106 expected_url: &str,
107) -> Result<InnerHeader, KeyChangeError> {
108 let protected_bytes = BASE64_URL_SAFE_NO_PAD
109 .decode(&inner.protected)
110 .map_err(|_| KeyChangeError::Malformed("inner protected base64 invalid"))?;
111
112 let header: InnerHeader = serde_json::from_slice(&protected_bytes)
113 .map_err(|_| KeyChangeError::Malformed("inner protected JSON invalid"))?;
114
115 if header.url != expected_url {
116 return Err(KeyChangeError::Malformed(
117 "inner url does not match the request",
118 ));
119 }
120
121 Ok(header)
122}
123
124pub fn verify_signature(
128 inner: &KeyChangeJws,
129 header: &InnerHeader,
130) -> Result<Vec<u8>, KeyChangeError> {
131 let signing_input = format!("{}.{}", inner.protected, inner.payload);
132 Ok(verify_jwk_signature_and_get_der(
133 &header.alg,
134 &header.jwk,
135 &signing_input,
136 &inner.signature,
137 )?)
138}
139
140pub fn verify_payload(
148 inner: &KeyChangeJws,
149 expected_account_url: &str,
150 expected_old_key: &Jwk,
151) -> Result<InnerPayload, KeyChangeError> {
152 let payload_bytes = BASE64_URL_SAFE_NO_PAD
153 .decode(&inner.payload)
154 .map_err(|_| KeyChangeError::Malformed("inner payload base64 invalid"))?;
155
156 let payload: InnerPayload = serde_json::from_slice(&payload_bytes)
157 .map_err(|_| KeyChangeError::Malformed("inner payload is not a keyChange object"))?;
158
159 if payload.account != expected_account_url {
160 return Err(KeyChangeError::Malformed(
161 "inner account does not match the signer",
162 ));
163 }
164 if &payload.old_key != expected_old_key {
165 return Err(KeyChangeError::Malformed(
166 "inner oldKey does not match the account key",
167 ));
168 }
169
170 Ok(payload)
171}
172
173pub fn key_change_problem(error: KeyChangeError) -> Problem {
181 let (reason, detail) = match &error {
182 KeyChangeError::Malformed(detail) => ("malformed", *detail),
183 KeyChangeError::BadSignature => ("bad_signature", "inner JWS signature invalid"),
184 KeyChangeError::Internal(detail) => ("internal", *detail),
185 };
186 warn!(
187 event = "key_change_rejected",
188 outcome = "failure",
189 reason,
190 detail
191 );
192
193 match error {
194 KeyChangeError::Malformed(detail) => Problem::malformed(detail),
195 KeyChangeError::BadSignature => Problem::unauthorized("Inner JWS signature invalid"),
196 KeyChangeError::Internal(detail) => Problem::server_internal(detail),
197 }
198}
199
200#[cfg(test)]
201mod tests {
202 use super::*;
203 use ring::rand::SystemRandom;
204 use ring::signature::{EcdsaKeyPair, KeyPair};
205 use serde_json::json;
206
207 const URL: &str = "http://localhost:3000/keyChange";
208
209 fn b64(data: &[u8]) -> String {
210 BASE64_URL_SAFE_NO_PAD.encode(data)
211 }
212
213 fn b64_json(value: &serde_json::Value) -> String {
214 b64(&serde_json::to_vec(value).unwrap())
215 }
216
217 fn generate_ec_key() -> EcdsaKeyPair {
218 let rng = SystemRandom::new();
219 let doc =
220 EcdsaKeyPair::generate_pkcs8(&ring::signature::ECDSA_P256_SHA256_FIXED_SIGNING, &rng)
221 .unwrap();
222 EcdsaKeyPair::from_pkcs8(
223 &ring::signature::ECDSA_P256_SHA256_FIXED_SIGNING,
224 doc.as_ref(),
225 &rng,
226 )
227 .unwrap()
228 }
229
230 fn ec_jwk(key_pair: &EcdsaKeyPair) -> Jwk {
231 let point = key_pair.public_key().as_ref();
232 Jwk::EC {
233 crv: "P-256".to_string(),
234 x: b64(&point[1..33]),
235 y: b64(&point[33..65]),
236 }
237 }
238
239 fn build(new_key: &EcdsaKeyPair, url: &str, payload: &serde_json::Value) -> KeyChangeJws {
242 let protected = json!({ "alg": "ES256", "jwk": ec_jwk_value(new_key), "url": url });
243 let protected_b64 = b64_json(&protected);
244 let payload_b64 = b64_json(payload);
245 let signing_input = format!("{protected_b64}.{payload_b64}");
246 let rng = SystemRandom::new();
247 let sig = new_key.sign(&rng, signing_input.as_bytes()).unwrap();
248 KeyChangeJws {
249 protected: protected_b64,
250 payload: payload_b64,
251 signature: b64(sig.as_ref()),
252 }
253 }
254
255 fn ec_jwk_value(key_pair: &EcdsaKeyPair) -> serde_json::Value {
256 let point = key_pair.public_key().as_ref();
257 json!({ "kty": "EC", "crv": "P-256", "x": b64(&point[1..33]), "y": b64(&point[33..65]) })
258 }
259
260 fn account_url() -> &'static str {
261 "http://localhost:3000/acct/1"
262 }
263
264 fn payload_for(old_key: &Jwk) -> serde_json::Value {
265 let old_key_json = match old_key {
266 Jwk::EC { crv, x, y } => json!({ "kty": "EC", "crv": crv, "x": x, "y": y }),
267 Jwk::RSA { n, e } => json!({ "kty": "RSA", "n": n, "e": e }),
268 };
269 json!({ "account": account_url(), "oldKey": old_key_json })
270 }
271
272 #[test]
273 fn parse_header_accepts_well_formed_and_matching_url() {
274 let new_key = generate_ec_key();
275 let old_key = ec_jwk(&generate_ec_key());
276 let inner = build(&new_key, URL, &payload_for(&old_key));
277
278 let header = parse_header(&inner, URL).unwrap();
279 assert_eq!(header.alg, "ES256");
280 }
281
282 #[test]
283 fn parse_header_rejects_url_mismatch() {
284 let new_key = generate_ec_key();
285 let old_key = ec_jwk(&generate_ec_key());
286 let inner = build(
287 &new_key,
288 "http://localhost:3000/other",
289 &payload_for(&old_key),
290 );
291
292 assert!(matches!(
293 parse_header(&inner, URL),
294 Err(KeyChangeError::Malformed(_))
295 ));
296 }
297
298 #[test]
299 fn parse_header_rejects_malformed_base64_and_json() {
300 let new_key = generate_ec_key();
301 let old_key = ec_jwk(&generate_ec_key());
302
303 let mut inner = build(&new_key, URL, &payload_for(&old_key));
304 inner.protected = "!!!not-base64!!!".to_string();
305 assert!(matches!(
306 parse_header(&inner, URL),
307 Err(KeyChangeError::Malformed(_))
308 ));
309
310 let mut inner = build(&new_key, URL, &payload_for(&old_key));
311 inner.protected = b64(b"not json");
312 assert!(matches!(
313 parse_header(&inner, URL),
314 Err(KeyChangeError::Malformed(_))
315 ));
316 }
317
318 #[test]
319 fn parse_header_rejects_missing_jwk() {
320 let protected_b64 = b64_json(&json!({ "alg": "ES256", "url": URL }));
321 let inner = KeyChangeJws {
322 protected: protected_b64,
323 payload: String::new(),
324 signature: String::new(),
325 };
326 assert!(matches!(
327 parse_header(&inner, URL),
328 Err(KeyChangeError::Malformed(_))
329 ));
330 }
331
332 #[test]
333 fn verify_signature_accepts_a_correctly_self_signed_inner_jws() {
334 let new_key = generate_ec_key();
335 let old_key = ec_jwk(&generate_ec_key());
336 let inner = build(&new_key, URL, &payload_for(&old_key));
337 let header = parse_header(&inner, URL).unwrap();
338
339 let der = verify_signature(&inner, &header).unwrap();
340 assert!(!der.is_empty());
341 }
342
343 #[test]
344 fn verify_signature_rejects_a_tampered_signature() {
345 let new_key = generate_ec_key();
346 let old_key = ec_jwk(&generate_ec_key());
347 let mut inner = build(&new_key, URL, &payload_for(&old_key));
348 let header = parse_header(&inner, URL).unwrap();
349 inner.signature = b64(&[0u8; 64]);
350
351 assert!(matches!(
352 verify_signature(&inner, &header),
353 Err(KeyChangeError::BadSignature)
354 ));
355 }
356
357 #[test]
358 fn verify_signature_rejects_a_tampered_payload() {
359 let new_key = generate_ec_key();
360 let old_key = ec_jwk(&generate_ec_key());
361 let mut inner = build(&new_key, URL, &payload_for(&old_key));
362 let header = parse_header(&inner, URL).unwrap();
363 inner.payload = b64_json(&payload_for(&ec_jwk(&generate_ec_key())));
366
367 assert!(matches!(
368 verify_signature(&inner, &header),
369 Err(KeyChangeError::BadSignature)
370 ));
371 }
372
373 #[test]
374 fn verify_payload_accepts_correct_account_and_old_key() {
375 let new_key = generate_ec_key();
376 let old_key = ec_jwk(&generate_ec_key());
377 let inner = build(&new_key, URL, &payload_for(&old_key));
378
379 let payload = verify_payload(&inner, account_url(), &old_key).unwrap();
380 assert_eq!(payload.account, account_url());
381 }
382
383 #[test]
384 fn verify_payload_rejects_account_mismatch() {
385 let new_key = generate_ec_key();
386 let old_key = ec_jwk(&generate_ec_key());
387 let inner = build(&new_key, URL, &payload_for(&old_key));
388
389 assert!(matches!(
390 verify_payload(&inner, "http://localhost:3000/acct/999", &old_key),
391 Err(KeyChangeError::Malformed(_))
392 ));
393 }
394
395 #[test]
396 fn verify_payload_rejects_old_key_mismatch() {
397 let new_key = generate_ec_key();
398 let old_key = ec_jwk(&generate_ec_key());
399 let unrelated_key = ec_jwk(&generate_ec_key());
400 let inner = build(&new_key, URL, &payload_for(&old_key));
401
402 assert!(matches!(
403 verify_payload(&inner, account_url(), &unrelated_key),
404 Err(KeyChangeError::Malformed(_))
405 ));
406 }
407
408 #[test]
409 fn verify_payload_rejects_malformed_payload_json() {
410 let new_key = generate_ec_key();
411 let old_key = ec_jwk(&generate_ec_key());
412 let mut inner = build(&new_key, URL, &payload_for(&old_key));
413 inner.payload = b64(b"not a keyChange object");
414
415 assert!(matches!(
416 verify_payload(&inner, account_url(), &old_key),
417 Err(KeyChangeError::Malformed(_))
418 ));
419 }
420
421 #[test]
422 fn verify_payload_rejects_malformed_payload_base64() {
423 let new_key = generate_ec_key();
424 let old_key = ec_jwk(&generate_ec_key());
425 let mut inner = build(&new_key, URL, &payload_for(&old_key));
426 inner.payload = "!!!not-base64!!!".to_string();
427
428 assert!(matches!(
429 verify_payload(&inner, account_url(), &old_key),
430 Err(KeyChangeError::Malformed(_))
431 ));
432 }
433}