Repository: rusoto/rusoto (language: rust)

Issue:
"X-Amz-Content-Sha256 is not signed" error when upload file to Yandex Object Storage
When uploading to [Yandex Object Storage](https://cloud.yandex.com/services/storage) using the following code:

```rust
use rusoto_s3::{PutObjectRequest, S3, S3Client};

fn main() {
    let access_key = "<MY_ACCESS_KEY>".to_owned();
    let secret_key = "<MY_SECRET_KEY>".to_owned();

    let credentials_provider = rusoto_credential::StaticProvider::new(access_key, secret_key, None, None);
    let region = rusoto_core::Region::Custom {
        name: String::from("us-east-1"),
        endpoint: String::from("storage.yandexcloud.net"),
    };
    let s3_client = S3Client::new_with(rusoto_core::HttpClient::new().unwrap(), credentials_provider, region);
    s3_client.put_object(PutObjectRequest {
        bucket: String::from("<MY_BUCKET>"),
        body: Some("example string".to_owned().into_bytes().into()),
        ..Default::default()
    }).sync().expect("could not upload");
}
```

I receive error with following response from yandex storage api:

```xml
<Error>
    <Code>AccessDenied</Code>
    <Message>There were headers present in the request which were not signed</Message>
    <Resource>/MY_BUCKET/</Resource>
    <RequestId>8a9c7ce77.......</RequestId>
    <HeadersNotSigned>x-amz-content-sha256</HeadersNotSigned>
</Error>
```

---

Yandex Object Storage [promise to support](https://cloud.yandex.com/docs/storage/instruments/) some of the Amazon S3 HTTP API methods. Also, I am able to successfully upload to Yandex Object Storage using [rust-s3](https://github.com/durch/rust-s3) library, with the following code:
<details>

```rust
use std::str;

extern crate s3;
use s3::bucket::Bucket;
use s3::credentials::Credentials;
use s3::error::S3Result;

const BUCKET: &str = "<MY_BUCKET>";
const MESSAGE: &str = "I want to go to S3";
const REGION: &str = "storage.yandexcloud.net";

pub fn main() -> S3Result<()> {
    let region = REGION.parse()?;
    let access_key = "<MY_ACCESS_KEY>".to_owned();
    let secret_key = "<MY_SECRET_KEY>".to_owned();
    let credentials = Credentials::new(Some(access_key), Some(secret_key), None, None);
    let bucket = Bucket::new(BUCKET, region, credentials)?;

    let (_, code) = bucket.put_object("test_file", MESSAGE.as_bytes(), "text/plain")?;
    assert_eq!(200, code);

    // Get the "test_file" contents and make sure that the returned message matches what we sent.
    let (data, code) = bucket.get_object("test_file")?;
    let string = str::from_utf8(&data).unwrap();
    assert_eq!(200, code);
    assert_eq!(MESSAGE, string);

    Ok(())
}
```

</details>


Discussion / hints:
Thanks for the report!

Looks like we do indeed sign the request then add a couple headers afterwards, such as `x-amz-content-sha256`. This could likely be fixed by moving [these lines](https://github.com/rusoto/rusoto/blob/master/rusoto/core/src/signature.rs#L474-L482) up to [here](https://github.com/rusoto/rusoto/blob/master/rusoto/core/src/signature.rs#L419).

(edit) actually it'll be a little more involved, but the gist of it is putting the headers in before signing the request headers.

Fix patch:
diff --git a/rusoto/core/src/signature.rs b/rusoto/core/src/signature.rs
index 25ee9e3d058..623902df81b 100644
--- a/rusoto/core/src/signature.rs
+++ b/rusoto/core/src/signature.rs
@@ -431,10 +431,6 @@ impl SignedRequest {
             self.add_header("X-Amz-Security-Token", token);
         }
 
-        let signed_headers = signed_headers(&self.headers);
-
-        let canonical_headers = canonical_headers(&self.headers);
-
         let digest = match self.payload {
             None => Cow::Borrowed(EMPTY_SHA256_HASH),
             Some(SignedRequestPayload::Buffer(ref payload)) => {
@@ -446,6 +442,10 @@ impl SignedRequest {
         self.remove_header("x-amz-content-sha256");
         self.add_header("x-amz-content-sha256", &digest);
 
+        let signed_headers = signed_headers(&self.headers);
+
+        let canonical_headers = canonical_headers(&self.headers);
+
         // Normalize URI paths according to RFC 3986. Remove redundant and relative path components. Each path segment must be URI-encoded twice (except for Amazon S3 which only gets URI-encoded once).
         // see https://docs.aws.amazon.com/general/latest/gr/sigv4-create-canonical-request.html
         let canonical_uri = if &self.service != "s3" {
@@ -1000,4 +1000,29 @@ mod tests {
             "hostname.with.scheme"
         );
     }
+    
+    #[test]
+    fn x_amz_content_sha256_header_is_signed() {
+        // https://github.com/rusoto/rusoto/issues/1463
+
+        let provider = ProfileProvider::with_configuration(
+            "test_resources/multiple_profile_credentials",
+            "foo",
+        );
+        let mut request = SignedRequest::new(
+            "GET",
+            "s3",
+            &Region::UsEast1,
+            "/path",
+        );
+        request.sign(provider.credentials().wait().as_ref().unwrap());
+
+        let authorization_headers = request.headers.get("authorization").unwrap();
+        let authorization_header = authorization_headers[0].clone();
+        let authorization_header = String::from_utf8(authorization_header).unwrap();
+
+        // we want to check that "x-amz-content-sha256" header is signed
+        // and "authorization" header includes all signed headers
+        assert!(authorization_header.contains("x-amz-content-sha256"));
+    }
 }


Test patch: