Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions example.c
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@

#include "sigv4.h"

/* the signer works out of caller-owned buffers; ~8 KB, so keep it out of
automatic storage -- on a kernel stack it would not fit at all */
static aws_sigv4_scratch_t scratch;

int HMAC_SHA256(const unsigned char *data, size_t data_len,
const unsigned char *key, size_t key_len,
unsigned char *out, size_t *out_len)
Expand All @@ -29,6 +33,7 @@ int main()
.hmac_sha256 = HMAC_SHA256,
.sha256 = (void *)SHA256,
.sort = qsort,
.scratch = &scratch,
};

char auth_buf[AWS_SIGV4_AUTH_HEADER_MAX_LEN] = {0};
Expand Down
65 changes: 41 additions & 24 deletions sigv4.c
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,8 @@
#define AWS_SIGV4_SIGNING_ALGORITHM "AWS4-HMAC-SHA256"
#define SHA256_DIGEST_SIZE 32
#define AWS_SIGV4_HEX_SHA256_LENGTH SHA256_DIGEST_SIZE * 2
#define AWS_SIGV4_CANONICAL_REQUEST_BUF_LEN 4096 // Increased for large session tokens
#define AWS_SIGV4_STRING_TO_SIGN_BUF_LEN 1024
#define AWS_SIGV4_KEY_BUF_LEN 64
#define AWS_SIGV4_MAX_NUM_QUERY_COMPONENTS 50
#define HMAC_MAX_MD_CBLOCK 128
/* host, x-amz-date and x-amz-content-sha256 are appended to the caller's headers */
#define AWS_SIGV4_MAX_NUM_CANONICAL_HEADERS (AWS_SIGV4_MAX_NUM_HEADERS + 3)

/* writable space left in an output buffer ending at last (exclusive) */
static unsigned int space_left(unsigned char *str, unsigned char *last)
Expand Down Expand Up @@ -148,18 +143,23 @@ static unsigned char *construct_query_str(unsigned char *dst_cstr,
return dst_cstr;
}

static void parse_query_params(aws_sigv4_str_t *query_str,
aws_sigv4_kv_t *query_params,
size_t *arr_len)
/* parses at most AWS_SIGV4_MAX_NUM_QUERY_COMPONENTS components into query_params and
returns the number parsed; a longer query string is rejected by the caller rather
than silently signed without its trailing components */
static int parse_query_params(aws_sigv4_str_t *query_str,
aws_sigv4_kv_t *query_params,
size_t *arr_len)
{
*arr_len = 0;
if (aws_sigv4_empty_str(query_str) || query_params == NULL)
{
arr_len = 0;
return;
return AWS_SIGV4_OK;
}
size_t idx = 0;
unsigned char *c_ptr = query_str->data;
query_params[0].key.data = c_ptr;
/* a component without '=' never reaches the assignments below, so start every one
of them empty rather than reading whatever the scratch buffer happened to hold */
query_params[0] = (aws_sigv4_kv_t){.key = {.data = c_ptr}};
/* here we assume query string are well-formed */
while (c_ptr != query_str->data + query_str->len)
{
Expand All @@ -170,16 +170,27 @@ static void parse_query_params(aws_sigv4_str_t *query_str,
}
else if (*c_ptr == '&')
{
query_params[idx].value.len = c_ptr - query_params[idx].value.data;
query_params[++idx].key.data = ++c_ptr;
if (idx + 1 >= AWS_SIGV4_MAX_NUM_QUERY_COMPONENTS)
{
return AWS_SIGV4_BUFFER_OVERFLOW_ERROR;
}
if (query_params[idx].value.data != NULL)
{
query_params[idx].value.len = c_ptr - query_params[idx].value.data;
}
query_params[++idx] = (aws_sigv4_kv_t){.key = {.data = ++c_ptr}};
}
else
{
c_ptr++;
}
}
query_params[idx].value.len = c_ptr - query_params[idx].value.data;
if (query_params[idx].value.data != NULL)
{
query_params[idx].value.len = c_ptr - query_params[idx].value.data;
}
*arr_len = idx + 1;
return AWS_SIGV4_OK;
}

void get_hexdigest(aws_sigv4_str_t *str_in, aws_sigv4_str_t *hex_out)
Expand Down Expand Up @@ -250,11 +261,11 @@ void get_credential_scope(aws_sigv4_params_t *sigv4_params,
credential_scope->len = str - credential_scope->data;
}

void get_signed_headers(aws_sigv4_params_t *sigv4_params,
static void get_signed_headers(aws_sigv4_params_t *sigv4_params,
aws_sigv4_str_t *signed_headers,
unsigned char *last)
{
aws_sigv4_str_t headers[AWS_SIGV4_MAX_NUM_CANONICAL_HEADERS];
aws_sigv4_str_t *headers = sigv4_params->scratch->signed_headers;
unsigned num_headers = 0;
bool has_amz_content_sha256_header = false;
Comment thread
bonifaido marked this conversation as resolved.

Expand Down Expand Up @@ -295,11 +306,11 @@ void get_signed_headers(aws_sigv4_params_t *sigv4_params,
signed_headers->len = str - signed_headers->data;
}

void get_canonical_headers(aws_sigv4_params_t *sigv4_params,
static void get_canonical_headers(aws_sigv4_params_t *sigv4_params,
aws_sigv4_str_t *canonical_headers,
unsigned char *last)
{
aws_sigv4_kv_t headers[AWS_SIGV4_MAX_NUM_CANONICAL_HEADERS];
aws_sigv4_kv_t *headers = sigv4_params->scratch->canonical_headers;
unsigned num_headers = 0;
aws_sigv4_kv_t *amz_content_sha256_header = NULL;

Expand Down Expand Up @@ -351,7 +362,7 @@ void get_canonical_headers(aws_sigv4_params_t *sigv4_params,
canonical_headers->len = str - canonical_headers->data;
}

int get_canonical_request(aws_sigv4_params_t *sigv4_params,
static int get_canonical_request(aws_sigv4_params_t *sigv4_params,
aws_sigv4_str_t *canonical_request,
unsigned char *last)
{
Expand All @@ -366,9 +377,13 @@ int get_canonical_request(aws_sigv4_params_t *sigv4_params,
/* query string can be empty */
if (!aws_sigv4_empty_str(&sigv4_params->query_str))
{
aws_sigv4_kv_t query_params[AWS_SIGV4_MAX_NUM_QUERY_COMPONENTS];
aws_sigv4_kv_t *query_params = sigv4_params->scratch->query_params;
size_t query_num = 0;
parse_query_params(&sigv4_params->query_str, query_params, &query_num);
int rc = parse_query_params(&sigv4_params->query_str, query_params, &query_num);
if (rc != AWS_SIGV4_OK)
{
return rc;
}
sigv4_params->sort(query_params, query_num, sizeof(aws_sigv4_kv_t),
(aws_sigv4_compare_func_t)aws_sigv4_kv_cmp);
str = construct_query_str(str, last, query_params, query_num);
Expand Down Expand Up @@ -450,7 +465,7 @@ int get_string_to_sign(aws_sigv4_params_t *sigv4_params,
int aws_sigv4_sign(aws_sigv4_params_t *sigv4_params, aws_sigv4_header_t *auth_header)
{
int rc = AWS_SIGV4_OK;
if (auth_header == NULL || sigv4_params == NULL || aws_sigv4_empty_str(&sigv4_params->secret_access_key) || aws_sigv4_empty_str(&sigv4_params->access_key_id) || aws_sigv4_empty_str(&sigv4_params->method) || aws_sigv4_empty_str(&sigv4_params->uri) || aws_sigv4_empty_str(&sigv4_params->host) || aws_sigv4_empty_str(&sigv4_params->x_amz_date) || aws_sigv4_empty_str(&sigv4_params->region) || aws_sigv4_empty_str(&sigv4_params->service) || sigv4_params->sort == NULL || sigv4_params->sha256 == NULL || sigv4_params->hmac_sha256 == NULL)
if (auth_header == NULL || sigv4_params == NULL || aws_sigv4_empty_str(&sigv4_params->secret_access_key) || aws_sigv4_empty_str(&sigv4_params->access_key_id) || aws_sigv4_empty_str(&sigv4_params->method) || aws_sigv4_empty_str(&sigv4_params->uri) || aws_sigv4_empty_str(&sigv4_params->host) || aws_sigv4_empty_str(&sigv4_params->x_amz_date) || aws_sigv4_empty_str(&sigv4_params->region) || aws_sigv4_empty_str(&sigv4_params->service) || sigv4_params->sort == NULL || sigv4_params->sha256 == NULL || sigv4_params->hmac_sha256 == NULL || sigv4_params->scratch == NULL)
{
rc = AWS_SIGV4_INVALID_INPUT_ERROR;
goto err;
Expand Down Expand Up @@ -500,7 +515,8 @@ int aws_sigv4_sign(aws_sigv4_params_t *sigv4_params, aws_sigv4_header_t *auth_he
goto err;
}
/* Task 1: Create a canonical request */
unsigned char canonical_request_buf[AWS_SIGV4_CANONICAL_REQUEST_BUF_LEN] = {0};
unsigned char *canonical_request_buf = sigv4_params->scratch->canonical_request;
memset(canonical_request_buf, 0, AWS_SIGV4_CANONICAL_REQUEST_BUF_LEN);
aws_sigv4_str_t canonical_request = {.data = canonical_request_buf};
rc = get_canonical_request(sigv4_params, &canonical_request,
canonical_request_buf + AWS_SIGV4_CANONICAL_REQUEST_BUF_LEN - 1);
Expand All @@ -509,7 +525,8 @@ int aws_sigv4_sign(aws_sigv4_params_t *sigv4_params, aws_sigv4_header_t *auth_he
goto err;
}
/* Task 2: Create a string to sign */
unsigned char string_to_sign_buf[AWS_SIGV4_STRING_TO_SIGN_BUF_LEN] = {0};
unsigned char *string_to_sign_buf = sigv4_params->scratch->string_to_sign;
memset(string_to_sign_buf, 0, AWS_SIGV4_STRING_TO_SIGN_BUF_LEN);
aws_sigv4_str_t string_to_sign = {.data = string_to_sign_buf};
rc = get_string_to_sign(sigv4_params, &sigv4_params->x_amz_date, &credential_scope,
&canonical_request, &string_to_sign,
Expand Down
58 changes: 28 additions & 30 deletions sigv4.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@
#define AWS_SIGV4_OK 0
#define AWS_SIGV4_MAX_NUM_HEADERS 24
#define AWS_SIGV4_AUTH_HEADER_MAX_LEN 2048
#define AWS_SIGV4_CANONICAL_REQUEST_BUF_LEN 4096 // large enough for large session tokens
#define AWS_SIGV4_STRING_TO_SIGN_BUF_LEN 1024
#define AWS_SIGV4_MAX_NUM_QUERY_COMPONENTS 50
/* host, x-amz-date and x-amz-content-sha256 are appended to the caller's headers */
#define AWS_SIGV4_MAX_NUM_CANONICAL_HEADERS (AWS_SIGV4_MAX_NUM_HEADERS + 3)

typedef struct aws_sigv4_str_s
{
Expand All @@ -34,6 +39,20 @@ typedef struct aws_sigv4_kv_s
aws_sigv4_str_t value;
} aws_sigv4_kv_t;

/* Working buffers used while signing. This is ~8 KB, which does not fit on the
stack of every caller -- a Linux kernel task stack is 16 KB in total -- so the
caller owns the storage and passes it in via aws_sigv4_params_t.scratch. The
library itself never allocates. Its contents are meaningless to the caller and
need no initialisation; aws_sigv4_sign() only reads what it has written. */
typedef struct aws_sigv4_scratch_s
{
unsigned char canonical_request[AWS_SIGV4_CANONICAL_REQUEST_BUF_LEN];
unsigned char string_to_sign[AWS_SIGV4_STRING_TO_SIGN_BUF_LEN];
aws_sigv4_kv_t query_params[AWS_SIGV4_MAX_NUM_QUERY_COMPONENTS];
aws_sigv4_kv_t canonical_headers[AWS_SIGV4_MAX_NUM_CANONICAL_HEADERS];
aws_sigv4_str_t signed_headers[AWS_SIGV4_MAX_NUM_CANONICAL_HEADERS];
} aws_sigv4_scratch_t;

aws_sigv4_str_t aws_sigv4_string(const unsigned char *cstr);

int aws_sigv4_strcmp(aws_sigv4_str_t *str1, aws_sigv4_str_t *str2);
Expand Down Expand Up @@ -86,6 +105,11 @@ typedef struct aws_sigv4_params_s
const unsigned char *key, size_t key_len,
unsigned char *out, size_t *out_len);

/* Caller-owned working buffers, see aws_sigv4_scratch_t. Must be non-NULL;
aws_sigv4_sign() rejects the request with AWS_SIGV4_INVALID_INPUT_ERROR
otherwise. */
aws_sigv4_scratch_t *scratch;

} aws_sigv4_params_t;

/** @brief get hex encoding of a given string
Expand Down Expand Up @@ -124,36 +148,10 @@ void get_credential_scope(aws_sigv4_params_t *sigv4_params,
aws_sigv4_str_t *credential_scope,
unsigned char *last);

/** @brief get signed headers string
*
* @param[in] sigv4_params Pointer to a struct of sigv4 parameters
* @param[out] signed_headers Struct of buffer to store signed headers string
* @param[in] last End of the writable output buffer (exclusive)
*/
void get_signed_headers(aws_sigv4_params_t *sigv4_params,
aws_sigv4_str_t *signed_headers,
unsigned char *last);

/** @brief get canonical headers string
*
* @param[in] sigv4_params Pointer to a struct of sigv4 parameters
* @param[out] canonical_headers Struct of buffer to store canonical headers string
* @param[in] last End of the writable output buffer (exclusive)
*/
void get_canonical_headers(aws_sigv4_params_t *sigv4_params,
aws_sigv4_str_t *canonical_headers,
unsigned char *last);

/** @brief get canonical request string
*
* @param[in] sigv4_params Pointer to a struct of sigv4 parameters
* @param[out] canonical_request Struct of buffer to store canonical request string
* @param[in] last End of the writable output buffer (exclusive)
* @return Status code where zero for success and non-zero for failure
*/
int get_canonical_request(aws_sigv4_params_t *sigv4_params,
aws_sigv4_str_t *canonical_request,
unsigned char *last);
/* get_signed_headers(), get_canonical_headers() and get_canonical_request() build
their intermediate results in aws_sigv4_params_t.scratch and are only meaningful
part-way through a signing pass, so they are internal to sigv4.c rather than
exported. Call aws_sigv4_sign(), which owns the sequencing and validates scratch. */

/** @brief get string to sign
*
Expand Down
77 changes: 77 additions & 0 deletions test.c
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@

#include "sigv4.h"

/* the signer works out of caller-owned buffers; ~8 KB, so keep it out of
automatic storage -- on a kernel stack it would not fit at all */
static aws_sigv4_scratch_t scratch;

int HMAC_SHA256(const unsigned char *data, size_t data_len,
const unsigned char *key, size_t key_len,
unsigned char *out, size_t *out_len)
Expand All @@ -29,6 +33,7 @@ START_TEST(AwsSigv4Test_AwsSigv4Sign)
.hmac_sha256 = HMAC_SHA256,
.sha256 = (void *)SHA256,
.sort = qsort,
.scratch = &scratch,
};

char auth_buf[AWS_SIGV4_AUTH_HEADER_MAX_LEN] = {0};
Expand Down Expand Up @@ -80,6 +85,7 @@ START_TEST(AwsSigv4Test_AdditionalHeadersAreSigned)
.hmac_sha256 = HMAC_SHA256,
.sha256 = (void *)SHA256,
.sort = qsort,
.scratch = &scratch,
};

char auth_buf[AWS_SIGV4_AUTH_HEADER_MAX_LEN] = {0};
Expand Down Expand Up @@ -131,6 +137,7 @@ START_TEST(AwsSigv4Test_PrefixHeaderNamesAreOrdered)
.hmac_sha256 = HMAC_SHA256,
.sha256 = (void *)SHA256,
.sort = qsort,
.scratch = &scratch,
};

char auth_buf[AWS_SIGV4_AUTH_HEADER_MAX_LEN] = {0};
Expand Down Expand Up @@ -165,6 +172,7 @@ START_TEST(AwsSigv4Test_TooLargeCanonicalRequestFails)
.hmac_sha256 = HMAC_SHA256,
.sha256 = (void *)SHA256,
.sort = qsort,
.scratch = &scratch,
};

unsigned int i;
Expand All @@ -187,6 +195,73 @@ START_TEST(AwsSigv4Test_TooLargeCanonicalRequestFails)
}
END_TEST

/* the scratch buffers are mandatory: without them the signer has nowhere to build the
canonical request, so it must refuse rather than dereference NULL */
START_TEST(AwsSigv4Test_MissingScratchIsRejected)
{
aws_sigv4_params_t sigv4_params = {
.access_key_id = aws_sigv4_string((unsigned char *)"AKIDEXAMPLE"),
.secret_access_key = aws_sigv4_string((unsigned char *)"wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY"),
.method = aws_sigv4_string((unsigned char *)"GET"),
.uri = aws_sigv4_string((unsigned char *)"/"),
.host = aws_sigv4_string((unsigned char *)"riptides-logos.s3.eu-central-1.amazonaws.com"),
.region = aws_sigv4_string((unsigned char *)"eu-central-1"),
.service = aws_sigv4_string((unsigned char *)"s3"),
.x_amz_date = aws_sigv4_string((unsigned char *)"20260803T120000Z"),
.hmac_sha256 = HMAC_SHA256,
.sha256 = (void *)SHA256,
.sort = qsort,
.scratch = NULL,
};

char auth_buf[AWS_SIGV4_AUTH_HEADER_MAX_LEN] = {0};
aws_sigv4_header_t auth_header = {
.value = aws_sigv4_string((unsigned char *)auth_buf)};

int rc = aws_sigv4_sign(&sigv4_params, &auth_header);
ck_assert_int_eq(rc, AWS_SIGV4_INVALID_INPUT_ERROR);
}
END_TEST

/* a query string with more components than the parser can hold must fail the signing:
silently dropping the tail would write past the scratch array and produce a
canonical request that does not match what the request actually carries */
START_TEST(AwsSigv4Test_TooManyQueryParamsFails)
{
static char query[8 * 1024];
char *w = query;
int i;
for (i = 0; i < AWS_SIGV4_MAX_NUM_QUERY_COMPONENTS + 5; i++)
{
w += snprintf(w, sizeof(query) - (w - query), "%sk%02d=v%02d", i ? "&" : "", i, i);
}

aws_sigv4_params_t sigv4_params = {
.access_key_id = aws_sigv4_string((unsigned char *)"AKIDEXAMPLE"),
.secret_access_key = aws_sigv4_string((unsigned char *)"wJalrXUtnFEMI/K7MDENG+bPxRfiCYEXAMPLEKEY"),
.method = aws_sigv4_string((unsigned char *)"GET"),
.uri = aws_sigv4_string((unsigned char *)"/"),
.query_str = aws_sigv4_string((unsigned char *)query),
.host = aws_sigv4_string((unsigned char *)"riptides-logos.s3.eu-central-1.amazonaws.com"),
.region = aws_sigv4_string((unsigned char *)"eu-central-1"),
.service = aws_sigv4_string((unsigned char *)"s3"),
.x_amz_date = aws_sigv4_string((unsigned char *)"20260803T120000Z"),
.unsigned_payload = true,
.hmac_sha256 = HMAC_SHA256,
.sha256 = (void *)SHA256,
.sort = qsort,
.scratch = &scratch,
};

char auth_buf[AWS_SIGV4_AUTH_HEADER_MAX_LEN] = {0};
aws_sigv4_header_t auth_header = {
.value = aws_sigv4_string((unsigned char *)auth_buf)};

int rc = aws_sigv4_sign(&sigv4_params, &auth_header);
ck_assert_int_eq(rc, AWS_SIGV4_BUFFER_OVERFLOW_ERROR);
}
END_TEST

Suite *aws_sigv4_test_suite(void)
{
Suite *s;
Expand All @@ -197,6 +272,8 @@ Suite *aws_sigv4_test_suite(void)
tcase_add_test(tc_aws_sigv4_sign, AwsSigv4Test_AdditionalHeadersAreSigned);
tcase_add_test(tc_aws_sigv4_sign, AwsSigv4Test_PrefixHeaderNamesAreOrdered);
tcase_add_test(tc_aws_sigv4_sign, AwsSigv4Test_TooLargeCanonicalRequestFails);
tcase_add_test(tc_aws_sigv4_sign, AwsSigv4Test_MissingScratchIsRejected);
tcase_add_test(tc_aws_sigv4_sign, AwsSigv4Test_TooManyQueryParamsFails);
suite_add_tcase(s, tc_aws_sigv4_sign);
return s;
}
Expand Down
Loading