Skip to main content

oc_crypto/
transcript.rs

1// SPDX-License-Identifier: MPL-2.0
2//! Transcript: the only way to obtain bytes for a signature or MAC.
3//!
4//! This type exists for one invariant: **signing bytes without domain
5//! separation is impossible**. Its constructor requires a label, and signing functions accept
6//! only [`Transcript`], so a label cannot be forgotten: the code simply will
7//! not compile.
8//!
9//! Otherwise, an author's signature made in one context (revocation record, activation
10//! request, permission grant) becomes replayable in another if
11//! encodings can collide. A silent error detectable only by an attack.
12
13use crate::label::Label;
14
15/// Bytes prepared for signing or MAC computation, with a mandatory domain label.
16#[derive(Clone, PartialEq, Eq)]
17pub struct Transcript {
18    buf: Vec<u8>,
19}
20
21impl core::fmt::Debug for Transcript {
22    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
23        write!(f, "Transcript({} байт)", self.buf.len())
24    }
25}
26
27impl Transcript {
28    /// Begin a transcript with a domain label.
29    ///
30    /// The label has type [`Label`], not `&'static [u8]`, a substantive
31    /// difference: a `Label` cannot be invented; only constants in
32    /// [`crate::label`] supply it. When bytes were accepted, I-12 checks
33    /// inspected the registry while a caller could bypass it with a string
34    /// absent from the registry (for example an extension of an occupied label), invisible to
35    /// every probe. That no longer compiles.
36    #[must_use]
37    pub fn new(label: Label) -> Self {
38        let bytes = label.as_bytes();
39        let mut buf = Vec::with_capacity(bytes.len().saturating_add(64));
40        buf.extend_from_slice(bytes);
41        buf.push(0x00);
42        Self { buf }
43    }
44
45    /// Append one byte: algorithm identifier, version, tag.
46    pub fn u8(&mut self, value: u8) -> &mut Self {
47        self.buf.push(value);
48        self
49    }
50
51    /// Append a little-endian `u32`. Order is specified by the format.
52    pub fn u32le(&mut self, value: u32) -> &mut Self {
53        self.buf.extend_from_slice(&value.to_le_bytes());
54        self
55    }
56
57    /// Append a big-endian `u32`, used for chunk indices.
58    pub fn u32be(&mut self, value: u32) -> &mut Self {
59        self.buf.extend_from_slice(&value.to_be_bytes());
60        self
61    }
62
63    /// Append a big-endian `u64`, used for lease sequences.
64    pub fn u64be(&mut self, value: u64) -> &mut Self {
65        self.buf.extend_from_slice(&value.to_be_bytes());
66        self
67    }
68
69    /// Append fixed-length data: key, fingerprint, identifier.
70    ///
71    /// For **variable-length** fields use [`Transcript::field`], or
72    /// two different field sequences can produce identical bytes and the signature
73    /// ceases to identify contents unambiguously.
74    pub fn fixed(&mut self, value: &[u8]) -> &mut Self {
75        self.buf.extend_from_slice(value);
76        self
77    }
78
79    /// Append a variable-length field with a length prefix.
80    pub fn field(&mut self, value: &[u8]) -> &mut Self {
81        let len = u32::try_from(value.len()).unwrap_or(u32::MAX);
82        self.buf.extend_from_slice(&len.to_le_bytes());
83        self.buf.extend_from_slice(value);
84        self
85    }
86
87    /// Append a final block without a length prefix.
88    ///
89    /// Permitted **only** when its length was already written into the transcript,
90    /// as in a header signature, where `u32le(HeaderLen)` precedes
91    /// the header itself.
92    pub fn tail_after_declared_length(&mut self, value: &[u8]) -> &mut Self {
93        self.buf.extend_from_slice(value);
94        self
95    }
96
97    /// Completed bytes.
98    pub fn as_bytes(&self) -> &[u8] {
99        &self.buf
100    }
101
102    /// Byte length.
103    pub fn len(&self) -> usize {
104        self.buf.len()
105    }
106
107    /// Whether the transcript is empty. A label is always present, so always `false`.
108    pub fn is_empty(&self) -> bool {
109        self.buf.is_empty()
110    }
111}
112
113#[cfg(test)]
114#[allow(clippy::unwrap_used, clippy::panic)]
115mod tests {
116    use super::*;
117    use crate::label;
118
119    #[test]
120    fn every_transcript_starts_with_its_label() {
121        let t = Transcript::new(label::LEASE);
122        assert!(t.as_bytes().starts_with(label::LEASE.as_bytes()));
123        assert_eq!(t.as_bytes().get(label::LEASE.len()), Some(&0x00));
124    }
125
126    #[test]
127    fn prefix_labels_do_not_collide_even_though_the_set_is_prefix_free() {
128        // Двойная страховка: набор меток беспрефиксный, и сверх того транскрипт
129        // ставит нулевой байт после метки. Тест фиксирует вторую защиту, чтобы
130        // её не убрали как «избыточную».
131        let mut a = Transcript::new(label::LEASE);
132        a.fixed(b"-cache-and-more");
133        let mut b = Transcript::new(label::CACHED_LEASE);
134        b.fixed(b"-and-more");
135        assert_ne!(a.as_bytes(), b.as_bytes());
136    }
137
138    #[test]
139    fn different_labels_never_collide() {
140        // Смысл всего типа: одни и те же данные под разными метками дают разные
141        // байты, поэтому подпись из одного контекста не проходит в другом.
142        let mut a = Transcript::new(label::LEASE);
143        a.fixed(&[1, 2, 3]);
144        let mut b = Transcript::new(label::REVOCATION);
145        b.fixed(&[1, 2, 3]);
146        assert_ne!(a.as_bytes(), b.as_bytes());
147    }
148
149    #[test]
150    fn variable_length_fields_are_unambiguous() {
151        // Без префикса длины ("ab","c") и ("a","bc") дали бы одни байты.
152        let mut a = Transcript::new(label::GRANT);
153        a.field(b"ab").field(b"c");
154        let mut b = Transcript::new(label::GRANT);
155        b.field(b"a").field(b"bc");
156        assert_ne!(a.as_bytes(), b.as_bytes());
157    }
158
159    #[test]
160    fn fixed_length_fields_are_concatenated_verbatim() {
161        let mut t = Transcript::new(label::CHUNK);
162        t.fixed(&[0xaa; 16]).u32be(7).u8(1);
163        let expected_len = label::CHUNK.len() + 1 + 16 + 4 + 1;
164        assert_eq!(t.len(), expected_len);
165        assert_eq!(t.as_bytes().get(t.len() - 5..t.len()), Some(&[0, 0, 0, 7, 1][..]));
166    }
167
168    #[test]
169    fn endianness_is_explicit_and_distinct() {
170        let mut le = Transcript::new(label::CHUNK);
171        le.u32le(1);
172        let mut be = Transcript::new(label::CHUNK);
173        be.u32be(1);
174        assert_ne!(le.as_bytes(), be.as_bytes(), "порядок байтов обязан быть явным");
175    }
176}