Skip to main content

macula_rust/frame/
request.rs

1//! Requests (D25): a CALL or STREAM_OPEN, a signed object under
2//! MACULA-PQ-REQUEST-V1 by the caller's identity key, with routing fields
3//! outside the signature.
4
5use sha2::{Digest, Sha384};
6
7use crate::cbor::Value;
8use crate::node_key::{node_id_of, NodeKey};
9use crate::profile::Profile;
10use crate::signed_object::{sign_object, verify_object};
11
12use super::sealed::{clear_or_sealed, sealed_field, Sealed, SealedContext};
13use super::{
14    bounded_text, check_payload, entry, fixed, has_fields, identity_signer, object_refusal,
15    protocol_uint, read_fields, received_frame, text_of, uint, FrameError, Rule, StreamMode,
16    MAX_PROCEDURE_BYTES, MAX_PROTOCOL_INT, PROTOCOL_VERSION, REQUEST_LABEL,
17};
18
19/// The bound on a request's proofs (D7, chain transport): eight tokens, 256
20/// KiB in all, none repeated.
21pub const MAX_PROOFS: usize = 8;
22pub const MAX_PROOFS_BYTES: usize = 256 * 1024;
23
24const CALL: &str = "call";
25const STREAM_OPEN: &str = "stream_open";
26
27/// A request's frame type.
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub enum RequestType {
30    Call,
31    StreamOpen,
32}
33
34impl RequestType {
35    fn name(self) -> &'static str {
36        match self {
37            RequestType::Call => CALL,
38            RequestType::StreamOpen => STREAM_OPEN,
39        }
40    }
41}
42
43/// A request as its caller gives it: `sealed` is the payload sealed end to
44/// end, carried in place of `payload`, which is then not sent, and `None` for
45/// a clear request; `mode` is a STREAM_OPEN's and `None` for a CALL; `token` is `None` when the request carries none; `proofs` are the
46/// tokens of the delegation chain the token rests on, empty for none;
47/// `source_route` and `retry_budget` are routing fields outside the
48/// signature.
49#[derive(Debug, Clone, PartialEq)]
50pub struct RequestSpec {
51    pub request_id: [u8; 16],
52    pub realm: [u8; 32],
53    pub procedure: String,
54    pub target: [u8; 32],
55    pub deadline: u64,
56    pub payload: Value,
57    pub sealed: Option<Sealed>,
58    pub mode: Option<StreamMode>,
59    pub token: Option<Vec<u8>>,
60    pub proofs: Vec<Vec<u8>>,
61    pub source_route: Option<Vec<u8>>,
62    pub retry_budget: Option<u64>,
63}
64
65/// A CALL or STREAM_OPEN whose request verified: its fields, the caller's key
66/// as carried, and `request_hash`, the SHA-384 of its tbs, which replies and
67/// stream frames name. A sealed request's payload is in `sealed`, and its
68/// `payload` is null.
69#[derive(Debug, Clone, PartialEq)]
70pub struct VerifiedRequest {
71    pub frame_type: RequestType,
72    pub key: Vec<u8>,
73    pub request_hash: [u8; 48],
74    pub caller: [u8; 32],
75    pub request_id: [u8; 16],
76    pub realm: [u8; 32],
77    pub procedure: String,
78    pub target: [u8; 32],
79    pub deadline: u64,
80    pub payload: Value,
81    pub sealed: Option<Sealed>,
82    pub mode: Option<StreamMode>,
83    pub token: Option<Vec<u8>>,
84    pub proofs: Option<Vec<Vec<u8>>>,
85}
86
87/// Signs a CALL with the caller's identity key: caller is the key's key id.
88/// Refused, in this order: a key that is not an identity key, a procedure over
89/// 512 bytes, a sealed payload of another shape than a request's or a clear
90/// payload the wire cannot carry, a deadline or retry budget of
91/// 2^53 or more, or a stream mode, which a CALL does not carry; then proofs
92/// outside their bound.
93pub fn sign_call(spec: &RequestSpec, key: &NodeKey) -> Result<Value, FrameError> {
94    sign_request(RequestType::Call, spec, key)
95}
96
97/// Signs a STREAM_OPEN, which carries `spec.mode`, with [`sign_call`]'s
98/// checks, the last of them refusing no mode.
99pub fn sign_stream_open(spec: &RequestSpec, key: &NodeKey) -> Result<Value, FrameError> {
100    sign_request(RequestType::StreamOpen, spec, key)
101}
102
103fn sign_request(
104    frame_type: RequestType,
105    spec: &RequestSpec,
106    key: &NodeKey,
107) -> Result<Value, FrameError> {
108    let proofs = check_request(frame_type, spec, key)?;
109    let fields = request_fields(frame_type, spec, key, proofs);
110    let request = sign_object(REQUEST_LABEL, &fields, key).map_err(object_refusal)?;
111    Ok(request_frame(frame_type, spec, request.to_value()))
112}
113
114/// Runs [`sign_call`]'s refusals in their order and gives back the proofs as
115/// they will be carried.
116fn check_request(
117    frame_type: RequestType,
118    spec: &RequestSpec,
119    key: &NodeKey,
120) -> Result<Value, FrameError> {
121    identity_signer(key)?;
122    bounded_text("procedure", spec.procedure.as_bytes(), MAX_PROCEDURE_BYTES)?;
123    match &spec.sealed {
124        Some(sealed) if !sealed.shaped(SealedContext::Request) => {
125            return Err(FrameError::SealedShape)
126        }
127        Some(_) => {}
128        None => check_payload(&spec.payload)?,
129    }
130    if spec.deadline >= MAX_PROTOCOL_INT || spec.retry_budget.is_some_and(|b| b >= MAX_PROTOCOL_INT)
131    {
132        return Err(FrameError::OutOfRange(
133            "a deadline or retry budget of 2^53 or more".into(),
134        ));
135    }
136    match (frame_type, spec.mode) {
137        (RequestType::Call, Some(_)) => {
138            return Err(FrameError::OutOfRange(
139                "a CALL carries no stream mode".into(),
140            ))
141        }
142        (RequestType::StreamOpen, None) => {
143            return Err(FrameError::OutOfRange(
144                "a STREAM_OPEN carries one of the three stream modes".into(),
145            ))
146        }
147        _ => {}
148    }
149    let proofs = proofs_value(&spec.proofs);
150    if !proofs_within_bound(&proofs) {
151        return Err(FrameError::ProofsOutOfBound);
152    }
153    Ok(proofs)
154}
155
156/// The signed fields of a request: the caller's key id, the spec's fields,
157/// and the mode, token and proofs when it carries them.
158fn request_fields(
159    frame_type: RequestType,
160    spec: &RequestSpec,
161    key: &NodeKey,
162    proofs: Value,
163) -> Vec<(Value, Value)> {
164    let mut fields = vec![
165        entry("frame_type", Value::text(frame_type.name())),
166        entry("caller", Value::Bytes(key.key_id().to_vec())),
167        entry("request_id", Value::Bytes(spec.request_id.to_vec())),
168        entry("realm", Value::Bytes(spec.realm.to_vec())),
169        entry("procedure", Value::text(spec.procedure.clone())),
170        entry("target", Value::Bytes(spec.target.to_vec())),
171        entry("deadline", uint(spec.deadline)),
172        match &spec.sealed {
173            Some(sealed) => entry("sealed", sealed.value()),
174            None => entry("payload", spec.payload.clone()),
175        },
176    ];
177    if let Some(mode) = spec.mode {
178        fields.push(entry("mode", Value::text(mode.name())));
179    }
180    if let Some(token) = &spec.token {
181        fields.push(entry("token", Value::Bytes(token.clone())));
182    }
183    if !spec.proofs.is_empty() {
184        fields.push(entry("proofs", proofs));
185    }
186    fields
187}
188
189/// The request frame around the signed `request`, with the routing fields
190/// outside the signature.
191fn request_frame(frame_type: RequestType, spec: &RequestSpec, request: Value) -> Value {
192    let mut frame = vec![
193        entry("version", Value::Int(i128::from(PROTOCOL_VERSION))),
194        entry("frame_type", Value::text(frame_type.name())),
195        entry("request", request),
196    ];
197    if let Some(route) = &spec.source_route {
198        frame.push(entry("source_route", Value::Bytes(route.clone())));
199    }
200    if let Some(budget) = spec.retry_budget {
201        frame.push(entry("retry_budget", uint(budget)));
202    }
203    Value::Map(frame)
204}
205
206/// The stream modes a STREAM_OPEN names.
207const STREAM_MODES: &[&str] = &["server_stream", "client_stream", "bidi"];
208
209pub(super) const REQUEST_ROUTES: &[(&str, Rule)] = &[
210    ("source_route", Rule::AnyBytes),
211    ("retry_budget", Rule::ProtocolUint),
212];
213
214/// Verifies a received CALL or STREAM_OPEN under the connection's `profile`:
215/// the frame's shape, the request's signature and fields, and caller as the
216/// key id of its key. A station checks this before it routes, and a provider
217/// before its own checks, which stay with the caller: its node_id as target,
218/// the deadline window, replays and tokens.
219pub fn verify_request(frame: &Value, profile: Profile) -> Result<VerifiedRequest, FrameError> {
220    let (frame_type, object) = received_frame(
221        frame,
222        "request",
223        Rule::CarriedObject,
224        REQUEST_ROUTES,
225        &[CALL, STREAM_OPEN],
226    )
227    .ok_or(FrameError::Malformed)?;
228    let frame_type = if frame_type == CALL {
229        RequestType::Call
230    } else {
231        RequestType::StreamOpen
232    };
233    let verified = verify_object(REQUEST_LABEL, &object, profile).map_err(object_refusal)?;
234    let fields =
235        read_fields(&verified.fields, &request_table(frame_type)).ok_or(FrameError::Malformed)?;
236    let has_mode = fields.contains_key("mode");
237    if !has_fields(
238        &fields,
239        &[
240            "frame_type",
241            "caller",
242            "request_id",
243            "realm",
244            "procedure",
245            "target",
246            "deadline",
247        ],
248    ) || !clear_or_sealed(&fields, "payload")
249        || has_mode != (frame_type == RequestType::StreamOpen)
250    {
251        return Err(FrameError::Malformed);
252    }
253    super::tbs_enum(&fields, "mode", STREAM_MODES)?;
254    super::signed_ext(&fields)?;
255    let request = VerifiedRequest {
256        frame_type,
257        request_hash: Sha384::digest(&verified.tbs).into(),
258        caller: fixed(&fields["caller"]),
259        request_id: fixed(&fields["request_id"]),
260        realm: fixed(&fields["realm"]),
261        procedure: text_of(&fields["procedure"]),
262        target: fixed(&fields["target"]),
263        deadline: protocol_uint(&fields["deadline"]).unwrap_or(0),
264        payload: fields.get("payload").cloned().unwrap_or(Value::Null),
265        sealed: sealed_field(&fields, SealedContext::Request),
266        mode: fields
267            .get("mode")
268            .and_then(|m| StreamMode::parse(&text_of(m))),
269        token: fields.get("token").map(super::bytes_of),
270        proofs: fields.get("proofs").map(|p| match p {
271            Value::List(items) => items.iter().map(super::bytes_of).collect(),
272            _ => Vec::new(),
273        }),
274        key: verified.key,
275    };
276    if request.caller != node_id_of(&request.key, profile) {
277        return Err(FrameError::KeyIdMismatch);
278    }
279    Ok(request)
280}
281
282fn request_table(frame_type: RequestType) -> Vec<(&'static str, Rule)> {
283    vec![
284        (
285            "frame_type",
286            Rule::TextIn(match frame_type {
287                RequestType::Call => &[CALL],
288                RequestType::StreamOpen => &[STREAM_OPEN],
289            }),
290        ),
291        ("alg", Rule::Any),
292        ("caller", Rule::BytesOf(32)),
293        ("request_id", Rule::BytesOf(16)),
294        ("realm", Rule::BytesOf(32)),
295        ("procedure", Rule::TextWithin(MAX_PROCEDURE_BYTES)),
296        ("target", Rule::BytesOf(32)),
297        ("deadline", Rule::ProtocolUint),
298        ("payload", Rule::Any),
299        ("sealed", Rule::Sealed(SealedContext::Request)),
300        ("mode", Rule::EnumText),
301        ("token", Rule::AnyBytes),
302        ("proofs", Rule::Proofs),
303        ("ext", Rule::Ext),
304    ]
305}
306
307/// Whether `fields` read as a CALL's under the request table, where a
308/// delegation chain's proofs are bounded: the reading the shared decoding
309/// rule vectors name `request_fields`.
310pub fn request_fields_accepted(fields: &Value) -> bool {
311    read_fields(fields, &request_table(RequestType::Call)).is_some()
312}
313
314fn proofs_value(proofs: &[Vec<u8>]) -> Value {
315    Value::List(proofs.iter().map(|p| Value::Bytes(p.clone())).collect())
316}
317
318/// macula's bytes_set rule for proofs: a list of at most [`MAX_PROOFS`] byte
319/// strings, [`MAX_PROOFS_BYTES`] in all, none repeated.
320pub(super) fn proofs_within_bound(v: &Value) -> bool {
321    let Value::List(items) = v else {
322        return false;
323    };
324    if items.len() > MAX_PROOFS {
325        return false;
326    }
327    let mut seen = std::collections::HashSet::with_capacity(items.len());
328    let mut total = 0;
329    for item in items {
330        let Value::Bytes(b) = item else {
331            return false;
332        };
333        if !seen.insert(b.as_slice()) {
334            return false;
335        }
336        total += b.len();
337    }
338    total <= MAX_PROOFS_BYTES
339}