Skip to main content

bcx_core/
lib.rs

1#![no_std]
2#![doc = "Core identifiers and validation primitives for BCX."]
3
4#[cfg(test)]
5extern crate std;
6
7mod error;
8mod ids;
9
10pub use error::ValidationError;
11pub use ids::{
12    CapabilityRef, CheckpointId, Digest, EventId, NativeBindingId, Nonce, OperationSequence,
13    PolicyEpoch, PolicyId, ProfileId, ProofSuiteId, RealmId, StatementId, SubjectId,
14    ZeroizedDigest,
15};
16
17#[cfg(test)]
18mod tests {
19    use super::*;
20    use std::{format, string::String};
21
22    #[test]
23    fn zero_digest_is_detected() {
24        let digest = Digest::new([0; Digest::LEN]);
25        assert!(digest.is_zero());
26    }
27
28    #[test]
29    fn digest_constant_shape_equality_matches_structural_equality() {
30        let left = Digest::new([7; Digest::LEN]);
31        let same = Digest::new([7; Digest::LEN]);
32        let different = Digest::new([8; Digest::LEN]);
33
34        assert!(left.ct_eq(&same));
35        assert!(!left.ct_eq(&different));
36    }
37
38    #[test]
39    fn zeroized_digest_exposes_redacted_boundary_wrapper() {
40        let wrapped = ZeroizedDigest::new(Digest::new([7; Digest::LEN]));
41        let same = ZeroizedDigest::new(Digest::new([7; Digest::LEN]));
42
43        assert_eq!(wrapped.as_bytes(), &[7; Digest::LEN]);
44        assert_eq!(wrapped.digest(), Digest::new([7; Digest::LEN]));
45        assert!(wrapped.ct_eq(&same));
46        assert_eq!(format!("{wrapped:?}"), String::from("ZeroizedDigest(..)"));
47    }
48
49    #[test]
50    fn nonce_debug_is_redacted() {
51        let nonce = Nonce::new([3; Nonce::LEN]);
52
53        assert_eq!(
54            nonce.map(|value| format!("{value:?}")),
55            Ok(String::from("Nonce(..)"))
56        );
57    }
58
59    #[test]
60    fn nonce_rejects_zero_and_compares_constant_shape() {
61        let left = Nonce::new([9; Nonce::LEN]);
62        let same = Nonce::new([9; Nonce::LEN]);
63        let different = Nonce::new([8; Nonce::LEN]);
64
65        assert_eq!(Nonce::new([0; Nonce::LEN]), Err(ValidationError::ZeroValue));
66        assert!(matches!((&left, &same), (Ok(a), Ok(b)) if a.ct_eq(b)));
67        assert!(matches!((&left, &different), (Ok(a), Ok(b)) if !a.ct_eq(b)));
68    }
69
70    #[test]
71    fn operation_sequence_rejects_zero() {
72        assert_eq!(OperationSequence::new(0), Err(ValidationError::ZeroValue));
73        assert_eq!(OperationSequence::new(7).map(OperationSequence::get), Ok(7));
74    }
75
76    #[test]
77    fn operation_sequence_checks_monotonic_successor() {
78        let previous = OperationSequence::new(7);
79        let next = previous.and_then(OperationSequence::next);
80
81        assert_eq!(next.map(OperationSequence::get), Ok(8));
82        assert!(matches!(
83            (OperationSequence::new(8), OperationSequence::new(7)),
84            (Ok(current), Ok(previous)) if current.immediately_follows(previous)
85        ));
86        assert_eq!(
87            OperationSequence::new(u64::MAX).and_then(OperationSequence::next),
88            Err(ValidationError::TooLarge)
89        );
90    }
91
92    #[test]
93    fn statement_and_checkpoint_ids_require_digest_length() {
94        assert_eq!(StatementId::new(&[]), Err(ValidationError::Empty));
95        assert_eq!(
96            StatementId::new(&[1; Digest::LEN - 1]),
97            Err(ValidationError::Malformed)
98        );
99        assert_eq!(
100            CheckpointId::new(&[1; Digest::LEN + 1]),
101            Err(ValidationError::TooLarge)
102        );
103
104        let statement = StatementId::new(&[1; Digest::LEN]);
105        assert_eq!(statement.map(|id| id.len()), Ok(Digest::LEN));
106    }
107
108    #[test]
109    fn public_identifier_constructors_reject_zero_values() {
110        assert_eq!(
111            StatementId::new(&[0; Digest::LEN]),
112            Err(ValidationError::ZeroValue)
113        );
114        assert_eq!(SubjectId::new(&[0]), Err(ValidationError::ZeroValue));
115        assert_eq!(RealmId::new(&[0]), Err(ValidationError::ZeroValue));
116        assert_eq!(ProfileId::new(&[0]), Err(ValidationError::ZeroValue));
117        assert_eq!(ProofSuiteId::new(&[0]), Err(ValidationError::ZeroValue));
118        assert_eq!(PolicyId::new(&[0]), Err(ValidationError::ZeroValue));
119        assert_eq!(
120            CheckpointId::new(&[0; Digest::LEN]),
121            Err(ValidationError::ZeroValue)
122        );
123        assert_eq!(NativeBindingId::new(&[0]), Err(ValidationError::ZeroValue));
124    }
125
126    #[test]
127    fn bounded_identifiers_validate_lengths_and_preserve_bytes() -> Result<(), ValidationError> {
128        let subject = SubjectId::new(b"subject:invoice:123")?;
129        let realm = RealmId::new(b"realm:valkyoth")?;
130        let profile = ProfileId::new(b"bcx-core")?;
131        let proof_suite = ProofSuiteId::new(b"ed25519")?;
132        let policy = PolicyId::new(b"strict")?;
133        let native = NativeBindingId::new(b"fluxheim/request")?;
134
135        assert_eq!(subject.as_bytes(), b"subject:invoice:123");
136        assert_eq!(realm.as_bytes(), b"realm:valkyoth");
137        assert_eq!(profile.as_bytes(), b"bcx-core");
138        assert_eq!(proof_suite.as_bytes(), b"ed25519");
139        assert_eq!(policy.as_bytes(), b"strict");
140        assert_eq!(native.as_bytes(), b"fluxheim/request");
141        assert_eq!(
142            SubjectId::new(&[1; SubjectId::MAX_LEN + 1]),
143            Err(ValidationError::TooLarge)
144        );
145        Ok(())
146    }
147}