Unverified Commit fd19e678 authored by Kefu Chai's avatar Kefu Chai Committed by GitHub
Browse files

sig_v4: Normalize header values per AWS SigV4 specification (#393)

* sig_v4: Normalize header values per AWS SigV4 specification

According to the AWS Signature Version 4 specification, canonical header values must be normalized by:
1. Trimming leading and trailing whitespace
2. Converting sequential spaces to a single space

From AWS documentation:
> Trim any leading or trailing spaces, and convert sequential spaces
> in the value to a single space.

Reference: https://docs.aws.amazon.com/IAM/latest/UserGuide/create-signed-request.html



Without proper whitespace normalization, requests with header values containing multiple consecutive spaces or leading/trailing whitespace would fail signature validation with SignatureDoesNotMatch error.

Test case reproduction:
```python
metadata = {
    "My-header1": "    a   b   c  ",  # Leading/trailing spaces
    "My-Header2": "\"a   b   c\""     # Quoted string
}
client.put_object(bucket, key, data, user_metadata=metadata)

```

Signed-off-by: default avatarKefu Chai <tchaikov@gmail.com>

* remove allocation

---------

Signed-off-by: default avatarKefu Chai <tchaikov@gmail.com>
Co-authored-by: default avatarNugine <nugine@foxmail.com>
parent 0fb256d6
Loading
Loading
Loading
Loading
+51 −2
Original line number Diff line number Diff line
@@ -65,6 +65,31 @@ fn is_skipped_query_string(name: &str) -> bool {
    name == "X-Amz-Signature"
}

/// Normalize header value according to AWS `SigV4` specification:
/// Trim leading and trailing whitespace and replace sequential whitespace with a single space.
///
/// Reference: <https://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html>
fn normalize_header_value(ans: &mut String, value: &str) {
    let trimmed = value.trim();

    // Fast path: if no internal whitespace, append as-is
    if !trimmed.chars().any(char::is_whitespace) {
        ans.push_str(trimmed);
        return;
    }

    // Split on any whitespace and rejoin with single spaces
    let mut first = true;
    for word in trimmed.split_whitespace() {
        if first {
            first = false;
        } else {
            ans.push(' ');
        }
        ans.push_str(word);
    }
}

/// sha256 hash of an empty string
const EMPTY_STRING_SHA256_HASH: &str = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";

@@ -150,7 +175,7 @@ pub fn create_canonical_request(
            }
            ans.push_str(name);
            ans.push(':');
            ans.push_str(value.trim());
            normalize_header_value(&mut ans, value);
            ans.push('\n');
        }
        ans.push('\n');
@@ -401,7 +426,7 @@ pub fn create_presigned_canonical_request(
            }
            ans.push_str(name);
            ans.push(':');
            ans.push_str(value.trim());
            normalize_header_value(&mut ans, value);
            ans.push('\n');
        }
        ans.push('\n');
@@ -1182,4 +1207,28 @@ mod tests {
            assert_eq!(signature, "7ed3ea6c69ed841068bbdd3cc1eb92a9ae5a4b1b0635267066bd676f6edc0189");
        }
    }

    #[test]
    fn normalize_header_value_no_internal_whitespace() {
        let mut ans = String::new();
        // leading/trailing spaces should be trimmed, no internal whitespace => fast path
        normalize_header_value(&mut ans, "  value  ");
        assert_eq!(ans, "value");
    }

    #[test]
    fn normalize_header_value_collapse_whitespace() {
        let mut ans = String::new();
        // multiple spaces, tabs and newlines should collapse into single spaces
        normalize_header_value(&mut ans, "  foo   bar\tbaz\nqux  ");
        assert_eq!(ans, "foo bar baz qux");
    }

    #[test]
    fn normalize_header_value_only_spaces() {
        let mut ans = String::new();
        // value with only whitespace becomes empty string after trimming
        normalize_header_value(&mut ans, "    ");
        assert_eq!(ans, "");
    }
}