Skip to main content

riff_core/
signed.rs

1//! Signed messages (R195-R201).
2//!
3//! # Design
4//!
5//! Each message carries a signature from the device key of its sender:
6//! the key that its token is bound to (see [`crate::dpop`]). A reader
7//! checks the signature. So it finds a message that changed in storage,
8//! or a message with a false sender, and it does not need to trust the
9//! storage of the server.
10//!
11//! The signature is a JWS with a detached payload (RFC 7515, appendix
12//! F). The message carries the payload next to the signature:
13//!
14//! ```text
15//!  sig     = <header>..<signature>
16//!            header  {"typ":"riff-message","alg":"ES256","jwk":{public key}}
17//!  payload = base64url of the JSON of Content
18//! ```
19//!
20//! [`Content`] is what the signature covers: the who and the lead mark
21//! of the sender, the thread of the post, the `to` selectors, the body,
22//! the kind and the time (R196).
23//!
24//! The sender makes the payload one time. The server and each reader
25//! keep its bytes unchanged, and never encode them again. A check runs
26//! over the kept bytes ([`check`]), then decodes them ([`Signed`]). A
27//! build skips a field of the payload that it does not know, so a new
28//! field never stops a check (01M3T411N0HM699VJXW6RTVWKB).
29//!
30//! # Rules
31//!
32//! - A post to a thread signs that thread. A direct message has no
33//!   thread when it is sent, so it signs no thread. Its one selector
34//!   names the other session.
35//! - `riff-server` accepts a signed post only from the key of the token
36//!   of its caller, and only with a time near its own time (R197).
37//! - A reader counts a message as verified only when the signature is
38//!   valid, covers the message as the reader got it, and comes from a
39//!   key of the user of the sender (R199). See
40//!   [`crate::wire::Message::verified`].
41//! - ECDSA can give two valid signatures for the same bytes. So a copy
42//!   check compares [`payload_hash`], not the signature.
43//!
44//! # Example
45//!
46//! ```
47//! use riff_core::dpop::Key;
48//! use riff_core::name::Who;
49//! use riff_core::signed::Content;
50//! use riff_core::wire::Kind;
51//!
52//! let key = Key::generate();
53//! let mike = Who::new("mike", Some("a6cf"))?;
54//! let thread = "como-technologies/riff".parse()?;
55//! let content = Content {
56//!     from: &mike,
57//!     thread: Some(&thread),
58//!     to: &[],
59//!     body: "ready",
60//!     lead: false,
61//!     kind: Kind::Message,
62//!     at_ms: 1_000,
63//! };
64//! let sig = content.sign(&key);
65//! assert_eq!(content.verify(&sig).unwrap(), key.thumbprint());
66//!
67//! // A changed body does not verify.
68//! let changed = Content { body: "not ready", ..content };
69//! assert!(changed.verify(&sig).is_err());
70//!
71//! // The check of the kept payload gives the key and the fields.
72//! let (jkt, signed) = riff_core::signed::check(&content.payload(), &sig).unwrap();
73//! assert_eq!(jkt, key.thumbprint());
74//! assert!(signed.covers(&content));
75//! # Ok::<(), riff_core::name::NameError>(())
76//! ```
77
78use 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
92/// The `typ` of a message signature. It is not `dpop+jwt`, so a proof
93/// never counts as a message signature, and a signature never counts as
94/// a proof.
95pub const TYP: &str = "riff-message";
96
97/// The fields of a signed payload (R196). One struct is the layout of
98/// both sides (01M49W17GGV1K5FZEKJRPV0GNA): the sender writes a
99/// [`Content`] that borrows the fields of its post, and a reader reads
100/// a [`Signed`] that owns them. A reader skips each field that it does
101/// not know.
102#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
103pub struct Payload<F, T, S, B> {
104    /// The user and the session ID of the sender.
105    pub from: F,
106    /// True when the sender posts as the lead (R198). The place and the
107    /// claims of the sender are not signed.
108    pub lead: bool,
109    /// The thread of the post. `None` for a direct message.
110    pub thread: Option<T>,
111    #[serde(default)]
112    pub to: S,
113    pub body: B,
114    #[serde(default)]
115    pub kind: Kind,
116    /// The time of the message, in milliseconds since the Unix epoch.
117    pub at_ms: u64,
118}
119
120/// What the signature of a message covers: the payload that the sender
121/// writes, with the fields of its post.
122pub type Content<'a> = Payload<&'a Who, &'a ThreadName, &'a [Selector], &'a str>;
123
124/// The fields of a signed payload, as a reader decodes them.
125pub type Signed = Payload<Who, ThreadName, Vec<Selector>, String>;
126
127impl Content<'_> {
128    /// The payload of the signature: the base64url of the JSON of the
129    /// content. The sender makes it one time. Each other side keeps its
130    /// bytes.
131    pub fn payload(&self) -> String {
132        json_b64(self)
133    }
134
135    /// The signature of the content by the device key `key`.
136    pub fn sign(&self, key: &Key) -> String {
137        sign_payload(&self.payload(), key)
138    }
139
140    /// Checks that `sig` signs this content. Gives the thumbprint of the
141    /// key that signed it. The caller checks that the key belongs to the
142    /// sender.
143    pub fn verify(&self, sig: &str) -> Result<String, SignError> {
144        verify(&self.payload(), sig)
145    }
146}
147
148impl Signed {
149    /// True when the payload holds the fields of `content`.
150    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
170/// Checks that `sig` signs the kept bytes of `payload`, then decodes
171/// them. Gives the thumbprint of the key and the fields. It never
172/// encodes the payload again.
173///
174/// ```
175/// use base64::Engine;
176/// use riff_core::dpop::Key;
177/// use riff_core::signed::{check, sign_payload};
178///
179/// let key = Key::generate();
180/// // A payload with a field that this build does not know.
181/// let json = r#"{"from":{"user":"ann","session":"s1"},"lead":false,"thread":"design",
182///     "to":[],"body":"hi","kind":"message","at_ms":5,"reply_to":7}"#;
183/// let payload = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(json);
184/// let sig = sign_payload(&payload, &key);
185/// let (jkt, signed) = check(&payload, &sig).unwrap();
186/// assert_eq!(jkt, key.thumbprint());
187/// assert_eq!(signed.body, "hi");
188/// ```
189pub 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
199/// The signature of the kept bytes `payload` by the device key `key`.
200pub 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
210/// The hash of a payload, in hex. Two copies of one message have the
211/// same hash.
212///
213/// ```
214/// use riff_core::signed::payload_hash;
215///
216/// assert_eq!(payload_hash("abc"), payload_hash("abc"));
217/// assert_ne!(payload_hash("abc"), payload_hash("abd"));
218/// assert_eq!(payload_hash("abc").len(), 64);
219/// ```
220pub fn payload_hash(payload: &str) -> String {
221    Sha256::digest(payload.as_bytes())
222        .iter()
223        .map(|byte| format!("{byte:02x}"))
224        .collect()
225}
226
227/// Checks that `sig` signs `payload` as it is. Gives the thumbprint of
228/// the key.
229fn 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/// Why a signature is not valid.
261#[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    /// A signed message of a later build: this build reads its kind as
366    /// `other`, and keeps each field of its selector
367    /// (01M3XSF90E9JYYTC13D9THY4WE). The check still covers the
368    /// selector as it came.
369    #[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        // A change of the field that the build does not know, or a
395        // selector with only the fields that it knows, is not covered.
396        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        // A kind that the build knows is not the kind of the payload.
401        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}