Skip to main content

macula_rust/frame/
sealed.rs

1//! A payload sealed end to end (macula 13, E2E seal scheme 1, E2E_SEAL_V1.md
2//! "The sealed map"): the recipient key's id, the ciphertext with its tag,
3//! and, by the frame it rides in, a request's kem_ct (it agrees the keys; its
4//! nonce is fixed) or a reply's or a provider stream frame's carried nonce. A
5//! caller stream frame's nonce is its seq, so it carries neither. A frame
6//! carries `sealed` in place of its clear field, never beside it. Nothing
7//! here opens a payload: [`crate::seal`] does.
8
9use crate::cbor::Value;
10
11use super::{entry, fixed, has_fields, protocol_uint, read_fields, uint, Fields, Rule};
12
13/// The only scheme a sealed map may name.
14pub const SEALED_SCHEME: u64 = 1;
15
16const KEM_CT_PURE_SIZE: usize = 1568;
17const KEM_CT_HYBRID_SIZE: usize = 1665;
18const SEALED_NONCE_SIZE: usize = 12;
19
20/// A payload sealed end to end, as a frame carries it.
21#[derive(Debug, Clone, PartialEq, Eq)]
22pub struct Sealed {
23    pub key_id: [u8; 8],
24    pub kem_ct: Option<Vec<u8>>,
25    pub nonce: Option<Vec<u8>>,
26    pub ct: Vec<u8>,
27}
28
29/// The frame a sealed map rides in, which sets its shape.
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub enum SealedContext {
32    Request,
33    Reply,
34    ProviderStream,
35    CallerStream,
36}
37
38impl Sealed {
39    /// The sealed map as it travels.
40    pub(super) fn value(&self) -> Value {
41        let mut entries = vec![
42            entry("scheme", uint(SEALED_SCHEME)),
43            entry("key_id", Value::Bytes(self.key_id.to_vec())),
44            entry("ct", Value::Bytes(self.ct.clone())),
45        ];
46        if let Some(kem_ct) = &self.kem_ct {
47            entries.push(entry("kem_ct", Value::Bytes(kem_ct.clone())));
48        }
49        if let Some(nonce) = &self.nonce {
50            entries.push(entry("nonce", Value::Bytes(nonce.clone())));
51        }
52        Value::Map(entries)
53    }
54
55    /// Whether the map has the shape its frame's context requires, as
56    /// macula_frame's sealed_shape/3 holds it.
57    pub(super) fn shaped(&self, context: SealedContext) -> bool {
58        if self
59            .nonce
60            .as_ref()
61            .is_some_and(|n| n.len() != SEALED_NONCE_SIZE)
62        {
63            return false;
64        }
65        match (context, &self.kem_ct, &self.nonce) {
66            (SealedContext::Request, Some(kem_ct), None) => {
67                kem_ct.len() == KEM_CT_PURE_SIZE || kem_ct.len() == KEM_CT_HYBRID_SIZE
68            }
69            (SealedContext::Reply | SealedContext::ProviderStream, None, Some(_)) => true,
70            (SealedContext::CallerStream, None, None) => true,
71            _ => false,
72        }
73    }
74}
75
76const SEALED_TABLE: &[(&str, Rule)] = &[
77    ("scheme", Rule::ProtocolUint),
78    ("key_id", Rule::BytesOf(8)),
79    ("kem_ct", Rule::AnyBytes),
80    ("nonce", Rule::BytesOf(SEALED_NONCE_SIZE)),
81    ("ct", Rule::AnyBytes),
82];
83
84/// A sealed map as a verifier reads it, opening nothing: scheme 1, a key id
85/// and a ciphertext, and the shape its frame's context requires. Any other
86/// map, or a key the table cannot name, is not one.
87pub(super) fn read_sealed(v: &Value, context: SealedContext) -> Option<Sealed> {
88    let fields = read_fields(v, SEALED_TABLE)?;
89    if !has_fields(&fields, &["scheme", "key_id", "ct"])
90        || protocol_uint(&fields["scheme"]) != Some(SEALED_SCHEME)
91    {
92        return None;
93    }
94    let bytes = |name: &str| match fields.get(name) {
95        Some(Value::Bytes(b)) => Some(b.clone()),
96        _ => None,
97    };
98    let sealed = Sealed {
99        key_id: fixed(&fields["key_id"]),
100        kem_ct: bytes("kem_ct"),
101        nonce: bytes("nonce"),
102        ct: bytes("ct")?,
103    };
104    sealed.shaped(context).then_some(sealed)
105}
106
107/// Whether `fields` carry `clear` in the clear or `sealed`, exactly one of
108/// the two (macula_frame's payload_or_sealed/1).
109pub(super) fn clear_or_sealed(fields: &Fields, clear: &str) -> bool {
110    fields.contains_key(clear) != fields.contains_key("sealed")
111}
112
113/// The sealed map `fields` carry, `None` for none: the fields were read
114/// against a table holding [`Rule::Sealed`], so it reads.
115pub(super) fn sealed_field(fields: &Fields, context: SealedContext) -> Option<Sealed> {
116    fields.get("sealed").and_then(|v| read_sealed(v, context))
117}
118
119#[cfg(test)]
120mod tests {
121    use super::*;
122
123    fn sealed(kem_ct: Option<usize>, nonce: Option<usize>) -> Sealed {
124        Sealed {
125            key_id: [7; 8],
126            kem_ct: kem_ct.map(|n| vec![1; n]),
127            nonce: nonce.map(|n| vec![2; n]),
128            ct: vec![3; 20],
129        }
130    }
131
132    #[test]
133    fn each_frame_takes_only_its_own_sealed_shape() {
134        use SealedContext::*;
135        for (s, request, reply, provider_stream, caller_stream) in [
136            (sealed(Some(1568), None), true, false, false, false),
137            (sealed(Some(1665), None), true, false, false, false),
138            (sealed(Some(1567), None), false, false, false, false),
139            (sealed(None, Some(12)), false, true, true, false),
140            (sealed(None, Some(11)), false, false, false, false),
141            (sealed(None, None), false, false, false, true),
142            (sealed(Some(1568), Some(12)), false, false, false, false),
143        ] {
144            assert_eq!(s.shaped(Request), request, "{s:?}");
145            assert_eq!(s.shaped(Reply), reply, "{s:?}");
146            assert_eq!(s.shaped(ProviderStream), provider_stream, "{s:?}");
147            assert_eq!(s.shaped(CallerStream), caller_stream, "{s:?}");
148            assert_eq!(
149                read_sealed(&s.value(), Request).is_some(),
150                request,
151                "{s:?} read back"
152            );
153        }
154    }
155
156    #[test]
157    fn a_sealed_map_of_another_scheme_or_with_a_key_the_table_cannot_name_is_not_one() {
158        let Value::Map(mut other_scheme) = sealed(None, Some(12)).value() else {
159            unreachable!()
160        };
161        other_scheme[0] = entry("scheme", uint(2));
162        assert_eq!(
163            read_sealed(&Value::Map(other_scheme), SealedContext::Reply),
164            None
165        );
166        let Value::Map(mut extra) = sealed(None, Some(12)).value() else {
167            unreachable!()
168        };
169        extra.push(entry("payload", Value::Null));
170        assert_eq!(read_sealed(&Value::Map(extra), SealedContext::Reply), None);
171    }
172}