zerodds_security_runtime/
anti_squatter.rs1use alloc::collections::BTreeMap;
32use alloc::vec::Vec;
33
34type IdentityFingerprint = Vec<u8>;
40
41pub type GuidPrefixBytes = [u8; 12];
43
44#[derive(Debug, Default)]
46pub struct IdentityBindingCache {
47 bindings: BTreeMap<GuidPrefixBytes, IdentityFingerprint>,
48 capacity: usize,
49}
50
51#[derive(Debug, Clone, PartialEq, Eq)]
53pub enum BindingDecision {
54 NewBinding,
56 Reaffirmed,
58 SquatterRejected {
61 previous: Vec<u8>,
63 },
64 CapacityExhausted,
67}
68
69impl IdentityBindingCache {
70 #[must_use]
72 pub fn new() -> Self {
73 Self {
74 bindings: BTreeMap::new(),
75 capacity: usize::MAX,
76 }
77 }
78
79 #[must_use]
83 pub fn with_capacity(capacity: usize) -> Self {
84 Self {
85 bindings: BTreeMap::new(),
86 capacity,
87 }
88 }
89
90 #[must_use]
92 pub fn len(&self) -> usize {
93 self.bindings.len()
94 }
95
96 #[must_use]
98 pub fn is_empty(&self) -> bool {
99 self.bindings.is_empty()
100 }
101
102 pub fn observe(
106 &mut self,
107 guid_prefix: GuidPrefixBytes,
108 identity_token_bytes: &[u8],
109 ) -> BindingDecision {
110 if let Some(existing) = self.bindings.get(&guid_prefix) {
111 return if existing.as_slice() == identity_token_bytes {
112 BindingDecision::Reaffirmed
113 } else {
114 BindingDecision::SquatterRejected {
115 previous: existing.clone(),
116 }
117 };
118 }
119 if self.bindings.len() >= self.capacity {
120 return BindingDecision::CapacityExhausted;
121 }
122 self.bindings
123 .insert(guid_prefix, identity_token_bytes.to_vec());
124 BindingDecision::NewBinding
125 }
126
127 pub fn evict(&mut self, guid_prefix: &GuidPrefixBytes) -> bool {
131 self.bindings.remove(guid_prefix).is_some()
132 }
133
134 #[must_use]
137 pub fn fingerprint_for(&self, guid_prefix: &GuidPrefixBytes) -> Option<&[u8]> {
138 self.bindings.get(guid_prefix).map(Vec::as_slice)
139 }
140}
141
142#[cfg(test)]
143#[allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
144mod tests {
145 use super::*;
146
147 fn px(byte: u8) -> GuidPrefixBytes {
148 [byte; 12]
149 }
150
151 #[test]
152 fn first_observe_is_new_binding() {
153 let mut c = IdentityBindingCache::new();
154 let d = c.observe(px(0xAA), b"identity-token-A");
155 assert_eq!(d, BindingDecision::NewBinding);
156 assert_eq!(c.len(), 1);
157 }
158
159 #[test]
160 fn second_observe_with_same_token_is_reaffirmed() {
161 let mut c = IdentityBindingCache::new();
162 c.observe(px(0xAA), b"identity-token-A");
163 let d = c.observe(px(0xAA), b"identity-token-A");
164 assert_eq!(d, BindingDecision::Reaffirmed);
165 assert_eq!(c.len(), 1);
166 }
167
168 #[test]
169 fn squatter_with_diff_token_is_rejected() {
170 let mut c = IdentityBindingCache::new();
171 c.observe(px(0xAA), b"identity-token-A");
172 let d = c.observe(px(0xAA), b"identity-token-B");
173 match d {
174 BindingDecision::SquatterRejected { previous } => {
175 assert_eq!(previous, b"identity-token-A");
176 }
177 other => panic!("expected SquatterRejected, got {other:?}"),
178 }
179 assert_eq!(c.fingerprint_for(&px(0xAA)), Some(&b"identity-token-A"[..]));
181 }
182
183 #[test]
184 fn distinct_prefixes_are_independent() {
185 let mut c = IdentityBindingCache::new();
186 assert_eq!(
187 c.observe(px(0xAA), b"alice-token"),
188 BindingDecision::NewBinding
189 );
190 assert_eq!(
191 c.observe(px(0xBB), b"bob-token"),
192 BindingDecision::NewBinding
193 );
194 assert_eq!(c.len(), 2);
195 }
196
197 #[test]
198 fn evict_allows_rebinding_with_new_token() {
199 let mut c = IdentityBindingCache::new();
200 c.observe(px(0xAA), b"old-token");
201 assert!(c.evict(&px(0xAA)));
202 let d = c.observe(px(0xAA), b"new-token");
203 assert_eq!(d, BindingDecision::NewBinding);
204 }
205
206 #[test]
207 fn evict_unknown_prefix_returns_false() {
208 let mut c = IdentityBindingCache::new();
209 assert!(!c.evict(&px(0xCC)));
210 }
211
212 #[test]
213 fn capacity_cap_rejects_new_bindings() {
214 let mut c = IdentityBindingCache::with_capacity(2);
215 assert_eq!(c.observe(px(0x01), b"a"), BindingDecision::NewBinding);
216 assert_eq!(c.observe(px(0x02), b"b"), BindingDecision::NewBinding);
217 assert_eq!(
218 c.observe(px(0x03), b"c"),
219 BindingDecision::CapacityExhausted
220 );
221 assert_eq!(c.len(), 2);
222 }
223
224 #[test]
225 fn capacity_cap_still_allows_reaffirm() {
226 let mut c = IdentityBindingCache::with_capacity(1);
227 c.observe(px(0x01), b"a");
228 assert_eq!(c.observe(px(0x01), b"a"), BindingDecision::Reaffirmed);
231 }
232
233 #[test]
234 fn capacity_cap_still_detects_squatter() {
235 let mut c = IdentityBindingCache::with_capacity(1);
236 c.observe(px(0x01), b"a");
237 match c.observe(px(0x01), b"b") {
240 BindingDecision::SquatterRejected { .. } => {}
241 other => panic!("expected SquatterRejected, got {other:?}"),
242 }
243 }
244
245 #[test]
246 fn empty_token_is_distinct_from_some_token() {
247 let mut c = IdentityBindingCache::new();
248 c.observe(px(0x01), b"");
249 let d = c.observe(px(0x01), b"non-empty");
250 assert!(matches!(d, BindingDecision::SquatterRejected { .. }));
251 }
252}