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}