zino_auth/
authentication.rs

1use super::{AccessKeyId, SecretAccessKey};
2use hmac::{
3    Mac,
4    digest::{FixedOutput, KeyInit, MacMarker, Update},
5};
6use http::HeaderMap;
7use std::time::Duration;
8use zino_core::{Map, datetime::DateTime, encoding::base64, error::Error, validation::Validation};
9
10/// HTTP signature using HMAC.
11pub struct Authentication {
12    /// Service name.
13    service_name: String,
14    /// Access key ID.
15    access_key_id: AccessKeyId,
16    /// Signature.
17    signature: String,
18    /// HTTP method.
19    method: String,
20    /// Accept header value.
21    accept: Option<String>,
22    /// Content-MD5 header value.
23    content_md5: Option<String>,
24    /// Content-Type header value.
25    content_type: Option<String>,
26    /// Date header.
27    date_header: (&'static str, DateTime),
28    /// Expires.
29    expires: Option<DateTime>,
30    /// Canonicalized headers.
31    headers: Vec<(String, String)>,
32    /// Canonicalized resource.
33    resource: String,
34}
35
36impl Authentication {
37    /// Creates a new instance.
38    #[inline]
39    pub fn new(method: &str) -> Self {
40        Self {
41            service_name: String::new(),
42            access_key_id: AccessKeyId::default(),
43            signature: String::new(),
44            method: method.to_ascii_uppercase(),
45            accept: None,
46            content_md5: None,
47            content_type: None,
48            date_header: ("date", DateTime::now()),
49            expires: None,
50            headers: Vec::new(),
51            resource: String::new(),
52        }
53    }
54
55    /// Sets the service name.
56    #[inline]
57    pub fn set_service_name(&mut self, service_name: &str) {
58        self.service_name = service_name.to_ascii_uppercase();
59    }
60
61    /// Sets the access key ID.
62    #[inline]
63    pub fn set_access_key_id(&mut self, access_key_id: impl Into<AccessKeyId>) {
64        self.access_key_id = access_key_id.into();
65    }
66
67    /// Sets the signature.
68    #[inline]
69    pub fn set_signature(&mut self, signature: String) {
70        self.signature = signature;
71    }
72
73    /// Sets the `accept` header value.
74    #[inline]
75    pub fn set_accept(&mut self, accept: Option<String>) {
76        self.accept = accept;
77    }
78
79    /// Sets the `content-md5` header value.
80    #[inline]
81    pub fn set_content_md5(&mut self, content_md5: String) {
82        self.content_md5 = Some(content_md5);
83    }
84
85    /// Sets the `content-type` header value.
86    #[inline]
87    pub fn set_content_type(&mut self, content_type: Option<String>) {
88        self.content_type = content_type;
89    }
90
91    /// Sets the header value for the date.
92    #[inline]
93    pub fn set_date_header(&mut self, header_name: &'static str, date: DateTime) {
94        self.date_header = (header_name, date);
95    }
96
97    /// Sets the expires timestamp.
98    #[inline]
99    pub fn set_expires(&mut self, expires: Option<DateTime>) {
100        self.expires = expires;
101    }
102
103    /// Sets the canonicalized headers.
104    /// The header is matched if it has a prefix in the filter list.
105    #[inline]
106    pub fn set_headers(&mut self, headers: HeaderMap, filters: &[&'static str]) {
107        let mut headers = headers
108            .into_iter()
109            .filter_map(|(name, value)| {
110                name.and_then(|name| {
111                    let key = name.as_str();
112                    if filters.iter().any(|&s| key.starts_with(s)) {
113                        value
114                            .to_str()
115                            .inspect_err(|err| tracing::warn!("invalid header value: {err}"))
116                            .ok()
117                            .map(|value| (key.to_ascii_lowercase(), value.to_owned()))
118                    } else {
119                        None
120                    }
121                })
122            })
123            .collect::<Vec<_>>();
124        headers.sort_by(|a, b| a.0.cmp(&b.0));
125        self.headers = headers;
126    }
127
128    /// Sets the canonicalized resource.
129    #[inline]
130    pub fn set_resource(&mut self, path: String, query: Option<&Map>) {
131        if let Some(query) = query {
132            if query.is_empty() {
133                self.resource = path;
134            } else {
135                let mut query_pairs = query.iter().collect::<Vec<_>>();
136                query_pairs.sort_by(|a, b| a.0.cmp(b.0));
137
138                let query = query_pairs
139                    .iter()
140                    .map(|(key, value)| format!("{key}={value}"))
141                    .collect::<Vec<_>>();
142                self.resource = path + "?" + &query.join("&");
143            }
144        } else {
145            self.resource = path;
146        }
147    }
148
149    /// Returns the service name.
150    #[inline]
151    pub fn service_name(&self) -> &str {
152        self.service_name.as_str()
153    }
154
155    /// Returns the access key ID.
156    #[inline]
157    pub fn access_key_id(&self) -> &str {
158        self.access_key_id.as_str()
159    }
160
161    /// Returns the signature.
162    #[inline]
163    pub fn signature(&self) -> &str {
164        self.signature.as_str()
165    }
166
167    /// Returns an `authorization` header value.
168    #[inline]
169    pub fn authorization(&self) -> String {
170        let service_name = self.service_name();
171        let access_key_id = self.access_key_id();
172        let signature = self.signature();
173        if service_name.is_empty() {
174            format!("{access_key_id}:{signature}")
175        } else {
176            format!("{service_name} {access_key_id}:{signature}")
177        }
178    }
179
180    /// Returns the string to sign.
181    pub fn string_to_sign(&self) -> String {
182        let mut sign_parts = Vec::new();
183
184        // HTTP verb
185        sign_parts.push(self.method.clone());
186
187        // Accept
188        if let Some(accept) = self.accept.as_ref() {
189            sign_parts.push(accept.to_owned());
190        }
191
192        // Content-MD5
193        let content_md5 = self
194            .content_md5
195            .as_ref()
196            .map(|s| s.to_owned())
197            .unwrap_or_default();
198        sign_parts.push(content_md5);
199
200        // Content-Type
201        let content_type = self
202            .content_type
203            .as_ref()
204            .map(|s| s.to_owned())
205            .unwrap_or_default();
206        sign_parts.push(content_type);
207
208        // Expires
209        if let Some(expires) = self.expires.as_ref() {
210            sign_parts.push(expires.timestamp().to_string());
211        } else {
212            // Date
213            let date_header = &self.date_header;
214            let date = if date_header.0.eq_ignore_ascii_case("date") {
215                date_header.1.to_utc_string()
216            } else {
217                "".to_owned()
218            };
219            sign_parts.push(date);
220        }
221
222        // Canonicalized headers
223        let headers = self
224            .headers
225            .iter()
226            .map(|(name, values)| format!("{}:{}", name, values.trim()))
227            .collect::<Vec<_>>();
228        sign_parts.extend(headers);
229
230        // Canonicalized resource
231        sign_parts.push(self.resource.clone());
232
233        sign_parts.join("\n")
234    }
235
236    /// Generates a signature with the secret access key.
237    pub fn sign_with<H>(&self, secret_access_key: &SecretAccessKey) -> Result<String, Error>
238    where
239        H: FixedOutput + KeyInit + MacMarker + Update,
240    {
241        let string_to_sign = self.string_to_sign();
242        let mut mac = H::new_from_slice(secret_access_key.as_ref())?;
243        mac.update(string_to_sign.as_ref());
244        Ok(base64::encode(mac.finalize().into_bytes()))
245    }
246
247    /// Validates the signature using the secret access key.
248    pub fn validate_with<H>(&self, secret_access_key: &SecretAccessKey) -> Validation
249    where
250        H: FixedOutput + KeyInit + MacMarker + Update,
251    {
252        let mut validation = Validation::new();
253        let current = DateTime::now();
254        let date = self.date_header.1;
255        let max_tolerance = Duration::from_secs(900);
256        if date < current && date < current - max_tolerance
257            || date > current && date > current + max_tolerance
258        {
259            validation.record("date", "untrusted date");
260        }
261        if let Some(expires) = self.expires
262            && current > expires
263        {
264            validation.record("expires", "valid period has expired");
265        }
266
267        let signature = self.signature();
268        if signature.is_empty() {
269            validation.record("signature", "should be nonempty");
270        } else if self
271            .sign_with::<H>(secret_access_key)
272            .is_ok_and(|s| s != signature)
273        {
274            validation.record("signature", "invalid signature");
275        }
276        validation
277    }
278}