1use crate::Context;
19use crate::Error;
20use crate::ProvideCredential;
21use crate::ProvideCredentialDyn;
22use crate::Result;
23use crate::SignRequest;
24use crate::SignRequestDyn;
25use crate::SigningCredential;
26use std::any::type_name;
27use std::sync::{Arc, Mutex};
28use std::time::Duration;
29
30#[derive(Clone, Debug)]
35pub struct Signer<K: SigningCredential> {
36 ctx: Context,
37 loader: Arc<dyn ProvideCredentialDyn<Credential = K>>,
38 builder: Arc<dyn SignRequestDyn<Credential = K>>,
39 credential: Arc<Mutex<Option<K>>>,
40}
41
42impl<K: SigningCredential> Signer<K> {
43 pub fn new(
45 ctx: Context,
46 loader: impl ProvideCredential<Credential = K>,
47 builder: impl SignRequest<Credential = K>,
48 ) -> Self {
49 Self {
50 ctx,
51
52 loader: Arc::new(loader),
53 builder: Arc::new(builder),
54 credential: Arc::new(Mutex::new(None)),
55 }
56 }
57
58 pub fn with_context(mut self, ctx: Context) -> Self {
60 self.ctx = ctx;
61 self
62 }
63
64 pub fn with_credential_provider(
66 mut self,
67 provider: impl ProvideCredential<Credential = K>,
68 ) -> Self {
69 self.loader = Arc::new(provider);
70 self.credential = Arc::new(Mutex::new(None)); self
72 }
73
74 pub fn with_request_signer(mut self, signer: impl SignRequest<Credential = K>) -> Self {
76 self.builder = Arc::new(signer);
77 self
78 }
79
80 pub async fn sign(
100 &self,
101 req: &mut http::request::Parts,
102 expires_in: Option<Duration>,
103 ) -> Result<()> {
104 let credential = self.credential.lock().expect("lock poisoned").clone();
105 let credential = match credential {
106 Some(credential)
107 if credential.is_valid()
108 && credential.is_valid_at(
109 self.builder
110 .required_valid_until_dyn(&credential, expires_in),
111 ) =>
112 {
113 credential
114 }
115 _ => {
116 let credential = self
117 .loader
118 .provide_credential_dyn(&self.ctx)
119 .await?
120 .ok_or_else(|| {
121 Error::credential_invalid("failed to load signing credential")
122 .with_context(format!("credential_type: {}", type_name::<K>()))
123 })?;
124
125 *self.credential.lock().expect("lock poisoned") = Some(credential.clone());
126
127 let required_until = self
128 .builder
129 .required_valid_until_dyn(&credential, expires_in);
130 if !credential.is_valid_at(required_until) {
131 return Err(Error::credential_invalid(
132 "refreshed signing credential expires before the requested operation deadline",
133 )
134 .with_context(format!("credential_type: {}", type_name::<K>()))
135 .with_context(format!("required_valid_until: {required_until}")));
136 }
137
138 credential
139 }
140 };
141
142 let mut candidate = req.clone();
143 self.builder
144 .sign_request_dyn(&self.ctx, &mut candidate, Some(&credential), expires_in)
145 .await?;
146
147 req.uri = candidate.uri;
148 req.headers = candidate.headers;
149 Ok(())
150 }
151}
152
153#[cfg(test)]
154mod tests {
155 use super::*;
156 use crate::time::Timestamp;
157 use crate::{ErrorKind, ProvideCredential, SignRequest};
158 use http::{HeaderValue, Method, Request, Version};
159 use std::collections::VecDeque;
160 use std::sync::atomic::{AtomicUsize, Ordering};
161
162 #[derive(Clone, Debug)]
163 struct TestCredential;
164
165 impl SigningCredential for TestCredential {
166 fn is_valid(&self) -> bool {
167 true
168 }
169 }
170
171 #[derive(Debug)]
172 struct StaticProvider;
173
174 impl ProvideCredential for StaticProvider {
175 type Credential = TestCredential;
176
177 async fn provide_credential(&self, _ctx: &Context) -> Result<Option<Self::Credential>> {
178 Ok(Some(TestCredential))
179 }
180 }
181
182 #[derive(Clone, Debug, PartialEq, Eq)]
183 struct Extension(&'static str);
184
185 #[derive(Debug)]
186 struct MutatingSigner {
187 fail: bool,
188 }
189
190 impl SignRequest for MutatingSigner {
191 type Credential = TestCredential;
192
193 async fn sign_request(
194 &self,
195 _ctx: &Context,
196 req: &mut http::request::Parts,
197 _credential: Option<&Self::Credential>,
198 _expires_in: Option<Duration>,
199 ) -> Result<()> {
200 req.method = Method::POST;
201 req.uri = "https://signed.example.com/result?auth=1"
202 .parse()
203 .expect("URI must parse");
204 req.version = Version::HTTP_2;
205 req.headers.clear();
206 req.headers
207 .insert("authorization", HeaderValue::from_static("signed"));
208 req.extensions.insert(Extension("candidate"));
209
210 if self.fail {
211 Err(Error::unexpected("injected signing failure"))
212 } else {
213 Ok(())
214 }
215 }
216 }
217
218 #[derive(Clone, Debug)]
219 struct ExpiringCredential {
220 generation: u8,
221 fresh: bool,
222 expires_at: Timestamp,
223 required_until: Timestamp,
224 }
225
226 impl SigningCredential for ExpiringCredential {
227 fn is_valid(&self) -> bool {
228 self.fresh
229 }
230
231 fn is_valid_at(&self, timestamp: Timestamp) -> bool {
232 self.expires_at > timestamp
233 }
234 }
235
236 #[derive(Debug)]
237 struct SequenceProvider {
238 responses: Mutex<VecDeque<Result<Option<ExpiringCredential>>>>,
239 calls: Arc<AtomicUsize>,
240 }
241
242 impl SequenceProvider {
243 fn new(
244 responses: impl IntoIterator<Item = Result<Option<ExpiringCredential>>>,
245 ) -> (Self, Arc<AtomicUsize>) {
246 let calls = Arc::new(AtomicUsize::new(0));
247 (
248 Self {
249 responses: Mutex::new(responses.into_iter().collect()),
250 calls: calls.clone(),
251 },
252 calls,
253 )
254 }
255 }
256
257 impl ProvideCredential for SequenceProvider {
258 type Credential = ExpiringCredential;
259
260 async fn provide_credential(&self, _ctx: &Context) -> Result<Option<Self::Credential>> {
261 self.calls.fetch_add(1, Ordering::SeqCst);
262 self.responses
263 .lock()
264 .expect("lock poisoned")
265 .pop_front()
266 .unwrap_or(Ok(None))
267 }
268 }
269
270 #[derive(Debug)]
271 struct OperationSigner;
272
273 impl SignRequest for OperationSigner {
274 type Credential = ExpiringCredential;
275
276 fn required_valid_until(
277 &self,
278 credential: &Self::Credential,
279 _expires_in: Option<Duration>,
280 ) -> Timestamp {
281 credential.required_until
282 }
283
284 async fn sign_request(
285 &self,
286 _ctx: &Context,
287 req: &mut http::request::Parts,
288 credential: Option<&Self::Credential>,
289 expires_in: Option<Duration>,
290 ) -> Result<()> {
291 let credential = credential.expect("credential must be present");
292 if !credential.is_valid_at(self.required_valid_until(credential, expires_in)) {
293 return Err(Error::credential_invalid(
294 "credential is not valid for operation",
295 ));
296 }
297 req.headers.insert(
298 "x-credential-generation",
299 credential.generation.to_string().parse()?,
300 );
301 Ok(())
302 }
303 }
304
305 fn request_parts() -> http::request::Parts {
306 let mut parts = Request::get("https://example.com/original?x=%2F")
307 .version(Version::HTTP_11)
308 .header("x-original", "value")
309 .body(())
310 .expect("request must build")
311 .into_parts()
312 .0;
313 parts.extensions.insert(Extension("caller"));
314 parts
315 }
316
317 #[test]
318 fn failure_leaves_entire_request_head_unchanged() {
319 let signer = Signer::new(
320 Context::new(),
321 StaticProvider,
322 MutatingSigner { fail: true },
323 );
324 let mut parts = request_parts();
325 let original = parts.clone();
326
327 let result = futures::executor::block_on(signer.sign(&mut parts, None));
328
329 assert!(result.is_err());
330 assert_eq!(parts.method, original.method);
331 assert_eq!(parts.uri, original.uri);
332 assert_eq!(parts.version, original.version);
333 assert_eq!(parts.headers, original.headers);
334 assert_eq!(
335 parts.extensions.get::<Extension>(),
336 original.extensions.get::<Extension>()
337 );
338 }
339
340 #[test]
341 fn success_commits_only_uri_and_headers() {
342 let signer = Signer::new(
343 Context::new(),
344 StaticProvider,
345 MutatingSigner { fail: false },
346 );
347 let mut parts = request_parts();
348 let original = parts.clone();
349
350 futures::executor::block_on(signer.sign(&mut parts, None)).expect("signing must succeed");
351
352 assert_eq!(parts.method, original.method);
353 assert_eq!(parts.version, original.version);
354 assert_eq!(
355 parts.extensions.get::<Extension>(),
356 original.extensions.get::<Extension>()
357 );
358 assert_eq!(
359 parts.uri,
360 "https://signed.example.com/result?auth=1"
361 .parse::<http::Uri>()
362 .expect("URI must parse")
363 );
364 assert_eq!(
365 parts.headers.get("authorization"),
366 Some(&HeaderValue::from_static("signed"))
367 );
368 assert!(!parts.headers.contains_key("x-original"));
369 }
370
371 #[test]
372 fn refreshes_cached_credential_for_operation_requirement() {
373 let base = Timestamp::from_second(1_000).expect("timestamp must be valid");
374 let cached = ExpiringCredential {
375 generation: 1,
376 fresh: true,
377 expires_at: base + Duration::from_secs(20),
378 required_until: base + Duration::from_secs(30),
379 };
380 let refreshed = ExpiringCredential {
381 generation: 2,
382 fresh: true,
383 expires_at: base + Duration::from_secs(20),
384 required_until: base + Duration::from_secs(10),
385 };
386 let (provider, calls) = SequenceProvider::new([Ok(Some(refreshed))]);
387 let signer = Signer::new(Context::new(), provider, OperationSigner);
388 *signer.credential.lock().expect("lock poisoned") = Some(cached);
389
390 let mut parts = request_parts();
391 futures::executor::block_on(signer.sign(&mut parts, None))
392 .expect("refreshed credential must satisfy the recomputed requirement");
393
394 assert_eq!(calls.load(Ordering::SeqCst), 1);
395 assert_eq!(
396 parts.headers.get("x-credential-generation"),
397 Some(&HeaderValue::from_static("2"))
398 );
399 }
400
401 #[test]
402 fn uses_refreshed_credential_that_is_usable_but_not_fresh() {
403 let base = Timestamp::from_second(2_000).expect("timestamp must be valid");
404 let credential = ExpiringCredential {
405 generation: 1,
406 fresh: false,
407 expires_at: base + Duration::from_secs(30),
408 required_until: base + Duration::from_secs(10),
409 };
410 let (provider, calls) =
411 SequenceProvider::new([Ok(Some(credential.clone())), Ok(Some(credential))]);
412 let signer = Signer::new(Context::new(), provider, OperationSigner);
413
414 for _ in 0..2 {
415 let mut parts = request_parts();
416 futures::executor::block_on(signer.sign(&mut parts, None))
417 .expect("usable refreshed credential must be accepted");
418 }
419
420 assert_eq!(calls.load(Ordering::SeqCst), 2);
421 }
422
423 #[test]
424 fn refresh_error_does_not_fall_back_and_caller_can_retry() {
425 let base = Timestamp::from_second(3_000).expect("timestamp must be valid");
426 let cached = ExpiringCredential {
427 generation: 1,
428 fresh: false,
429 expires_at: base + Duration::from_secs(30),
430 required_until: base + Duration::from_secs(10),
431 };
432 let refreshed = ExpiringCredential {
433 generation: 2,
434 fresh: true,
435 expires_at: base + Duration::from_secs(30),
436 required_until: base + Duration::from_secs(10),
437 };
438 let (provider, calls) = SequenceProvider::new([
439 Err(Error::unexpected("injected refresh failure")),
440 Ok(Some(refreshed)),
441 ]);
442 let signer = Signer::new(Context::new(), provider, OperationSigner);
443 *signer.credential.lock().expect("lock poisoned") = Some(cached);
444
445 let mut parts = request_parts();
446 let original = parts.clone();
447 let err = futures::executor::block_on(signer.sign(&mut parts, None))
448 .expect_err("refresh error must be returned");
449 assert_eq!(err.kind(), ErrorKind::Unexpected);
450 assert_eq!(parts.uri, original.uri);
451 assert_eq!(parts.headers, original.headers);
452 assert_eq!(calls.load(Ordering::SeqCst), 1);
453
454 futures::executor::block_on(signer.sign(&mut parts, None))
455 .expect("caller retry must attempt refresh again");
456 assert_eq!(calls.load(Ordering::SeqCst), 2);
457 assert_eq!(
458 parts.headers.get("x-credential-generation"),
459 Some(&HeaderValue::from_static("2"))
460 );
461 }
462
463 #[test]
464 fn missing_refresh_does_not_fall_back_and_caller_can_retry() {
465 let base = Timestamp::from_second(4_000).expect("timestamp must be valid");
466 let cached = ExpiringCredential {
467 generation: 1,
468 fresh: false,
469 expires_at: base + Duration::from_secs(30),
470 required_until: base + Duration::from_secs(10),
471 };
472 let refreshed = ExpiringCredential {
473 generation: 2,
474 fresh: true,
475 expires_at: base + Duration::from_secs(30),
476 required_until: base + Duration::from_secs(10),
477 };
478 let (provider, calls) = SequenceProvider::new([Ok(None), Ok(Some(refreshed))]);
479 let signer = Signer::new(Context::new(), provider, OperationSigner);
480 *signer.credential.lock().expect("lock poisoned") = Some(cached);
481 let mut parts = request_parts();
482 let original = parts.clone();
483
484 let err = futures::executor::block_on(signer.sign(&mut parts, None))
485 .expect_err("missing credential must fail");
486
487 assert_eq!(err.kind(), ErrorKind::CredentialInvalid);
488 assert_eq!(calls.load(Ordering::SeqCst), 1);
489 assert_eq!(parts.uri, original.uri);
490 assert_eq!(parts.headers, original.headers);
491
492 futures::executor::block_on(signer.sign(&mut parts, None))
493 .expect("caller retry must attempt refresh again");
494 assert_eq!(calls.load(Ordering::SeqCst), 2);
495 assert_eq!(
496 parts.headers.get("x-credential-generation"),
497 Some(&HeaderValue::from_static("2"))
498 );
499 }
500}