Skip to main content

macula_rust/frame/
publication.rs

1//! Publications (D17): signed under MACULA-PQ-PUBLICATION-V1 by the
2//! publisher's identity key, with no frame_type in the tbs, since the same
3//! bytes ride in every EVENT and GOSSIP made from a PUBLISH. A verifier
4//! accepts one published up to 5 minutes ahead of its clock, until its
5//! ttl_ms, or 10 minutes without one, and 5 minutes more; a ttl_ms is at most
6//! one hour.
7
8use sha2::{Digest, Sha384};
9
10use crate::cbor::Value;
11use crate::node_key::{node_id_of, NodeKey};
12use crate::profile::Profile;
13use crate::signed_object::{sign_object, verify_object};
14
15use super::{
16    bounded_text, check_payload, entry, fixed, has_fields, identity_signer, object_refusal,
17    protocol_uint, read_fields, received_frame, text_of, uint, FrameError, Rule, MAX_PROTOCOL_INT,
18    MAX_TOPIC_BYTES, PROTOCOL_VERSION, PUBLICATION_LABEL,
19};
20
21const PUBLISH: &str = "publish";
22const EVENT: &str = "event";
23const PLUMTREE_GOSSIP: &str = "plumtree_gossip";
24const TOLERANCE_MS: u64 = 5 * 60_000;
25const DEFAULT_TTL_MS: u64 = 10 * 60_000;
26const MAX_TTL_MS: u64 = 60 * 60_000;
27
28/// A publication as its publisher gives it: a realm, a topic, the publisher's
29/// own seq, when it was published in Unix milliseconds, a payload, and a
30/// ttl_ms, `None` for the 10 minutes a publication lives without one.
31#[derive(Debug, Clone, PartialEq)]
32pub struct PublicationSpec {
33    pub realm: [u8; 32],
34    pub topic: String,
35    pub seq: u64,
36    pub published_at: u64,
37    pub payload: Value,
38    pub ttl_ms: Option<u64>,
39}
40
41/// A publication that verified: its fields, the publisher's key as carried,
42/// `publication_hash`, the SHA-384 of its tbs, which deduplication keys on,
43/// and `expires_at`, the last moment a verifier accepts it.
44#[derive(Debug, Clone, PartialEq)]
45pub struct VerifiedPublication {
46    pub publisher: [u8; 32],
47    pub realm: [u8; 32],
48    pub topic: String,
49    pub seq: u64,
50    pub published_at: u64,
51    pub ttl_ms: Option<u64>,
52    pub payload: Value,
53    pub key: Vec<u8>,
54    pub publication_hash: [u8; 48],
55    pub expires_at: u64,
56}
57
58/// Signs a publication as a PUBLISH with the publisher's identity key.
59/// Refused, in macula's order: a key that is not an identity key; a seq or
60/// published_at of 2^53 or more; a topic over 512 bytes; a payload the wire
61/// cannot carry; a ttl_ms over one hour.
62pub fn sign_publish(spec: &PublicationSpec, key: &NodeKey) -> Result<Value, FrameError> {
63    identity_signer(key)?;
64    if spec.seq >= MAX_PROTOCOL_INT || spec.published_at >= MAX_PROTOCOL_INT {
65        return Err(FrameError::OutOfRange(
66            "a seq or published_at of 2^53 or more".into(),
67        ));
68    }
69    bounded_text("topic", spec.topic.as_bytes(), MAX_TOPIC_BYTES)?;
70    check_payload(&spec.payload)?;
71    if spec.ttl_ms.is_some_and(|t| t > MAX_TTL_MS) {
72        return Err(FrameError::OutOfRange("a ttl_ms over one hour".into()));
73    }
74    let mut fields = vec![
75        entry("publisher", Value::Bytes(key.key_id().to_vec())),
76        entry("realm", Value::Bytes(spec.realm.to_vec())),
77        entry("topic", Value::text(spec.topic.clone())),
78        entry("seq", uint(spec.seq)),
79        entry("published_at", uint(spec.published_at)),
80        entry("payload", spec.payload.clone()),
81    ];
82    if let Some(ttl) = spec.ttl_ms {
83        fields.push(entry("ttl_ms", uint(ttl)));
84    }
85    let publication = sign_object(PUBLICATION_LABEL, &fields, key).map_err(object_refusal)?;
86    Ok(Value::Map(vec![
87        entry("version", Value::Int(i128::from(PROTOCOL_VERSION))),
88        entry("frame_type", Value::text(PUBLISH)),
89        entry("publication", publication.to_value()),
90    ]))
91}
92
93const PUBLICATION_TABLE: &[(&str, Rule)] = &[
94    ("alg", Rule::Any),
95    ("publisher", Rule::BytesOf(32)),
96    ("realm", Rule::BytesOf(32)),
97    ("topic", Rule::TextWithin(MAX_TOPIC_BYTES)),
98    ("seq", Rule::ProtocolUint),
99    ("published_at", Rule::ProtocolUint),
100    ("ttl_ms", Rule::ProtocolUint),
101    ("payload", Rule::Any),
102];
103
104/// Verifies the publication a received PUBLISH, EVENT or GOSSIP carries,
105/// under the connection's `profile` and the verifier's clock `now_ms`: the
106/// frame is exactly version, frame_type and publication, with an EVENT's
107/// delivered_via or a GOSSIP's round; then the publication's signature and
108/// fields, a ttl_ms of at most an hour, publisher as the key id of its key,
109/// and its time.
110pub fn verify_publication(
111    frame: &Value,
112    profile: Profile,
113    now_ms: i64,
114) -> Result<VerifiedPublication, FrameError> {
115    let frame_type = frame.get("frame_type").map(text_of).unwrap_or_default();
116    let (extra, types): (&[(&str, Rule)], &'static [&'static str]) = match frame_type.as_str() {
117        PUBLISH => (&[], &[PUBLISH]),
118        EVENT => (
119            &[("delivered_via", Rule::TextIn(&["plumtree", "direct"]))],
120            &[EVENT],
121        ),
122        PLUMTREE_GOSSIP => (&[("round", Rule::ProtocolUint)], &[PLUMTREE_GOSSIP]),
123        _ => return Err(FrameError::Malformed),
124    };
125    let (_, object) = received_frame(frame, "publication", Rule::CarriedObject, extra, types)
126        .ok_or(FrameError::Malformed)?;
127    if extra.iter().any(|(name, _)| frame.get(name).is_none()) {
128        return Err(FrameError::Malformed);
129    }
130    let verified = verify_object(PUBLICATION_LABEL, &object, profile).map_err(object_refusal)?;
131    let fields = read_fields(&verified.fields, PUBLICATION_TABLE).ok_or(FrameError::Malformed)?;
132    if !has_fields(
133        &fields,
134        &[
135            "publisher",
136            "realm",
137            "topic",
138            "seq",
139            "published_at",
140            "payload",
141        ],
142    ) {
143        return Err(FrameError::Malformed);
144    }
145    let ttl_ms = fields.get("ttl_ms").and_then(protocol_uint);
146    let published_at = protocol_uint(&fields["published_at"]).unwrap_or(0);
147    let publication = VerifiedPublication {
148        publisher: fixed(&fields["publisher"]),
149        realm: fixed(&fields["realm"]),
150        topic: text_of(&fields["topic"]),
151        seq: protocol_uint(&fields["seq"]).unwrap_or(0),
152        published_at,
153        ttl_ms,
154        payload: fields["payload"].clone(),
155        publication_hash: Sha384::digest(&verified.tbs).into(),
156        expires_at: published_at + ttl_ms.unwrap_or(DEFAULT_TTL_MS) + TOLERANCE_MS,
157        key: verified.key,
158    };
159    let valid_from = published_at as i64 - TOLERANCE_MS as i64;
160    if ttl_ms.is_some_and(|t| t > MAX_TTL_MS) {
161        return Err(FrameError::Malformed);
162    }
163    if publication.publisher != node_id_of(&publication.key, profile) {
164        return Err(FrameError::KeyIdMismatch);
165    }
166    if valid_from > now_ms {
167        return Err(FrameError::NotYetValid(valid_from - now_ms));
168    }
169    if now_ms > publication.expires_at as i64 {
170        return Err(FrameError::Expired(now_ms - publication.expires_at as i64));
171    }
172    Ok(publication)
173}