Skip to main content

reqsign_core/
signer.rs

1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements.  See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership.  The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License.  You may obtain a copy of the License at
8//
9//   http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied.  See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18use 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/// Loads credentials and atomically signs request heads.
31///
32/// The service-specific [`SignRequest`] runs against a private candidate. Only the
33/// candidate URI and headers are committed after successful signing.
34#[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    /// Create a new signer.
44    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    /// Replace the context while keeping credential provider and request signer.
59    pub fn with_context(mut self, ctx: Context) -> Self {
60        self.ctx = ctx;
61        self
62    }
63
64    /// Replace the credential provider while keeping context and request signer.
65    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)); // Clear cached credential
71        self
72    }
73
74    /// Replace the request signer while keeping context and credential provider.
75    pub fn with_request_signer(mut self, signer: impl SignRequest<Credential = K>) -> Self {
76        self.builder = Arc::new(signer);
77        self
78    }
79
80    /// Sign a wire-ready request head.
81    ///
82    /// The request URI must satisfy the input contract of the configured
83    /// [`SignRequest`]. Built-in signers require an authority and expect path and query
84    /// data to be percent-encoded exactly once before this call. Signing does not
85    /// perform general-purpose URI encoding for the caller.
86    ///
87    /// If credential loading or request signing returns an error, `req` is unchanged.
88    /// On success, only `req.uri` and `req.headers` may change; the method, version, and
89    /// extensions retain their input values.
90    ///
91    /// `expires_in` is a service-specific validity input and does not universally
92    /// select query authentication. The configured service signer and credential type
93    /// determine how it is interpreted.
94    ///
95    /// Cached credentials must be fresh according to [`SigningCredential::is_valid`]
96    /// and usable through [`SignRequest::required_valid_until`]. A refreshed credential
97    /// only needs to satisfy the exact operation deadline. Provider errors are returned
98    /// without internal retry or fallback to the previous cached credential.
99    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}