From 7c4f3473afa3e5501a21bf6e9466da52726bac8e Mon Sep 17 00:00:00 2001 From: asonix Date: Sun, 3 Mar 2024 14:35:01 -0600 Subject: Lowercase query parameter keys when parsing --- src/api/signature/payload.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'src/api/signature/payload.rs') diff --git a/src/api/signature/payload.rs b/src/api/signature/payload.rs index 0029716a..250e007e 100644 --- a/src/api/signature/payload.rs +++ b/src/api/signature/payload.rs @@ -197,7 +197,7 @@ pub fn parse_query_map(uri: &http::uri::Uri) -> Result { if let Some(query_str) = uri.query() { let query_pairs = url::form_urlencoded::parse(query_str.as_bytes()); for (key, val) in query_pairs { - if query.insert(key.to_string(), val.into_owned()).is_some() { + if query.insert(key.to_lowercase().to_string(), val.into_owned()).is_some() { return Err(Error::bad_request(format!( "duplicate query parameter: `{}`", key -- cgit v1.2.3 From c94bf45cbab61ad6471d43d8a7aabb8a32eaad38 Mon Sep 17 00:00:00 2001 From: asonix Date: Sun, 3 Mar 2024 16:26:57 -0600 Subject: Store original-cased query keys alongside query values --- src/api/signature/payload.rs | 33 +++++++++++++++++++++++---------- 1 file changed, 23 insertions(+), 10 deletions(-) (limited to 'src/api/signature/payload.rs') diff --git a/src/api/signature/payload.rs b/src/api/signature/payload.rs index 250e007e..515c0f91 100644 --- a/src/api/signature/payload.rs +++ b/src/api/signature/payload.rs @@ -31,7 +31,12 @@ pub const AWS4_HMAC_SHA256: &str = "AWS4-HMAC-SHA256"; pub const UNSIGNED_PAYLOAD: &str = "UNSIGNED-PAYLOAD"; pub const STREAMING_AWS4_HMAC_SHA256_PAYLOAD: &str = "STREAMING-AWS4-HMAC-SHA256-PAYLOAD"; -pub type QueryMap = HashMap; +pub type QueryMap = HashMap; + +pub struct QueryValue { + key: String, + value: String, +} pub async fn check_payload_signature( garage: &Garage, @@ -123,7 +128,7 @@ async fn check_presigned_signature( mut query: QueryMap, ) -> Result<(Option, Option), Error> { let algorithm = query.get(X_AMZ_ALGORITHM.as_str()).unwrap(); - let authorization = Authorization::parse_presigned(algorithm, &query)?; + let authorization = Authorization::parse_presigned(&algorithm.value, &query)?; // Verify that all necessary request headers are included in signed_headers // For AWSv4 pre-signed URLs, the following must be incldued: @@ -165,7 +170,7 @@ async fn check_presigned_signature( let name = HeaderName::from_bytes(name.as_bytes()).ok_or_bad_request("Invalid header name")?; if let Some(existing) = headers_mut.get(&name) { - if signed_headers.contains(&name) && existing.as_bytes() != value.as_bytes() { + if signed_headers.contains(&name) && existing.as_bytes() != value.value.as_bytes() { return Err(Error::bad_request(format!( "Conflicting values for `{}` in query parameters and request headers", name @@ -181,7 +186,7 @@ async fn check_presigned_signature( // that are not signed, however there is not much reason that this would happen) headers_mut.insert( name, - HeaderValue::from_bytes(value.as_bytes()) + HeaderValue::from_bytes(value.value.as_bytes()) .ok_or_bad_request("invalid query parameter value")?, ); } @@ -197,7 +202,14 @@ pub fn parse_query_map(uri: &http::uri::Uri) -> Result { if let Some(query_str) = uri.query() { let query_pairs = url::form_urlencoded::parse(query_str.as_bytes()); for (key, val) in query_pairs { - if query.insert(key.to_lowercase().to_string(), val.into_owned()).is_some() { + let key = key.into_owned(); + + let value = QueryValue { + key: key.clone(), + value: val.into_owned(), + }; + + if query.insert(key.to_lowercase(), value).is_some() { return Err(Error::bad_request(format!( "duplicate query parameter: `{}`", key @@ -306,7 +318,7 @@ pub fn canonical_request( // Canonical query string from passed HeaderMap let canonical_query_string = { let mut items = Vec::with_capacity(query.len()); - for (key, value) in query.iter() { + for (_, QueryValue { key, value }) in query.iter() { items.push(uri_encode(&key, true) + "=" + &uri_encode(&value, true)); } items.sort(); @@ -476,6 +488,7 @@ impl Authorization { let duration = query .get(X_AMZ_EXPIRES.as_str()) .ok_or_bad_request("X-Amz-Expires not found in query parameters")? + .value .parse() .map_err(|_| Error::bad_request("X-Amz-Expires is not a number".to_string()))?; @@ -488,18 +501,18 @@ impl Authorization { let date = query .get(X_AMZ_DATE.as_str()) .ok_or_bad_request("Missing X-Amz-Date field")?; - let date = parse_date(date)?; + let date = parse_date(&date.value)?; if Utc::now() - date > Duration::seconds(duration) { return Err(Error::bad_request("Date is too old".to_string())); } - let (key_id, scope) = parse_credential(cred)?; + let (key_id, scope) = parse_credential(&cred.value)?; Ok(Authorization { key_id, scope, - signed_headers: signed_headers.to_string(), - signature: signature.to_string(), + signed_headers: signed_headers.value.clone(), + signature: signature.value.clone(), content_sha256: UNSIGNED_PAYLOAD.to_string(), date, }) -- cgit v1.2.3 From c8e416aaa5203347a35407a929ac5dfcb1f2a6bc Mon Sep 17 00:00:00 2001 From: Alex Auvolat Date: Mon, 4 Mar 2024 13:28:54 +0100 Subject: [test-presigned] Use a HeaderMap type for QueryMap --- src/api/signature/payload.rs | 46 ++++++++++++++++++++++---------------------- 1 file changed, 23 insertions(+), 23 deletions(-) (limited to 'src/api/signature/payload.rs') diff --git a/src/api/signature/payload.rs b/src/api/signature/payload.rs index 515c0f91..d72736bb 100644 --- a/src/api/signature/payload.rs +++ b/src/api/signature/payload.rs @@ -31,11 +31,12 @@ pub const AWS4_HMAC_SHA256: &str = "AWS4-HMAC-SHA256"; pub const UNSIGNED_PAYLOAD: &str = "UNSIGNED-PAYLOAD"; pub const STREAMING_AWS4_HMAC_SHA256_PAYLOAD: &str = "STREAMING-AWS4-HMAC-SHA256-PAYLOAD"; -pub type QueryMap = HashMap; - +pub type QueryMap = HeaderMap; pub struct QueryValue { - key: String, - value: String, + /// Original key with potential uppercase characters, + /// for use in signature calculation + key: String, + value: String, } pub async fn check_payload_signature( @@ -45,7 +46,7 @@ pub async fn check_payload_signature( ) -> Result<(Option, Option), Error> { let query = parse_query_map(request.uri())?; - if query.contains_key(X_AMZ_ALGORITHM.as_str()) { + if query.contains_key(&X_AMZ_ALGORITHM) { // We check for presigned-URL-style authentification first, because // the browser or someting else could inject an Authorization header // that is totally unrelated to AWS signatures. @@ -127,7 +128,7 @@ async fn check_presigned_signature( request: &mut Request, mut query: QueryMap, ) -> Result<(Option, Option), Error> { - let algorithm = query.get(X_AMZ_ALGORITHM.as_str()).unwrap(); + let algorithm = query.get(&X_AMZ_ALGORITHM).unwrap(); let authorization = Authorization::parse_presigned(&algorithm.value, &query)?; // Verify that all necessary request headers are included in signed_headers @@ -141,7 +142,7 @@ async fn check_presigned_signature( // but the signature cannot be computed from a string that contains itself. // AWS specifies that all query params except X-Amz-Signature are included // in the canonical request. - query.remove(X_AMZ_SIGNATURE.as_str()); + query.remove(&X_AMZ_SIGNATURE); let canonical_request = canonical_request( service, request.method(), @@ -167,9 +168,7 @@ async fn check_presigned_signature( // then an InvalidRequest error is raised. let headers_mut = request.headers_mut(); for (name, value) in query.iter() { - let name = - HeaderName::from_bytes(name.as_bytes()).ok_or_bad_request("Invalid header name")?; - if let Some(existing) = headers_mut.get(&name) { + if let Some(existing) = headers_mut.get(name) { if signed_headers.contains(&name) && existing.as_bytes() != value.value.as_bytes() { return Err(Error::bad_request(format!( "Conflicting values for `{}` in query parameters and request headers", @@ -198,18 +197,19 @@ async fn check_presigned_signature( } pub fn parse_query_map(uri: &http::uri::Uri) -> Result { - let mut query = QueryMap::new(); + let mut query = QueryMap::with_capacity(0); if let Some(query_str) = uri.query() { let query_pairs = url::form_urlencoded::parse(query_str.as_bytes()); for (key, val) in query_pairs { - let key = key.into_owned(); + let name = + HeaderName::from_bytes(key.as_bytes()).ok_or_bad_request("Invalid header name")?; - let value = QueryValue { - key: key.clone(), - value: val.into_owned(), - }; + let value = QueryValue { + key: key.to_string(), + value: val.into_owned(), + }; - if query.insert(key.to_lowercase(), value).is_some() { + if query.insert(name, value).is_some() { return Err(Error::bad_request(format!( "duplicate query parameter: `{}`", key @@ -476,19 +476,19 @@ impl Authorization { } let cred = query - .get(X_AMZ_CREDENTIAL.as_str()) + .get(&X_AMZ_CREDENTIAL) .ok_or_bad_request("X-Amz-Credential not found in query parameters")?; let signed_headers = query - .get(X_AMZ_SIGNEDHEADERS.as_str()) + .get(&X_AMZ_SIGNEDHEADERS) .ok_or_bad_request("X-Amz-SignedHeaders not found in query parameters")?; let signature = query - .get(X_AMZ_SIGNATURE.as_str()) + .get(&X_AMZ_SIGNATURE) .ok_or_bad_request("X-Amz-Signature not found in query parameters")?; let duration = query - .get(X_AMZ_EXPIRES.as_str()) + .get(&X_AMZ_EXPIRES) .ok_or_bad_request("X-Amz-Expires not found in query parameters")? - .value + .value .parse() .map_err(|_| Error::bad_request("X-Amz-Expires is not a number".to_string()))?; @@ -499,7 +499,7 @@ impl Authorization { } let date = query - .get(X_AMZ_DATE.as_str()) + .get(&X_AMZ_DATE) .ok_or_bad_request("Missing X-Amz-Date field")?; let date = parse_date(&date.value)?; -- cgit v1.2.3