mirror of
https://github.com/libretro/RetroArch.git
synced 2026-11-07 08:36:02 -10:00
S3: sign with the shared SHA-256 and HMAC, streamed
The SigV4 signer assembled the canonical request and the string to sign in 4 KiB stack buffers and MACed them with its own HMAC. A URI or copy source past 4 KiB was signed cut short, and so was a secret key over 123 octets (the setting takes 185), so those requests came back 403. The two buffers also put the signer's frame at 9 KiB. The canonical request now goes straight into sha256_stream_update as it is made, and the string to sign into hmac_sha256_update, both from crypto/kdf.c: nothing is assembled, so nothing is cut short. The query is sorted as an index of spans over the caller's string, on the stack up to 16 parameters, so the signer allocates only its result. The hand-written HMAC, the query copy and the old signature helper go. HAVE_S3 now needs HAVE_CRYPTO, which every build that defines it (Apple) already has; griffin says so if it is missing. s3_backup_test checks seven signatures against an independent SigV4 computation, including a 5001-character URI and a 150-octet secret, which master signs wrong. The query and out-of-memory tests follow the new shape, and the test rebuilds when the driver changes.
This commit is contained in:
1 parent
a7b2a2ef96
commit
44090f5ea9
4 files changed
+282
-351
No files matched your search
@@ -2144,6 +2144,10 @@ CLOUD SYNC
|
||||
#include "../network/cloud_sync/nfs.c"
|
||||
#endif
|
||||
#ifdef HAVE_S3
|
||||
/* SigV4 signs with crypto/kdf.c's SHA-256 and HMAC */
|
||||
#ifndef HAVE_CRYPTO
|
||||
#error "HAVE_S3 needs HAVE_CRYPTO"
|
||||
#endif
|
||||
#include "../network/cloud_sync/s3.c"
|
||||
#endif
|
||||
#endif
|
||||
|
||||
+163
-331
@@ -14,6 +14,8 @@
|
||||
|
||||
#include <encodings/base64.h>
|
||||
#include <lrc_hash.h>
|
||||
#include <crypto/crypto.h>
|
||||
#include <crypto/kdf.h>
|
||||
#include <string/stdstring.h>
|
||||
#include <time/rtime.h>
|
||||
#include <formats/rjson.h>
|
||||
@@ -444,6 +446,19 @@ static char* s3_url_encode(const char *input)
|
||||
return output;
|
||||
}
|
||||
|
||||
/* One query parameter: a span of the caller's string, and how much of
|
||||
* it is the name. */
|
||||
typedef struct
|
||||
{
|
||||
const char *p;
|
||||
size_t len;
|
||||
size_t name;
|
||||
} s3_query_param_t;
|
||||
|
||||
/* Parameters sorted without allocating, past which the index is on
|
||||
* the heap. Every request this client signs has one or two. */
|
||||
#define S3_QUERY_INLINE 16
|
||||
|
||||
/* qsort comparator: SigV4 sorts query parameters by URL-encoded name,
|
||||
* then by URL-encoded value, in byte order. The names are compared on
|
||||
* their own: on the whole "name=value" string, a name that extends
|
||||
@@ -451,280 +466,90 @@ static char* s3_url_encode(const char *input)
|
||||
* digits, '-', '.' and '%' all sort below '='. */
|
||||
static int s3_query_param_cmp(const void *a, const void *b)
|
||||
{
|
||||
const char *x = *(const char* const*)a;
|
||||
const char *y = *(const char* const*)b;
|
||||
size_t xn = strcspn(x, "=");
|
||||
size_t yn = strcspn(y, "=");
|
||||
int r = strncmp(x, y, xn < yn ? xn : yn);
|
||||
const s3_query_param_t *x = (const s3_query_param_t*)a;
|
||||
const s3_query_param_t *y = (const s3_query_param_t*)b;
|
||||
size_t xr = x->len - x->name;
|
||||
size_t yr = y->len - y->name;
|
||||
int r = memcmp(x->p, y->p,
|
||||
x->name < y->name ? x->name : y->name);
|
||||
|
||||
if (r)
|
||||
return r;
|
||||
if (xn != yn)
|
||||
return xn < yn ? -1 : 1;
|
||||
return strcmp(x + xn, y + yn);
|
||||
if (x->name != y->name)
|
||||
return x->name < y->name ? -1 : 1;
|
||||
if ((r = memcmp(x->p + x->name, y->p + y->name, xr < yr ? xr : yr)))
|
||||
return r;
|
||||
return xr == yr ? 0 : (xr < yr ? -1 : 1);
|
||||
}
|
||||
|
||||
/* Canonicalize query string parameters per SigV4: split on '&', sort
|
||||
* with s3_query_param_cmp, re-join. The caller encodes each name and
|
||||
* value with s3_url_encode_query_value, and writes every parameter as
|
||||
* name=value, even with an empty value ("uploads="). Returns NULL only
|
||||
* when out of memory; an empty query gives "". */
|
||||
static char* s3_canonicalize_query_string(const char *query_string)
|
||||
typedef void (*s3_emit_t)(void *data, const char *s, size_t len);
|
||||
|
||||
/* The canonical query string per SigV4, handed to @emit piece by
|
||||
* piece: @query's parameters, sorted with s3_query_param_cmp, joined
|
||||
* by '&'. Names and values arrive URL-encoded, and every parameter as
|
||||
* name=value, even with an empty value ("uploads="). False only out of
|
||||
* memory. */
|
||||
static bool s3_query_emit(const char *query, s3_emit_t emit, void *data)
|
||||
{
|
||||
size_t i;
|
||||
size_t query_len;
|
||||
size_t count = 1;
|
||||
size_t pos = 0;
|
||||
char *copy;
|
||||
char *out;
|
||||
char **params;
|
||||
s3_query_param_t inline_params[S3_QUERY_INLINE];
|
||||
s3_query_param_t *params = inline_params;
|
||||
const char *q;
|
||||
size_t count = 1;
|
||||
size_t i;
|
||||
|
||||
if (!query_string || !*query_string)
|
||||
return strdup("");
|
||||
|
||||
if (!(copy = strdup(query_string)))
|
||||
return NULL;
|
||||
|
||||
query_len = strlen(copy);
|
||||
for (i = 0; i < query_len; i++)
|
||||
if (copy[i] == '&')
|
||||
if (!query || !*query)
|
||||
return true;
|
||||
for (q = query; *q; q++)
|
||||
if (*q == '&')
|
||||
count++;
|
||||
if ( count > S3_QUERY_INLINE
|
||||
&& !(params = (s3_query_param_t*)malloc(count * sizeof(*params))))
|
||||
return false;
|
||||
|
||||
params = (char**)malloc(count * sizeof(char*));
|
||||
/* Sorting moves the parameters but adds nothing, so the result is
|
||||
* the input's length. */
|
||||
out = (char*)malloc(query_len + 1);
|
||||
|
||||
if (!params || !out)
|
||||
for (i = 0, q = query; i < count; i++)
|
||||
{
|
||||
free(params);
|
||||
free(out);
|
||||
free(copy);
|
||||
return NULL;
|
||||
const char *amp = strchr(q, '&');
|
||||
size_t len = amp ? (size_t)(amp - q) : strlen(q);
|
||||
const char *eq = (const char*)memchr(q, '=', len);
|
||||
params[i].p = q;
|
||||
params[i].len = len;
|
||||
params[i].name = eq ? (size_t)(eq - q) : len;
|
||||
q += len + (amp ? 1 : 0);
|
||||
}
|
||||
|
||||
/* Cut the copy into one string per parameter. */
|
||||
params[0] = copy;
|
||||
for (i = 0, count = 1; i < query_len; i++)
|
||||
{
|
||||
if (copy[i] == '&')
|
||||
{
|
||||
copy[i] = '\0';
|
||||
params[count++] = copy + i + 1;
|
||||
}
|
||||
}
|
||||
|
||||
qsort(params, count, sizeof(char*), s3_query_param_cmp);
|
||||
qsort(params, count, sizeof(*params), s3_query_param_cmp);
|
||||
|
||||
for (i = 0; i < count; i++)
|
||||
{
|
||||
size_t len = strlen(params[i]);
|
||||
if (i > 0)
|
||||
out[pos++] = '&';
|
||||
memcpy(out + pos, params[i], len);
|
||||
pos += len;
|
||||
if (i)
|
||||
emit(data, "&", 1);
|
||||
emit(data, params[i].p, params[i].len);
|
||||
}
|
||||
out[pos] = '\0';
|
||||
|
||||
free(params);
|
||||
free(copy);
|
||||
return out;
|
||||
if (params != inline_params)
|
||||
free(params);
|
||||
return true;
|
||||
}
|
||||
|
||||
/* Calculate HMAC-SHA256 and return binary result */
|
||||
static uint8_t* s3_hmac_sha256_bin(const uint8_t *key, size_t key_len, const char *data, uint8_t *output)
|
||||
/* @s, NUL-terminated, into the hash or the MAC being made */
|
||||
#define S3_SHA_STR(st, str) \
|
||||
sha256_stream_update((st), (const uint8_t*)(str), strlen(str))
|
||||
#define S3_MAC_STR(ctx, str) \
|
||||
hmac_sha256_update((ctx), (const uint8_t*)(str), strlen(str))
|
||||
|
||||
static void s3_sha_emit(void *data, const char *s, size_t len)
|
||||
{
|
||||
unsigned i = 0;
|
||||
char *inner_data = NULL;
|
||||
size_t data_len = strlen(data);
|
||||
|
||||
char inner_pad[64];
|
||||
char outer_pad[64];
|
||||
uint8_t key_hash[32];
|
||||
|
||||
char inner_hash_hex[65];
|
||||
uint8_t inner_hash_bin[32];
|
||||
char final_hash_hex[65];
|
||||
|
||||
/* Hash the key if it's longer than 64 bytes */
|
||||
if (key_len > 64)
|
||||
{
|
||||
char temp_hash[65];/* Use a temporary buffer for the hash */
|
||||
sha256_hash(temp_hash, (const uint8_t*)key, key_len);
|
||||
|
||||
/* Convert hex to binary */
|
||||
for (i = 0; i < 32; i++)
|
||||
{
|
||||
char hex_byte[3];
|
||||
hex_byte[0] = temp_hash[i*2];
|
||||
hex_byte[1] = temp_hash[i*2+1];
|
||||
hex_byte[2] = '\0';
|
||||
key_hash[i] = (uint8_t)strtol(hex_byte, NULL, 16);
|
||||
}
|
||||
key = key_hash;
|
||||
key_len = 32;
|
||||
}
|
||||
else
|
||||
{
|
||||
}
|
||||
|
||||
/* Pad the key to 64 bytes */
|
||||
memset(outer_pad, 0, 64);
|
||||
memset(inner_pad, 0, 64);
|
||||
if (key_len <= 64)
|
||||
{
|
||||
memcpy(outer_pad, key, key_len);
|
||||
memcpy(inner_pad, key, key_len);
|
||||
}
|
||||
else
|
||||
{
|
||||
/* Key is longer than 64 bytes, this shouldn't happen after hashing */
|
||||
memcpy(outer_pad, key, 64);
|
||||
memcpy(inner_pad, key, 64);
|
||||
}
|
||||
|
||||
/* XOR with constants */
|
||||
for (i = 0; i < 64; i++)
|
||||
{
|
||||
outer_pad[i] ^= 0x5c;
|
||||
inner_pad[i] ^= 0x36;
|
||||
}
|
||||
|
||||
/* Inner hash: H(K XOR 0x36, text) */
|
||||
inner_data = malloc(64 + data_len);
|
||||
if (!inner_data)
|
||||
return NULL;
|
||||
|
||||
memcpy(inner_data, inner_pad, 64);
|
||||
memcpy(inner_data + 64, data, data_len);
|
||||
|
||||
/* Hash and convert to binary */
|
||||
sha256_hash(inner_hash_hex, (const uint8_t*)inner_data, 64 + data_len);
|
||||
|
||||
for (i = 0; i < 32; i++)
|
||||
{
|
||||
char hex_byte[3];
|
||||
hex_byte[0] = inner_hash_hex[i*2];
|
||||
hex_byte[1] = inner_hash_hex[i*2+1];
|
||||
hex_byte[2] = '\0';
|
||||
inner_hash_bin[i] = (uint8_t)strtol(hex_byte, NULL, 16);
|
||||
}
|
||||
|
||||
/* Outer hash: H(K XOR 0x5c, inner_hash_bin) */
|
||||
/* Ensure we have enough space for outer hash (64 + 32 = 96 bytes) */
|
||||
if (64 + data_len < 96)
|
||||
{
|
||||
/* Reallocate if needed */
|
||||
char *outer_data = realloc(inner_data, 96);
|
||||
if (!outer_data)
|
||||
{
|
||||
free(inner_data);
|
||||
return NULL;
|
||||
}
|
||||
inner_data = outer_data;
|
||||
}
|
||||
|
||||
memcpy(inner_data, outer_pad, 64);
|
||||
memcpy(inner_data + 64, inner_hash_bin, 32);
|
||||
|
||||
sha256_hash(final_hash_hex, (const uint8_t*)inner_data, 64 + 32);
|
||||
|
||||
/* Convert final result to binary */
|
||||
for (i = 0; i < 32; i++)
|
||||
{
|
||||
char hex_byte[3];
|
||||
hex_byte[0] = final_hash_hex[i*2];
|
||||
hex_byte[1] = final_hash_hex[i*2+1];
|
||||
hex_byte[2] = '\0';
|
||||
output[i] = (uint8_t)strtol(hex_byte, NULL, 16);
|
||||
}
|
||||
|
||||
/* Cleanup */
|
||||
free(inner_data);
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
/* Calculate AWS Signature Version 4 signature with proper key derivation */
|
||||
static char* s3_calculate_aws4_signature(const char *secret_key, const char *date,
|
||||
const char *region, const char *service,
|
||||
const char *string_to_sign)
|
||||
{
|
||||
unsigned i;
|
||||
char *signature;
|
||||
char aws4_key[128];
|
||||
uint8_t k_date[32];
|
||||
uint8_t k_region[32];
|
||||
uint8_t k_service[32];
|
||||
uint8_t k_signing[32];
|
||||
uint8_t final_signature[32];
|
||||
char k_date_hex[65];
|
||||
char k_region_hex[65];
|
||||
char k_service_hex[65];
|
||||
|
||||
signature = malloc(65);
|
||||
if (!signature)
|
||||
return NULL;
|
||||
|
||||
/* Build AWS4 key string */
|
||||
snprintf(aws4_key, sizeof(aws4_key), "AWS4%s", secret_key);
|
||||
|
||||
/* Derive keys step by step using binary HMAC throughout */
|
||||
/* Step 1: DateKey = HMAC-SHA256("AWS4" + SecretAccessKey, Date) */
|
||||
/* Note: aws4_key is a string, but HMAC treats it as byte data */
|
||||
if (!s3_hmac_sha256_bin((uint8_t*)aws4_key, strlen(aws4_key), date, k_date))
|
||||
goto error;
|
||||
|
||||
/* Debug: Show k_date */
|
||||
for (i = 0; i < 32; i++)
|
||||
snprintf(k_date_hex + 2 * i, 3, "%02x", (unsigned)k_date[i]);
|
||||
k_date_hex[64] = '\0';
|
||||
/* Step 2: DateRegionKey = HMAC-SHA256(DateKey, Region) */
|
||||
if (!s3_hmac_sha256_bin(k_date, 32, region, k_region))
|
||||
goto error;
|
||||
|
||||
/* Debug: Show k_region */
|
||||
for (i = 0; i < 32; i++)
|
||||
snprintf(k_region_hex + 2 * i, 3, "%02x", (unsigned)k_region[i]);
|
||||
k_region_hex[64] = '\0';
|
||||
/* Step 3: DateRegionServiceKey = HMAC-SHA256(DateRegionKey, Service) */
|
||||
if (!s3_hmac_sha256_bin(k_region, 32, service, k_service))
|
||||
goto error;
|
||||
|
||||
/* Debug: Show k_service */
|
||||
for (i = 0; i < 32; i++)
|
||||
snprintf(k_service_hex + 2 * i, 3, "%02x", (unsigned)k_service[i]);
|
||||
k_service_hex[64] = '\0';
|
||||
/* Step 4: SigningKey = HMAC-SHA256(DateRegionServiceKey, "aws4_request") */
|
||||
if (!s3_hmac_sha256_bin(k_service, 32, "aws4_request", k_signing))
|
||||
goto error;
|
||||
|
||||
/* Calculate final signature using the derived signing key in binary
|
||||
* format
|
||||
*/
|
||||
if (!s3_hmac_sha256_bin(k_signing, 32, string_to_sign, final_signature))
|
||||
{
|
||||
RARCH_ERR(S3_PFX "Failed to calculate final signature\n");
|
||||
goto error;
|
||||
}
|
||||
|
||||
/* Convert final signature to hex string */
|
||||
for (i = 0; i < 32; i++)
|
||||
snprintf(signature + 2 * i, 3, "%02x", (unsigned)final_signature[i]);
|
||||
|
||||
signature[64] = '\0';
|
||||
return signature;
|
||||
|
||||
error:
|
||||
free(signature);
|
||||
return NULL;
|
||||
sha256_stream_update((struct sha256_state*)data, (const uint8_t*)s, len);
|
||||
}
|
||||
|
||||
/* AWS Signature Version 4 calculation with provided timestamp */
|
||||
/* @copy_source, when set, is the x-amz-copy-source value of a
|
||||
* CopyObject request. S3 requires every x-amz-* header a request
|
||||
* carries to be signed, so it joins the canonical and signed headers
|
||||
* in its sorted place, between x-amz-content-sha256 and x-amz-date. */
|
||||
* in its sorted place, between x-amz-content-sha256 and x-amz-date.
|
||||
* The canonical request is hashed as it is made, and the string to
|
||||
* sign MACed the same way: nothing is assembled, so nothing is cut
|
||||
* short. Returns the signature in hex, NULL out of memory. */
|
||||
static char* s3_calculate_signature_v4_with_time(const char *method, const char *canonical_uri,
|
||||
const char *query_string, const char *headers,
|
||||
const char *payload_hash, const char *region,
|
||||
@@ -732,101 +557,108 @@ static char* s3_calculate_signature_v4_with_time(const char *method, const char
|
||||
const char *secret_key, const char *host,
|
||||
const char *copy_source, time_t now)
|
||||
{
|
||||
char *signature = NULL;
|
||||
char date[16];
|
||||
char datetime[32];
|
||||
char credential_scope[128];
|
||||
char canonical_request[4096];
|
||||
char string_to_sign[4096];
|
||||
char signed_headers[256];
|
||||
char *canonical_headers = NULL;
|
||||
char *canonical_query_string = NULL;
|
||||
char *canonical_request_hash = NULL;
|
||||
|
||||
/* Use provided timestamp */
|
||||
static const char hex[] = "0123456789abcdef";
|
||||
struct sha256_state cr;
|
||||
struct hmac_sha256_ctx mac;
|
||||
uint8_t digest[32];
|
||||
uint8_t key[32];
|
||||
char cr_hash[65];
|
||||
char date[16];
|
||||
char datetime[32];
|
||||
/* "AWS4" and the secret, which the settings keep to 185 octets */
|
||||
char aws4_key[4 + 186];
|
||||
size_t secret_len = strlen(secret_key);
|
||||
unsigned i;
|
||||
char *signature;
|
||||
struct tm *tm_info = gmtime(&now);
|
||||
|
||||
(void)access_key;
|
||||
if (secret_len > sizeof(aws4_key) - 4)
|
||||
{
|
||||
RARCH_ERR(S3_PFX "Secret access key too long to sign with\n");
|
||||
return NULL;
|
||||
}
|
||||
if (!(signature = (char*)malloc(65)))
|
||||
return NULL;
|
||||
strftime(date, sizeof(date), "%Y%m%d", tm_info);
|
||||
strftime(datetime, sizeof(datetime), "%Y%m%dT%H%M%SZ", tm_info);
|
||||
|
||||
/* Build credential scope */
|
||||
snprintf(credential_scope, sizeof(credential_scope), "%s/%s/%s/aws4_request",
|
||||
date, region, service);
|
||||
|
||||
/* Build canonical headers */
|
||||
/* The canonical request, into its hash */
|
||||
sha256_stream_init(&cr, 0);
|
||||
S3_SHA_STR(&cr, method);
|
||||
S3_SHA_STR(&cr, "\n");
|
||||
S3_SHA_STR(&cr, canonical_uri);
|
||||
S3_SHA_STR(&cr, "\n");
|
||||
/* Out of memory, the request is not signed at all: a signature made
|
||||
* without the query only shows up later, as a 403. */
|
||||
if (!s3_query_emit(query_string, s3_sha_emit, &cr))
|
||||
{
|
||||
free(signature);
|
||||
return NULL;
|
||||
}
|
||||
S3_SHA_STR(&cr, "\n");
|
||||
if (headers)
|
||||
{
|
||||
/* Format headers for canonical request - must be sorted alphabetically
|
||||
* and lowercase. Each header must be trimmed and end with a newline.
|
||||
* Each header must be in the following order:
|
||||
* host
|
||||
* x-amz-content-sha256
|
||||
* x-amz-date
|
||||
*/
|
||||
size_t canonical_len = 512 + (copy_source ? strlen(copy_source) : 0);
|
||||
/* Out of memory, the request is not signed at all: a signature
|
||||
* made without the headers or the query only shows up later, as
|
||||
* a 403. */
|
||||
if (!(canonical_headers = malloc(canonical_len)))
|
||||
goto cleanup;
|
||||
|
||||
/* sorted, lowercase, each ending in a newline */
|
||||
S3_SHA_STR(&cr, "host:");
|
||||
S3_SHA_STR(&cr, host);
|
||||
S3_SHA_STR(&cr, "\nx-amz-content-sha256:");
|
||||
S3_SHA_STR(&cr, payload_hash);
|
||||
if (copy_source)
|
||||
snprintf(canonical_headers, canonical_len,
|
||||
"host:%s\nx-amz-content-sha256:%s\nx-amz-copy-source:%s\nx-amz-date:%s\n",
|
||||
host, payload_hash, copy_source, datetime);
|
||||
else
|
||||
snprintf(canonical_headers, canonical_len,
|
||||
"host:%s\nx-amz-content-sha256:%s\nx-amz-date:%s\n",
|
||||
host, payload_hash, datetime);
|
||||
{
|
||||
S3_SHA_STR(&cr, "\nx-amz-copy-source:");
|
||||
S3_SHA_STR(&cr, copy_source);
|
||||
}
|
||||
S3_SHA_STR(&cr, "\nx-amz-date:");
|
||||
S3_SHA_STR(&cr, datetime);
|
||||
S3_SHA_STR(&cr, "\n");
|
||||
}
|
||||
|
||||
/* Build signed headers list */
|
||||
strlcpy(signed_headers, copy_source
|
||||
S3_SHA_STR(&cr, "\n");
|
||||
S3_SHA_STR(&cr, copy_source
|
||||
? "host;x-amz-content-sha256;x-amz-copy-source;x-amz-date"
|
||||
: "host;x-amz-content-sha256;x-amz-date", sizeof(signed_headers));
|
||||
: "host;x-amz-content-sha256;x-amz-date");
|
||||
S3_SHA_STR(&cr, "\n");
|
||||
S3_SHA_STR(&cr, payload_hash);
|
||||
sha256_stream_final(&cr, digest);
|
||||
for (i = 0; i < 32; i++)
|
||||
{
|
||||
cr_hash[2 * i] = hex[digest[i] >> 4];
|
||||
cr_hash[2 * i + 1] = hex[digest[i] & 15];
|
||||
}
|
||||
cr_hash[64] = '\0';
|
||||
|
||||
/* Build canonical query string */
|
||||
if (query_string
|
||||
&& !(canonical_query_string =
|
||||
s3_canonicalize_query_string(query_string)))
|
||||
goto cleanup;
|
||||
/* The signing key: HMAC chained from "AWS4" and the secret, through
|
||||
* the date, the region, the service and "aws4_request" */
|
||||
memcpy(aws4_key, "AWS4", 4);
|
||||
memcpy(aws4_key + 4, secret_key, secret_len);
|
||||
hmac_sha256((const uint8_t*)aws4_key, 4 + secret_len,
|
||||
(const uint8_t*)date, strlen(date), key);
|
||||
hmac_sha256(key, 32, (const uint8_t*)region, strlen(region), key);
|
||||
hmac_sha256(key, 32, (const uint8_t*)service, strlen(service), key);
|
||||
hmac_sha256(key, 32, (const uint8_t*)"aws4_request", 12, key);
|
||||
|
||||
/* Build canonical request */
|
||||
snprintf(canonical_request, sizeof(canonical_request),
|
||||
"%s\n%s\n%s\n%s\n%s\n%s",
|
||||
method,
|
||||
canonical_uri,
|
||||
canonical_query_string ? canonical_query_string : "",
|
||||
canonical_headers ? canonical_headers : "",
|
||||
signed_headers,
|
||||
payload_hash);
|
||||
|
||||
/* Calculate SHA256 hash of canonical request */
|
||||
canonical_request_hash = s3_sha256_hash(canonical_request, strlen(canonical_request));
|
||||
if (!canonical_request_hash)
|
||||
goto cleanup;
|
||||
|
||||
/* Build string to sign */
|
||||
snprintf(string_to_sign, sizeof(string_to_sign),
|
||||
"%s\n%s\n%s\n%s",
|
||||
S3_SIGNATURE_VERSION,
|
||||
datetime,
|
||||
credential_scope,
|
||||
canonical_request_hash);
|
||||
|
||||
/* Calculate signature using AWS Signature Version 4 key derivation */
|
||||
signature = s3_calculate_aws4_signature(secret_key, date, region, service, string_to_sign);
|
||||
if (!signature)
|
||||
RARCH_ERR(S3_PFX "Failed to calculate signature\n");
|
||||
|
||||
/* Cleanup */
|
||||
cleanup:
|
||||
if (canonical_headers)
|
||||
free(canonical_headers);
|
||||
if (canonical_query_string)
|
||||
free(canonical_query_string);
|
||||
if (canonical_request_hash)
|
||||
free(canonical_request_hash);
|
||||
/* The string to sign, into its MAC */
|
||||
hmac_sha256_init(&mac, key, 32);
|
||||
S3_MAC_STR(&mac, S3_SIGNATURE_VERSION "\n");
|
||||
S3_MAC_STR(&mac, datetime);
|
||||
S3_MAC_STR(&mac, "\n");
|
||||
S3_MAC_STR(&mac, date);
|
||||
S3_MAC_STR(&mac, "/");
|
||||
S3_MAC_STR(&mac, region);
|
||||
S3_MAC_STR(&mac, "/");
|
||||
S3_MAC_STR(&mac, service);
|
||||
S3_MAC_STR(&mac, "/aws4_request\n");
|
||||
S3_MAC_STR(&mac, cr_hash);
|
||||
hmac_sha256_final(&mac, digest);
|
||||
|
||||
for (i = 0; i < 32; i++)
|
||||
{
|
||||
signature[2 * i] = hex[digest[i] >> 4];
|
||||
signature[2 * i + 1] = hex[digest[i] & 15];
|
||||
}
|
||||
signature[64] = '\0';
|
||||
crypto_memzero(aws4_key, sizeof(aws4_key));
|
||||
crypto_memzero(key, sizeof(key));
|
||||
return signature;
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,8 @@ flags += -std=gnu99
|
||||
|
||||
# The test includes network/cloud_sync/s3.c so it can drive the driver
|
||||
# as shipped; everything below is what that translation unit needs to
|
||||
# link. The HTTP task layer is stubbed in stubs_retroarch.c; net_http.c
|
||||
# link (the signer's HMAC is libretro-common's crypto/kdf.c, which
|
||||
# brings crypto.c and md5.c). The HTTP task layer is stubbed in stubs_retroarch.c; net_http.c
|
||||
# is linked for the response header helpers the driver reads with.
|
||||
SOURCES_C := \
|
||||
s3_backup_test.c \
|
||||
@@ -28,6 +29,9 @@ SOURCES_C := \
|
||||
$(LIBRETRO_COMM_DIR)/encodings/encoding_base64.c \
|
||||
$(LIBRETRO_COMM_DIR)/encodings/encoding_utf.c \
|
||||
$(LIBRETRO_COMM_DIR)/hash/lrc_hash.c \
|
||||
$(LIBRETRO_COMM_DIR)/crypto/kdf.c \
|
||||
$(LIBRETRO_COMM_DIR)/crypto/crypto.c \
|
||||
$(LIBRETRO_COMM_DIR)/utils/md5.c \
|
||||
$(LIBRETRO_COMM_DIR)/features/features_cpu.c \
|
||||
$(LIBRETRO_COMM_DIR)/formats/json/rjson.c \
|
||||
$(LIBRETRO_COMM_DIR)/string/stdstring.c \
|
||||
@@ -54,6 +58,9 @@ OBJS := $(call sample_objs,$(SOURCES_C:.c=.o))
|
||||
|
||||
all: $(TARGET)
|
||||
|
||||
# The test includes the driver, so it rebuilds when the driver changes
|
||||
s3_backup_test.o: $(CORE_DIR)/network/cloud_sync/s3.c
|
||||
|
||||
%.o: %.c
|
||||
$(CC) $(INCDIRS) $(CFLAGS) $(flags) -c -o$@ $<
|
||||
|
||||
|
||||
@@ -353,22 +353,55 @@ static char *sign_failing_alloc(unsigned nth)
|
||||
return sig;
|
||||
}
|
||||
|
||||
/* The signer allocates the canonical headers first, then the canonical
|
||||
* query. Losing either must give no signature: one made without them
|
||||
* only shows up later, as a 403 from S3. */
|
||||
/* The signer allocates nothing but its result, and the index of a query
|
||||
* with more parameters than it sorts in place. Losing either must give
|
||||
* no signature: one made without the query only shows up later, as a
|
||||
* 403 from S3. */
|
||||
static void test_signer_out_of_memory(void)
|
||||
{
|
||||
char *sig = sign_failing_alloc(0);
|
||||
static char many[128];
|
||||
char *sig = sign_failing_alloc(0);
|
||||
int n;
|
||||
int i;
|
||||
|
||||
CHECK(sig, "the signer must sign when memory is available");
|
||||
free(sig);
|
||||
|
||||
sig = sign_failing_alloc(1);
|
||||
CHECK(!sig, "no memory for the headers, but it signed: %s", sig);
|
||||
CHECK(!sig, "no memory for the signature, but it signed: %s", sig);
|
||||
free(sig);
|
||||
|
||||
sig = sign_failing_alloc(2);
|
||||
CHECK(!sig, "no memory for the query, but it signed: %s", sig);
|
||||
CHECK(sig, "a short query must sign without another allocation");
|
||||
free(sig);
|
||||
|
||||
for (n = 0, i = 0; i < 17; i++)
|
||||
n += snprintf(many + n, sizeof(many) - n, "%sq%02d=v", i ? "&" : "", i);
|
||||
fail_alloc = 2;
|
||||
sig = s3_calculate_signature_v4_with_time("GET", "/", many,
|
||||
"host;x-amz-content-sha256;x-amz-date", S3_EMPTY_PAYLOAD_SHA256,
|
||||
"us-east-1", S3_SERVICE, "access", "secret", "example.com",
|
||||
NULL, 0);
|
||||
fail_alloc = 0;
|
||||
CHECK(!sig, "no memory for the query index, but it signed: %s", sig);
|
||||
free(sig);
|
||||
}
|
||||
|
||||
struct query_sink
|
||||
{
|
||||
size_t len;
|
||||
char buf[512];
|
||||
};
|
||||
|
||||
static void query_sink_emit(void *data, const char *s, size_t len)
|
||||
{
|
||||
struct query_sink *q = (struct query_sink*)data;
|
||||
if (q->len + len < sizeof(q->buf))
|
||||
{
|
||||
memcpy(q->buf + q->len, s, len);
|
||||
q->len += len;
|
||||
}
|
||||
q->buf[q->len] = '\0';
|
||||
}
|
||||
|
||||
/* SigV4 sorts query parameters by name, then by value. Sorting the
|
||||
@@ -381,29 +414,83 @@ static void test_query_order(void)
|
||||
{ "a-b=1&a=2", "a=2&a-b=1" },
|
||||
{ "uploadId=x&partNumber=3", "partNumber=3&uploadId=x" },
|
||||
{ "k=2&k=1", "k=1&k=2" },
|
||||
{ "", "" }
|
||||
{ "k=10&k=1", "k=1&k=10" },
|
||||
{ "uploads=", "uploads=" },
|
||||
{ "", "" },
|
||||
{ "b=2&c=3&a=1&b=1", "a=1&b=1&b=2&c=3" }
|
||||
};
|
||||
size_t i;
|
||||
|
||||
for (i = 0; i < sizeof(cases) / sizeof(cases[0]); i++)
|
||||
{
|
||||
char *got = s3_canonicalize_query_string(cases[i][0]);
|
||||
CHECK(got && !strcmp(got, cases[i][1]),
|
||||
struct query_sink q;
|
||||
bool ok;
|
||||
q.len = 0;
|
||||
q.buf[0] = '\0';
|
||||
ok = s3_query_emit(cases[i][0], query_sink_emit, &q);
|
||||
CHECK(ok && !strcmp(q.buf, cases[i][1]),
|
||||
"query \"%s\": sorted to \"%s\", SigV4 wants \"%s\"",
|
||||
cases[i][0], got ? got : "(null)", cases[i][1]);
|
||||
free(got);
|
||||
cases[i][0], q.buf, cases[i][1]);
|
||||
}
|
||||
}
|
||||
|
||||
/* Each of its three allocations can fail, and none may leak. */
|
||||
for (i = 1; i <= 3; i++)
|
||||
/* Signatures against an independent SigV4 computation (Python's
|
||||
* hashlib and hmac over the same canonical request), at time 0: the
|
||||
* plain requests, a CopyObject, a query too long for the signer's own
|
||||
* index, a URI past 4 KiB and a 150-octet secret, both signed whole. */
|
||||
static void test_signature_known(void)
|
||||
{
|
||||
static char many[256];
|
||||
static char long_uri[5002];
|
||||
static char long_secret[151];
|
||||
static const char *const E = S3_EMPTY_PAYLOAD_SHA256;
|
||||
static const char *const HD = "host;x-amz-content-sha256;x-amz-date";
|
||||
struct
|
||||
{
|
||||
char *got;
|
||||
const char *method, *uri, *query, *region, *ak, *sk, *host, *copy;
|
||||
const char *want;
|
||||
} c[7];
|
||||
size_t i, n = 0;
|
||||
|
||||
fail_alloc = (unsigned)i;
|
||||
got = s3_canonicalize_query_string("b=2&a=1");
|
||||
fail_alloc = 0;
|
||||
CHECK(!got, "allocation %u failed, but it returned: %s", (unsigned)i, got);
|
||||
free(got);
|
||||
for (i = 20; i-- > 0;)
|
||||
n += (size_t)snprintf(many + n, sizeof(many) - n, "%sp%02u=%u",
|
||||
n ? "&" : "", (unsigned)i, (unsigned)(i % 3));
|
||||
long_uri[0] = '/';
|
||||
memset(long_uri + 1, 'k', 5000);
|
||||
long_uri[5001] = '\0';
|
||||
memset(long_secret, 'S', 150);
|
||||
long_secret[150] = '\0';
|
||||
|
||||
c[0].method = "PUT"; c[0].uri = "/key"; c[0].query = "uploadId=x";
|
||||
c[0].region = "us-east-1"; c[0].ak = "access"; c[0].sk = "secret";
|
||||
c[0].host = "example.com"; c[0].copy = NULL;
|
||||
c[0].want = "d808fe25cbc9179929e8278876efe7eafa74c6f8e61b5fcd48f336e049b05f09";
|
||||
c[1].method = "GET"; c[1].uri = "/retroarch/saves/a.srm"; c[1].query = "";
|
||||
c[1].region = "eu-west-2"; c[1].ak = "AKIDEXAMPLE";
|
||||
c[1].sk = "wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY";
|
||||
c[1].host = "bucket.s3.eu-west-2.amazonaws.com"; c[1].copy = NULL;
|
||||
c[1].want = "88628b3cd21ef9099ec3e65c4206ffdbfbdcb1c8bcea85788f1369b50c8dce9c";
|
||||
c[2] = c[0]; c[2].uri = "/dst"; c[2].query = "partNumber=3&uploadId=abc";
|
||||
c[2].copy = "bucket/src%20name";
|
||||
c[2].want = "3b1f9dc7df0aa60e39acdbb7e83ffe00eff9d1dc48a31b667155c7728bc090c7";
|
||||
c[3] = c[0]; c[3].method = "POST"; c[3].uri = "/k"; c[3].query = "uploads=";
|
||||
c[3].want = "ea8e6b2d0d077940803de2f7528709ee5051d071c3dd9abff1cb2f2547b27456";
|
||||
c[4] = c[0]; c[4].method = "GET"; c[4].uri = "/"; c[4].query = many;
|
||||
c[4].want = "f3afef1486dc1ad64f77dc483359a0e40c69fcb4ffba1efc623f9735dc1a700d";
|
||||
c[5] = c[0]; c[5].uri = long_uri; c[5].query = "";
|
||||
c[5].want = "d5e860eac53dd460cea810f05454bbd084c3a37166a2a059ad362592f0efc2d3";
|
||||
c[6] = c[0]; c[6].query = ""; c[6].sk = long_secret;
|
||||
c[6].want = "789d5f99519bb1bc6cd055d8f9d60eaee5204e62d093b432428e7e80b77beb16";
|
||||
|
||||
for (i = 0; i < sizeof(c) / sizeof(c[0]); i++)
|
||||
{
|
||||
char *sig = s3_calculate_signature_v4_with_time(c[i].method, c[i].uri,
|
||||
c[i].query, HD, E, c[i].region, S3_SERVICE, c[i].ak, c[i].sk,
|
||||
c[i].host, c[i].copy, 0);
|
||||
CHECK(sig && !strcmp(sig, c[i].want),
|
||||
"signature %u: %s, SigV4 gives %s", (unsigned)i,
|
||||
sig ? sig : "(none)", c[i].want);
|
||||
free(sig);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -427,6 +514,7 @@ int main(void)
|
||||
test_no_backup_wanted(tmp);
|
||||
test_signer_out_of_memory();
|
||||
test_query_order();
|
||||
test_signature_known();
|
||||
|
||||
remove(tmp);
|
||||
|
||||
|
||||
Reference in new issue
Block a user