/*************************************************************************** * _ _ ____ _ * Project ___| | | | _ \| | * / __| | | | |_) | | * | (__| |_| | _ <| |___ * \___|\___/|_| \_\_____| * * Copyright (C) Daniel Stenberg, , et al. * * This software is licensed as described in the file COPYING, which * you should have received as part of this distribution. The terms * are also available at https://curl.haxx.se/docs/copyright.html. * * You may opt to use, copy, modify, merge, publish, distribute and/or sell * copies of the Software, and permit persons to whom the Software is * furnished to do so, under the terms of the COPYING file. * * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY * KIND, either express or implied. * * SPDX-License-Identifier: curl * ***************************************************************************/ #include "curl_setup.h" #if !defined(CURL_DISABLE_HTTP) && !defined(CURL_DISABLE_CRYPTO_AUTH) #include "urldata.h" #include "strcase.h" #include "strdup.h" #include "http_aws_sigv4.h" #include "curl_sha256.h" #include "transfer.h" #include "parsedate.h" #include "sendf.h" #include /* The last 3 #include files should be in this order */ #include "curl_printf.h" #include "curl_memory.h" #include "memdebug.h" #include "slist.h" #define HMAC_SHA256(k, kl, d, dl, o) \ do { \ ret = Curl_hmacit(Curl_HMAC_SHA256, \ (unsigned char *)k, \ kl, \ (unsigned char *)d, \ dl, o); \ if(ret) { \ goto fail; \ } \ } while(0) #define TIMESTAMP_SIZE 17 /* hex-encoded with trailing null */ #define SHA256_HEX_LENGTH (2 * SHA256_DIGEST_LENGTH + 1) static void sha256_to_hex(char *dst, unsigned char *sha) { int i; for(i = 0; i < SHA256_DIGEST_LENGTH; ++i) { msnprintf(dst + (i * 2), SHA256_HEX_LENGTH - (i * 2), "%02x", sha[i]); } } static char *find_date_hdr(struct Curl_easy *data, const char *sig_hdr) { char *tmp = Curl_checkheaders(data, sig_hdr, strlen(sig_hdr)); if(tmp) return tmp; return Curl_checkheaders(data, STRCONST("Date")); } /* remove whitespace, and lowercase all headers */ static void trim_headers(struct curl_slist *head) { struct curl_slist *l; for(l = head; l; l = l->next) { char *value; /* to read from */ char *store; size_t colon = strcspn(l->data, ":"); Curl_strntolower(l->data, l->data, colon); value = &l->data[colon]; if(!*value) continue; ++value; store = value; /* skip leading whitespace */ while(*value && ISBLANK(*value)) value++; while(*value) { int space = 0; while(*value && ISBLANK(*value)) { value++; space++; } if(space) { /* replace any number of consecutive whitespace with a single space, unless at the end of the string, then nothing */ if(*value) *store++ = ' '; } else *store++ = *value++; } *store = 0; /* null terminate */ } } /* maximum length for the aws sivg4 parts */ #define MAX_SIGV4_LEN 64 #define MAX_SIGV4_LEN_TXT "64" #define DATE_HDR_KEY_LEN (MAX_SIGV4_LEN + sizeof("X--Date")) #define MAX_HOST_LEN 255 /* FQDN + host: */ #define FULL_HOST_LEN (MAX_HOST_LEN + sizeof("host:")) /* string been x-PROVIDER-date:TIMESTAMP, I need +1 for ':' */ #define DATE_FULL_HDR_LEN (DATE_HDR_KEY_LEN + TIMESTAMP_SIZE + 1) /* timestamp should point to a buffer of at last TIMESTAMP_SIZE bytes */ static CURLcode make_headers(struct Curl_easy *data, const char *hostname, char *timestamp, char *provider1, char **date_header, char *content_sha256_header, struct dynbuf *canonical_headers, struct dynbuf *signed_headers) { char date_hdr_key[DATE_HDR_KEY_LEN]; char date_full_hdr[DATE_FULL_HDR_LEN]; struct curl_slist *head = NULL; struct curl_slist *tmp_head = NULL; CURLcode ret = CURLE_OUT_OF_MEMORY; struct curl_slist *l; int again = 1; /* provider1 mid */ Curl_strntolower(provider1, provider1, strlen(provider1)); provider1[0] = Curl_raw_toupper(provider1[0]); msnprintf(date_hdr_key, DATE_HDR_KEY_LEN, "X-%s-Date", provider1); /* provider1 lowercase */ Curl_strntolower(provider1, provider1, 1); /* first byte only */ msnprintf(date_full_hdr, DATE_FULL_HDR_LEN, "x-%s-date:%s", provider1, timestamp); if(Curl_checkheaders(data, STRCONST("Host"))) { head = NULL; } else { char full_host[FULL_HOST_LEN + 1]; if(data->state.aptr.host) { size_t pos; if(strlen(data->state.aptr.host) > FULL_HOST_LEN) { ret = CURLE_URL_MALFORMAT; goto fail; } strcpy(full_host, data->state.aptr.host); /* remove /r/n as the separator for canonical request must be '\n' */ pos = strcspn(full_host, "\n\r"); full_host[pos] = 0; } else { if(strlen(hostname) > MAX_HOST_LEN) { ret = CURLE_URL_MALFORMAT; goto fail; } msnprintf(full_host, FULL_HOST_LEN, "host:%s", hostname); } head = curl_slist_append(NULL, full_host); if(!head) goto fail; } if(*content_sha256_header) { tmp_head = curl_slist_append(head, content_sha256_header); if(!tmp_head) goto fail; head = tmp_head; } for(l = data->set.headers; l; l = l->next) { tmp_head = curl_slist_append(head, l->data); if(!tmp_head) goto fail; head = tmp_head; } trim_headers(head); *date_header = find_date_hdr(data, date_hdr_key); if(!*date_header) { tmp_head = curl_slist_append(head, date_full_hdr); if(!tmp_head) goto fail; head = tmp_head; *date_header = curl_maprintf("%s: %s", date_hdr_key, timestamp); } else { char *value; *date_header = strdup(*date_header); if(!*date_header) goto fail; value = strchr(*date_header, ':'); if(!value) goto fail; ++value; while(ISBLANK(*value)) ++value; strncpy(timestamp, value, TIMESTAMP_SIZE - 1); timestamp[TIMESTAMP_SIZE - 1] = 0; } /* alpha-sort in a case sensitive manner */ do { again = 0; for(l = head; l; l = l->next) { struct curl_slist *next = l->next; if(next && strcmp(l->data, next->data) > 0) { char *tmp = l->data; l->data = next->data; next->data = tmp; again = 1; } } } while(again); for(l = head; l; l = l->next) { char *tmp; if(Curl_dyn_add(canonical_headers, l->data)) goto fail; if(Curl_dyn_add(canonical_headers, "\n")) goto fail; tmp = strchr(l->data, ':'); if(tmp) *tmp = 0; if(l != head) { if(Curl_dyn_add(signed_headers, ";")) goto fail; } if(Curl_dyn_add(signed_headers, l->data)) goto fail; } ret = CURLE_OK; fail: curl_slist_free_all(head); return ret; } #define CONTENT_SHA256_KEY_LEN (MAX_SIGV4_LEN + sizeof("X--Content-Sha256")) /* add 2 for ": " between header name and value */ #define CONTENT_SHA256_HDR_LEN (CONTENT_SHA256_KEY_LEN + 2 + \ SHA256_HEX_LENGTH) /* try to parse a payload hash from the content-sha256 header */ static char *parse_content_sha_hdr(struct Curl_easy *data, const char *provider1, size_t *value_len) { char key[CONTENT_SHA256_KEY_LEN]; size_t key_len; char *value; size_t len; key_len = msnprintf(key, sizeof(key), "x-%s-content-sha256", provider1); value = Curl_checkheaders(data, key, key_len); if(!value) return NULL; value = strchr(value, ':'); if(!value) return NULL; ++value; while(*value && ISBLANK(*value)) ++value; len = strlen(value); while(len > 0 && ISBLANK(value[len-1])) --len; *value_len = len; return value; } static CURLcode calc_payload_hash(struct Curl_easy *data, unsigned char *sha_hash, char *sha_hex) { const char *post_data = data->set.postfields; size_t post_data_len = 0; CURLcode result; if(post_data) { if(data->set.postfieldsize < 0) post_data_len = strlen(post_data); else post_data_len = (size_t)data->set.postfieldsize; } result = Curl_sha256it(sha_hash, (const unsigned char *) post_data, post_data_len); if(!result) sha256_to_hex(sha_hex, sha_hash); return result; } #define S3_UNSIGNED_PAYLOAD "UNSIGNED-PAYLOAD" static CURLcode calc_s3_payload_hash(struct Curl_easy *data, Curl_HttpReq httpreq, char *provider1, unsigned char *sha_hash, char *sha_hex, char *header) { bool empty_method = (httpreq == HTTPREQ_GET || httpreq == HTTPREQ_HEAD); /* The request method or filesize indicate no request payload */ bool empty_payload = (empty_method || data->set.filesize == 0); /* The POST payload is in memory */ bool post_payload = (httpreq == HTTPREQ_POST && data->set.postfields); CURLcode ret = CURLE_OUT_OF_MEMORY; if(empty_payload || post_payload) { /* Calculate a real hash when we know the request payload */ ret = calc_payload_hash(data, sha_hash, sha_hex); if(ret) goto fail; } else { /* Fall back to s3's UNSIGNED-PAYLOAD */ size_t len = sizeof(S3_UNSIGNED_PAYLOAD) - 1; DEBUGASSERT(len < SHA256_HEX_LENGTH); /* 16 < 65 */ memcpy(sha_hex, S3_UNSIGNED_PAYLOAD, len); sha_hex[len] = 0; } /* format the required content-sha256 header */ msnprintf(header, CONTENT_SHA256_HDR_LEN, "x-%s-content-sha256: %s", provider1, sha_hex); ret = CURLE_OK; fail: return ret; } CURLcode Curl_output_aws_sigv4(struct Curl_easy *data, bool proxy) { CURLcode ret = CURLE_OUT_OF_MEMORY; struct connectdata *conn = data->conn; size_t len; const char *arg; char provider0[MAX_SIGV4_LEN + 1]=""; char provider1[MAX_SIGV4_LEN + 1]=""; char region[MAX_SIGV4_LEN + 1]=""; char service[MAX_SIGV4_LEN + 1]=""; bool sign_as_s3 = false; const char *hostname = conn->host.name; time_t clock; struct tm tm; char timestamp[TIMESTAMP_SIZE]; char date[9]; struct dynbuf canonical_headers; struct dynbuf signed_headers; char *date_header = NULL; Curl_HttpReq httpreq; const char *method = NULL; char *payload_hash = NULL; size_t payload_hash_len = 0; unsigned char sha_hash[SHA256_DIGEST_LENGTH]; char sha_hex[SHA256_HEX_LENGTH]; char content_sha256_hdr[CONTENT_SHA256_HDR_LEN + 2] = ""; /* add \r\n */ char *canonical_request = NULL; char *request_type = NULL; char *credential_scope = NULL; char *str_to_sign = NULL; const char *user = data->state.aptr.user ? data->state.aptr.user : ""; char *secret = NULL; unsigned char sign0[SHA256_DIGEST_LENGTH] = {0}; unsigned char sign1[SHA256_DIGEST_LENGTH] = {0}; char *auth_headers = NULL; DEBUGASSERT(!proxy); (void)proxy; if(Curl_checkheaders(data, STRCONST("Authorization"))) { /* Authorization already present, Bailing out */ return CURLE_OK; } /* we init those buffers here, so goto fail will free initialized dynbuf */ Curl_dyn_init(&canonical_headers, CURL_MAX_HTTP_HEADER); Curl_dyn_init(&signed_headers, CURL_MAX_HTTP_HEADER); /* * Parameters parsing * Google and Outscale use the same OSC or GOOG, * but Amazon uses AWS and AMZ for header arguments. * AWS is the default because most of non-amazon providers * are still using aws:amz as a prefix. */ arg = data->set.str[STRING_AWS_SIGV4] ? data->set.str[STRING_AWS_SIGV4] : "aws:amz"; /* provider1[:provider2[:region[:service]]] No string can be longer than N bytes of non-whitespace */ (void)sscanf(arg, "%" MAX_SIGV4_LEN_TXT "[^:]" ":%" MAX_SIGV4_LEN_TXT "[^:]" ":%" MAX_SIGV4_LEN_TXT "[^:]" ":%" MAX_SIGV4_LEN_TXT "s", provider0, provider1, region, service); if(!provider0[0]) { failf(data, "first provider can't be empty"); ret = CURLE_BAD_FUNCTION_ARGUMENT; goto fail; } else if(!provider1[0]) strcpy(provider1, provider0); if(!service[0]) { char *hostdot = strchr(hostname, '.'); if(!hostdot) { failf(data, "service missing in parameters and hostname"); ret = CURLE_URL_MALFORMAT; goto fail; } len = hostdot - hostname; if(len > MAX_SIGV4_LEN) { failf(data, "service too long in hostname"); ret = CURLE_URL_MALFORMAT; goto fail; } strncpy(service, hostname, len); service[len] = '\0'; if(!region[0]) { const char *reg = hostdot + 1; const char *hostreg = strchr(reg, '.'); if(!hostreg) { failf(data, "region missing in parameters and hostname"); ret = CURLE_URL_MALFORMAT; goto fail; } len = hostreg - reg; if(len > MAX_SIGV4_LEN) { failf(data, "region too long in hostname"); ret = CURLE_URL_MALFORMAT; goto fail; } strncpy(region, reg, len); region[len] = '\0'; } } Curl_http_method(data, conn, &method, &httpreq); /* AWS S3 requires a x-amz-content-sha256 header, and supports special * values like UNSIGNED-PAYLOAD */ sign_as_s3 = (strcasecompare(provider0, "aws") && strcasecompare(service, "s3")); payload_hash = parse_content_sha_hdr(data, provider1, &payload_hash_len); if(!payload_hash) { if(sign_as_s3) ret = calc_s3_payload_hash(data, httpreq, provider1, sha_hash, sha_hex, content_sha256_hdr); else ret = calc_payload_hash(data, sha_hash, sha_hex); if(ret) goto fail; payload_hash = sha_hex; /* may be shorter than SHA256_HEX_LENGTH, like S3_UNSIGNED_PAYLOAD */ payload_hash_len = strlen(sha_hex); } #ifdef DEBUGBUILD { char *force_timestamp = getenv("CURL_FORCETIME"); if(force_timestamp) clock = 0; else time(&clock); } #else time(&clock); #endif ret = Curl_gmtime(clock, &tm); if(ret) { goto fail; } if(!strftime(timestamp, sizeof(timestamp), "%Y%m%dT%H%M%SZ", &tm)) { ret = CURLE_OUT_OF_MEMORY; goto fail; } ret = make_headers(data, hostname, timestamp, provider1, &date_header, content_sha256_hdr, &canonical_headers, &signed_headers); if(ret) goto fail; ret = CURLE_OUT_OF_MEMORY; if(*content_sha256_hdr) { /* make_headers() needed this without the \r\n for canonicalization */ size_t hdrlen = strlen(content_sha256_hdr); DEBUGASSERT(hdrlen + 3 < sizeof(content_sha256_hdr)); memcpy(content_sha256_hdr + hdrlen, "\r\n", 3); } memcpy(date, timestamp, sizeof(date)); date[sizeof(date) - 1] = 0; canonical_request = curl_maprintf("%s\n" /* HTTPRequestMethod */ "%s\n" /* CanonicalURI */ "%s\n" /* CanonicalQueryString */ "%s\n" /* CanonicalHeaders */ "%s\n" /* SignedHeaders */ "%.*s", /* HashedRequestPayload in hex */ method, data->state.up.path, data->state.up.query ? data->state.up.query : "", Curl_dyn_ptr(&canonical_headers), Curl_dyn_ptr(&signed_headers), (int)payload_hash_len, payload_hash); if(!canonical_request) goto fail; /* provider 0 lowercase */ Curl_strntolower(provider0, provider0, strlen(provider0)); request_type = curl_maprintf("%s4_request", provider0); if(!request_type) goto fail; credential_scope = curl_maprintf("%s/%s/%s/%s", date, region, service, request_type); if(!credential_scope) goto fail; if(Curl_sha256it(sha_hash, (unsigned char *) canonical_request, strlen(canonical_request))) goto fail; sha256_to_hex(sha_hex, sha_hash); /* provider 0 uppercase */ Curl_strntoupper(provider0, provider0, strlen(provider0)); /* * Google allows using RSA key instead of HMAC, so this code might change * in the future. For now we only support HMAC. */ str_to_sign = curl_maprintf("%s4-HMAC-SHA256\n" /* Algorithm */ "%s\n" /* RequestDateTime */ "%s\n" /* CredentialScope */ "%s", /* HashedCanonicalRequest in hex */ provider0, timestamp, credential_scope, sha_hex); if(!str_to_sign) { goto fail; } /* provider 0 uppercase */ secret = curl_maprintf("%s4%s", provider0, data->state.aptr.passwd ? data->state.aptr.passwd : ""); if(!secret) goto fail; HMAC_SHA256(secret, strlen(secret), date, strlen(date), sign0); HMAC_SHA256(sign0, sizeof(sign0), region, strlen(region), sign1); HMAC_SHA256(sign1, sizeof(sign1), service, strlen(service), sign0); HMAC_SHA256(sign0, sizeof(sign0), request_type, strlen(request_type), sign1); HMAC_SHA256(sign1, sizeof(sign1), str_to_sign, strlen(str_to_sign), sign0); sha256_to_hex(sha_hex, sign0); /* provider 0 uppercase */ auth_headers = curl_maprintf("Authorization: %s4-HMAC-SHA256 " "Credential=%s/%s, " "SignedHeaders=%s, " "Signature=%s\r\n" "%s\r\n" "%s", /* optional sha256 header includes \r\n */ provider0, user, credential_scope, Curl_dyn_ptr(&signed_headers), sha_hex, date_header, content_sha256_hdr); if(!auth_headers) { goto fail; } Curl_safefree(data->state.aptr.userpwd); data->state.aptr.userpwd = auth_headers; data->state.authhost.done = TRUE; ret = CURLE_OK; fail: Curl_dyn_free(&canonical_headers); Curl_dyn_free(&signed_headers); free(canonical_request); free(request_type); free(credential_scope); free(str_to_sign); free(secret); free(date_header); return ret; } #endif /* !defined(CURL_DISABLE_HTTP) && !defined(CURL_DISABLE_CRYPTO_AUTH) */