1use std::fmt;
79
80use base64::Engine;
81use base64::engine::general_purpose::URL_SAFE_NO_PAD as B64;
82use p256::ecdsa::Signature;
83use p256::ecdsa::signature::Verifier;
84use serde::{Deserialize, Serialize};
85use sha2::{Digest, Sha256};
86
87use crate::dpop::{ALG, Header, Key, json_b64};
88use crate::name::{ThreadName, Who};
89use crate::selector::Selector;
90use crate::wire::Kind;
91
92pub const TYP: &str = "riff-message";
96
97#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
103pub struct Payload<F, T, S, B> {
104 pub from: F,
106 pub lead: bool,
109 pub thread: Option<T>,
111 #[serde(default)]
112 pub to: S,
113 pub body: B,
114 #[serde(default)]
115 pub kind: Kind,
116 pub at_ms: u64,
118}
119
120pub type Content<'a> = Payload<&'a Who, &'a ThreadName, &'a [Selector], &'a str>;
123
124pub type Signed = Payload<Who, ThreadName, Vec<Selector>, String>;
126
127impl Content<'_> {
128 pub fn payload(&self) -> String {
132 json_b64(self)
133 }
134
135 pub fn sign(&self, key: &Key) -> String {
137 sign_payload(&self.payload(), key)
138 }
139
140 pub fn verify(&self, sig: &str) -> Result<String, SignError> {
144 verify(&self.payload(), sig)
145 }
146}
147
148impl Signed {
149 pub fn covers(&self, content: &Content<'_>) -> bool {
151 let Signed {
152 from,
153 lead,
154 thread,
155 to,
156 body,
157 kind,
158 at_ms,
159 } = self;
160 from == content.from
161 && *lead == content.lead
162 && thread.as_ref() == content.thread
163 && to == content.to
164 && body == content.body
165 && *kind == content.kind
166 && *at_ms == content.at_ms
167 }
168}
169
170pub fn check(payload: &str, sig: &str) -> Result<(String, Signed), SignError> {
190 let jkt = verify(payload, sig)?;
191 let bytes = B64
192 .decode(payload)
193 .map_err(|_| SignError::new("the payload is not base64url"))?;
194 let signed = serde_json::from_slice(&bytes)
195 .map_err(|e| SignError(format!("the payload does not decode: {e}")))?;
196 Ok((jkt, signed))
197}
198
199pub fn sign_payload(payload: &str, key: &Key) -> String {
201 let header = json_b64(&Header {
202 typ: TYP.into(),
203 alg: ALG.into(),
204 jwk: key.jwk(),
205 });
206 let input = format!("{header}.{payload}");
207 format!("{header}..{}", key.sign(input.as_bytes()))
208}
209
210pub fn payload_hash(payload: &str) -> String {
221 Sha256::digest(payload.as_bytes())
222 .iter()
223 .map(|byte| format!("{byte:02x}"))
224 .collect()
225}
226
227fn verify(payload: &str, sig: &str) -> Result<String, SignError> {
230 let Some((header, signature)) = sig.split_once("..") else {
231 return Err(SignError::new("the signature is not a detached JWS"));
232 };
233 let header_json: serde_json::Value = decode(header)?;
234 if header_json.pointer("/jwk/d").is_some() {
235 return Err(SignError::new("the signature jwk holds a private key"));
236 }
237 let header_json: Header = serde_json::from_value(header_json)
238 .map_err(|_| SignError::new("the signature header is not valid"))?;
239 if header_json.typ != TYP {
240 return Err(SignError::new("the signature typ is not riff-message"));
241 }
242 if header_json.alg != ALG {
243 return Err(SignError::new("the signature alg is not ES256"));
244 }
245 let key = header_json
246 .jwk
247 .verifying_key()
248 .map_err(|e| SignError(e.to_string()))?;
249 let signature = B64
250 .decode(signature)
251 .ok()
252 .and_then(|bytes| Signature::from_slice(&bytes).ok())
253 .ok_or_else(|| SignError::new("the signature is malformed"))?;
254 let input = format!("{header}.{payload}");
255 key.verify(input.as_bytes(), &signature)
256 .map_err(|_| SignError::new("the signature is not valid for this message"))?;
257 Ok(header_json.jwk.thumbprint())
258}
259
260#[derive(Clone, Debug, PartialEq, Eq)]
262pub struct SignError(String);
263
264impl SignError {
265 fn new(message: &str) -> Self {
266 SignError(message.to_owned())
267 }
268}
269
270impl fmt::Display for SignError {
271 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
272 f.write_str(&self.0)
273 }
274}
275
276impl std::error::Error for SignError {}
277
278fn decode<T: for<'de> Deserialize<'de>>(part: &str) -> Result<T, SignError> {
279 let bytes = B64
280 .decode(part)
281 .map_err(|_| SignError::new("the signature header is not base64url"))?;
282 serde_json::from_slice(&bytes)
283 .map_err(|_| SignError::new("the signature header is not valid JSON"))
284}
285
286#[cfg(test)]
287mod tests {
288 use super::*;
289
290 fn mike() -> Who {
291 Who::new("mike", Some("a6cf")).unwrap()
292 }
293
294 fn thread() -> ThreadName {
295 "como-technologies/riff".parse().unwrap()
296 }
297
298 fn to() -> Vec<Selector> {
299 vec!["claim=issue-6".parse().unwrap()]
300 }
301
302 #[test]
303 fn each_part_of_the_content_counts() {
304 let key = Key::generate();
305 let (mike, thread, to) = (mike(), thread(), to());
306 let content = Content {
307 from: &mike,
308 thread: Some(&thread),
309 to: &to,
310 body: "ready",
311 lead: false,
312 kind: Kind::Message,
313 at_ms: 1_000,
314 };
315 let sig = content.sign(&key);
316 assert_eq!(content.verify(&sig).unwrap(), key.thumbprint());
317
318 let brett = Who::new("brett", Some("a6cf")).unwrap();
319 let other_session = Who::new("mike", Some("b7d0")).unwrap();
320 let other_thread: ThreadName = "design".parse().unwrap();
321 let changed = [
322 Content {
323 from: &brett,
324 ..content
325 },
326 Content {
327 from: &other_session,
328 ..content
329 },
330 Content {
331 thread: Some(&other_thread),
332 ..content
333 },
334 Content {
335 thread: None,
336 ..content
337 },
338 Content { to: &[], ..content },
339 Content {
340 body: "ready!",
341 ..content
342 },
343 Content {
344 lead: true,
345 ..content
346 },
347 Content {
348 kind: Kind::Status,
349 ..content
350 },
351 Content {
352 at_ms: 1_001,
353 ..content
354 },
355 ];
356 for c in changed {
357 assert_eq!(
358 c.verify(&sig).unwrap_err().to_string(),
359 "the signature is not valid for this message",
360 "{c:?}"
361 );
362 }
363 }
364
365 #[test]
370 fn a_payload_with_a_kind_and_a_selector_field_of_a_later_build_checks() {
371 let key = Key::generate();
372 let json = r#"{"from":{"user":"mike","session":"a6cf"},"lead":true,"thread":"design",
373 "to":[{"user":"brett","wave":"17"}],"body":"go","kind":"poll","at_ms":5}"#;
374 let payload = B64.encode(json);
375 let sig = sign_payload(&payload, &key);
376 let (jkt, signed) = check(&payload, &sig).unwrap();
377 assert_eq!(jkt, key.thumbprint());
378 assert_eq!(signed.kind, Kind::Other);
379 assert!(signed.to[0].is_other());
380
381 let (mike, thread) = (mike(), "design".parse().unwrap());
382 let read = |selector: &str| -> Vec<Selector> { serde_json::from_str(selector).unwrap() };
383 let to = read(r#"[{"user":"brett","wave":"17"}]"#);
384 let content = Content {
385 from: &mike,
386 lead: true,
387 thread: Some(&thread),
388 to: &to,
389 body: "go",
390 kind: Kind::Other,
391 at_ms: 5,
392 };
393 assert!(signed.covers(&content));
394 for changed in [r#"[{"user":"brett","wave":"18"}]"#, r#"[{"user":"brett"}]"#] {
397 let to = read(changed);
398 assert!(!signed.covers(&Content { to: &to, ..content }), "{changed}");
399 }
400 let known = Content {
402 kind: Kind::Message,
403 ..content
404 };
405 assert!(!signed.covers(&known));
406 }
407
408 #[test]
409 fn the_signature_names_its_key() {
410 let mike = mike();
411 let content = Content {
412 from: &mike,
413 thread: None,
414 to: &[],
415 body: "hi",
416 lead: false,
417 kind: Kind::Message,
418 at_ms: 5,
419 };
420 let (a, b) = (Key::generate(), Key::generate());
421 assert_eq!(content.verify(&content.sign(&a)).unwrap(), a.thumbprint());
422 assert_eq!(content.verify(&content.sign(&b)).unwrap(), b.thumbprint());
423 }
424
425 #[test]
426 fn a_signature_from_another_key_under_this_header_is_refused() {
427 let mike = mike();
428 let content = Content {
429 from: &mike,
430 thread: None,
431 to: &[],
432 body: "hi",
433 lead: false,
434 kind: Kind::Message,
435 at_ms: 5,
436 };
437 let sig = content.sign(&Key::generate());
438 let other = content.sign(&Key::generate());
439 let (header, _) = sig.split_once("..").unwrap();
440 let (_, signature) = other.split_once("..").unwrap();
441 let forged = format!("{header}..{signature}");
442 assert!(content.verify(&forged).is_err());
443 }
444
445 #[test]
446 fn a_dpop_proof_is_not_a_signature() {
447 let key = Key::generate();
448 let proof = key.proof("POST", "https://riff.example.com/v1/post", None, 5);
449 let mut parts = proof.split('.');
450 let (header, _, signature) = (parts.next(), parts.next(), parts.next());
451 let as_sig = format!("{}..{}", header.unwrap(), signature.unwrap());
452 let mike = mike();
453 let content = Content {
454 from: &mike,
455 thread: None,
456 to: &[],
457 body: "hi",
458 lead: false,
459 kind: Kind::Message,
460 at_ms: 5,
461 };
462 assert_eq!(
463 content.verify(&as_sig).unwrap_err().to_string(),
464 "the signature typ is not riff-message"
465 );
466 }
467
468 #[test]
469 fn malformed_signatures_are_refused() {
470 let mike = mike();
471 let content = Content {
472 from: &mike,
473 thread: None,
474 to: &[],
475 body: "hi",
476 lead: false,
477 kind: Kind::Message,
478 at_ms: 5,
479 };
480 for bad in ["", "a.b.c", "a..b", "!!..!!"] {
481 assert!(content.verify(bad).is_err(), "{bad}");
482 }
483 }
484
485 #[test]
486 fn a_private_jwk_and_other_algorithms_are_refused() {
487 let key = Key::generate();
488 let mike = mike();
489 let content = Content {
490 from: &mike,
491 thread: None,
492 to: &[],
493 body: "hi",
494 lead: false,
495 kind: Kind::Message,
496 at_ms: 5,
497 };
498 let with = |header: serde_json::Value| {
499 let header = json_b64(&header);
500 let input = format!("{header}.{}", json_b64(&content));
501 format!("{header}..{}", key.sign(input.as_bytes()))
502 };
503 let good = serde_json::json!({"typ": TYP, "alg": ALG, "jwk": key.jwk()});
504 assert!(content.verify(&with(good.clone())).is_ok());
505
506 let mut private = good.clone();
507 private["jwk"]["d"] = "secret".into();
508 assert_eq!(
509 content.verify(&with(private)).unwrap_err().to_string(),
510 "the signature jwk holds a private key"
511 );
512 let mut none = good;
513 none["alg"] = "none".into();
514 assert_eq!(
515 content.verify(&with(none)).unwrap_err().to_string(),
516 "the signature alg is not ES256"
517 );
518 }
519}