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///
106/// `Digest` is `Copy` for ergonomic identifier wrappers and therefore cannot
107/// zero its backing bytes on drop. Use [`ZeroizedDigest`] at processing
108/// boundaries where digest residue must not survive the scope.
109#[derive(Clone, Copy, Eq)]
110pub struct Digest([u8; Self::LEN]);
111
112impl Digest {
113    /// Digest byte length for the first BCX profile.
114    pub const LEN: usize = 32;
115
116    /// Creates a digest from raw bytes.
117    #[must_use]
118    pub const fn new(bytes: [u8; Self::LEN]) -> Self {
119        Self(bytes)
120    }
121
122    /// Returns the digest as bytes.
123    #[must_use]
124    pub const fn as_bytes(&self) -> &[u8; Self::LEN] {
125        &self.0
126    }
127
128    /// Returns true when every byte is zero.
129    #[must_use]
130    pub fn is_zero(&self) -> bool {
131        bool::from(self.0.ct_eq(&[0u8; Self::LEN]))
132    }
133
134    /// Compares two digests without data-dependent early exit.
135    ///
136    /// This is also used by `PartialEq` to avoid byte-by-byte early exit.
137    #[must_use]
138    pub fn ct_eq(&self, other: &Self) -> bool {
139        bool::from(self.0.ct_eq(&other.0))
140    }
141}
142
143impl PartialEq for Digest {
144    fn eq(&self, other: &Self) -> bool {
145        self.ct_eq(other)
146    }
147}
148
149impl core::hash::Hash for Digest {
150    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
151        core::hash::Hash::hash(&self.0, state);
152    }
153}
154
155impl core::fmt::Debug for Digest {
156    fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
157        formatter.write_str("Digest(..)")
158    }
159}
160
161/// Digest wrapper that clears its backing bytes when dropped.
162///
163/// Use this for key identifiers, event commitments, or capability references
164/// held at a boundary where memory residue is part of the threat model. The
165/// base [`Digest`] remains `Copy`; this wrapper intentionally does not.
166#[derive(Eq)]
167pub struct ZeroizedDigest(Digest);
168
169impl ZeroizedDigest {
170    /// Wraps a digest so its local backing bytes are cleared on drop.
171    #[must_use]
172    pub const fn new(digest: Digest) -> Self {
173        Self(digest)
174    }
175
176    /// Returns a copy of the wrapped digest.
177    ///
178    /// The returned [`Digest`] is `Copy` and will not be zeroed on drop. Do not
179    /// store the return value beyond its immediate use.
180    #[must_use]
181    pub const fn digest(&self) -> Digest {
182        self.0
183    }
184
185    /// Returns the wrapped digest bytes.
186    #[must_use]
187    pub const fn as_bytes(&self) -> &[u8; Digest::LEN] {
188        self.0.as_bytes()
189    }
190
191    /// Compares two wrapped digests without data-dependent early exit.
192    #[must_use]
193    pub fn ct_eq(&self, other: &Self) -> bool {
194        self.0.ct_eq(&other.0)
195    }
196}
197
198impl From<Digest> for ZeroizedDigest {
199    fn from(digest: Digest) -> Self {
200        Self::new(digest)
201    }
202}
203
204impl PartialEq for ZeroizedDigest {
205    fn eq(&self, other: &Self) -> bool {
206        self.ct_eq(other)
207    }
208}
209
210impl Drop for ZeroizedDigest {
211    fn drop(&mut self) {
212        self.0.0.zeroize();
213    }
214}
215
216impl core::fmt::Debug for ZeroizedDigest {
217    fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
218        formatter.write_str("ZeroizedDigest(..)")
219    }
220}
221
222define_identifier!(
223    /// Statement identifier for BCX causal statements.
224    StatementId,
225    Digest::LEN,
226    Digest::LEN
227);
228
229define_identifier!(
230    /// Subject identifier for the thing a statement describes or affects.
231    SubjectId,
232    1,
233    64
234);
235
236define_identifier!(
237    /// Realm identifier for an authority, namespace, tenant, or trust domain.
238    RealmId,
239    1,
240    64
241);
242
243define_identifier!(
244    /// Profile identifier for a BCX profile or native binding family.
245    ProfileId,
246    1,
247    32
248);
249
250define_identifier!(
251    /// Proof-suite identifier for signature or verification policy families.
252    ProofSuiteId,
253    1,
254    32
255);
256
257define_identifier!(
258    /// Policy identifier for disclosure, replay, settlement, or admission rules.
259    PolicyId,
260    1,
261    32
262);
263
264define_identifier!(
265    /// Checkpoint identifier for committed state, graph, or settlement checkpoints.
266    CheckpointId,
267    Digest::LEN,
268    Digest::LEN
269);
270
271define_identifier!(
272    /// Native binding identifier for host-system specific anchors.
273    NativeBindingId,
274    1,
275    64
276);
277
278/// Globally unique event identifier within a trust domain.
279#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
280pub struct EventId(Digest);
281
282impl EventId {
283    /// Creates an event identifier when the digest is non-zero.
284    pub fn new(digest: Digest) -> Result<Self, ValidationError> {
285        if digest.is_zero() {
286            Err(ValidationError::ZeroValue)
287        } else {
288            Ok(Self(digest))
289        }
290    }
291
292    /// Returns the underlying digest commitment.
293    #[must_use]
294    pub const fn digest(&self) -> Digest {
295        self.0
296    }
297}
298
299/// Reference to a capability object or capability commitment.
300#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
301pub struct CapabilityRef(Digest);
302
303impl CapabilityRef {
304    /// Creates a capability reference when the digest is non-zero.
305    pub fn new(digest: Digest) -> Result<Self, ValidationError> {
306        if digest.is_zero() {
307            Err(ValidationError::ZeroValue)
308        } else {
309            Ok(Self(digest))
310        }
311    }
312
313    /// Returns the underlying digest commitment.
314    #[must_use]
315    pub const fn digest(&self) -> Digest {
316        self.0
317    }
318}
319
320/// Reference to a policy epoch.
321#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
322pub struct PolicyEpoch(Digest);
323
324impl PolicyEpoch {
325    /// Creates a policy epoch when the digest is non-zero.
326    pub fn new(digest: Digest) -> Result<Self, ValidationError> {
327        if digest.is_zero() {
328            Err(ValidationError::ZeroValue)
329        } else {
330            Ok(Self(digest))
331        }
332    }
333
334    /// Returns the underlying digest commitment.
335    #[must_use]
336    pub const fn digest(&self) -> Digest {
337        self.0
338    }
339}
340
341/// Per-issuer operation sequence used for replay detection.
342#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
343pub struct OperationSequence(u64);
344
345impl OperationSequence {
346    /// Creates a non-zero operation sequence.
347    pub const fn new(value: u64) -> Result<Self, ValidationError> {
348        if value == 0 {
349            Err(ValidationError::ZeroValue)
350        } else {
351            Ok(Self(value))
352        }
353    }
354
355    /// Returns the raw sequence number.
356    #[must_use]
357    pub const fn get(self) -> u64 {
358        self.0
359    }
360
361    /// Returns the next sequence value.
362    pub const fn next(self) -> Result<Self, ValidationError> {
363        match self.0.checked_add(1) {
364            Some(value) => Ok(Self(value)),
365            None => Err(ValidationError::TooLarge),
366        }
367    }
368
369    /// Returns true when this sequence immediately follows `previous`.
370    #[must_use]
371    pub const fn immediately_follows(self, previous: Self) -> bool {
372        match previous.0.checked_add(1) {
373            Some(expected) => self.0 == expected,
374            None => false,
375        }
376    }
377}
378
379/// Nonce bytes carried by signed invocations and WHY queries.
380///
381/// `Nonce` intentionally does not implement `Clone` or `Copy`; duplicating
382/// nonce bytes increases the number of plaintext copies that must be cleared.
383/// It also intentionally does not implement `Hash`; replay caches should use
384/// a keyed or constant-time structure instead of exposing nonce bytes to a
385/// general-purpose hash table.
386#[derive(Eq)]
387pub struct Nonce([u8; Self::LEN]);
388
389impl Nonce {
390    /// Nonce byte length for the first BCX profile.
391    pub const LEN: usize = 16;
392
393    /// Creates a nonce from non-zero raw bytes.
394    pub fn new(bytes: [u8; Self::LEN]) -> Result<Self, ValidationError> {
395        if bool::from(bytes.ct_eq(&[0u8; Self::LEN])) {
396            Err(ValidationError::ZeroValue)
397        } else {
398            Ok(Self(bytes))
399        }
400    }
401
402    /// Returns the nonce as bytes.
403    #[must_use]
404    pub const fn as_bytes(&self) -> &[u8; Self::LEN] {
405        &self.0
406    }
407
408    /// Compares two nonces without data-dependent early exit.
409    #[must_use]
410    pub fn ct_eq(&self, other: &Self) -> bool {
411        bool::from(self.0.ct_eq(&other.0))
412    }
413}
414
415impl PartialEq for Nonce {
416    fn eq(&self, other: &Self) -> bool {
417        self.ct_eq(other)
418    }
419}
420
421impl Drop for Nonce {
422    fn drop(&mut self) {
423        self.0.zeroize();
424    }
425}
426
427impl core::fmt::Debug for Nonce {
428    fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
429        formatter.write_str("Nonce(..)")
430    }
431}