1515// specific language governing permissions and limitations
1616// under the License.
1717
18- use crate :: aws:: { STORE , STRICT_ENCODE_SET } ;
18+ use crate :: aws:: { STORE , STRICT_ENCODE_SET , STRICT_PATH_ENCODE_SET } ;
1919use crate :: client:: retry:: RetryExt ;
2020use crate :: client:: token:: { TemporaryToken , TokenCache } ;
2121use crate :: client:: TokenProvider ;
@@ -39,7 +39,8 @@ type StdError = Box<dyn std::error::Error + Send + Sync>;
3939/// SHA256 hash of empty string
4040static EMPTY_SHA256_HASH : & str =
4141 "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" ;
42- static UNSIGNED_PAYLOAD_LITERAL : & str = "UNSIGNED-PAYLOAD" ;
42+ static UNSIGNED_PAYLOAD : & str = "UNSIGNED-PAYLOAD" ;
43+ static STREAMING_PAYLOAD : & str = "STREAMING-AWS4-HMAC-SHA256-PAYLOAD" ;
4344
4445/// A set of AWS security credentials
4546#[ derive( Debug , Eq , PartialEq ) ]
@@ -72,8 +73,12 @@ impl AwsCredential {
7273 }
7374}
7475
75- struct RequestSigner < ' a > {
76- date : DateTime < Utc > ,
76+ /// Authorize a [`Request`] with an [`AwsCredential`] using [AWS SigV4]
77+ ///
78+ /// [AWS SigV4]: https://docs.aws.amazon.com/general/latest/gr/sigv4-calculate-signature.html
79+ #[ derive( Debug ) ]
80+ pub struct AwsAuthorizer < ' a > {
81+ date : Option < DateTime < Utc > > ,
7782 credential : & ' a AwsCredential ,
7883 service : & ' a str ,
7984 region : & ' a str ,
@@ -85,47 +90,86 @@ const HASH_HEADER: &str = "x-amz-content-sha256";
8590const TOKEN_HEADER : & str = "x-amz-security-token" ;
8691const AUTH_HEADER : & str = "authorization" ;
8792
88- impl < ' a > RequestSigner < ' a > {
89- fn sign ( & self , request : & mut Request , pre_calculated_digest : Option < Vec < u8 > > ) {
93+ impl < ' a > AwsAuthorizer < ' a > {
94+ /// Create a new [`AwsAuthorizer`]
95+ pub fn new ( credential : & ' a AwsCredential , service : & ' a str , region : & ' a str ) -> Self {
96+ Self {
97+ credential,
98+ service,
99+ region,
100+ date : None ,
101+ sign_payload : true ,
102+ }
103+ }
104+
105+ /// Controls whether this [`AwsAuthorizer`] will attempt to sign the request payload,
106+ /// the default is `true`
107+ pub fn with_sign_payload ( mut self , signed : bool ) -> Self {
108+ self . sign_payload = signed;
109+ self
110+ }
111+
112+ /// Authorize `request` with an optional pre-calculated SHA256 digest by attaching
113+ /// the relevant [AWS SigV4] headers
114+ ///
115+ /// # Payload Signature
116+ ///
117+ /// AWS SigV4 requests must contain the `x-amz-content-sha256` header, it is set as follows:
118+ ///
119+ /// * If not configured to sign payloads, it is set to `UNSIGNED-PAYLOAD`
120+ /// * If a `pre_calculated_digest` is provided, it is set to the hex encoding of it
121+ /// * If it is a streaming request, it is set to `STREAMING-AWS4-HMAC-SHA256-PAYLOAD`
122+ /// * Otherwise it is set to the hex encoded SHA256 of the request body
123+ ///
124+ /// [AWS SigV4]: https://docs.aws.amazon.com/IAM/latest/UserGuide/create-signed-request.html
125+ pub fn authorize ( & self , request : & mut Request , pre_calculated_digest : Option < & [ u8 ] > ) {
90126 if let Some ( ref token) = self . credential . token {
91127 let token_val = HeaderValue :: from_str ( token) . unwrap ( ) ;
92128 request. headers_mut ( ) . insert ( TOKEN_HEADER , token_val) ;
93129 }
94130
95- let host_val = HeaderValue :: from_str (
96- & request. url ( ) [ url:: Position :: BeforeHost ..url:: Position :: AfterPort ] ,
97- )
98- . unwrap ( ) ;
131+ let host = & request. url ( ) [ url:: Position :: BeforeHost ..url:: Position :: AfterPort ] ;
132+ let host_val = HeaderValue :: from_str ( host) . unwrap ( ) ;
99133 request. headers_mut ( ) . insert ( "host" , host_val) ;
100134
101- let date_str = self . date . format ( "%Y%m%dT%H%M%SZ" ) . to_string ( ) ;
135+ let date = self . date . unwrap_or_else ( Utc :: now) ;
136+ let date_str = date. format ( "%Y%m%dT%H%M%SZ" ) . to_string ( ) ;
102137 let date_val = HeaderValue :: from_str ( & date_str) . unwrap ( ) ;
103138 request. headers_mut ( ) . insert ( DATE_HEADER , date_val) ;
104139
105- let digest = if self . sign_payload {
106- if let Some ( digest ) = pre_calculated_digest {
107- hex_encode ( & digest )
108- } else {
109- match request. body ( ) {
140+ let digest = match self . sign_payload {
141+ false => UNSIGNED_PAYLOAD . to_string ( ) ,
142+ true => match pre_calculated_digest {
143+ Some ( digest ) => hex_encode ( digest ) ,
144+ None => match request. body ( ) {
110145 None => EMPTY_SHA256_HASH . to_string ( ) ,
111- Some ( body) => hex_digest ( body. as_bytes ( ) . unwrap ( ) ) ,
112- }
113- }
114- } else {
115- UNSIGNED_PAYLOAD_LITERAL . to_string ( )
146+ Some ( body) => match body. as_bytes ( ) {
147+ Some ( bytes) => hex_digest ( bytes) ,
148+ None => STREAMING_PAYLOAD . to_string ( ) ,
149+ } ,
150+ } ,
151+ } ,
116152 } ;
117153
118154 let header_digest = HeaderValue :: from_str ( & digest) . unwrap ( ) ;
119155 request. headers_mut ( ) . insert ( HASH_HEADER , header_digest) ;
120156
157+ // Each path segment must be URI-encoded twice (except for Amazon S3 which only gets URI-encoded once).
158+ // see https://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html
159+ let canonical_uri = match self . service {
160+ "s3" => request. url ( ) . path ( ) . to_string ( ) ,
161+ _ => utf8_percent_encode ( request. url ( ) . path ( ) , & STRICT_PATH_ENCODE_SET )
162+ . to_string ( ) ,
163+ } ;
164+
121165 let ( signed_headers, canonical_headers) = canonicalize_headers ( request. headers ( ) ) ;
122166 let canonical_query = canonicalize_query ( request. url ( ) ) ;
123167
124168 // https://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html
125169 let canonical_request = format ! (
126170 "{}\n {}\n {}\n {}\n {}\n {}" ,
127171 request. method( ) . as_str( ) ,
128- request . url ( ) . path ( ) , // S3 doesn't percent encode this like other services
172+ canonical_uri ,
129173 canonical_query,
130174 canonical_headers,
131175 signed_headers,
@@ -135,22 +179,22 @@ impl<'a> RequestSigner<'a> {
135179 let hashed_canonical_request = hex_digest ( canonical_request. as_bytes ( ) ) ;
136180 let scope = format ! (
137181 "{}/{}/{}/aws4_request" ,
138- self . date. format( "%Y%m%d" ) ,
182+ date. format( "%Y%m%d" ) ,
139183 self . region,
140184 self . service
141185 ) ;
142186
143187 let string_to_sign = format ! (
144188 "AWS4-HMAC-SHA256\n {}\n {}\n {}" ,
145- self . date. format( "%Y%m%dT%H%M%SZ" ) ,
189+ date. format( "%Y%m%dT%H%M%SZ" ) ,
146190 scope,
147191 hashed_canonical_request
148192 ) ;
149193
150194 // sign the string
151195 let signature =
152196 self . credential
153- . sign ( & string_to_sign, self . date , self . region , self . service ) ;
197+ . sign ( & string_to_sign, date, self . region , self . service ) ;
154198
155199 // build the actual auth header
156200 let authorisation = format ! (
@@ -171,7 +215,7 @@ pub trait CredentialExt {
171215 region : & str ,
172216 service : & str ,
173217 sign_payload : bool ,
174- payload_sha256 : Option < Vec < u8 > > ,
218+ payload_sha256 : Option < & [ u8 ] > ,
175219 ) -> Self ;
176220}
177221
@@ -182,21 +226,15 @@ impl CredentialExt for RequestBuilder {
182226 region : & str ,
183227 service : & str ,
184228 sign_payload : bool ,
185- payload_sha256 : Option < Vec < u8 > > ,
229+ payload_sha256 : Option < & [ u8 ] > ,
186230 ) -> Self {
187231 let ( client, request) = self . build_split ( ) ;
188232 let mut request = request. expect ( "request valid" ) ;
189233
190- let date = Utc :: now ( ) ;
191- let signer = RequestSigner {
192- date,
193- credential,
194- service,
195- region,
196- sign_payload,
197- } ;
234+ AwsAuthorizer :: new ( credential, service, region)
235+ . with_sign_payload ( sign_payload)
236+ . authorize ( & mut request, payload_sha256) ;
198237
199- signer. sign ( & mut request, payload_sha256) ;
200238 Self :: from_parts ( client, request)
201239 }
202240}
@@ -539,15 +577,15 @@ mod tests {
539577 . build ( )
540578 . unwrap ( ) ;
541579
542- let signer = RequestSigner {
543- date,
580+ let signer = AwsAuthorizer {
581+ date : Some ( date ) ,
544582 credential : & credential,
545583 service : "ec2" ,
546584 region : "us-east-1" ,
547585 sign_payload : true ,
548586 } ;
549587
550- signer. sign ( & mut request, None ) ;
588+ signer. authorize ( & mut request, None ) ;
551589 assert_eq ! ( request. headers( ) . get( AUTH_HEADER ) . unwrap( ) , "AWS4-HMAC-SHA256 Credential=AKIAIOSFODNN7EXAMPLE/20220806/us-east-1/ec2/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=a3c787a7ed37f7fdfbfd2d7056a3d7c9d85e6d52a2bfbec73793c0be6e7862d4" )
552590 }
553591
@@ -577,15 +615,15 @@ mod tests {
577615 . build ( )
578616 . unwrap ( ) ;
579617
580- let signer = RequestSigner {
581- date,
618+ let authorizer = AwsAuthorizer {
619+ date : Some ( date ) ,
582620 credential : & credential,
583621 service : "ec2" ,
584622 region : "us-east-1" ,
585623 sign_payload : false ,
586624 } ;
587625
588- signer . sign ( & mut request, None ) ;
626+ authorizer . authorize ( & mut request, None ) ;
589627 assert_eq ! ( request. headers( ) . get( AUTH_HEADER ) . unwrap( ) , "AWS4-HMAC-SHA256 Credential=AKIAIOSFODNN7EXAMPLE/20220806/us-east-1/ec2/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=653c3d8ea261fd826207df58bc2bb69fbb5003e9eb3c0ef06e4a51f2a81d8699" )
590628 }
591629
@@ -614,15 +652,15 @@ mod tests {
614652 . build ( )
615653 . unwrap ( ) ;
616654
617- let signer = RequestSigner {
618- date,
655+ let authorizer = AwsAuthorizer {
656+ date : Some ( date ) ,
619657 credential : & credential,
620658 service : "s3" ,
621659 region : "us-east-1" ,
622660 sign_payload : true ,
623661 } ;
624662
625- signer . sign ( & mut request, None ) ;
663+ authorizer . authorize ( & mut request, None ) ;
626664 assert_eq ! ( request. headers( ) . get( AUTH_HEADER ) . unwrap( ) , "AWS4-HMAC-SHA256 Credential=H20ABqCkLZID4rLe/20220809/us-east-1/s3/aws4_request, SignedHeaders=host;x-amz-content-sha256;x-amz-date, Signature=9ebf2f92872066c99ac94e573b4e1b80f4dbb8a32b1e8e23178318746e7d1b4d" )
627665 }
628666
0 commit comments