AWS S3 HTTPS error SignatureDoesNotMatch

Hi, I am trying to connect to S3 with my ESP32 and I am using the arduino SDK. I followed these two AWS documentations on how to properly make a HTTP Get request: Create a signed AWS API request and Signature Calculations for the Authorization Header.

I verified my SHA256 caluclations with this online tool and my HMAC-SHA256 calcs with this tool

This is the error message I get (I removed the StringToSignBytes and CanonicalRequestBytes):

<Error><Code>SignatureDoesNotMatch</Code><Message>The request signature we calculated does not match the signature you provided. Check your key and signing method.</Message><AWSAccessKeyId>AKIAZOKIXEXAMPLE</AWSAccessKeyId>
<StringToSign>
AWS4-HMAC-SHA256
20230310T134304Z
20230310/eu-central-1/s3/aws4_request
5263692f39c1379ed930dfd5c7ed75b40ba0619c7dc316bb7630152162a5d5ef
</StringToSign><SignatureProvided>
2b974103174d38e1946911813cd76aff7271f4ace1b75947b09c67487d2c39da
</SignatureProvided><StringToSignBytes></StringToSignBytes>
<CanonicalRequest>
GET
/firmware.bin

content-type:application/octet-stream
host:esp-data-exchange.s3.amazonaws.com
x-amz-content-sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
x-amz-date:20230310T134304Z

content-type;host;x-amz-content-sha256;x-amz-date
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855

According to this AWS site I have a

Canonicalization errors

If you incorrectly calculated the canonical request or the string to sign, the signature verification step performed by the service fails with the following error message:

The request signature we calculated does not match the signature you provided

The error response includes the canonical request and the string to sign that the service calculated. You can compare these strings with the strings that you calculated.

You can also verify that you didn’t send the request through a proxy that modifies the headers or the request.

Checking the content of the error message, especially the Canonical request and String to Sign of my request are identical (or did I miss something?), see them below.
Canonical request:

GET
/firmware.bin

content-type:application/octet-stream
host:esp-data-exchange.s3.amazonaws.com
x-amz-content-sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
x-amz-date:20230310T134304Z

content-type;host;x-amz-content-sha256;x-amz-date
e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855

My string to sign:

AWS4-HMAC-SHA256
20230310T134304Z
20230310/eu-central-1/s3/aws4_request
5263692f39c1379ed930dfd5c7ed75b40ba0619c7dc316bb7630152162a5d5ef

Besides, the final signature, also present in the response, is also identical with my hash caluclations, both are: 2b974103174d38e1946911813cd76aff7271f4ace1b75947b09c67487d2c39da

Has anyone an idea what could be wrong? Do I misinterpret the error code from AWS and it could be something else? Any help is appreciated.

Since I wanted to make my code public anyway, this is a “minimal” example of my HTTPS S3 communication attempts. It has been tested in VScode, platformIO project (copy all content into main.cpp). It works if you add your wifi credentials. However, the request is outdated and my keys are not included - but for support it might help to understand what I am doing and help find the error.
PlatformIO.ini:

[env:firebeetle32]
platform = espressif32
board = firebeetle32
framework = arduino

EDIT: Here a “minimal” example.

  • Parameters like date and time are hardcoded but are the same like above.
  • Obviously, I cannot provide my keys. For the sig_calc I can provide the resulting hash from the first calculation where the AWS_SECRET_KEY is needed: 43d0ce257da1febfc3e750bb7c6eeab529041a7b69cc420a97039ce7e4e97a87
  • You need to be connected to WIFI - add your SSID and PASSWORD
  • The error you get is now, that the request is outdated otherwise the calculation results are like exaplained above.
    #include <Arduino.h>
    #include "mbedtls/md.h"
    #include "WiFiClientSecure.h"
    #include "WiFi.h"
    
    
    String generate_hmac_SHA256_hash(const char *hmac_key, const char *payload)
    {
        /* Instantiating hash context structure */
        mbedtls_md_context_t ctx;
        /* Defining hash type */
        mbedtls_md_type_t md_type = MBEDTLS_MD_SHA256;
        /* Byte buffer array for the hash result */
        byte hash_result[32];
        /* Initialize final hash string to be returned */
        String hash_str = "";
    
        /* Initialize the hash calculator structur */
        mbedtls_md_init(&ctx);
        /* Setup the hash type */
        mbedtls_md_setup(&ctx, mbedtls_md_info_from_type(md_type), 1);
        /* Proivde the key used for hashing */
        mbedtls_md_hmac_starts(&ctx, (const unsigned char *)hmac_key, strlen(hmac_key));
        /* Provide the payload to be hashed */
        mbedtls_md_hmac_update(&ctx, (const unsigned char *)payload, strlen(payload));
        /* Perform the hash process */
        mbedtls_md_hmac_finish(&ctx, hash_result);
        /* Free memory allocated for has process */
        mbedtls_md_free(&ctx);
        /* Convert the bytes into a hexadecimal string */
        for (int i = 0; i < sizeof(hash_result); i++)
        {
            /* Buffer for storing hex values */
            char hex_buffer[3];
            /* Converting the bytes and storing them in hex buffer */
            sprintf(hex_buffer, "%02x", (int)hash_result[i]);
            /* Concatenating the hex bytes to a string */
            hash_str += String(hex_buffer);
        }
    
        return hash_str;
    }
    
    String generate_SHA256_hash(const char *payload)
    {
        /* Instantiating hash context structure */
        mbedtls_md_context_t ctx;
        /* Defining hash type */
        mbedtls_md_type_t md_type = MBEDTLS_MD_SHA256;
        /* Byte buffer array for the hash result */
        byte hash_result[32];
        /* Initialize final hash string to be returned */
        String hash_str = "";
    
        /* Initialize the hash calculator structur */
        mbedtls_md_init(&ctx);
        /* Setup the hash type */
        mbedtls_md_setup(&ctx, mbedtls_md_info_from_type(md_type), 0);
        /* Start the message-digest computation */
        mbedtls_md_starts(&ctx);
        /* Provide the payload to be hashed */
        mbedtls_md_update(&ctx, (const unsigned char *)payload, strlen(payload));
        /* Perform the hash process */
        mbedtls_md_finish(&ctx, hash_result);
        /* Free memory allocated for has process */
        mbedtls_md_free(&ctx);
        /* Convert the bytes into a hexadecimal string */
        for (int i = 0; i < sizeof(hash_result); i++)
        {
            /* Buffer for storing hex values */
            char hex_buffer[3];
            /* Converting the bytes and storing them in hex buffer */
            sprintf(hex_buffer, "%02x", (int)hash_result[i]);
            /* Concatenating the hex bytes to a string */
            hash_str += String(hex_buffer);
        }
    
        return hash_str;
    }
    
    /* Step 1: Create a canonical request */
    String canonical_request(String payload, String method, String object, String query, String content_type, String bucket_name, String service, String host, String date, String time)
    {
      String payload_hash = "";
      if (payload == "")
      {
        payload_hash = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855";
      }
      else
      {
        payload_hash = generate_SHA256_hash(payload.c_str());
      }
      return method + "\n" +
              "/" + object + "\n" +
              query + "\n" +
              "content-type:" + content_type + "\n" +
              "host:" + bucket_name + "." + service + "." + host + "\n" +
              "x-amz-content-sha256:" + payload_hash + "\n" +
              "x-amz-date:" + date + "T" + time + "Z\n" +
              "\n" +
              "content-type;host;x-amz-content-sha256;x-amz-date\n" +
              payload_hash;
    }
    
    /* Step 2: Create a canonical request hash */
    String canoncial_request_hash(String canoncial_request)
    {
      return generate_SHA256_hash(canoncial_request.c_str());
    }
    
    /* Step 3: Create a string to sign */
    String string_2_sign(String algorithm, String date, String time, String region, String service, String hashed_canonical_request)
    {
      return algorithm + "\n" +
              date + "T" + time + "Z\n" +
              date + "/" + region + "/" + service + "/aws4_request\n" +
              hashed_canonical_request;
    }
    
    /* Step 4: Calculate the signature */
    String signature_calculation(int length, String array[])
    {
      String signing_key = array[0];
    
      for (int i = 1; i < length; i++)
      {
        Serial.println(signing_key.c_str());
        Serial.println(array[i].c_str());
        signing_key = generate_hmac_SHA256_hash(signing_key.c_str(), array[i].c_str());
      }
      return signing_key;
    }
    
    /* Step 5: Add the signature to the request */
    String authorization_header(String algorithm, String aws_access_key, String date, String region, String service, String signature)
    {
      return algorithm + " Credential=" + aws_access_key + "/" + date + "/" + region + "/" + service + "/aws4_request," +
              "SignedHeaders=content-type;host;x-amz-content-sha256;x-amz-date," + "Signature=" + signature;
    }
    
    // https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetObject.html
    /* Create a sample request header */
    String GET_header(String object, String content_type, String bucket, String service, String date, String time, String authorization_header)
    {
      return "GET /" + object + " HTTP/1.1\r\n" +
              "Content-Type: " + content_type + "\r\n" +
              "Host: " + bucket + "." + service + "." + "amazonaws.com\r\n" +
              "Authorization: " + authorization_header + "\r\n" +
              "x-amz-content-sha256: e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855\r\n" +
              "x-amz-date: " + date + "T" + time + "Z" + "\r\n\r\n";
    }
    
    
    void setup()
    {
      WiFiClientSecure ota1_client;
    
      String algorithm = "AWS4-HMAC-SHA256";
      String region = "eu-central-1";
      String method = "GET";
      String service = "s3";
      String bucket_name = "esp-data-exchange";
      String aws_secret_key = "AWS_SECRET_KEY";
      String aws_access_key = "AWS_ACCESS_KEY";
    
      String object = "firmware.bin";
      String content_type = "application/octet-stream";
      String host = "amazonaws.com";
      String payload = "";
      String query = "";
      String date = "20230310";
      String time = "134304";
      String date_time = "20230310T134304Z";
      int length = 6;
    
      Serial.begin(9600);
      delay(5000);
    
      WiFi.begin("SSID", "PASSWORD");
      
      String can_req = canonical_request(payload, method, object, query, content_type, bucket_name, service, host, date, time);
      String can_req_hash = canoncial_request_hash(can_req);
      String str_2_sign = string_2_sign(algorithm, date, time, region, service, can_req_hash);
      String hash_array[length] = {"AWS4" + aws_secret_key, date, region, service, "aws4_request", str_2_sign};
      String sig_calc = signature_calculation(length, hash_array);
      String auth_header = authorization_header(algorithm, aws_access_key, date, region, service, sig_calc);
      String http_header = GET_header(object, content_type, bucket_name, service, date, time, auth_header);
      String bucket_url = bucket_name + "." + service + "." + region + "." + host;
      
      //ota1_client.setCACert(AWS_CERT_CA);
      ota1_client.setInsecure();
      ota1_client.connect(bucket_url.c_str(), 443);
      Serial.println("Test");
      ota1_client.print(http_header);
    
      // Wait for the response
      while (ota1_client.connected() && !ota1_client.available())
        ;
    
      // Read and print the response
      while (ota1_client.available())
      {
        String line = ota1_client.readStringUntil('\r');
        Serial.print(line);
      }
      ota1_client.stop();
    }
    
    void loop()
    {
      // put your main code here, to run repeatedly:
    }

So I was finally able to get a valid signature. I know that my error was using the function generate_hmac_SHA256_hash they way I used it inside signature_calculation() but to be honest, until the end I did not find the explanation why only the first iteration was correct and afterwards using the hash result of the first calculation as the hash key for the second calculation (and so on…) did not work.
Anyway, I used a javascript to check if my hmac-sha256 calculation were correct. This is the verification code:

const crypto = require("crypto");

function HMAC(key, text) {
    return crypto.createHmac("sha256", key).update(text).digest();
}


const kSecret = "hCVSg1n1+mZgRwjktOv52NqA70jeayS/Z4yqJcD4";
const kDate = HMAC("AWS4" + kSecret,"20230314");
console.log("kDate hash is    :", kDate.toString('hex'));
const kRegion = HMAC(kDate,"eu-central-1");
console.log("kRegion hash is    :", kRegion.toString('hex'));
const kService = HMAC(kRegion,"s3");
console.log("kService hash is    :", kService.toString('hex'));
const kSigning = HMAC(kService,"aws4_request");
console.log("kSigning hash is    :", kSigning.toString('hex'));

I compiled it using this online compiler.
To ensure my credentials were working - I used postman as an online tool. It helped me to understand HTTP header design and response interpretation.

My posted approach from above did not work because I returned the hex-encoded version of my hash and used this hex-encoded string for the next hash, but as indicated in the documentation from AWS you have to provide the unsigned char buffer directly from the previous hash calculation. You are not allowed to convert this unsigned char into a hex-String or const char hex.

So far, so good but I was simply not able to calculate the correct hash result using the following approach. As you can see, I added a buffer as input argument for the generate_hmac_SHA256_hash() function to store the result in a buffer making it possible to directly use the previously calculated hash as key for the next hash calculation.

	String kDate_str, kRegion_str, kService_str, kSign_str, signature = "";
	byte hash_buffer[32];

    kDate_str = generate_hmac_SHA256_hash(AWS4_key.c_str(), "20230313", hash_buffer);
	kRegion_str = generate_hmac_SHA256_hash((const char*) hash_buffer, "eu-central-1", hash_buffer);
	kService_str = generate_hmac_SHA256_hash((const char*) hash_buffer, "s3", hash_buffer);
	kSign_str = generate_hmac_SHA256_hash((const char*) hash_buffer, "aws4_request", hash_buffer);
	signature = generate_hmac_SHA256_hash((const char*) hash_buffer, string_2_sign, hash_buffer);

Only the first hash was correct. Can someone explain to me why this approach is not working? Looks like I am overseeing something very simple I guess…

Finally, I ended up with this function, calculating the correct signature:

String signature_calculation(String* payload, size_t payload_size)
{
    /* Instantiating hash context structure */
    mbedtls_md_context_t ctx;
    /* Defining hash type */
    mbedtls_md_type_t md_type = MBEDTLS_MD_SHA256;
    /* Byte buffer array for the hash result */
	size_t buffer_length = 32;
    unsigned char hash_buffer[buffer_length]; 
    /* Initialize final hash string to be returned */
    String hash_str = "";
    
    /* Initialize the hash calculator structur */
    mbedtls_md_init(&ctx);
    /* Setup the hash type */
    mbedtls_md_setup(&ctx, mbedtls_md_info_from_type(md_type), 1);
	/* First iteration for signature has different key length */
	/* Start the message-digest computation */
	mbedtls_md_hmac_starts(&ctx, (const unsigned char *)payload[0].c_str(), strlen(payload[0].c_str()));
	/* Provide the payload to be hashed */
	mbedtls_md_hmac_update(&ctx, (const unsigned char *)payload[1].c_str(), strlen(payload[1].c_str()));
	/* Store result in hash_buffer */
	mbedtls_md_hmac_finish(&ctx, hash_buffer);
	/* Convert hash result into hex string an print it */
	hash_byte_to_hex(buffer_length, hash_buffer);

	/* Calculate a cascade of hashs using previous hash results as keys */
	for(int i = 2; i < payload_size; i++)
	{
		mbedtls_md_hmac_starts(&ctx, hash_buffer, buffer_length);
		mbedtls_md_hmac_update(&ctx, (const unsigned char *)payload[i].c_str(), strlen(payload[i].c_str()));
		mbedtls_md_hmac_finish(&ctx, hash_buffer);
		hash_str = hash_byte_to_hex(buffer_length, hash_buffer);
	}
	/* Free memory allocated for has process */
  	mbedtls_md_free(&ctx);
	/* Return final hash result as hex string*/
  	return hash_str;
}

Note that the hash_byte_to_hex() function simply converts the hash result into a hex string and prints it.

Hi, sorry if this a bit off-topic, but you can run the javascript on your desktop, too: You can just install nodejs and just test your code interactively, or if you’re using CLion or Visual Studio code those can also run javascript (I believe visual studio code requires an additional plugin, though)

Good point, I think my background as embedded developer, where you cannot simply perform quick code snippet tests in the same IDE (e.g. Code Composer Studio from TI) caused me to automatically go to “online compiler” :slight_smile: