Skip to main content

bcx_core/
ids.rs

1use crate::ValidationError;
2use subtle::{Choice, ConstantTimeEq};
3use zeroize::Zeroize;
4
5#[derive(Eq)]
6struct IdentifierBytes<const MIN: usize, const MAX: usize> {
7    bytes: [u8; MAX],
8    len: u8,
9}
10
11impl<const MIN: usize, const MAX: usize> IdentifierBytes<MIN, MAX> {
12    fn new(bytes: &[u8]) -> Result<Self, ValidationError> {
13        if bytes.is_empty() {
14            return Err(ValidationError::Empty);
15        }
16        if bytes.len() < MIN {
17            return Err(ValidationError::Malformed);
18        }
19        if bytes.len() > MAX {
20            return Err(ValidationError::TooLarge);
21        }
22        if bytes.iter().all(|byte| *byte == 0) {
23            return Err(ValidationError::ZeroValue);
24        }
25
26        let len = u8::try_from(bytes.len()).map_err(|_| ValidationError::TooLarge)?;
27        let mut stored = [0; MAX];
28        stored[..bytes.len()].copy_from_slice(bytes);
29        Ok(Self { bytes: stored, len })
30    }
31
32    fn as_bytes(&self) -> &[u8] {
33        self.bytes.split_at(self.len as usize).0
34    }
35
36    const fn len(&self) -> usize {
37        self.len as usize
38    }
39
40    fn ct_eq(&self, other: &Self) -> bool {
41        let len_eq: Choice = self.len.ct_eq(&other.len);
42        let bytes_eq: Choice = self.bytes.ct_eq(&other.bytes);
43        bool::from(len_eq & bytes_eq)
44    }
45}
46
47impl<const MIN: usize, const MAX: usize> PartialEq for IdentifierBytes<MIN, MAX> {
48    fn eq(&self, other: &Self) -> bool {
49        self.ct_eq(other)
50    }
51}
52
53impl<const MIN: usize, const MAX: usize> Drop for IdentifierBytes<MIN, MAX> {
54    fn drop(&mut self) {
55        self.bytes.zeroize();
56        self.len.zeroize();
57    }
58}
59
60macro_rules! define_identifier {
61    ($(#[$meta:meta])* $name:ident, $min:expr, $max:expr) => {
62        $(#[$meta])*
63        #[derive(Eq, PartialEq)]
64        pub struct $name(IdentifierBytes<{ $min }, { $max }>);
65
66        impl $name {
67            /// Minimum accepted identifier length in bytes.
68            pub const MIN_LEN: usize = $min;
69            /// Maximum accepted identifier length in bytes.
70            pub const MAX_LEN: usize = $max;
71
72            /// Creates a validated identifier from canonical bytes.
73            pub fn new(bytes: &[u8]) -> Result<Self, ValidationError> {
74                IdentifierBytes::new(bytes).map(Self)
75            }
76
77            /// Returns the canonical identifier bytes.
78            #[must_use]
79            pub fn as_bytes(&self) -> &[u8] {
80                self.0.as_bytes()
81            }
82
83            /// Returns the canonical identifier length in bytes.
84            #[must_use]
85            pub const fn len(&self) -> usize {
86                self.0.len()
87            }
88
89            /// Returns false because validated BCX identifiers cannot be empty.
90            #[must_use]
91            pub const fn is_empty(&self) -> bool {
92                false
93            }
94        }
95
96        impl core::fmt::Debug for $name {
97            fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
98                formatter.write_str(concat!(stringify!($name), "(..)"))
99            }
100        }
101    };
102}
103
104/// Fixed-width digest used for protocol commitments.
105#[derive(Clone, Copy, Eq)]
106pub struct Digest([u8; Self::LEN]);
107
108impl Digest {
109    /// Digest byte length for the first BCX profile.
110    pub const LEN: usize = 32;
111
112    /// Creates a digest from raw bytes.
113    #[must_use]
114    pub const fn new(bytes: [u8; Self::LEN]) -> Self {
115        Self(bytes)
116    }
117
118    /// Returns the digest as bytes.
119    #[must_use]
120    pub const fn as_bytes(&self) -> &[u8; Self::LEN] {
121        &self.0
122    }
123
124    /// Returns true when every byte is zero.
125    #[must_use]
126    pub fn is_zero(&self) -> bool {
127        bool::from(self.0.ct_eq(&[0u8; Self::LEN]))
128    }
129
130    /// Compares two digests without data-dependent early exit.
131    ///
132    /// This is also used by `PartialEq` to avoid byte-by-byte early exit.
133    #[must_use]
134    pub fn ct_eq(&self, other: &Self) -> bool {
135        bool::from(self.0.ct_eq(&other.0))
136    }
137}
138
139impl PartialEq for Digest {
140    fn eq(&self, other: &Self) -> bool {
141        self.ct_eq(other)
142    }
143}
144
145impl core::hash::Hash for Digest {
146    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
147        core::hash::Hash::hash(&self.0, state);
148    }
149}
150
151impl core::fmt::Debug for Digest {
152    fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
153        formatter.write_str("Digest(..)")
154    }
155}
156
157define_identifier!(
158    /// Statement identifier for BCX causal statements.
159    StatementId,
160    Digest::LEN,
161    Digest::LEN
162);
163
164define_identifier!(
165    /// Subject identifier for the thing a statement describes or affects.
166    SubjectId,
167    1,
168    64
169);
170
171define_identifier!(
172    /// Realm identifier for an authority, namespace, tenant, or trust domain.
173    RealmId,
174    1,
175    64
176);
177
178define_identifier!(
179    /// Profile identifier for a BCX profile or native binding family.
180    ProfileId,
181    1,
182    32
183);
184
185define_identifier!(
186    /// Proof-suite identifier for signature or verification policy families.
187    ProofSuiteId,
188    1,
189    32
190);
191
192define_identifier!(
193    /// Policy identifier for disclosure, replay, settlement, or admission rules.
194    PolicyId,
195    1,
196    32
197);
198
199define_identifier!(
200    /// Checkpoint identifier for committed state, graph, or settlement checkpoints.
201    CheckpointId,
202    Digest::LEN,
203    Digest::LEN
204);
205
206define_identifier!(
207    /// Native binding identifier for host-system specific anchors.
208    NativeBindingId,
209    1,
210    64
211);
212
213/// Globally unique event identifier within a trust domain.
214#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
215pub struct EventId(Digest);
216
217impl EventId {
218    /// Creates an event identifier when the digest is non-zero.
219    pub fn new(digest: Digest) -> Result<Self, ValidationError> {
220        if digest.is_zero() {
221            Err(ValidationError::ZeroValue)
222        } else {
223            Ok(Self(digest))
224        }
225    }
226
227    /// Returns the underlying digest commitment.
228    #[must_use]
229    pub const fn digest(&self) -> Digest {
230        self.0
231    }
232}
233
234/// Reference to a capability object or capability commitment.
235#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
236pub struct CapabilityRef(Digest);
237
238impl CapabilityRef {
239    /// Creates a capability reference when the digest is non-zero.
240    pub fn new(digest: Digest) -> Result<Self, ValidationError> {
241        if digest.is_zero() {
242            Err(ValidationError::ZeroValue)
243        } else {
244            Ok(Self(digest))
245        }
246    }
247
248    /// Returns the underlying digest commitment.
249    #[must_use]
250    pub const fn digest(&self) -> Digest {
251        self.0
252    }
253}
254
255/// Reference to a policy epoch.
256#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
257pub struct PolicyEpoch(Digest);
258
259impl PolicyEpoch {
260    /// Creates a policy epoch when the digest is non-zero.
261    pub fn new(digest: Digest) -> Result<Self, ValidationError> {
262        if digest.is_zero() {
263            Err(ValidationError::ZeroValue)
264        } else {
265            Ok(Self(digest))
266        }
267    }
268
269    /// Returns the underlying digest commitment.
270    #[must_use]
271    pub const fn digest(&self) -> Digest {
272        self.0
273    }
274}
275
276/// Per-issuer operation sequence used for replay detection.
277#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
278pub struct OperationSequence(u64);
279
280impl OperationSequence {
281    /// Creates a non-zero operation sequence.
282    pub const fn new(value: u64) -> Result<Self, ValidationError> {
283        if value == 0 {
284            Err(ValidationError::ZeroValue)
285        } else {
286            Ok(Self(value))
287        }
288    }
289
290    /// Returns the raw sequence number.
291    #[must_use]
292    pub const fn get(self) -> u64 {
293        self.0
294    }
295
296    /// Returns the next sequence value.
297    pub const fn next(self) -> Result<Self, ValidationError> {
298        match self.0.checked_add(1) {
299            Some(value) => Ok(Self(value)),
300            None => Err(ValidationError::TooLarge),
301        }
302    }
303
304    /// Returns true when this sequence immediately follows `previous`.
305    #[must_use]
306    pub const fn immediately_follows(self, previous: Self) -> bool {
307        match previous.0.checked_add(1) {
308            Some(expected) => self.0 == expected,
309            None => false,
310        }
311    }
312}
313
314/// Nonce bytes carried by signed invocations and WHY queries.
315///
316/// `Nonce` intentionally does not implement `Clone` or `Copy`; duplicating
317/// nonce bytes increases the number of plaintext copies that must be cleared.
318/// It also intentionally does not implement `Hash`; replay caches should use
319/// a keyed or constant-time structure instead of exposing nonce bytes to a
320/// general-purpose hash table.
321#[derive(Eq)]
322pub struct Nonce([u8; Self::LEN]);
323
324impl Nonce {
325    /// Nonce byte length for the first BCX profile.
326    pub const LEN: usize = 16;
327
328    /// Creates a nonce from non-zero raw bytes.
329    pub fn new(bytes: [u8; Self::LEN]) -> Result<Self, ValidationError> {
330        if bool::from(bytes.ct_eq(&[0u8; Self::LEN])) {
331            Err(ValidationError::ZeroValue)
332        } else {
333            Ok(Self(bytes))
334        }
335    }
336
337    /// Returns the nonce as bytes.
338    #[must_use]
339    pub const fn as_bytes(&self) -> &[u8; Self::LEN] {
340        &self.0
341    }
342
343    /// Compares two nonces without data-dependent early exit.
344    #[must_use]
345    pub fn ct_eq(&self, other: &Self) -> bool {
346        bool::from(self.0.ct_eq(&other.0))
347    }
348}
349
350impl PartialEq for Nonce {
351    fn eq(&self, other: &Self) -> bool {
352        self.ct_eq(other)
353    }
354}
355
356impl Drop for Nonce {
357    fn drop(&mut self) {
358        self.0.zeroize();
359    }
360}
361
362impl core::fmt::Debug for Nonce {
363    fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
364        formatter.write_str("Nonce(..)")
365    }
366}