Skip to main content

macula_rust/station_link/
confidential.rs

1//! End-to-end payload confidentiality on a link, as macula 13 has it (E2E
2//! design §5, §8, amendment A1; seal scheme 1). A provider that names its KEM
3//! key in its advertisement opens the requests sealed to it and seals every
4//! answer to them; what it cannot open it refuses in the clear, from a
5//! closed set that carries no application data. A call or a stream to a
6//! provider states how it is kept: sealed to the key the provider's verified
7//! advertisement names, or in the clear by the application's own decision.
8//! A sealed request is never answered in the clear except from that closed
9//! set, and never falls back to the clear.
10
11use std::fmt;
12use std::str::FromStr;
13
14use crate::cbor::{self, Value};
15use crate::frame::{
16    self, Sealed, StreamEncoding, StreamFields, VerifiedReply, VerifiedRequest, VerifiedStreamFrame,
17};
18use crate::profile::Profile;
19use crate::record::CLOCK_TOLERANCE_MS;
20use crate::seal::{self, Direction, Keyring, Parties, KEY_ID_SIZE, NONCE_SIZE};
21
22use super::LinkError;
23
24/// How a procedure takes its requests, and how a pool's call or open must
25/// be kept, as macula-go's stationlink.Confidentiality.
26///
27/// Serving: `Preferred`, the default, names this node's KEM key in the
28/// advertisement when the link's `kem_advertise` is on, and still takes a
29/// clear request while the procedure's last keyless advertisement could be
30/// served, then refuses it sealed_required. `Required` names the key and
31/// refuses every clear request; it needs `kem_advertise` on. `Off` names no
32/// key: the procedure is served in the clear.
33///
34/// Calling through a pool: `Preferred` seals to a provider that names a key
35/// and calls one that names none in the clear; `Required` never calls one
36/// that names none. `Off` is refused: a clear call is an explicit target's
37/// (a [`Seal::Clear`] on a link).
38#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
39pub enum Confidentiality {
40    #[default]
41    Preferred,
42    Required,
43    Off,
44}
45
46impl FromStr for Confidentiality {
47    type Err = String;
48
49    /// "preferred" (or "", the default), "required" or "off", as macula-go's
50    /// options name them.
51    fn from_str(s: &str) -> Result<Self, String> {
52        match s {
53            "" | "preferred" => Ok(Confidentiality::Preferred),
54            "required" => Ok(Confidentiality::Required),
55            "off" => Ok(Confidentiality::Off),
56            other => Err(format!(
57                "confidential is preferred, required or off, not {other:?}"
58            )),
59        }
60    }
61}
62
63/// How a call or an open to a provider is kept.
64#[derive(Debug, Clone, PartialEq, Eq)]
65pub enum Seal {
66    /// In the clear, by the application's own decision: only for a provider
67    /// whose advertisement names no key. On a link nothing checks that: a
68    /// pool refuses a clear call to a provider that names a key, but a
69    /// direct [`super::Link`] caller must read the advertisement itself.
70    Clear,
71    /// Sealed to this KEM key as carried, the one the provider's verified
72    /// advertisement names.
73    To(Vec<u8>),
74}
75
76/// The refusal codes of a sealed request, and of a clear one to a procedure
77/// past its keyless window.
78pub(super) const CODE_SEALED_REFUSED: &str = "sealed_refused";
79pub(super) const CODE_SEALED_REQUIRED: &str = "sealed_required";
80
81/// macula's default and longest advertisement lifetime, which bounds the
82/// keyless window.
83pub(super) const MAX_ADVERTISEMENT_TTL_MS: i64 = 5 * 60 * 1000;
84
85/// A sealed_refused's detail from a node that holds no KEM key.
86pub(super) const NO_KEY_DETAIL: &str = "this node opens no sealed payload";
87
88/// The codes a provider may answer a sealed request with in the clear (E2E
89/// design §5.1): the admission refusals, which carry no application data, a
90/// STREAM_OPEN's session admission included. sealed_refused is read on its
91/// own.
92const CLEAR_REFUSALS: &[&str] = &[
93    "expired",
94    "not_yet_valid",
95    "request_id_reused",
96    "request_copy",
97    "reply_not_kept",
98    "caller_quota",
99    "share_full",
100    "admission_full",
101    "too_many_sessions",
102    "unavailable",
103];
104
105/// Whether `code` may answer a sealed request in the clear.
106pub fn is_clear_refusal(code: &str) -> bool {
107    CLEAR_REFUSALS.contains(&code)
108}
109
110/// Why a call could not be kept confidential, as macula's
111/// {error, {confidentiality, Reason}} names it.
112#[derive(Debug, Clone, Copy, PartialEq, Eq)]
113pub enum ConfidentialityReason {
114    /// The provider names a key this node cannot seal to, or none where the
115    /// call requires one.
116    NoKemKey,
117    /// The provider's sealed_refused named one key and its advertisement
118    /// another.
119    KeyMismatch,
120    /// A sealed answer that does not open. It is signed by the provider and
121    /// bound to its request, so it is the only answer the request gets.
122    ReplyNotOpened,
123    /// A call or an open to a provider that states neither a key to seal to
124    /// nor the clear. Nothing is sent.
125    NoSignedState,
126}
127
128impl ConfidentialityReason {
129    /// The reason as macula names it.
130    pub fn name(self) -> &'static str {
131        match self {
132            ConfidentialityReason::NoKemKey => "no_kem_key",
133            ConfidentialityReason::KeyMismatch => "key_mismatch",
134            ConfidentialityReason::ReplyNotOpened => "reply_not_opened",
135            ConfidentialityReason::NoSignedState => "no_signed_state",
136        }
137    }
138}
139
140/// A call or an open that could not be kept confidential, and so was not
141/// made, or failed rather than be taken in the clear. `advertised` holds the
142/// key ids the trusted providers' advertisements named; `named` is the key
143/// a provider's refusal named, for a key mismatch.
144#[derive(Debug, Clone, PartialEq, Eq)]
145pub struct ConfidentialityError {
146    pub reason: ConfidentialityReason,
147    pub advertised: Vec<[u8; KEY_ID_SIZE]>,
148    pub named: Option<[u8; KEY_ID_SIZE]>,
149}
150
151impl ConfidentialityError {
152    /// The error for `reason`, naming no key.
153    pub fn new(reason: ConfidentialityReason) -> ConfidentialityError {
154        ConfidentialityError {
155            reason,
156            advertised: Vec::new(),
157            named: None,
158        }
159    }
160}
161
162impl fmt::Display for ConfidentialityError {
163    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
164        write!(f, "confidentiality: {}", self.reason.name())?;
165        for id in &self.advertised {
166            write!(f, " {}", hex(id))?;
167        }
168        if let Some(named) = &self.named {
169            write!(f, " (the provider named {})", hex(named))?;
170        }
171        Ok(())
172    }
173}
174
175fn hex(id: &[u8]) -> String {
176    id.iter().map(|b| format!("{b:02x}")).collect()
177}
178
179fn confidentiality(reason: ConfidentialityReason) -> LinkError {
180    LinkError::Confidentiality(ConfidentialityError::new(reason))
181}
182
183/// Refuses a call or an open to a provider that does not say how it is kept.
184/// A call to the connected station (the zero target, or its node_id) is
185/// always clear.
186pub(super) fn stated(
187    target: &[u8; 32],
188    station: &[u8; 32],
189    seal: &Option<Seal>,
190) -> Result<(), LinkError> {
191    match seal {
192        None if target != &[0; 32] && target != station => {
193            Err(confidentiality(ConfidentialityReason::NoSignedState))
194        }
195        _ => Ok(()),
196    }
197}
198
199/// What a sealed request agreed: the key id, the reply key, a stream's two
200/// keys, and the routing fields its answers are bound to. Showing it gives
201/// the key id, never a key.
202#[derive(Clone)]
203pub(super) struct CallSeal {
204    pub(super) key_id: [u8; KEY_ID_SIZE],
205    k_rep: [u8; 32],
206    pub(super) k_c2p: [u8; 32],
207    pub(super) k_p2c: [u8; 32],
208    request: seal::Request,
209}
210
211impl fmt::Debug for CallSeal {
212    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
213        write!(f, "CallSeal(key id {})", hex(&self.key_id))
214    }
215}
216
217/// A request's payload sealed to the provider key carried as `to`, in place
218/// of its clear payload, and the keys the caller keeps. A key that is not
219/// one of this profile's is no_kem_key.
220#[allow(clippy::too_many_arguments)]
221pub(super) fn sealed_request(
222    profile: Profile,
223    to: &[u8],
224    frame_type: &str,
225    realm: [u8; 32],
226    procedure: &str,
227    caller: [u8; 32],
228    target: [u8; 32],
229    request_id: [u8; 16],
230    deadline: u64,
231    payload: &Value,
232) -> Result<(Sealed, CallSeal), LinkError> {
233    let key = seal::parse_public_key(profile, to)
234        .map_err(|_| confidentiality(ConfidentialityReason::NoKemKey))?;
235    frame::check_payload(payload)?;
236    let (secret, kem_ct) =
237        seal::sender_secret(&key).map_err(|_| confidentiality(ConfidentialityReason::NoKemKey))?;
238    let parties = Parties {
239        request_id,
240        caller,
241        target,
242    };
243    let (k_req, k_rep) = seal::call_keys(&secret, frame_type, &parties);
244    let (k_c2p, k_p2c) = seal::stream_keys(&secret, &parties);
245    let request = seal::Request {
246        frame_type: frame_type.to_string(),
247        realm,
248        procedure: procedure.to_string(),
249        caller,
250        target,
251        request_id,
252        deadline,
253    };
254    let plain = cbor::encode(payload)
255        .map_err(|e| LinkError::Frame(frame::FrameError::Payload(e.to_string())))?;
256    let key_id = key.key_id();
257    let sealed = Sealed {
258        key_id,
259        kem_ct: Some(kem_ct),
260        nonce: None,
261        ct: seal::seal(
262            &k_req,
263            &[0; NONCE_SIZE],
264            &seal::request_aad(&request),
265            &plain,
266        ),
267    };
268    Ok((
269        sealed,
270        CallSeal {
271            key_id,
272            k_rep,
273            k_c2p,
274            k_p2c,
275            request,
276        },
277    ))
278}
279
280/// The key id a sealed_refused's detail names, `None` for none: exactly 16
281/// lowercase hex digits, as macula writes a key id, and nothing else.
282pub(super) fn refused_key(detail: Option<&str>) -> Option<[u8; KEY_ID_SIZE]> {
283    let detail = detail?;
284    let lowercase_hex = detail.len() == 2 * KEY_ID_SIZE
285        && detail
286            .bytes()
287            .all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b));
288    if !lowercase_hex {
289        return None;
290    }
291    let mut id = [0; KEY_ID_SIZE];
292    for (i, byte) in id.iter_mut().enumerate() {
293        *byte = u8::from_str_radix(&detail[2 * i..2 * i + 2], 16).ok()?;
294    }
295    Some(id)
296}
297
298/// What a verified provider reply means to a call: for a sealed call, a
299/// sealed answer under the request's key id opened; a clear sealed_refused
300/// naming the provider's key; a clear refusal from the closed set; anything
301/// else clear refused, never taken as the answer. For a clear call, a sealed
302/// answer is one this node sealed nothing for.
303pub(super) fn reply_outcome(
304    reply: VerifiedReply,
305    request: &VerifiedRequest,
306    seal: Option<&CallSeal>,
307) -> Result<Value, LinkError> {
308    let provider = |code: String, detail: Option<String>| LinkError::Provider {
309        responded_by: reply.responded_by,
310        code,
311        detail,
312    };
313    let Some(s) = seal else {
314        return match (reply.sealed.is_some(), reply.frame_type) {
315            (true, _) => Err(provider(CODE_SEALED_REFUSED.into(), None)),
316            (false, frame::ReplyType::Result) => Ok(reply.payload.unwrap_or(Value::Null)),
317            (false, frame::ReplyType::Error) => {
318                Err(provider(reply.code.unwrap_or_default(), reply.detail))
319            }
320        };
321    };
322    let not_opened = || confidentiality(ConfidentialityReason::ReplyNotOpened);
323    match (&reply.sealed, reply.frame_type, reply.code.as_deref()) {
324        (Some(sealed), frame_type, _) => {
325            // As macula's open_reply/4: an answer naming another key than
326            // the request's is not opened.
327            if sealed.key_id != s.key_id {
328                return Err(not_opened());
329            }
330            let nonce: [u8; NONCE_SIZE] = sealed
331                .nonce
332                .as_deref()
333                .and_then(|n| n.try_into().ok())
334                .ok_or_else(not_opened)?;
335            let name = match frame_type {
336                frame::ReplyType::Result => seal::FRAME_RESULT,
337                frame::ReplyType::Error => seal::FRAME_ERROR,
338            };
339            let aad = seal::reply_aad(&s.request, name, &request.request_hash, &reply.responded_by);
340            let plain = seal::open(&s.k_rep, &nonce, &aad, &sealed.ct).map_err(|_| not_opened())?;
341            match frame_type {
342                frame::ReplyType::Result => cbor::decode(&plain).map_err(|_| not_opened()),
343                frame::ReplyType::Error => {
344                    let (code, detail) =
345                        seal::open_error_plain(&plain).map_err(|_| not_opened())?;
346                    Err(provider(code, detail))
347                }
348            }
349        }
350        (None, frame::ReplyType::Error, Some(CODE_SEALED_REFUSED)) => {
351            Err(LinkError::SealedRefused {
352                named: refused_key(reply.detail.as_deref()),
353            })
354        }
355        (None, frame::ReplyType::Error, Some(code)) if is_clear_refusal(code) => {
356            Err(provider(code.to_string(), reply.detail.clone()))
357        }
358        _ => Err(LinkError::ClearAnswerToSealed),
359    }
360}
361
362/// Whether a procedure under `confidential` takes a clear request at
363/// `now_ms`, as macula's clear_allowed/2 decides it: never when required,
364/// always when its advertisement names no key (`keyed_since` is `None`),
365/// and once keyed only while its last keyless advertisement could still be
366/// served, the longest advertisement lifetime and the clock tolerance from
367/// the moment it was first keyed.
368pub(super) fn clear_allowed(
369    confidential: Confidentiality,
370    keyed_since: Option<i64>,
371    now_ms: i64,
372) -> bool {
373    match (confidential, keyed_since) {
374        (Confidentiality::Required, _) => false,
375        (_, None) => true,
376        (_, Some(since)) => now_ms <= since + MAX_ADVERTISEMENT_TTL_MS + CLOCK_TOLERANCE_MS as i64,
377    }
378}
379
380/// A sealed request opened with this node's keyring: its plaintext payload
381/// and the keys it agreed, or the detail of the sealed_refused it is
382/// answered with, naming the key this node holds now, or that it holds none.
383pub(super) fn opened_request(
384    keyring: Option<&Keyring>,
385    request: &VerifiedRequest,
386) -> Result<(Value, CallSeal), String> {
387    let Some(keyring) = keyring else {
388        return Err(NO_KEY_DETAIL.into());
389    };
390    let refused = hex(&keyring.current_id());
391    let Some(sealed) = &request.sealed else {
392        return Err(refused);
393    };
394    let key = keyring
395        .find(&sealed.key_id)
396        .ok_or_else(|| refused.clone())?;
397    let kem_ct = sealed.kem_ct.as_deref().ok_or_else(|| refused.clone())?;
398    let secret = seal::recipient_secret(&key, kem_ct).map_err(|_| refused.clone())?;
399    let frame_type = match request.frame_type {
400        frame::RequestType::Call => seal::FRAME_CALL,
401        frame::RequestType::StreamOpen => seal::FRAME_STREAM_OPEN,
402    };
403    let parties = Parties {
404        request_id: request.request_id,
405        caller: request.caller,
406        target: request.target,
407    };
408    let (k_req, k_rep) = seal::call_keys(&secret, frame_type, &parties);
409    let (k_c2p, k_p2c) = seal::stream_keys(&secret, &parties);
410    let bound = seal::Request {
411        frame_type: frame_type.to_string(),
412        realm: request.realm,
413        procedure: request.procedure.clone(),
414        caller: request.caller,
415        target: request.target,
416        request_id: request.request_id,
417        deadline: request.deadline,
418    };
419    let plain = seal::open(
420        &k_req,
421        &[0; NONCE_SIZE],
422        &seal::request_aad(&bound),
423        &sealed.ct,
424    )
425    .map_err(|_| refused.clone())?;
426    let payload = cbor::decode(&plain).map_err(|_| refused)?;
427    Ok((
428        payload,
429        CallSeal {
430            key_id: sealed.key_id,
431            k_rep,
432            k_c2p,
433            k_p2c,
434            request: bound,
435        },
436    ))
437}
438
439impl CallSeal {
440    /// A RESULT's payload, or an ERROR's cbor([code, detail]), sealed under
441    /// the request's reply key with a fresh nonce, carried, bound to the
442    /// request and to `responded_by` as the node that answered.
443    pub(super) fn sealed_answer(
444        &self,
445        frame_type: &str,
446        plain: &[u8],
447        request_hash: &[u8; 48],
448        responded_by: &[u8; 32],
449    ) -> Result<Sealed, LinkError> {
450        let nonce = seal::random_nonce().map_err(|_| LinkError::Io("no randomness".into()))?;
451        let aad = seal::reply_aad(&self.request, frame_type, request_hash, responded_by);
452        Ok(Sealed {
453            key_id: self.key_id,
454            kem_ct: None,
455            nonce: Some(nonce.to_vec()),
456            ct: seal::seal(&self.k_rep, &nonce, &aad, plain),
457        })
458    }
459}
460
461/// A provider seals at most this many frames under random nonces on one
462/// stream: GCM's bound, as macula's max_sealed_frames.
463const MAX_SEALED_PROVIDER_FRAMES: u64 = 1 << 32;
464
465/// One side's keys for a sealed stream (macula 13's E2E design §5.2): a
466/// caller's frames seal under k_c2p with their seq as the nonce; a
467/// provider's under k_p2c with a random nonce each, carried, since a
468/// provider restarted by a retried open numbers from 0 again. A STREAM_END
469/// has nothing to seal. Showing it gives the key id, never a key.
470#[derive(Clone)]
471pub(super) struct StreamSeal {
472    key_id: [u8; KEY_ID_SIZE],
473    request_id: [u8; 16],
474    caller: bool,
475    send: [u8; 32],
476    recv: [u8; 32],
477}
478
479impl fmt::Debug for StreamSeal {
480    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
481        write!(f, "StreamSeal(key id {})", hex(&self.key_id))
482    }
483}
484
485/// What a sealable frame seals: its type's name and its plaintext.
486pub(super) struct StreamPlain {
487    frame_type: &'static str,
488    plain: Vec<u8>,
489}
490
491impl StreamSeal {
492    /// The caller's side of the stream `s` opened.
493    pub(super) fn caller(s: &CallSeal) -> StreamSeal {
494        StreamSeal {
495            key_id: s.key_id,
496            request_id: s.request.request_id,
497            caller: true,
498            send: s.k_c2p,
499            recv: s.k_p2c,
500        }
501    }
502
503    /// The id of the KEM key the stream is sealed to.
504    pub(super) fn key_id(&self) -> [u8; KEY_ID_SIZE] {
505        self.key_id
506    }
507
508    /// The provider's side of the stream `s` opened.
509    pub(super) fn provider(s: &CallSeal) -> StreamSeal {
510        StreamSeal {
511            key_id: s.key_id,
512            request_id: s.request.request_id,
513            caller: false,
514            send: s.k_p2c,
515            recv: s.k_c2p,
516        }
517    }
518
519    fn directions(&self) -> (Direction, Direction) {
520        match self.caller {
521            true => (Direction::CallerToProvider, Direction::ProviderToCaller),
522            false => (Direction::ProviderToCaller, Direction::CallerToProvider),
523        }
524    }
525
526    /// What `fields` seals, checked before any nonce is spent: the
527    /// plaintext of a raw chunk is its bytes, of a structured chunk or a
528    /// reply the value's CBOR, and of a STREAM_ERROR cbor([code, message]),
529    /// as macula_stream's plain_of/1 has them. `None` for a STREAM_END,
530    /// which goes as it is.
531    pub(super) fn plain_of(&self, fields: &StreamFields) -> Result<Option<StreamPlain>, LinkError> {
532        let (frame_type, plain) = match fields {
533            StreamFields::Data {
534                encoding: StreamEncoding::Raw,
535                body: Value::Bytes(b),
536                ..
537            } => ("stream_data", b.clone()),
538            StreamFields::Data {
539                encoding: StreamEncoding::Raw,
540                ..
541            } => {
542                return Err(LinkError::Frame(frame::FrameError::OutOfRange(
543                    "a raw body that is not a byte string".into(),
544                )))
545            }
546            StreamFields::Data { body, .. } => {
547                frame::check_payload(body)?;
548                ("stream_data", encoded(body)?)
549            }
550            StreamFields::Reply { payload, .. } => {
551                frame::check_payload(payload)?;
552                ("stream_reply", encoded(payload)?)
553            }
554            StreamFields::Error { code, message, .. } => {
555                ("stream_error", seal::error_plain(code, message))
556            }
557            _ => return Ok(None),
558        };
559        if !self.caller && fields.seq() >= MAX_SEALED_PROVIDER_FRAMES {
560            return Err(LinkError::SealedFramesExhausted);
561        }
562        Ok(Some(StreamPlain { frame_type, plain }))
563    }
564
565    /// `fields` with what [`Self::plain_of`] took from it sealed in place.
566    /// Each call spends a nonce: the caller's seq or a fresh random one, so
567    /// a sender takes the seq before it seals and never seals under it
568    /// again.
569    pub(super) fn sealed(
570        &self,
571        fields: StreamFields,
572        p: StreamPlain,
573    ) -> Result<StreamFields, LinkError> {
574        let seq = fields.seq();
575        let (nonce, carried) = match self.caller {
576            true => (seal::stream_nonce(seq), None),
577            false => {
578                let nonce =
579                    seal::random_nonce().map_err(|_| LinkError::Io("no randomness".into()))?;
580                (nonce, Some(nonce.to_vec()))
581            }
582        };
583        let aad = seal::stream_aad(p.frame_type, &self.request_id, seq, self.directions().0);
584        let sealed = Sealed {
585            key_id: self.key_id,
586            kem_ct: None,
587            nonce: carried,
588            ct: seal::seal(&self.send, &nonce, &aad, &p.plain),
589        };
590        Ok(match fields {
591            StreamFields::Data { encoding, .. } => StreamFields::SealedData {
592                seq,
593                encoding,
594                sealed,
595            },
596            StreamFields::Reply { .. } => StreamFields::SealedReply { seq, sealed },
597            _ => StreamFields::SealedError { seq, sealed },
598        })
599    }
600
601    /// A verified sealed frame from the peer with its body, payload, or
602    /// code and message opened; one that does not open, or opens to nothing
603    /// its type holds, is reply_not_opened.
604    pub(super) fn opened(&self, frame: VerifiedStreamFrame) -> Result<StreamFields, LinkError> {
605        let not_opened = || confidentiality(ConfidentialityReason::ReplyNotOpened);
606        let (seq, frame_type, sealed) = match &frame.fields {
607            StreamFields::SealedData { seq, sealed, .. } => (*seq, "stream_data", sealed),
608            StreamFields::SealedReply { seq, sealed } => (*seq, "stream_reply", sealed),
609            StreamFields::SealedError { seq, sealed } => (*seq, "stream_error", sealed),
610            _ => return Ok(frame.fields),
611        };
612        // As macula_stream's opened/5: a frame naming another key than the
613        // stream's is not opened.
614        if sealed.key_id != self.key_id {
615            return Err(not_opened());
616        }
617        // A provider's frame carries its nonce; a caller's is its seq.
618        let nonce: [u8; NONCE_SIZE] = match self.caller {
619            true => sealed
620                .nonce
621                .as_deref()
622                .and_then(|n| n.try_into().ok())
623                .ok_or_else(not_opened)?,
624            false => seal::stream_nonce(seq),
625        };
626        let aad = seal::stream_aad(frame_type, &self.request_id, seq, self.directions().1);
627        let plain = seal::open(&self.recv, &nonce, &aad, &sealed.ct).map_err(|_| not_opened())?;
628        match frame.fields {
629            StreamFields::SealedData {
630                encoding: StreamEncoding::Raw,
631                ..
632            } => Ok(StreamFields::Data {
633                seq,
634                encoding: StreamEncoding::Raw,
635                body: Value::Bytes(plain),
636            }),
637            StreamFields::SealedData { encoding, .. } => Ok(StreamFields::Data {
638                seq,
639                encoding,
640                body: cbor::decode(&plain).map_err(|_| not_opened())?,
641            }),
642            StreamFields::SealedReply { .. } => Ok(StreamFields::Reply {
643                seq,
644                payload: cbor::decode(&plain).map_err(|_| not_opened())?,
645            }),
646            _ => {
647                let (code, message) = seal::open_error_plain(&plain).map_err(|_| not_opened())?;
648                Ok(StreamFields::Error {
649                    seq,
650                    code,
651                    message: message.unwrap_or_default(),
652                })
653            }
654        }
655    }
656}
657
658fn encoded(v: &Value) -> Result<Vec<u8>, LinkError> {
659    cbor::encode(v).map_err(|e| LinkError::Frame(frame::FrameError::Payload(e.to_string())))
660}
661
662/// A verified frame from the peer as a stream takes it, as macula_stream's
663/// peer_event/2 does. A clear stream takes no sealed frame: that ends the
664/// session as sealed_refused, this node holding no key for it. A sealed
665/// stream opens each sealed frame before anything of it takes effect, and
666/// takes nothing clear but a STREAM_END and, on a caller's side, the
667/// provider's refusal of the open at seq 0: sealed_refused, or one from the
668/// closed set. Anything else clear is [`LinkError::ClearAnswerToSealed`].
669pub(super) fn unsealed(
670    frame: VerifiedStreamFrame,
671    sealing: Option<&StreamSeal>,
672) -> Result<StreamFields, LinkError> {
673    let sealed = matches!(
674        frame.fields,
675        StreamFields::SealedData { .. }
676            | StreamFields::SealedReply { .. }
677            | StreamFields::SealedError { .. }
678    );
679    match (sealing, sealed, &frame.fields) {
680        (None, true, _) => Err(LinkError::Stream {
681            code: CODE_SEALED_REFUSED.into(),
682            message: NO_KEY_DETAIL.into(),
683            relay: false,
684        }),
685        (None, false, _) => Ok(frame.fields),
686        (Some(s), true, _) => s.opened(frame),
687        (Some(_), false, StreamFields::End { .. }) => Ok(frame.fields),
688        (Some(s), false, StreamFields::Error { seq: 0, code, .. })
689            if s.caller && (code == CODE_SEALED_REFUSED || is_clear_refusal(code)) =>
690        {
691            Ok(frame.fields)
692        }
693        (Some(_), false, _) => Err(LinkError::ClearAnswerToSealed),
694    }
695}
696
697#[cfg(test)]
698mod tests {
699    use super::*;
700    use crate::frame::{ReplyType, RequestType};
701
702    const CALLER: [u8; 32] = [1; 32];
703    const PROVIDER: [u8; 32] = [2; 32];
704
705    /// A request sealed by a caller to `keyring`'s current key, as the
706    /// provider verified it, and the caller's keys.
707    fn sealed_to(keyring: &Keyring, frame_type: RequestType) -> (VerifiedRequest, CallSeal) {
708        let name = match frame_type {
709            RequestType::Call => seal::FRAME_CALL,
710            RequestType::StreamOpen => seal::FRAME_STREAM_OPEN,
711        };
712        let to = keyring.current().public_key().carried().to_vec();
713        let (sealed, caller) = sealed_request(
714            keyring.profile(),
715            &to,
716            name,
717            [3; 32],
718            "~ring",
719            CALLER,
720            PROVIDER,
721            [4; 16],
722            1_790_000_000_000,
723            &Value::text("secret"),
724        )
725        .unwrap();
726        let request = VerifiedRequest {
727            frame_type,
728            key: Vec::new(),
729            request_hash: [5; 48],
730            caller: CALLER,
731            request_id: [4; 16],
732            realm: [3; 32],
733            procedure: "~ring".into(),
734            target: PROVIDER,
735            deadline: 1_790_000_000_000,
736            payload: Value::Null,
737            sealed: Some(sealed),
738            mode: None,
739            token: None,
740            proofs: None,
741        };
742        (request, caller)
743    }
744
745    fn clear_reply(frame_type: ReplyType, code: Option<&str>) -> VerifiedReply {
746        VerifiedReply {
747            frame_type,
748            responded_by: PROVIDER,
749            payload: code.is_none().then(|| Value::text("in the clear")),
750            code: code.map(str::to_string),
751            detail: None,
752            sealed: None,
753        }
754    }
755
756    #[test]
757    fn a_provider_opens_what_a_caller_sealed_and_its_answer_opens_to_the_caller() {
758        let keyring = Keyring::system(Profile::PqHybrid).unwrap();
759        let (request, caller) = sealed_to(&keyring, RequestType::Call);
760        let (payload, provider) = opened_request(Some(&keyring), &request).unwrap();
761        assert_eq!(payload, Value::text("secret"));
762
763        let plain = cbor::encode(&Value::text("answer")).unwrap();
764        let sealed = provider
765            .sealed_answer(seal::FRAME_RESULT, &plain, &request.request_hash, &PROVIDER)
766            .unwrap();
767        let reply = VerifiedReply {
768            frame_type: ReplyType::Result,
769            responded_by: PROVIDER,
770            payload: None,
771            code: None,
772            detail: None,
773            sealed: Some(sealed),
774        };
775        assert_eq!(
776            reply_outcome(reply, &request, Some(&caller)),
777            Ok(Value::text("answer"))
778        );
779    }
780
781    #[test]
782    fn a_sealed_request_this_node_cannot_open_is_refused_naming_its_key() {
783        let keyring = Keyring::system(Profile::PqPure).unwrap();
784        let other = Keyring::system(Profile::PqPure).unwrap();
785        let (request, _) = sealed_to(&other, RequestType::Call);
786        assert_eq!(
787            opened_request(Some(&keyring), &request).err(),
788            Some(hex(&keyring.current_id()))
789        );
790        assert_eq!(
791            opened_request(None, &request).err().as_deref(),
792            Some(NO_KEY_DETAIL)
793        );
794    }
795
796    /// Venus's review of package 1, observation 2: a forged clear answer to
797    /// a sealed call is never taken, unless it is sealed_refused or a
798    /// refusal from the closed set.
799    #[test]
800    fn a_forged_clear_answer_to_a_sealed_call_is_refused() {
801        let keyring = Keyring::system(Profile::PqPure).unwrap();
802        let (request, caller) = sealed_to(&keyring, RequestType::Call);
803        for forged in [
804            clear_reply(ReplyType::Result, None),
805            clear_reply(ReplyType::Error, Some("handler_error")),
806            clear_reply(ReplyType::Error, Some("sealed_required")),
807        ] {
808            assert_eq!(
809                reply_outcome(forged.clone(), &request, Some(&caller)),
810                Err(LinkError::ClearAnswerToSealed),
811                "{forged:?}"
812            );
813        }
814        assert!(matches!(
815            reply_outcome(
816                clear_reply(ReplyType::Error, Some("caller_quota")),
817                &request,
818                Some(&caller)
819            ),
820            Err(LinkError::Provider { code, .. }) if code == "caller_quota"
821        ));
822        assert_eq!(
823            reply_outcome(
824                clear_reply(ReplyType::Error, Some(CODE_SEALED_REFUSED)),
825                &request,
826                Some(&caller)
827            ),
828            Err(LinkError::SealedRefused { named: None })
829        );
830    }
831
832    fn data(seq: u64, body: &[u8]) -> StreamFields {
833        StreamFields::Data {
834            seq,
835            encoding: StreamEncoding::Raw,
836            body: Value::Bytes(body.to_vec()),
837        }
838    }
839
840    fn sealed(side: &StreamSeal, fields: StreamFields) -> StreamFields {
841        let plain = side.plain_of(&fields).unwrap().unwrap();
842        side.sealed(fields, plain).unwrap()
843    }
844
845    fn from(signer: [u8; 32], fields: StreamFields) -> VerifiedStreamFrame {
846        VerifiedStreamFrame { signer, fields }
847    }
848
849    #[test]
850    fn each_side_of_a_sealed_stream_opens_what_the_other_sealed() {
851        let keyring = Keyring::system(Profile::PqHybrid).unwrap();
852        let (open, caller_seal) = sealed_to(&keyring, RequestType::StreamOpen);
853        let (_, provider_seal) = opened_request(Some(&keyring), &open).unwrap();
854        let (caller, provider) = (
855            StreamSeal::caller(&caller_seal),
856            StreamSeal::provider(&provider_seal),
857        );
858
859        let up = sealed(&caller, data(0, b"up"));
860        assert_eq!(
861            unsealed(from(CALLER, up), Some(&provider)),
862            Ok(data(0, b"up"))
863        );
864        // A provider's frames carry a fresh random nonce each.
865        let (one, two) = (
866            sealed(&provider, data(0, b"down")),
867            sealed(&provider, data(0, b"down")),
868        );
869        let nonce = |f: &StreamFields| match f {
870            StreamFields::SealedData { sealed, .. } => sealed.nonce.clone().unwrap(),
871            other => panic!("{other:?}"),
872        };
873        assert_ne!(nonce(&one), nonce(&two));
874        assert_eq!(
875            unsealed(from(PROVIDER, one), Some(&caller)),
876            Ok(data(0, b"down"))
877        );
878        // Sealed by the caller, a frame does not open as the provider's.
879        let up = sealed(&caller, data(1, b"up"));
880        assert!(matches!(
881            unsealed(from(CALLER, up), Some(&caller)),
882            Err(LinkError::Confidentiality(_))
883        ));
884    }
885
886    /// Venus's review of package 1, observation 2: a forged clear frame on
887    /// a sealed stream is refused on either side; only a caller takes the
888    /// provider's clear refusal of the open at seq 0.
889    #[test]
890    fn a_forged_clear_frame_on_a_sealed_stream_is_refused() {
891        let keyring = Keyring::system(Profile::PqPure).unwrap();
892        let (open, caller_seal) = sealed_to(&keyring, RequestType::StreamOpen);
893        let (_, provider_seal) = opened_request(Some(&keyring), &open).unwrap();
894        let (caller, provider) = (
895            StreamSeal::caller(&caller_seal),
896            StreamSeal::provider(&provider_seal),
897        );
898        let refusal = StreamFields::Error {
899            seq: 0,
900            code: CODE_SEALED_REFUSED.into(),
901            message: String::new(),
902        };
903        for side in [&caller, &provider] {
904            assert_eq!(
905                unsealed(from(PROVIDER, data(0, b"clear")), Some(side)),
906                Err(LinkError::ClearAnswerToSealed)
907            );
908            let reply = StreamFields::Reply {
909                seq: 1,
910                payload: Value::Null,
911            };
912            assert_eq!(
913                unsealed(from(PROVIDER, reply), Some(side)),
914                Err(LinkError::ClearAnswerToSealed)
915            );
916        }
917        assert_eq!(
918            unsealed(from(PROVIDER, refusal.clone()), Some(&caller)),
919            Ok(refusal.clone())
920        );
921        assert_eq!(
922            unsealed(from(CALLER, refusal), Some(&provider)),
923            Err(LinkError::ClearAnswerToSealed)
924        );
925    }
926
927    #[test]
928    fn a_keyed_procedure_takes_clear_calls_only_within_its_keyless_window() {
929        let since = 1_790_000_000_000;
930        let window = MAX_ADVERTISEMENT_TTL_MS + CLOCK_TOLERANCE_MS as i64;
931        for c in [Confidentiality::Preferred, Confidentiality::Off] {
932            assert!(clear_allowed(c, None, since + 10 * window), "{c:?} keyless");
933        }
934        assert!(clear_allowed(
935            Confidentiality::Preferred,
936            Some(since),
937            since + window
938        ));
939        assert!(!clear_allowed(
940            Confidentiality::Preferred,
941            Some(since),
942            since + window + 1
943        ));
944        for keyed_since in [None, Some(since)] {
945            assert!(!clear_allowed(
946                Confidentiality::Required,
947                keyed_since,
948                since
949            ));
950        }
951    }
952
953    #[test]
954    fn a_refusal_names_a_key_only_as_sixteen_lowercase_hex_digits() {
955        assert_eq!(
956            refused_key(Some("0966848943d688e2")),
957            Some([0x09, 0x66, 0x84, 0x89, 0x43, 0xd6, 0x88, 0xe2])
958        );
959        for not_one in [
960            "+1+2+3+4+5+6+7+8",
961            "0966848943D688E2",
962            "0966848943d688e",
963            "0966848943d688e2f",
964            NO_KEY_DETAIL,
965            "",
966        ] {
967            assert_eq!(refused_key(Some(not_one)), None, "{not_one:?}");
968        }
969        assert_eq!(refused_key(None), None);
970    }
971}