Skip to main content

http_sig/
signing.rs

1use std::convert::TryInto;
2use std::sync::Arc;
3use std::time::SystemTime;
4
5use chrono::Utc;
6use http::header::{HeaderName, HeaderValue, AUTHORIZATION, DATE, HOST};
7use itertools::Itertools;
8use thiserror::Error;
9
10use sha2::Digest;
11
12use crate::algorithm::{HttpDigest, HttpSignatureSign};
13use crate::canonicalize::{CanonicalizeConfig, CanonicalizeError, CanonicalizeExt, RequestLike};
14use crate::header::{Header, PseudoHeader};
15use crate::{DefaultDigestAlgorithm, DefaultSignatureAlgorithm, DATE_FORMAT};
16
17/// This trait is to be implemented for types representing an outgoing
18/// HTTP request. The HTTP signing extension methods are available on
19/// any type implementing this trait.
20pub trait ClientRequestLike: RequestLike {
21    /// Returns the host for the request (eg. "example.com", "127.0.0.1:8080") in case the Host header has
22    /// not been set explicitly. Note, the correct form of the `Host` header is `<host>:<port>` if
23    /// the port is non-standard for the protocol used (e.g., 443 for an HTTPS URL, and 80 for an HTTP URL).
24    /// When implementing this trait, do not just read the `Host` header from the request -
25    /// this method will only be called when the `Host` header is not set.
26    fn host(&self) -> Option<String> {
27        None
28    }
29    /// Add a header to the request. This function may be used to set the `Date` and `Digest`
30    /// headers if not already present depending on the configuration. The `Authorization`
31    /// header will always be set assuming the message was signed successfully.
32    fn set_header(&mut self, header: HeaderName, value: HeaderValue);
33    /// Compute the digest using the provided HTTP digest algorithm. If this is not possible,
34    /// then return `None`. This may require buffering the request data into memory.
35    fn compute_digest(&mut self, digest: &dyn HttpDigest) -> Option<String>;
36}
37
38/// The types of error which may occur whilst signing.
39#[derive(Debug, Error)]
40#[non_exhaustive]
41pub enum SigningError {
42    /// A header required to be part of the signature was not present
43    /// on the request, and the `skip_missing` configuration option
44    /// was disabled.
45    #[error("Failed to canonicalize request")]
46    Canonicalize(#[source] CanonicalizeError),
47
48    /// The signature creation date was in the future
49    #[error("Signature creation date was in the future")]
50    InvalidSignatureCreationDate,
51
52    /// The signature expires date was in the past
53    #[error("Signature expires date was in the past")]
54    InvalidSignatureExpiresDate,
55}
56
57impl From<CanonicalizeError> for SigningError {
58    fn from(other: CanonicalizeError) -> Self {
59        Self::Canonicalize(other)
60    }
61}
62
63#[derive(Debug, Copy, Clone, PartialEq)]
64enum SignatureCreated {
65    Omit,
66    Automatic,
67    Absolute(i64),
68}
69
70impl SignatureCreated {
71    fn get(self, ts: i64) -> Option<i64> {
72        match self {
73            Self::Omit => None,
74            Self::Automatic => Some(ts),
75            Self::Absolute(ts) => Some(ts),
76        }
77    }
78}
79
80#[derive(Debug, Copy, Clone, PartialEq)]
81enum SignatureExpires {
82    Omit,
83    Relative(i64),
84    Absolute(i64),
85}
86
87impl SignatureExpires {
88    fn get(self, ts: i64) -> Option<i64> {
89        match self {
90            Self::Omit => None,
91            Self::Relative(offset) => Some(ts.saturating_add(offset)),
92            Self::Absolute(ts) => Some(ts),
93        }
94    }
95}
96
97/// The configuration used for signing HTTP requests.
98#[derive(Debug, Clone)]
99pub struct SigningConfig {
100    signature: Arc<dyn HttpSignatureSign>,
101    digest: Arc<dyn HttpDigest>,
102    key_id: String,
103    headers: Vec<Header>,
104    compute_digest: bool,
105    add_date: bool,
106    add_host: bool,
107    skip_missing: bool,
108    signature_created: SignatureCreated,
109    signature_expires: SignatureExpires,
110}
111
112impl SigningConfig {
113    /// Creates a new signing configuration using the default signature algorithm, and the
114    /// specified key ID and key.
115    pub fn new_default(key_id: &str, key: &[u8]) -> Self {
116        Self::new(key_id, DefaultSignatureAlgorithm::new(key))
117    }
118
119    /// Creates a new signing configuration using a custom signature algorithm, and the specified
120    /// key ID.
121    pub fn new<SigAlg: HttpSignatureSign>(key_id: &str, signature: SigAlg) -> Self {
122        SigningConfig {
123            signature: Arc::new(signature),
124            digest: Arc::new(DefaultDigestAlgorithm::new()),
125            key_id: key_id.into(),
126            headers: [
127                Header::Pseudo(PseudoHeader::RequestTarget),
128                Header::Normal(HOST),
129                Header::Normal(DATE),
130                Header::Normal(HeaderName::from_static("digest")),
131            ]
132            .to_vec(),
133            compute_digest: true,
134            add_date: true,
135            add_host: true,
136            skip_missing: true,
137            signature_created: SignatureCreated::Omit,
138            signature_expires: SignatureExpires::Omit,
139        }
140    }
141
142    /// Returns the key ID.
143    pub fn key_id(&self) -> &str {
144        self.key_id.as_ref()
145    }
146    /// Returns the HTTP digest algorithm.
147    pub fn digest(&self) -> &dyn HttpDigest {
148        &*self.digest
149    }
150    /// Sets the HTTP digest algorithm (in-place).
151    fn set_digest<DigestAlg: HttpDigest>(&mut self, digest: DigestAlg) -> &mut Self {
152        self.digest = Arc::new(digest);
153        self
154    }
155    /// Sets the HTTP digest algorithm.
156    pub fn with_digest<DigestAlg: HttpDigest>(mut self, digest: DigestAlg) -> Self {
157        self.set_digest(digest);
158        self
159    }
160    /// Returns whether the digest will be automatically computed
161    /// when not already present.
162    ///
163    /// This is set to `true` by default.
164    pub fn compute_digest(&self) -> bool {
165        self.compute_digest
166    }
167    /// Controls whether the digest will be automatically computed
168    /// when not already present (in-place).
169    ///
170    /// This is set to `true` by default.
171    pub fn set_compute_digest(&mut self, compute_digest: bool) -> &mut Self {
172        self.compute_digest = compute_digest;
173        self
174    }
175    /// Controls whether the digest will be automatically computed
176    /// when not already present.
177    ///
178    /// This is set to `true` by default.
179    pub fn with_compute_digest(mut self, compute_digest: bool) -> Self {
180        self.set_compute_digest(compute_digest);
181        self
182    }
183    /// Returns whether the current date and time will be added to the request
184    /// when not already present.
185    ///
186    /// This is set to `true` by default.
187    pub fn add_date(&self) -> bool {
188        self.add_date
189    }
190    /// Controls whether the current date and time will be added to the request
191    /// when not already present (in-place).
192    ///
193    /// This is set to `true` by default.
194    pub fn set_add_date(&mut self, add_date: bool) -> &mut Self {
195        self.add_date = add_date;
196        self
197    }
198    /// Controls whether the current date and time will be added to the request
199    /// when not already present.
200    ///
201    /// This is set to `true` by default.
202    pub fn with_add_date(mut self, add_date: bool) -> Self {
203        self.set_add_date(add_date);
204        self
205    }
206    /// Returns whether the host will be added to the request
207    /// when not already present.
208    ///
209    /// This is set to `true` by default.
210    pub fn add_host(&self) -> bool {
211        self.add_host
212    }
213    /// Controls whether the host will be added to the request
214    /// when not already present (in-place).
215    ///
216    /// This is set to `true` by default.
217    pub fn set_add_host(&mut self, add_host: bool) -> &mut Self {
218        self.add_host = add_host;
219        self
220    }
221    /// Controls whether the host will be added to the request
222    /// when not already present.
223    ///
224    /// This is set to `true` by default.
225    pub fn with_add_host(mut self, add_host: bool) -> Self {
226        self.set_add_host(add_host);
227        self
228    }
229    /// Returns the list of headers to include in the signature. Headers in this list
230    /// which are not present in the request itself will be skipped when signing the request.
231    ///
232    /// This list contains `(request-target)`, `host`, `date` and `digest` by default.
233    pub fn headers(&self) -> impl IntoIterator<Item = &Header> {
234        &self.headers
235    }
236    /// Controls the list of headers to include in the signature (in-place). Headers in this list
237    /// which are not present in the request itself will be skipped when signing the request.
238    ///
239    /// This list contains `(request-target)`, `host`, `date` and `digest` by default.
240    pub fn set_headers(&mut self, headers: &[Header]) -> &mut Self {
241        self.headers = headers.to_vec();
242        self
243    }
244    /// Controls the list of headers to include in the signature. Headers in this list
245    /// which are not present in the request itself will be skipped when signing the request.
246    ///
247    /// This list contains `(request-target)`, `host`, `date` and `digest` by default.
248    pub fn with_headers(mut self, headers: &[Header]) -> Self {
249        self.set_headers(headers);
250        self
251    }
252    /// Returns whether the missing headers will be skipped
253    /// when not present, or if signing will fail instead.
254    ///
255    /// This is set to `true` by default.
256    pub fn skip_missing(&self) -> bool {
257        self.skip_missing
258    }
259    /// Controls whether the missing headers will be skipped
260    /// when not present, or if signing will fail instead.
261    ///
262    /// This is set to `true` by default.
263    pub fn set_skip_missing(&mut self, skip_missing: bool) -> &mut Self {
264        self.skip_missing = skip_missing;
265        self
266    }
267    /// Controls whether the missing headers will be skipped
268    /// when not present, or if signing will fail instead.
269    ///
270    /// This is set to `true` by default.
271    pub fn with_skip_missing(mut self, skip_missing: bool) -> Self {
272        self.set_skip_missing(skip_missing);
273        self
274    }
275    /// Ensures a signature created date will be added
276    /// automatically with the current time.
277    ///
278    /// This is off by default.
279    pub fn set_signature_created_auto(&mut self) -> &mut Self {
280        self.signature_created = SignatureCreated::Automatic;
281        self
282    }
283    /// Ensures a signature created date will be added
284    /// automatically with the current time.
285    ///
286    /// This is off by default.
287    pub fn with_signature_created_auto(mut self) -> Self {
288        self.signature_created = SignatureCreated::Automatic;
289        self
290    }
291    /// Determines if a signature created date will be added
292    /// automatically with the current time.
293    ///
294    /// This is off by default.
295    pub fn signature_created_auto(&self) -> bool {
296        self.signature_created == SignatureCreated::Automatic
297    }
298    /// Ensures a signature created date will be added
299    /// with the specified unix timestamp.
300    ///
301    /// This is off by default.
302    pub fn set_signature_created_at(&mut self, ts: i64) -> &mut Self {
303        self.signature_created = SignatureCreated::Absolute(ts);
304        self
305    }
306    /// Ensures a signature created date will be added
307    /// with the specified unix timestamp.
308    ///
309    /// This is off by default.
310    pub fn with_signature_created_at(mut self, ts: i64) -> Self {
311        self.signature_created = SignatureCreated::Absolute(ts);
312        self
313    }
314    /// Determines if a signature created date will be added
315    /// with a specific unix timestamp.
316    ///
317    /// This is off by default.
318    pub fn signature_created_at(&self) -> Option<i64> {
319        if let SignatureCreated::Absolute(ts) = self.signature_created {
320            Some(ts)
321        } else {
322            None
323        }
324    }
325    /// Ensures a signature expires date will be added
326    /// automatically relative to the current time.
327    ///
328    /// This is off by default.
329    pub fn set_signature_expires_relative(&mut self, offset: i64) -> &mut Self {
330        self.signature_expires = SignatureExpires::Relative(offset);
331        self
332    }
333    /// Ensures a signature expires date will be added
334    /// automatically relative to the current time.
335    ///
336    /// This is off by default.
337    pub fn with_signature_expires_auto(mut self, offset: i64) -> Self {
338        self.signature_expires = SignatureExpires::Relative(offset);
339        self
340    }
341    /// Determines if a signature expires date will be added
342    /// automatically relative to the current time.
343    ///
344    /// This is off by default.
345    pub fn signature_expires_relative(&self) -> Option<i64> {
346        if let SignatureExpires::Relative(offset) = self.signature_expires {
347            Some(offset)
348        } else {
349            None
350        }
351    }
352    /// Ensures a signature expires date will be added
353    /// with the specified unix timestamp.
354    ///
355    /// This is off by default.
356    pub fn set_signature_expires_at(&mut self, ts: i64) -> &mut Self {
357        self.signature_expires = SignatureExpires::Absolute(ts);
358        self
359    }
360    /// Ensures a signature expires date will be added
361    /// with the specified unix timestamp.
362    ///
363    /// This is off by default.
364    pub fn with_signature_expires_at(mut self, ts: i64) -> Self {
365        self.signature_expires = SignatureExpires::Absolute(ts);
366        self
367    }
368    /// Determines if a signature expires date will be added
369    /// with a specific unix timestamp.
370    ///
371    /// This is off by default.
372    pub fn signature_expires_at(&self) -> Option<i64> {
373        if let SignatureExpires::Absolute(ts) = self.signature_expires {
374            Some(ts)
375        } else {
376            None
377        }
378    }
379}
380
381/// Import this trait to get access to access the `signed` and `sign` methods on all types implementing
382/// `ClientRequestLike`.
383pub trait SigningExt: Sized {
384    /// Consumes the request and returns it signed according to the provided configuration.
385    fn signed(mut self, config: &SigningConfig) -> Result<Self, SigningError> {
386        self.sign(config)?;
387        Ok(self)
388    }
389
390    /// Signs the request in-place according to the provided configuration.
391    fn sign(&mut self, config: &SigningConfig) -> Result<(), SigningError>;
392}
393
394fn add_auto_headers<R: ClientRequestLike>(request: &mut R, config: &SigningConfig) -> Vec<Header> {
395    let digest_header = HeaderName::from_static("digest");
396
397    // Add missing date header
398    if config.add_date && !request.has_header(&DATE.into()) {
399        let date = Utc::now().format(DATE_FORMAT).to_string();
400        request.set_header(
401            DATE,
402            date.try_into()
403                .expect("Dates should always be valid header values"),
404        );
405    }
406    // Add missing host header
407    if config.add_host && !request.has_header(&HOST.into()) {
408        if let Some(host) = request.host() {
409            request.set_header(
410                HOST,
411                host.try_into()
412                    .expect("Host should be valid in a HTTP header"),
413            );
414        }
415    }
416    // Add missing digest header
417    if config.compute_digest && !request.has_header(&digest_header.clone().into()) {
418        if let Some(digest_str) = request.compute_digest(&*config.digest) {
419            let digest = format!("{}={}", config.digest.name(), digest_str);
420            request.set_header(
421                digest_header,
422                digest
423                    .try_into()
424                    .expect("Digest should be valid in a HTTP header"),
425            );
426        }
427    }
428
429    // Build the content block
430    if config.skip_missing {
431        config
432            .headers
433            .iter()
434            .filter(|header| request.has_header(header))
435            .cloned()
436            .collect()
437    } else {
438        config.headers.clone()
439    }
440}
441
442fn unix_timestamp() -> i64 {
443    SystemTime::now()
444        .duration_since(SystemTime::UNIX_EPOCH)
445        .expect("Unix time to be positive")
446        .as_secs() as i64
447}
448
449impl<R: ClientRequestLike> SigningExt for R {
450    fn sign(&mut self, config: &SigningConfig) -> Result<(), SigningError> {
451        // Add missing headers
452        let headers = add_auto_headers(self, config);
453
454        let joined_headers = headers.iter().map(|header| header.as_str()).join(" ");
455
456        // Determine config for canonicalization
457        let ts = unix_timestamp();
458        let mut canonicalize_config = CanonicalizeConfig::new().with_headers(headers);
459        if let Some(created) = config.signature_created.get(ts) {
460            if created > ts {
461                return Err(SigningError::InvalidSignatureCreationDate);
462            }
463            canonicalize_config.set_signature_created(created.into());
464        }
465        if let Some(expires) = config.signature_expires.get(ts) {
466            if expires < ts {
467                return Err(SigningError::InvalidSignatureExpiresDate);
468            }
469            canonicalize_config.set_signature_expires(expires.into());
470        }
471
472        // Compute canonical representation
473        let content = self.canonicalize(&canonicalize_config)?;
474
475        // Sign the content
476        let signature = config.signature.http_sign(content.as_bytes());
477
478        // Construct the authorization header
479        let auth_header = format!(
480            r#"Signature keyId="{}",algorithm="{}",signature="{}",headers="{}""#,
481            config.key_id, "hs2019", signature, joined_headers
482        );
483
484        // Attach the authorization header to the request
485        self.set_header(
486            AUTHORIZATION,
487            auth_header
488                .try_into()
489                .expect("Signature scheme should generate a valid header"),
490        );
491
492        Ok(())
493    }
494}