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 e757f714465..0afc63facf4 100644
--- a/rusoto/core/src/signature.rs
+++ b/rusoto/core/src/signature.rs
@@ -418,7 +418,6 @@ impl SignedRequest {
         }
 
         // build the canonical request
-        let signed_headers = signed_headers(&self.headers);
         self.canonical_uri = canonical_uri(&self.path, &self.region);
         // 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
@@ -427,54 +426,36 @@ impl SignedRequest {
         } else {
             self.canonical_uri.clone()
         };
-        let canonical_headers = canonical_headers(&self.headers);
-
-        let canonical_request: String;
 
         let (digest, len) = match self.payload {
             None => {
-                canonical_request = format!(
-                    "{}\n{}\n{}\n{}\n{}\n{}",
-                    &self.method,
-                    canonical_uri,
-                    self.canonical_query_string,
-                    canonical_headers,
-                    signed_headers,
-                    EMPTY_SHA256_HASH
-                );
-                (Some(Cow::Borrowed(EMPTY_SHA256_HASH)), Some(0))
+                (Cow::Borrowed(EMPTY_SHA256_HASH), Some(0))
             }
             Some(SignedRequestPayload::Buffer(ref payload)) => {
                 let (digest, len) = digest_payload(&payload);
-                canonical_request = format!(
-                    "{}\n{}\n{}\n{}\n{}\n{}",
-                    &self.method,
-                    canonical_uri,
-                    self.canonical_query_string,
-                    canonical_headers,
-                    signed_headers,
-                    &digest
-                );
-                (Some(Cow::Owned(digest)), Some(len))
+                (Cow::Owned(digest), Some(len))
             }
             Some(SignedRequestPayload::Stream(ref stream)) => {
-                canonical_request = format!(
-                    "{}\n{}\n{}\n{}\n{}\n{}",
-                    &self.method,
-                    canonical_uri,
-                    self.canonical_query_string,
-                    canonical_headers,
-                    signed_headers,
-                    UNSIGNED_PAYLOAD
-                );
-                (Some(Cow::Borrowed(UNSIGNED_PAYLOAD)), stream.size_hint())
+                (Cow::Borrowed(UNSIGNED_PAYLOAD), stream.size_hint())
             }
         };
 
-        if let Some(digest) = digest {
-            self.remove_header("x-amz-content-sha256");
-            self.add_header("x-amz-content-sha256", &digest);
-        }
+        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);
+
+        let canonical_request = format!(
+            "{}\n{}\n{}\n{}\n{}\n{}",
+            &self.method,
+            canonical_uri,
+            self.canonical_query_string,
+            canonical_headers,
+            signed_headers,
+            digest
+        );
 
         if let Some(len) = len {
             self.remove_header("content-length");


Test patch: