Skip to main content

oc_crypto/
transcript.rs

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