termivar_scanner/payload_strategies/
api_authorization_context_pair.rs1use crate::payload_strategy::{
20 PayloadArtifact, PayloadSeed, PayloadStrategy, PayloadStrategyError, PayloadStrategyLimits,
21 PayloadStrategyRef, PayloadVariantRole,
22};
23
24pub const API_AUTHORIZATION_CONTEXT_PAIR_ID: &str = "api.authorization.context-pair";
26
27pub const API_AUTHORIZATION_CONTEXT_PAIR_REVISION: u32 = 1;
29
30pub const API_AUTHORIZATION_CONTEXT_PAIR_HEADER_NAME: &str = "authorization";
33
34#[derive(Debug, Clone)]
36pub struct ApiAuthorizationContextPairStrategy {
37 reference: PayloadStrategyRef,
38}
39
40impl ApiAuthorizationContextPairStrategy {
41 pub fn new() -> Self {
47 let reference = PayloadStrategyRef::new(
48 API_AUTHORIZATION_CONTEXT_PAIR_ID,
49 API_AUTHORIZATION_CONTEXT_PAIR_REVISION,
50 )
51 .expect("api.authorization.context-pair@1 is a valid strategy reference");
52 Self { reference }
53 }
54
55 fn is_safe_header_value(bytes: &[u8]) -> bool {
62 if bytes.is_empty() {
63 return false;
64 }
65 if bytes.first() == Some(&b' ') || bytes.last() == Some(&b' ') {
66 return false;
67 }
68 bytes
69 .iter()
70 .all(|&byte| byte == b' ' || (0x21..=0x7e).contains(&byte))
71 }
72}
73
74impl Default for ApiAuthorizationContextPairStrategy {
75 fn default() -> Self {
76 Self::new()
77 }
78}
79
80impl PayloadStrategy for ApiAuthorizationContextPairStrategy {
81 fn strategy_ref(&self) -> &PayloadStrategyRef {
82 &self.reference
83 }
84
85 fn derive_one(
86 &self,
87 role: PayloadVariantRole,
88 seed: &PayloadSeed,
89 limits: PayloadStrategyLimits,
90 ) -> Result<PayloadArtifact, PayloadStrategyError> {
91 if !Self::is_safe_header_value(seed.as_bytes()) {
94 return Err(PayloadStrategyError::DerivationFailed);
95 }
96
97 let bytes = match role {
98 PayloadVariantRole::Control => Vec::new(),
100 PayloadVariantRole::Candidate => seed.as_bytes().to_vec(),
102 };
103
104 PayloadArtifact::new(self.reference.clone(), role, bytes, limits)
105 }
106}
107
108#[cfg(test)]
109mod tests {
110 use std::{sync::Arc, thread};
111
112 use super::*;
113 use crate::payload_strategy::PayloadStrategyRegistry;
114
115 fn limits() -> PayloadStrategyLimits {
116 PayloadStrategyLimits::default()
117 }
118
119 fn seed(value: &[u8]) -> PayloadSeed {
120 PayloadSeed::new(value.to_vec(), limits()).unwrap()
121 }
122
123 #[test]
124 fn reference_is_stable_and_versioned() {
125 let strategy = ApiAuthorizationContextPairStrategy::new();
126 assert_eq!(
127 strategy.strategy_ref().id(),
128 API_AUTHORIZATION_CONTEXT_PAIR_ID
129 );
130 assert_eq!(
131 strategy.strategy_ref().revision(),
132 API_AUTHORIZATION_CONTEXT_PAIR_REVISION
133 );
134 assert_eq!(
135 strategy.strategy_ref().to_string(),
136 "api.authorization.context-pair@1"
137 );
138 }
139
140 #[test]
141 fn control_is_anonymous_and_candidate_is_the_credential() {
142 let strategy = ApiAuthorizationContextPairStrategy::new();
143
144 let control = strategy
145 .derive_one(
146 PayloadVariantRole::Control,
147 &seed(b"Bearer token"),
148 limits(),
149 )
150 .unwrap();
151 assert!(
152 control.as_bytes().is_empty(),
153 "control leg must be an empty (anonymous) artifact"
154 );
155
156 let candidate = strategy
157 .derive_one(
158 PayloadVariantRole::Candidate,
159 &seed(b"Bearer token"),
160 limits(),
161 )
162 .unwrap();
163 assert_eq!(candidate.as_bytes(), b"Bearer token");
164 assert_ne!(control.receipt().sha256(), candidate.receipt().sha256());
165 }
166
167 #[test]
168 fn control_is_seed_independent_but_still_requires_a_valid_seed() {
169 let strategy = ApiAuthorizationContextPairStrategy::new();
170 let first = strategy
171 .derive_one(PayloadVariantRole::Control, &seed(b"Bearer one"), limits())
172 .unwrap();
173 let second = strategy
174 .derive_one(PayloadVariantRole::Control, &seed(b"Basic two"), limits())
175 .unwrap();
176 assert!(first.as_bytes().is_empty());
177 assert_eq!(first.as_bytes(), second.as_bytes());
178 }
179
180 #[test]
181 fn same_input_is_deterministic_across_repeats() {
182 let strategy = ApiAuthorizationContextPairStrategy::new();
183 let first = strategy
184 .derive_one(PayloadVariantRole::Candidate, &seed(b"Bearer t"), limits())
185 .unwrap();
186 let second = strategy
187 .derive_one(PayloadVariantRole::Candidate, &seed(b"Bearer t"), limits())
188 .unwrap();
189 assert_eq!(first, second);
190 assert_eq!(first.receipt(), second.receipt());
191 }
192
193 #[test]
194 fn derivation_is_deterministic_under_concurrency() {
195 let strategy = Arc::new(ApiAuthorizationContextPairStrategy::new());
196 let expected = strategy
197 .derive_one(
198 PayloadVariantRole::Candidate,
199 &seed(b"Bearer probe"),
200 limits(),
201 )
202 .unwrap();
203
204 let handles: Vec<_> = (0..8)
205 .map(|_| {
206 let strategy = Arc::clone(&strategy);
207 thread::spawn(move || {
208 strategy
209 .derive_one(
210 PayloadVariantRole::Candidate,
211 &seed(b"Bearer probe"),
212 limits(),
213 )
214 .unwrap()
215 .receipt()
216 })
217 })
218 .collect();
219
220 for handle in handles {
221 assert_eq!(handle.join().unwrap(), expected.receipt());
222 }
223 }
224
225 #[test]
226 fn unsafe_seeds_fail_closed_on_both_legs() {
227 let strategy = ApiAuthorizationContextPairStrategy::new();
228 for unsafe_seed in [
229 b"".as_slice(),
230 b" leading",
231 b"trailing ",
232 b"line\nbreak",
233 b"carriage\rreturn",
234 b"tab\ttoken",
235 b"null\0byte",
236 ] {
237 let seed = PayloadSeed::new(unsafe_seed.to_vec(), limits()).unwrap();
238 for role in [PayloadVariantRole::Control, PayloadVariantRole::Candidate] {
239 assert!(
240 matches!(
241 strategy.derive_one(role, &seed, limits()),
242 Err(PayloadStrategyError::DerivationFailed)
243 ),
244 "expected fail-closed derivation for seed {unsafe_seed:?} role {role:?}"
245 );
246 }
247 }
248 }
249
250 #[test]
251 fn oversized_candidate_value_fails_closed() {
252 let strategy = ApiAuthorizationContextPairStrategy::new();
253 let tight = PayloadStrategyLimits::new(64, 4).unwrap();
254 let seed = PayloadSeed::new(b"Bearer token".to_vec(), tight).unwrap();
255 assert!(matches!(
256 strategy.derive_one(PayloadVariantRole::Candidate, &seed, tight),
257 Err(PayloadStrategyError::ArtifactTooLarge { .. })
258 ));
259 }
260
261 #[test]
262 fn resolves_and_revalidates_through_registry() {
263 let strategy = ApiAuthorizationContextPairStrategy::new();
264 let reference = strategy.strategy_ref().clone();
265 let mut registry = PayloadStrategyRegistry::new();
266 registry.register(Arc::new(strategy)).unwrap();
267
268 let control = registry
269 .derive_one(
270 &reference,
271 PayloadVariantRole::Control,
272 &seed(b"Bearer token"),
273 limits(),
274 )
275 .unwrap();
276 let candidate = registry
277 .derive_one(
278 &reference,
279 PayloadVariantRole::Candidate,
280 &seed(b"Bearer token"),
281 limits(),
282 )
283 .unwrap();
284 assert!(control.as_bytes().is_empty());
285 assert_eq!(candidate.as_bytes(), b"Bearer token");
286 }
287
288 #[test]
289 fn raw_seed_never_enters_receipt_json() {
290 let strategy = ApiAuthorizationContextPairStrategy::new();
291 let artifact = strategy
292 .derive_one(
293 PayloadVariantRole::Candidate,
294 &seed(b"Bearer secret-token"),
295 limits(),
296 )
297 .unwrap();
298 let receipt_json = serde_json::to_string(&artifact.receipt()).unwrap();
299 let debug = format!("{artifact:?}");
300 assert!(!receipt_json.contains("secret-token"));
301 assert!(!debug.contains("secret-token"));
302 assert_eq!(artifact.receipt().sha256().len(), 64);
303 }
304}