Skip to main content

vitaminc_context/
context.rs

1//! The encoded bytes of a context, and the contexts derived from one.
2
3use std::borrow::Cow;
4
5use crate::{pae, ContextPiece, IntoContext};
6
7const MAP_ENTRY_DOMAIN: &[u8] = b"vitaminc/context/map-entry/v1";
8const MARKER_DOMAIN: &[u8] = b"vitaminc/context/marker/v1";
9const LEAF_DOMAIN: &[u8] = b"vitaminc/context/leaf";
10const SEQ_ELEMENT_DOMAIN: &[u8] = b"vitaminc/context/seq-element/v1";
11const OPTION_SOME_DOMAIN: &[u8] = b"vitaminc/context/option-some/v1";
12const REFINE_DOMAIN: &[u8] = b"vitaminc/context/refine/v1";
13
14/// The canonical encoding of a context, as bytes.
15///
16/// A context is the same value whether an AEAD authenticates it as
17/// associated data or a PRF derives under it: `x.into_aad()` and
18/// `x.into_prf_context()` both produce this type and both hold the same
19/// bytes. Those bytes come from one place, the encoding of the context's
20/// [`ContextPiece`] tree, so no consumer can be given a different encoding
21/// of the same context on one side than on the other.
22///
23/// The storage is copy-on-write. A context built from a borrowed encoded
24/// slice ([`from_encoded`](Self::from_encoded)) borrows it; anything the
25/// encoder produces is owned.
26///
27/// # Raw bytes
28///
29/// Two methods take raw bytes rather than a typed value, and both mean
30/// something specific:
31///
32/// - [`from_encoded`](Self::from_encoded) takes bytes this encoder already
33///   produced, for a context that was stored or crossed a language boundary
34///   and is now being handed back. Passing it something else, say the raw
35///   bytes of a string, gives a context that is not the encoding of that
36///   string: `Context::from_encoded(b"7")` and `"7".into_aad()` are
37///   different contexts.
38/// - [`pae`](Self::pae) frames a list of byte pieces exactly as a composite
39///   context is framed. A crate that defines its own domain-separated
40///   context shapes builds them with it, leading with a domain label of its
41///   own.
42///
43/// `Context` deliberately does not implement `MaybeEmpty`. An encoded
44/// context can only be judged on its bytes, and framing makes the encoding
45/// of an empty value non-empty, so `NonEmpty<Context>` would certify exactly
46/// the degenerate value it exists to exclude. Its parts view, the
47/// [`ContextPiece::Encoded`] leaf, counts as empty for the same reason, so
48/// routing bytes through [`into_context`](IntoContext::into_context) is not
49/// a way around the rule. Prove non-emptiness on the value before it is
50/// encoded: `NonEmpty<T>` where `T: IntoContext`.
51#[derive(Clone, Debug, Default, PartialEq, Eq, Hash)]
52pub struct Context<'a>(pub(crate) Cow<'a, [u8]>);
53
54impl<'a> Context<'a> {
55    /// The empty context: no bytes at all. The same as `().into_context()`
56    /// encoded, and the value [`Default`] gives.
57    // `empty` and `Default::default` are intentionally identical, so replacing
58    // this body with `Default::default` is an equivalent mutation.
59    #[mutants::skip]
60    pub fn empty() -> Self {
61        Self::default()
62    }
63
64    /// A context from bytes this encoder already produced.
65    ///
66    /// Use it to hand back a context that was stored, logged, or received
67    /// across an FFI boundary as bytes. It does not encode anything: the
68    /// bytes are the context, verbatim, and when the result is used as a
69    /// part of a larger context it is written as a
70    /// [`ContextPiece::Encoded`] leaf, untagged. Do not use it to turn a
71    /// value into a context; implement or call [`IntoContext`] for that.
72    ///
73    /// ```rust
74    /// use vitaminc_context::{Context, IntoContext};
75    ///
76    /// let stored = ("users", 7u64).into_context().encode();
77    /// let restored = Context::from_encoded(stored.as_bytes());
78    /// assert_eq!(restored, stored);
79    /// // Re-encoding an encoded context leaves it unchanged.
80    /// assert_eq!(restored.clone().into_context().encode(), stored);
81    /// ```
82    pub fn from_encoded(bytes: impl Into<Cow<'a, [u8]>>) -> Self {
83        Self(bytes.into())
84    }
85
86    /// The encoded bytes.
87    pub fn as_bytes(&self) -> &[u8] {
88        self.0.as_ref()
89    }
90
91    /// Whether there are no bytes at all. Only the empty context and the
92    /// encoding of `()` are empty; every framed context has at least a count
93    /// word.
94    pub fn is_empty(&self) -> bool {
95        self.0.is_empty()
96    }
97
98    /// Copies the bytes if they are borrowed, so the context can outlive its
99    /// source.
100    pub fn into_owned(self) -> Context<'static> {
101        Context(Cow::Owned(self.0.into_owned()))
102    }
103
104    /// Pre-Authentication Encoding of a list of byte pieces, from the PASETO
105    /// specification: `LE64(count) || (LE64(len(piece)) || piece)*`.
106    ///
107    /// Structurally distinct inputs always encode to distinct byte strings.
108    /// This is the framing every composite context in this crate uses, and
109    /// the building block a crate uses to define a domain-separated context
110    /// shape of its own. Lead such a shape with a domain label that is
111    /// yours; the labels this crate reserves all begin `vitaminc/context/`.
112    pub fn pae(pieces: &[&[u8]]) -> Context<'static> {
113        pae::encode(pieces)
114    }
115
116    /// Adds a component under this context, with a domain tag so the result
117    /// cannot collide with any context this crate derives on its own.
118    ///
119    /// The encoding is `PAE(domain, self, component)`. Without the leading
120    /// domain, a caller could build the reserved option-some label as a
121    /// context and refine it by `x` to reach the same bytes
122    /// [`for_option_some`](Self::for_option_some) assigns under `x`.
123    pub fn refine<'b, C>(&self, component: C) -> Context<'static>
124    where
125        C: IntoContext<'b>,
126    {
127        let component = component.into_context().encode();
128        Self::pae(&[REFINE_DOMAIN, self.as_bytes(), component.as_bytes()])
129    }
130
131    /// The context a map entry's value is sealed or derived under, binding
132    /// the entry `key` to this context.
133    ///
134    /// Map keys travel in the clear inside a ciphertext container, so
135    /// without this binding an attacker holding a stored ciphertext could
136    /// swap or rename keys undetected and silently reassign values to
137    /// different fields. Every map cipher and map PRF derives each entry's
138    /// context through this method, on both the writing and the reading
139    /// side.
140    ///
141    /// The encoding is `PAE(domain, self, key)`. The leading domain keeps
142    /// the result apart from a caller binding the tuple `(context, key)` as
143    /// a context of its own, which is a two-piece list of typed leaves and
144    /// so can never equal a three-piece labelled frame.
145    pub fn for_map_entry(&self, key: &str) -> Context<'static> {
146        Self::pae(&[MAP_ENTRY_DOMAIN, self.as_bytes(), key.as_bytes()])
147    }
148
149    /// The context a structural marker is sealed under.
150    ///
151    /// A marker is a sealed empty plaintext whose tag is the only thing
152    /// authenticating a structural fact: this sequence is empty, this map is
153    /// empty, this value is absent. The encoding is the labelled three-piece
154    /// `PAE(domain, self, kind)`. The domain keeps marker contexts apart from
155    /// caller-built composites, and the `kind` piece keeps the marker kinds
156    /// apart from each other, so a stored marker can never be replayed as a
157    /// different structural claim.
158    fn for_marker(&self, kind: &[u8]) -> Context<'static> {
159        Self::pae(&[MARKER_DOMAIN, self.as_bytes(), kind])
160    }
161
162    /// The context every sealed leaf is finally authenticated under, binding
163    /// the wire-format `version` byte that prefixes the stored leaf.
164    ///
165    /// This is the outermost derivation. A cipher applies it at the AEAD
166    /// seal and open boundary, after every structural derivation
167    /// ([`for_map_entry`](Self::for_map_entry),
168    /// [`for_sequence_element`](Self::for_sequence_element), the markers) has
169    /// produced the caller-visible context. Binding the version under the
170    /// tag is what makes it more than a parse hint: a stored leaf relabelled
171    /// with a different version byte fails verification instead of selecting
172    /// a different, perhaps weaker, set of parsing rules.
173    ///
174    /// The domain label carries no `/v1` suffix on purpose. The version is a
175    /// parameter here, not part of the label.
176    pub fn for_leaf(&self, version: u8) -> Context<'static> {
177        Self::pae(&[LEAF_DOMAIN, &[version], self.as_bytes()])
178    }
179
180    /// The context a sequence element is sealed or derived under.
181    ///
182    /// Without this derivation a sequence element would share the caller's
183    /// bare context with a top-level single value, byte for byte, so an
184    /// attacker holding a stored ciphertext could rewrap a single leaf as a
185    /// one-element sequence and a self-describing decrypt path would verify
186    /// it. Sealing elements under a labelled derivation means a leaf
187    /// verifies only in the position it was sealed for.
188    ///
189    /// The element index is deliberately not bound. Records are retrieved in
190    /// a different order than they were inserted, so element order is a
191    /// caller obligation, not an authenticated fact.
192    pub fn for_sequence_element(&self) -> Context<'static> {
193        Self::pae(&[SEQ_ELEMENT_DOMAIN, self.as_bytes(), b"element"])
194    }
195
196    /// Marker context for an empty sequence. See
197    /// [`for_none`](Self::for_none) for the marker rule.
198    pub fn for_empty_sequence(&self) -> Context<'static> {
199        self.for_marker(b"empty-sequence")
200    }
201
202    /// Marker context for an empty map. See [`for_none`](Self::for_none)
203    /// for the marker rule.
204    pub fn for_empty_map(&self) -> Context<'static> {
205        self.for_marker(b"empty-map")
206    }
207
208    /// Marker context for an authenticated absent value, `Option::None`.
209    ///
210    /// A marker is a sealed empty plaintext whose tag is the only thing
211    /// authenticating a structural fact. Each marker kind is derived under
212    /// its own labelled context, `PAE(domain, self, kind)`, so a single leaf
213    /// sealed under the bare context can never be re-tagged as an absence
214    /// marker (silent authenticated deletion), an absence marker can never
215    /// validate as an encrypted empty byte string, and no marker can be
216    /// replayed as a different structural claim.
217    pub fn for_none(&self) -> Context<'static> {
218        self.for_marker(b"none")
219    }
220
221    /// The context an optional value's `Some` is derived under.
222    ///
223    /// This is the value side of `Option`, distinct from the context side.
224    /// `Some(x)` as a context is the one-element list `PAE([x])`, untagged,
225    /// so that a runtime list of one part is the same context as the static
226    /// `Some`. A `Some` value being derived under a context `c` uses
227    /// `PAE(domain, c)` instead, keeping the derivation of an optional value
228    /// apart from the derivation of its inner value under the same `c`.
229    pub fn for_option_some(&self) -> Context<'static> {
230        Self::pae(&[OPTION_SOME_DOMAIN, self.as_bytes()])
231    }
232}
233
234/// An encoded context is a context. As a part of a larger context it is
235/// the [`Encoded`](ContextPiece::Encoded) leaf, written verbatim and
236/// untagged, so re-encoding a context leaves its bytes unchanged.
237impl<'a> IntoContext<'a> for Context<'a> {
238    fn into_context(self) -> ContextPiece<'a> {
239        ContextPiece::Encoded(self.0)
240    }
241}
242
243#[cfg(test)]
244mod tests {
245    use super::*;
246    use quickcheck_macros::quickcheck;
247
248    fn raw(bytes: &[u8]) -> Context<'_> {
249        Context::from_encoded(bytes)
250    }
251
252    mod given_raw_bytes {
253        use super::*;
254
255        #[test]
256        fn from_encoded_keeps_them_verbatim() {
257            let borrowed = Context::from_encoded(&[1u8, 2, 3][..]);
258            let owned = Context::from_encoded(vec![1u8, 2, 3]);
259            assert_eq!(borrowed.as_bytes(), &[1, 2, 3]);
260            assert_eq!(owned, borrowed, "ownership does not change the context");
261            assert_eq!(
262                borrowed.into_owned().as_bytes(),
263                &[1, 2, 3],
264                "into_owned keeps the bytes"
265            );
266        }
267
268        #[test]
269        fn re_encoding_is_the_identity() {
270            let stored = ("users", 7u64).into_context().encode();
271            let restored = Context::from_encoded(stored.as_bytes());
272            assert_eq!(
273                restored.into_context().encode(),
274                stored,
275                "an encoded context re-encodes to itself"
276            );
277        }
278
279        #[test]
280        fn from_encoded_is_not_a_typed_value() {
281            // The documented footgun: raw bytes are not the encoding of the
282            // string with those bytes.
283            assert_ne!(
284                raw(b"7").into_context().encode(),
285                "7".into_context().encode()
286            );
287        }
288
289        #[test]
290        fn emptiness_is_judged_on_the_bytes_only() {
291            assert!(Context::empty().is_empty());
292            assert!(Context::default().is_empty());
293            assert!(!raw(b"raw").is_empty());
294            // Framing makes the encoding of an empty value non-empty, which
295            // is why `Context` has no `MaybeEmpty` impl.
296            assert!(!"".into_context().encode().is_empty());
297            assert!(!Some("").into_context().encode().is_empty());
298        }
299    }
300
301    /// The exact bytes of each derived context are a wire-format commitment.
302    /// Changing them breaks decryption of every stored ciphertext and every
303    /// saved index term derived through them.
304    mod given_a_derived_context {
305        use super::*;
306
307        #[test]
308        fn map_entry_pins_its_encoding() {
309            assert_eq!(
310                raw(b"ctx").for_map_entry("name"),
311                Context::pae(&[b"vitaminc/context/map-entry/v1", b"ctx", b"name"])
312            );
313        }
314
315        #[test]
316        fn markers_pin_their_encoding() {
317            let ctx = raw(b"ctx");
318            for (derived, kind) in [
319                (ctx.for_empty_sequence(), b"empty-sequence".as_slice()),
320                (ctx.for_empty_map(), b"empty-map"),
321                (ctx.for_none(), b"none"),
322            ] {
323                assert_eq!(
324                    derived,
325                    Context::pae(&[b"vitaminc/context/marker/v1", b"ctx", kind])
326                );
327            }
328        }
329
330        #[test]
331        fn leaf_pins_its_encoding() {
332            assert_eq!(
333                raw(b"ctx").for_leaf(1),
334                Context::pae(&[b"vitaminc/context/leaf", &[1u8], b"ctx"])
335            );
336        }
337
338        #[test]
339        fn sequence_element_pins_its_encoding() {
340            assert_eq!(
341                raw(b"ctx").for_sequence_element(),
342                Context::pae(&[b"vitaminc/context/seq-element/v1", b"ctx", b"element"])
343            );
344        }
345
346        #[test]
347        fn option_some_pins_its_encoding() {
348            assert_eq!(
349                raw(b"ctx").for_option_some(),
350                Context::pae(&[b"vitaminc/context/option-some/v1", b"ctx"])
351            );
352        }
353
354        #[test]
355        fn refine_pins_its_encoding() {
356            assert_eq!(
357                raw(b"parent").refine("child"),
358                Context::pae(&[
359                    b"vitaminc/context/refine/v1",
360                    b"parent",
361                    "child".into_context().encode().as_bytes(),
362                ])
363            );
364        }
365
366        #[test]
367        fn every_derivation_differs_from_the_bare_context_and_each_other() {
368            let ctx = raw(b"ctx");
369            let derived = [
370                ctx.for_map_entry("element"),
371                ctx.for_leaf(1),
372                ctx.for_sequence_element(),
373                ctx.for_empty_sequence(),
374                ctx.for_empty_map(),
375                ctx.for_none(),
376                ctx.for_option_some(),
377                ctx.refine("element"),
378            ];
379            for (i, left) in derived.iter().enumerate() {
380                assert_ne!(left, &ctx, "a derivation is never the bare context");
381                for right in &derived[i + 1..] {
382                    assert_ne!(left, right, "two derivations never coincide");
383                }
384            }
385        }
386
387        #[test]
388        fn every_derivation_differs_from_a_tuple_of_the_same_parts() {
389            // A caller binding the same pieces as a tuple builds a list of
390            // typed leaves, which the labelled frame can never equal.
391            let ctx = raw(b"ctx");
392            assert_ne!(
393                ctx.for_map_entry("name"),
394                (ctx.clone(), "name").into_context().encode()
395            );
396            assert_ne!(
397                ctx.for_leaf(1),
398                (b"vitaminc/context/leaf".as_slice(), (1u8, ctx.clone()))
399                    .into_context()
400                    .encode()
401            );
402            assert_ne!(
403                ctx.for_none(),
404                (b"vitaminc/context/marker/v1".as_slice(), ctx.clone())
405                    .into_context()
406                    .encode()
407            );
408            assert_ne!(
409                ctx.for_sequence_element(),
410                (b"vitaminc/context/seq-element/v1".as_slice(), ctx.clone())
411                    .into_context()
412                    .encode()
413            );
414        }
415
416        #[test]
417        fn leaf_is_version_sensitive() {
418            let ctx = raw(b"ctx");
419            assert_ne!(
420                ctx.for_leaf(1),
421                ctx.for_leaf(2),
422                "a relabelled version byte changes the context; that is the downgrade defence"
423            );
424        }
425
426        #[test]
427        fn refine_is_disjoint_from_the_reserved_derivations() {
428            // Without its own domain, refining a context that happens to be a
429            // reserved label would collide with the derivation that label
430            // names.
431            assert_ne!(
432                raw(OPTION_SOME_DOMAIN).refine("child"),
433                raw(b"child").for_option_some()
434            );
435            assert_ne!(
436                raw(MAP_ENTRY_DOMAIN).refine("child"),
437                raw(b"parent").for_map_entry("child")
438            );
439        }
440
441        #[quickcheck]
442        fn map_keys_are_separated(context: Vec<u8>, a: String, b: String) -> bool {
443            let context = Context::from_encoded(context);
444            a == b || context.for_map_entry(&a) != context.for_map_entry(&b)
445        }
446
447        #[test]
448        fn map_entry_bytes_cannot_move_between_context_and_key() {
449            assert_ne!(
450                raw(b"ctxa").for_map_entry(""),
451                raw(b"ctx").for_map_entry("a")
452            );
453            assert_ne!(
454                Context::empty().for_map_entry("ab"),
455                Context::empty().for_map_entry("a")
456            );
457        }
458    }
459}