scratchstack_aws_signature/
signing_key.rs

1use {
2    crate::{
3        crypto::{hmac_sha256, SHA256_OUTPUT_LEN},
4        KeyTooLongError,
5    },
6    chrono::NaiveDate,
7    derive_builder::Builder,
8    scratchstack_aws_principal::{Principal, SessionData},
9    std::{
10        fmt::{Debug, Display, Formatter, Result as FmtResult},
11        future::Future,
12        str::FromStr,
13    },
14    tower::{service_fn, util::ServiceFn, BoxError},
15};
16
17/// String included at the end of the AWS SigV4 credential scope
18const AWS4_REQUEST: &str = "aws4_request";
19
20/// A raw AWS secret key (`kSecret`).
21#[derive(Clone, Copy, PartialEq, Eq)]
22pub struct KSecretKey<const M: usize = 44> {
23    /// The secret key, prefixed with "AWS4".
24    prefixed_key: [u8; M],
25
26    /// The length of the key.
27    len: usize,
28}
29
30/// The `kDate` key: `HMAC_SHA256("AWS4" + KSecretKey, "YYYYMMDD")`
31#[derive(Clone, Copy, PartialEq, Eq)]
32pub struct KDateKey {
33    /// The raw key.
34    key: [u8; SHA256_OUTPUT_LEN],
35}
36
37/// The `kRegion` key: an AWS `kDate` key, HMAC-SHA256 hashed with the region.
38#[derive(Clone, Copy, PartialEq, Eq)]
39pub struct KRegionKey {
40    /// The raw key.
41    key: [u8; SHA256_OUTPUT_LEN],
42}
43
44/// The `kService` key: an AWS `kRegion` key, HMAC-SHA256 hashed with the service.
45#[derive(Clone, Copy, PartialEq, Eq)]
46pub struct KServiceKey {
47    /// The raw key.
48    key: [u8; SHA256_OUTPUT_LEN],
49}
50
51/// The `kSigning` key: an AWS `kService` key, HMAC-SHA256 hashed with the "aws4_request" string.
52#[derive(Clone, Copy, PartialEq, Eq)]
53pub struct KSigningKey {
54    /// The resulting raw signing key.
55    key: [u8; SHA256_OUTPUT_LEN],
56}
57
58impl AsRef<[u8]> for KSecretKey {
59    fn as_ref(&self) -> &[u8] {
60        // Remove the "AWS4" prefix.
61        &self.prefixed_key[4..self.len]
62    }
63}
64
65impl AsRef<[u8; SHA256_OUTPUT_LEN]> for KDateKey {
66    fn as_ref(&self) -> &[u8; SHA256_OUTPUT_LEN] {
67        &self.key
68    }
69}
70
71impl AsRef<[u8; SHA256_OUTPUT_LEN]> for KRegionKey {
72    fn as_ref(&self) -> &[u8; SHA256_OUTPUT_LEN] {
73        &self.key
74    }
75}
76
77impl AsRef<[u8; SHA256_OUTPUT_LEN]> for KServiceKey {
78    fn as_ref(&self) -> &[u8; SHA256_OUTPUT_LEN] {
79        &self.key
80    }
81}
82
83impl AsRef<[u8; SHA256_OUTPUT_LEN]> for KSigningKey {
84    fn as_ref(&self) -> &[u8; SHA256_OUTPUT_LEN] {
85        &self.key
86    }
87}
88
89impl Debug for KSecretKey {
90    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
91        f.write_str("KSecretKey")
92    }
93}
94
95impl Debug for KDateKey {
96    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
97        f.write_str("KDateKey")
98    }
99}
100
101impl Debug for KRegionKey {
102    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
103        f.write_str("KRegionKey")
104    }
105}
106
107impl Debug for KServiceKey {
108    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
109        f.write_str("KServiceKey")
110    }
111}
112
113impl Debug for KSigningKey {
114    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
115        f.write_str("KSigningKey")
116    }
117}
118
119impl Display for KSecretKey {
120    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
121        f.write_str("KSecretKey")
122    }
123}
124
125impl Display for KDateKey {
126    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
127        f.write_str("KDateKey")
128    }
129}
130
131impl Display for KRegionKey {
132    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
133        f.write_str("KRegionKey")
134    }
135}
136
137impl Display for KServiceKey {
138    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
139        f.write_str("KServiceKey")
140    }
141}
142
143impl Display for KSigningKey {
144    fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
145        f.write_str("KSigningKey")
146    }
147}
148
149impl<const M: usize> FromStr for KSecretKey<M> {
150    type Err = KeyTooLongError;
151
152    /// Create a new `KSecretKey` from a raw AWS secret key.
153    fn from_str(raw: &str) -> Result<Self, KeyTooLongError> {
154        let len = raw.len();
155        if len > M - 4 {
156            return Err(KeyTooLongError);
157        }
158
159        let mut prefixed_key = [0; M];
160
161        prefixed_key[..4].copy_from_slice(b"AWS4");
162        prefixed_key[4..].copy_from_slice(raw.as_bytes());
163        Ok(Self {
164            prefixed_key,
165            len: len + 4,
166        })
167    }
168}
169
170impl KSecretKey {
171    /// Create a new `KDateKey` from this `KSecretKey` and a date.
172    pub fn to_kdate(&self, date: NaiveDate) -> KDateKey {
173        let date = date.format("%Y%m%d").to_string();
174        let date = date.as_bytes();
175        let key = hmac_sha256(self.prefixed_key.as_slice(), date);
176        let mut key_bytes = [0; SHA256_OUTPUT_LEN];
177        key_bytes.copy_from_slice(key.as_ref());
178        KDateKey {
179            key: key_bytes,
180        }
181    }
182
183    /// Creeate a new `KRegionKey` from this `KSecretKey`, a date, and a region.
184    pub fn to_kregion(&self, date: NaiveDate, region: &str) -> KRegionKey {
185        self.to_kdate(date).to_kregion(region)
186    }
187
188    /// Creeate a new `KServiceKey` from this `KSecretKey`, a date, a region, and a service.
189    pub fn to_kservice(&self, date: NaiveDate, region: &str, service: &str) -> KServiceKey {
190        self.to_kdate(date).to_kservice(region, service)
191    }
192
193    /// Creeate a new `KSigningKey` from this `KSecretKey`, a date, a region, and a service.
194    pub fn to_ksigning(&self, date: NaiveDate, region: &str, service: &str) -> KSigningKey {
195        self.to_kdate(date).to_ksigning(region, service)
196    }
197}
198
199impl KDateKey {
200    /// Create a new `KRegionKey` from this `KDateKey` and a region.
201    pub fn to_kregion(&self, region: &str) -> KRegionKey {
202        let region = region.as_bytes();
203        let key = hmac_sha256(self.key.as_slice(), region);
204        let mut key_bytes = [0; SHA256_OUTPUT_LEN];
205        key_bytes.copy_from_slice(key.as_ref());
206        KRegionKey {
207            key: key_bytes,
208        }
209    }
210
211    /// Create a new `KServiceKey` from this `KDateKey`, a region, and a service.
212    pub fn to_kservice(&self, region: &str, service: &str) -> KServiceKey {
213        self.to_kregion(region).to_kservice(service)
214    }
215
216    /// Create a new `KSigningKey` from this `KDateKey`, a region, and a service.
217    pub fn to_ksigning(&self, region: &str, service: &str) -> KSigningKey {
218        self.to_kregion(region).to_ksigning(service)
219    }
220}
221
222impl KRegionKey {
223    /// Create a new `KServiceKey` from this `KRegionKey` and a service.
224    pub fn to_kservice(&self, service: &str) -> KServiceKey {
225        let service = service.as_bytes();
226        let key = hmac_sha256(self.key.as_slice(), service);
227        let mut key_bytes = [0; SHA256_OUTPUT_LEN];
228        key_bytes.copy_from_slice(key.as_ref());
229        KServiceKey {
230            key: key_bytes,
231        }
232    }
233
234    /// Create a new `KSigningKey` from this `KRegionKey` and a service.
235    pub fn to_ksigning(&self, service: &str) -> KSigningKey {
236        self.to_kservice(service).to_ksigning()
237    }
238}
239
240impl KServiceKey {
241    /// Create a new `KSigningKey` from this `KServiceKey`.
242    pub fn to_ksigning(&self) -> KSigningKey {
243        let key = hmac_sha256(self.key.as_slice(), AWS4_REQUEST.as_bytes());
244        let mut key_bytes = [0; SHA256_OUTPUT_LEN];
245        key_bytes.copy_from_slice(key.as_ref());
246        KSigningKey {
247            key: key_bytes,
248        }
249    }
250}
251
252/// A request for a signing key of a given kind for the specified request.
253///
254/// GetSigningKeyRequest structs are immutable. Use [GetSigningKeyRequestBuilder] to programmatically construct a
255/// request.
256#[derive(Builder, Clone, Debug)]
257#[non_exhaustive]
258pub struct GetSigningKeyRequest {
259    /// The access key used in the request.
260    #[builder(setter(into))]
261    access_key: String,
262
263    /// The session token provided in the request, if any.
264    #[builder(setter(into), default)]
265    session_token: Option<String>,
266
267    /// The date of the request.
268    request_date: NaiveDate,
269
270    /// The region of the request.
271    #[builder(setter(into))]
272    region: String,
273
274    /// The service of the request.
275    #[builder(setter(into))]
276    service: String,
277}
278
279impl GetSigningKeyRequest {
280    /// Create a [GetSigningKeyRequestBuilder] to construct a [GetSigningKeyRequest].
281    #[inline]
282    pub fn builder() -> GetSigningKeyRequestBuilder {
283        GetSigningKeyRequestBuilder::default()
284    }
285
286    /// Retrieve the access key used in the request.
287    #[inline]
288    pub fn access_key(&self) -> &str {
289        &self.access_key
290    }
291
292    /// Retrieve the session token provided in the request, if any.
293    #[inline]
294    pub fn session_token(&self) -> Option<&str> {
295        self.session_token.as_deref()
296    }
297
298    /// Retrieve the date of the request.
299    #[inline]
300    pub fn request_date(&self) -> NaiveDate {
301        self.request_date
302    }
303
304    /// Retrieve the region of the request.
305    #[inline]
306    pub fn region(&self) -> &str {
307        &self.region
308    }
309
310    /// Retrieve the service of the request.
311    #[inline]
312    pub fn service(&self) -> &str {
313        &self.service
314    }
315}
316
317/// A response from the signing key provider.
318///
319/// GetSigningKeyResponse structs are immutable. Use [GetSigningKeyResponseBuilder] to programmatically construct a
320/// response.
321#[derive(Builder, Clone, Debug)]
322pub struct GetSigningKeyResponse {
323    /// The principal actors of the request.
324    #[builder(setter(into), default)]
325    pub(crate) principal: Principal,
326
327    /// The session data associated with the principal.
328    #[builder(setter(into), default)]
329    pub(crate) session_data: SessionData,
330
331    /// The signing key.
332    signing_key: KSigningKey,
333}
334
335impl GetSigningKeyResponse {
336    /// Create a [GetSigningKeyResponseBuilder] to construct a [GetSigningKeyResponse].
337    #[inline]
338    pub fn builder() -> GetSigningKeyResponseBuilder {
339        GetSigningKeyResponseBuilder::default()
340    }
341
342    /// Retrieve the principal actors of the request.
343    #[inline]
344    pub fn principal(&self) -> &Principal {
345        &self.principal
346    }
347
348    /// Retrieve the session data associated with the principal.
349    #[inline]
350    pub fn session_data(&self) -> &SessionData {
351        &self.session_data
352    }
353
354    /// Retrieve the signing key.
355    #[inline]
356    pub fn signing_key(&self) -> &KSigningKey {
357        &self.signing_key
358    }
359}
360
361impl Default for GetSigningKeyResponse {
362    fn default() -> Self {
363        Self {
364            signing_key: KSigningKey {
365                key: [0; SHA256_OUTPUT_LEN],
366            },
367            session_data: SessionData::default(),
368            principal: Principal::new(vec![]),
369        }
370    }
371}
372
373// A trait alias that describes how we obtain a signing key of a given type given a request. If you need to encapsulate
374// additional data (e.g. a database connection) to look up a key, use this to implement a struct.
375//
376// This requires the trait_alias feature to be stabilized and is commented out until then.
377// https://github.com/rust-lang/rust/issues/41517
378//
379// I find trait bounds annoying since they have to be repeated everywhere.
380//
381// pub trait GetSigningKey<F> = Service<GetSigningKeyRequest, Response = (Principal, KSigningKey), Error = BoxError> + Send + 'static;
382
383/// Create a Service that wraps a function that can produce a signing key.
384pub fn service_for_signing_key_fn<F, Fut>(f: F) -> ServiceFn<F>
385where
386    F: FnOnce(GetSigningKeyRequest) -> Fut + Send + 'static,
387    Fut: Future<Output = Result<GetSigningKeyResponse, BoxError>> + Send + 'static,
388{
389    service_fn(f)
390}
391
392#[cfg(test)]
393mod tests {
394    use {
395        crate::{GetSigningKeyRequest, GetSigningKeyResponse, KSecretKey},
396        chrono::NaiveDate,
397        scratchstack_aws_principal::{AssumedRole, Principal},
398        std::str::FromStr,
399    };
400
401    #[test_log::test]
402    fn test_signing_key_derived() {
403        let date = NaiveDate::from_ymd_opt(2015, 8, 30).unwrap();
404
405        let ksecret1a = KSecretKey::from_str("wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY").unwrap();
406        let ksecret1b = KSecretKey::from_str("wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY").unwrap();
407        let ksecret2 = KSecretKey::from_str("wJalrXUtnFEMI/K7MDENG+bPxRfiCZEXAMPLEKEY").unwrap();
408
409        assert_eq!(ksecret1a, ksecret1b);
410        assert_eq!(ksecret1a, ksecret1a.clone());
411        assert_ne!(ksecret1a, ksecret2);
412        assert_eq!(format!("{:?}", ksecret1a).as_str(), "KSecretKey");
413        assert_eq!(format!("{}", ksecret1a).as_str(), "KSecretKey");
414        assert_eq!(ksecret1a.as_ref(), b"wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY");
415
416        let kdate1a = ksecret1a.to_kdate(date);
417        let kdate1b = ksecret1b.to_kdate(date);
418        let kdate2 = ksecret2.to_kdate(date);
419        assert_eq!(
420            kdate1a.as_ref(),
421            &[
422                0x01u8, 0x38u8, 0xc7u8, 0xa6u8, 0xcbu8, 0xd6u8, 0x0au8, 0xa7u8, 0x27u8, 0xb2u8, 0xf6u8, 0x53u8, 0xa5u8,
423                0x22u8, 0x56u8, 0x74u8, 0x39u8, 0xdfu8, 0xb9u8, 0xf3u8, 0xe7u8, 0x2bu8, 0x21u8, 0xf9u8, 0xb2u8, 0x59u8,
424                0x41u8, 0xa4u8, 0x2fu8, 0x04u8, 0xa7u8, 0xcdu8
425            ]
426        );
427        assert_eq!(kdate1a, kdate1b);
428        assert_eq!(kdate1a, kdate1a.clone());
429        assert_ne!(kdate1a, kdate2);
430        assert_eq!(format!("{:?}", kdate1a).as_str(), "KDateKey");
431        assert_eq!(format!("{}", kdate1a).as_str(), "KDateKey");
432
433        let kregion1a = kdate1a.to_kregion("us-east-1");
434        let kregion1b = kdate1b.to_kregion("us-east-1");
435        let kregion2 = kdate2.to_kregion("us-east-1");
436        assert_eq!(
437            kregion1a.as_ref(),
438            &[
439                0xf3u8, 0x3du8, 0x58u8, 0x08u8, 0x50u8, 0x4bu8, 0xf3u8, 0x48u8, 0x12u8, 0xe5u8, 0xfau8, 0xdeu8, 0x63u8,
440                0x30u8, 0x8bu8, 0x42u8, 0x4bu8, 0x24u8, 0x4cu8, 0x59u8, 0x18u8, 0x9bu8, 0xe2u8, 0xa5u8, 0x91u8, 0xddu8,
441                0x22u8, 0x82u8, 0xc7u8, 0xcbu8, 0x56u8, 0x3fu8
442            ]
443        );
444        assert_eq!(kregion1a, kregion1b);
445        assert_eq!(kregion1a, kregion1a.clone());
446        assert_ne!(kregion1a, kregion2);
447        assert_eq!(format!("{:?}", kregion1a).as_str(), "KRegionKey");
448        assert_eq!(format!("{}", kregion1a).as_str(), "KRegionKey");
449
450        let kservice1a = kregion1a.to_kservice("example");
451        let kservice1b = kregion1b.to_kservice("example");
452        let kservice2 = kregion2.to_kservice("example");
453        assert_eq!(
454            kservice1a.as_ref(),
455            &[
456                0xc6u8, 0x0cu8, 0xc4u8, 0xb1u8, 0xd0u8, 0x34u8, 0xc7u8, 0x57u8, 0x34u8, 0x8fu8, 0x2cu8, 0x67u8, 0x30u8,
457                0x04u8, 0xc1u8, 0x89u8, 0x08u8, 0xbbu8, 0xa9u8, 0xa4u8, 0x6fu8, 0xa1u8, 0xdbu8, 0x87u8, 0xa9u8, 0x83u8,
458                0x50u8, 0xf2u8, 0x7eu8, 0x7bu8, 0x2du8, 0xf6u8
459            ]
460        );
461        assert_eq!(kservice1a, kservice1b);
462        assert_eq!(kservice1a, kservice1a.clone());
463        assert_ne!(kservice1a, kservice2);
464        assert_eq!(format!("{:?}", kservice1a).as_str(), "KServiceKey");
465        assert_eq!(format!("{}", kservice1a).as_str(), "KServiceKey");
466
467        let ksigning1a = kservice1a.to_ksigning();
468        let ksigning1b = kservice1b.to_ksigning();
469        let ksigning2 = kservice2.to_ksigning();
470        assert_eq!(
471            ksigning1a.as_ref(),
472            &[
473                0x43u8, 0x1cu8, 0xc9u8, 0xefu8, 0x58u8, 0x76u8, 0x28u8, 0x7du8, 0xbbu8, 0x92u8, 0x5du8, 0x4bu8, 0xa4u8,
474                0x62u8, 0x9fu8, 0x45u8, 0x90u8, 0x02u8, 0xadu8, 0x1du8, 0x26u8, 0xb7u8, 0xc7u8, 0x51u8, 0x60u8, 0x1bu8,
475                0xb2u8, 0x04u8, 0xe1u8, 0x17u8, 0x18u8, 0xb8u8
476            ]
477        );
478        assert_eq!(ksigning1a, ksigning1b);
479        assert_eq!(ksigning1a, ksigning1a.clone());
480        assert_ne!(ksigning1a, ksigning2);
481        assert_eq!(format!("{:?}", ksigning1a).as_str(), "KSigningKey");
482        assert_eq!(format!("{}", ksigning1a).as_str(), "KSigningKey");
483
484        assert_eq!(ksecret1a.to_kregion(date, "us-east-1"), kregion1a);
485        assert_eq!(ksecret1a.to_kservice(date, "us-east-1", "example"), kservice1a);
486        assert_eq!(ksecret1a.to_ksigning(date, "us-east-1", "example"), ksigning1a);
487
488        assert_eq!(kdate1a.to_kservice("us-east-1", "example"), kservice1a);
489        assert_eq!(kdate1a.to_ksigning("us-east-1", "example"), ksigning1a);
490
491        assert_eq!(kregion1a.to_kservice("example"), kservice1a);
492    }
493
494    #[test_log::test]
495    fn test_gsk_derived() {
496        let date = NaiveDate::from_ymd_opt(2015, 8, 30).unwrap();
497
498        let gsk_req1a = GetSigningKeyRequest {
499            access_key: "AKIDEXAMPLE".to_string(),
500            session_token: Some("token".to_string()),
501            request_date: date,
502            region: "us-east-1".to_string(),
503            service: "example".to_string(),
504        };
505
506        // Make sure we can debug print the request.
507        let _ = format!("{:?}", gsk_req1a);
508
509        // Make sure clones are field-by-field equal.
510        let gsk_req1b = gsk_req1a.clone();
511        assert_eq!(gsk_req1a.access_key, gsk_req1b.access_key);
512        assert_eq!(gsk_req1a.session_token, gsk_req1b.session_token);
513        assert_eq!(gsk_req1a.request_date, gsk_req1b.request_date);
514        assert_eq!(gsk_req1a.region, gsk_req1b.region);
515        assert_eq!(gsk_req1a.service, gsk_req1b.service);
516
517        let signing_key = KSecretKey::from_str("wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY").unwrap().to_ksigning(
518            date,
519            "us-east-1",
520            "example",
521        );
522        let principal = Principal::from(AssumedRole::new("aws", "123456789012", "role", "session").unwrap());
523
524        let gsk_resp1a =
525            GetSigningKeyResponse::builder().signing_key(signing_key).principal(principal).build().unwrap();
526
527        // Make sure we can debug print the response.
528        let _ = format!("{:?}", gsk_resp1a);
529
530        // Make sure clones are field-by-field equal.
531        let gsk_resp1b = gsk_resp1a.clone();
532        assert_eq!(gsk_resp1a.signing_key, gsk_resp1b.signing_key);
533        assert_eq!(gsk_resp1a.principal, gsk_resp1b.principal);
534    }
535
536    #[test_log::test]
537    fn test_gsk_reponse_derived() {
538        let response: GetSigningKeyResponse = Default::default();
539        assert_eq!(response.signing_key.as_ref(), &[0u8; 32]);
540    }
541
542    #[test_log::test]
543    fn test_response_builder() {
544        let date = NaiveDate::from_ymd_opt(2015, 8, 30).unwrap();
545        let signing_key = KSecretKey::from_str("wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY").unwrap().to_ksigning(
546            date,
547            "us-east-1",
548            "example",
549        );
550        let response = GetSigningKeyResponse::builder().signing_key(signing_key).build().unwrap();
551        assert!(response.principal().is_empty());
552        assert!(response.session_data().is_empty());
553    }
554}