1use 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#[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#[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
58pub 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
104pub 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}