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}