dep-curl/lib/http_httpsig.c

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

690 lines
20 KiB
C
Raw Normal View History

/***************************************************************************
* _ _ ____ _
* Project ___| | | | _ \| |
* / __| | | | |_) | |
* | (__| |_| | _ <| |___
* \___|\___/|_| \_\_____|
*
* Copyright (C) Daniel Stenberg, <daniel@haxx.se>, 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.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_HTTPSIG)
#include "urldata.h"
#include "http_httpsig.h"
#include "curl_ed25519.h"
#include "curl_hmac.h"
#include "curl_sha256.h"
#include "http.h"
#include "transfer.h"
#include "curl_trc.h"
#include "slist.h"
#include "curlx/dynbuf.h"
#include "curlx/base64.h"
#include "curlx/strdup.h"
#include "curlx/strparse.h"
#include "strcase.h"
#include <time.h>
#define HTTPSIG_MAX_SIG_BASE CURL_MAX_HTTP_HEADER
#define HTTPSIG_MAX_COMPONENTS 16
#define HTTPSIG_MAX_RAW_SIG CURL_ED25519_SIGLEN
#define HTTPSIG_DEFAULT_LABEL "sig1"
enum httpsig_alg {
HTTPSIG_ALG_ED25519,
HTTPSIG_ALG_HMAC_SHA256,
HTTPSIG_ALG_UNKNOWN
};
static const char *alg_to_str(enum httpsig_alg alg)
{
switch(alg) {
case HTTPSIG_ALG_ED25519:
return "ed25519";
case HTTPSIG_ALG_HMAC_SHA256:
return "hmac-sha256";
default:
break;
}
return NULL;
}
static enum httpsig_alg id_to_alg(uint8_t val)
{
switch(val) {
case CURLHTTPSIG_ED25519:
return HTTPSIG_ALG_ED25519;
case CURLHTTPSIG_HMAC_SHA256:
return HTTPSIG_ALG_HMAC_SHA256;
default:
break;
}
return HTTPSIG_ALG_UNKNOWN;
}
static CURLcode decode_hex_key(struct Curl_easy *data,
const char *hexstr,
unsigned char **keyout,
size_t *keylen)
{
size_t len, i;
unsigned char *keybuf;
*keyout = NULL;
*keylen = 0;
len = strlen(hexstr);
while(len > 0 && ISNEWLINE(hexstr[len - 1]))
len--;
if(len == 0 || (len & 1) != 0) {
failf(data, "httpsig: invalid hex key (length %zu)", len);
return CURLE_BAD_FUNCTION_ARGUMENT;
}
if(len > CURL_MAX_INPUT_LENGTH) {
failf(data, "httpsig: hex key too long");
return CURLE_BAD_FUNCTION_ARGUMENT;
}
keybuf = curlx_malloc(len / 2);
if(!keybuf)
return CURLE_OUT_OF_MEMORY;
for(i = 0; i < len; i += 2) {
if(!ISXDIGIT(hexstr[i]) || !ISXDIGIT(hexstr[i + 1])) {
failf(data, "httpsig: invalid hex at position %zu ('%c%c')",
i, hexstr[i], hexstr[i + 1]);
curlx_free(keybuf);
return CURLE_BAD_FUNCTION_ARGUMENT;
}
keybuf[i / 2] = (unsigned char)((curlx_hexval(hexstr[i]) << 4) |
curlx_hexval(hexstr[i + 1]));
}
*keyout = keybuf;
*keylen = len / 2;
return CURLE_OK;
}
/* @authority matches the Host header field-value when available (RFC 9421).
data->state.http_host is produced by http_set_aptr_host() before auth. */
static CURLcode httpsig_authority(struct Curl_easy *data,
struct connectdata *conn,
struct dynbuf *authority_buf)
{
const char *h = data->state.http_host;
if(h && curl_strnequal(h, "host:", 5)) {
const char *value = h + 5;
const char *end;
while(ISBLANK(*value))
value++;
if(*value) {
CURLcode result;
end = value;
while(*end && !ISNEWLINE(*end))
end++;
while(end > value && ISBLANK(end[-1]))
end--;
result = curlx_dyn_addn(authority_buf, value, (size_t)(end - value));
if(result)
return result;
return CURLE_OK;
}
}
{
const char *hostname = conn->origin->hostname;
uint16_t port = conn->origin->port;
if((conn->given->defport != port) && port)
return curlx_dyn_addf(authority_buf, "%s:%u", hostname, port);
return curlx_dyn_add(authority_buf, hostname);
}
}
static CURLcode sf_append_quoted(struct dynbuf *buf, const char *str)
{
CURLcode result = curlx_dyn_addn(buf, "\"", 1);
if(result)
return result;
while(*str) {
if(ISCNTRL(*str))
return CURLE_BAD_FUNCTION_ARGUMENT;
if(*str == '\\' || *str == '"') {
result = curlx_dyn_addn(buf, "\\", 1);
if(result)
return result;
}
result = curlx_dyn_addn(buf, str, 1);
if(result)
return result;
str++;
}
return curlx_dyn_addn(buf, "\"", 1);
}
/* base64-encode raw bytes into an RFC 8941 byte sequence (:base64:) */
static CURLcode sf_encode_byte_seq(const unsigned char *raw, size_t rawlen,
struct dynbuf *out)
{
CURLcode result;
size_t b64len;
char *b64;
result = curlx_base64_encode(raw, rawlen, &b64, &b64len);
if(result)
return result;
result = curlx_dyn_addn(out, ":", 1);
if(!result)
result = curlx_dyn_addn(out, b64, b64len);
if(!result)
result = curlx_dyn_addn(out, ":", 1);
curlx_free(b64);
return result;
}
static CURLcode build_sig_params(struct dynbuf *params,
const char **components, size_t count,
time_t created, const char *keyid,
enum httpsig_alg alg)
{
CURLcode result;
size_t i;
result = curlx_dyn_addn(params, "(", 1);
if(result)
return result;
for(i = 0; i < count; i++) {
if(i > 0) {
result = curlx_dyn_addn(params, " ", 1);
if(result)
return result;
}
result = sf_append_quoted(params, components[i]);
if(result)
return result;
}
result = curlx_dyn_addn(params, ")", 1);
if(result)
return result;
result = curlx_dyn_addf(params, ";created=%lld", (long long)created);
if(result)
return result;
if(keyid && *keyid) {
result = curlx_dyn_add(params, ";keyid=");
if(result)
return result;
result = sf_append_quoted(params, keyid);
if(result)
return result;
}
result = curlx_dyn_addf(params, ";alg=\"%s\"", alg_to_str(alg));
return result;
}
/* strings defined by RFC 9421 */
#define SIG_METHOD "@method"
#define SIG_AUTHORITY "@authority"
#define SIG_PATH "@path"
#define SIG_QUERY "@query"
/* Resolve a component identifier to its value.
* For headers, we walk the full user-supplied header list to combine
* duplicate field values with ", " per RFC 9421 Section 2.1. Each
* individual value is trimmed of leading/trailing OWS and the trailing
* \r\n. The combined result is written into the caller-provided buffer. */
static CURLcode resolve_component(const char *name,
const char *method,
const char *authority,
const char *path,
const char *query,
struct Curl_easy *data,
struct dynbuf *valbuf,
const char **out)
{
*out = NULL;
if(name[0] == '@') {
if(curl_strequal(name, SIG_METHOD))
*out = method;
else if(curl_strequal(name, SIG_AUTHORITY))
*out = authority;
else if(curl_strequal(name, SIG_PATH))
*out = path;
else if(curl_strequal(name, SIG_QUERY))
*out = query;
else {
failf(data, "httpsig: unsupported derived component '%s'", name);
return CURLE_BAD_FUNCTION_ARGUMENT;
}
if(!*out) {
failf(data, "httpsig: derived component '%s' has no value", name);
return CURLE_BAD_FUNCTION_ARGUMENT;
}
return CURLE_OK;
}
else {
/* RFC 9421 Section 2.1: walk all user-supplied headers and combine
duplicate field values with ", " per HTTP field combination rules. */
struct curl_slist *head;
size_t namelen = strlen(name);
bool found = FALSE;
curlx_dyn_reset(valbuf);
for(head = data->set.headers; head; head = head->next) {
if(curl_strnequal(head->data, name, namelen) &&
Curl_headersep(head->data[namelen])) {
const char *p = strchr(head->data, ':');
if(p) {
CURLcode result;
struct Curl_str content;
curlx_str_assign(&content, p + 1, strlen(p + 1));
curlx_str_trimblanks(&content);
if(found) {
result = curlx_dyn_addn(valbuf, ", ", 2);
if(result)
return result;
}
result = curlx_dyn_addn(valbuf, curlx_str(&content),
curlx_strlen(&content));
if(result)
return result;
found = TRUE;
}
}
}
if(found) {
*out = curlx_dyn_ptr(valbuf);
return CURLE_OK;
}
failf(data, "httpsig: header '%s' not found in request", name);
return CURLE_BAD_FUNCTION_ARGUMENT;
}
}
static CURLcode build_sig_base(struct dynbuf *base,
const char **components, size_t count,
const char *method,
const char *authority,
const char *path,
const char *query,
struct Curl_easy *data,
const char *sig_params)
{
CURLcode result;
size_t i;
struct dynbuf hdrvalbuf;
curlx_dyn_init(&hdrvalbuf, CURL_MAX_HTTP_HEADER);
for(i = 0; i < count; i++) {
const char *val = NULL;
result = resolve_component(components[i], method,
authority, path, query, data,
&hdrvalbuf, &val);
if(result || !val) {
failf(data, "httpsig: cannot resolve component '%s'", components[i]);
curlx_dyn_free(&hdrvalbuf);
return result ? result : CURLE_BAD_FUNCTION_ARGUMENT;
}
result = curlx_dyn_addf(base, "\"%s\": %s\n", components[i], val);
if(result) {
curlx_dyn_free(&hdrvalbuf);
return result;
}
}
curlx_dyn_free(&hdrvalbuf);
result = curlx_dyn_addf(base, "\"@signature-params\": %s", sig_params);
return result;
}
static CURLcode parse_components(struct Curl_easy *data,
const char *query,
const char **components,
size_t *ncomp_out,
char **hdrs_copy_out)
{
const char *hdrs = CURL_EASY_STR(data, STRING_HTTPSIG_HEADERS);
size_t ncomp = 0;
*hdrs_copy_out = NULL;
if(hdrs && *hdrs) {
char *p;
char *hdrs_copy = curlx_strdup(hdrs);
if(!hdrs_copy)
return CURLE_OUT_OF_MEMORY;
*hdrs_copy_out = hdrs_copy;
p = hdrs_copy;
while(*p && ncomp < HTTPSIG_MAX_COMPONENTS) {
char *start;
size_t tlen = 0;
const char *p2 = p;
curlx_str_passblanks(&p2);
if(!*p2)
break;
p = start = CURL_UNCONST(p2);
while(*p && !ISBLANK(*p)) {
if((*p == '\"') || (*p == '\\'))
return CURLE_BAD_FUNCTION_ARGUMENT;
p++;
tlen++;
}
if(*p)
*p++ = '\0';
if(tlen && start[tlen - 1] == ':') {
/* Header field: drop the trailing ':' marker. RFC 9421 field
names are canonically lowercase (Section 2.1). */
start[--tlen] = '\0';
if(!tlen) {
failf(data, "httpsig: empty header component name");
return CURLE_BAD_FUNCTION_ARGUMENT;
}
Curl_strntolower(start, start, tlen);
components[ncomp++] = start;
}
else {
/* Derived component: map the bare name to its canonical RFC 9421
'@'-prefixed identifier (Section 2.2). */
Curl_strntolower(start, start, tlen);
if(!strcmp(start, "method"))
components[ncomp++] = SIG_METHOD;
else if(!strcmp(start, "authority"))
components[ncomp++] = SIG_AUTHORITY;
else if(!strcmp(start, "path"))
components[ncomp++] = SIG_PATH;
else if(!strcmp(start, "query"))
components[ncomp++] = SIG_QUERY;
else {
failf(data, "httpsig: unknown component '%s'", start);
return CURLE_BAD_FUNCTION_ARGUMENT;
}
}
}
if(!ncomp) {
failf(data, "httpsig: no signature components specified");
return CURLE_BAD_FUNCTION_ARGUMENT;
}
if(*p) {
failf(data, "httpsig: too many signature components (max %u)",
(unsigned int)HTTPSIG_MAX_COMPONENTS);
return CURLE_BAD_FUNCTION_ARGUMENT;
}
/* RFC 9421 Section 2: each covered component MUST occur only once */
{
size_t i, j;
for(i = 0; i < ncomp; i++) {
for(j = i + 1; j < ncomp; j++) {
if(!strcmp(components[i], components[j])) {
failf(data, "httpsig: duplicate signature component '%s'",
components[i]);
return CURLE_BAD_FUNCTION_ARGUMENT;
}
}
}
}
}
else {
components[ncomp++] = SIG_METHOD;
components[ncomp++] = SIG_AUTHORITY;
components[ncomp++] = SIG_PATH;
if(query)
components[ncomp++] = SIG_QUERY;
}
*ncomp_out = ncomp;
return CURLE_OK;
}
static time_t httpsig_get_created(void)
{
#ifdef DEBUGBUILD
char *force = getenv("CURL_FORCETIME");
if(force && *force) {
char *sigts = getenv("CURL_HTTPSIG_CREATED");
if(sigts && *sigts) {
const char *p = sigts;
curl_off_t num;
if(!curlx_str_number(&p, &num, CURL_OFF_T_MAX))
return (time_t)num;
}
return 0;
}
#endif
return time(NULL);
}
static CURLcode httpsig_sign_base(struct Curl_easy *data,
enum httpsig_alg alg,
const unsigned char *keybuf,
size_t keylen,
const struct dynbuf *sig_base,
unsigned char *raw_sig,
size_t *raw_sig_len)
{
CURLcode result;
switch(alg) {
case HTTPSIG_ALG_ED25519:
result = Curl_ed25519_sign(
keybuf, keylen,
(const unsigned char *)curlx_dyn_ptr(sig_base),
curlx_dyn_len(sig_base),
raw_sig, raw_sig_len);
break;
case HTTPSIG_ALG_HMAC_SHA256:
result = Curl_hmacit(&Curl_HMAC_SHA256, keybuf, keylen,
(const unsigned char *)curlx_dyn_ptr(sig_base),
curlx_dyn_len(sig_base), raw_sig);
if(!result)
*raw_sig_len = CURL_SHA256_DIGEST_LENGTH;
break;
default:
result = CURLE_BAD_FUNCTION_ARGUMENT;
break;
}
if(result && result == CURLE_NOT_BUILT_IN) {
failf(data, "httpsig: algorithm '%s' not supported by TLS backend",
alg_to_str(alg));
}
return result;
}
CURLcode Curl_output_httpsig(struct Curl_easy *data)
{
CURLcode result = CURLE_OUT_OF_MEMORY;
struct connectdata *conn = data->conn;
const char *path;
const char *query;
Curl_HttpReq httpreq;
const char *method = NULL;
const char *hexkey = CURL_EASY_STR(data, STRING_HTTPSIG_KEY);
const char *keyid = CURL_EASY_STR(data, STRING_HTTPSIG_KEYID);
enum httpsig_alg alg;
time_t created;
struct dynbuf sig_params;
struct dynbuf sig_base;
struct dynbuf sig_hdr;
struct dynbuf input_hdr;
struct dynbuf authority_buf;
const char *authority;
const char *components[HTTPSIG_MAX_COMPONENTS];
size_t ncomp = 0;
unsigned char *keybuf = NULL;
size_t keylen = 0;
unsigned char raw_sig[HTTPSIG_MAX_RAW_SIG];
size_t raw_sig_len = 0;
char *auth_headers = NULL;
char *hdrs_copy = NULL;
struct dynbuf query_dyn;
alg = id_to_alg(data->set.httpsig_algorithm);
if(alg == HTTPSIG_ALG_UNKNOWN) {
failf(data, "httpsig: CURLOPT_HTTPSIG_ALGORITHM is required");
return CURLE_BAD_FUNCTION_ARGUMENT;
}
if(!hexkey || !*hexkey) {
failf(data, "httpsig: CURLOPT_HTTPSIG_KEY is required");
return CURLE_BAD_FUNCTION_ARGUMENT;
}
if(!keyid || !*keyid) {
failf(data, "httpsig: CURLOPT_HTTPSIG_KEYID is required");
return CURLE_BAD_FUNCTION_ARGUMENT;
}
curlx_dyn_init(&sig_params, CURL_MAX_HTTP_HEADER);
curlx_dyn_init(&sig_base, HTTPSIG_MAX_SIG_BASE);
curlx_dyn_init(&sig_hdr, CURL_MAX_HTTP_HEADER);
curlx_dyn_init(&input_hdr, CURL_MAX_HTTP_HEADER);
curlx_dyn_init(&authority_buf, CURL_MAX_HTTP_HEADER);
curlx_dyn_init(&query_dyn, CURL_MAX_HTTP_HEADER);
if(Curl_checkheaders(data, STRCONST("Signature")) ||
Curl_checkheaders(data, STRCONST("Signature-Input"))) {
/* user provides their own Signature / Signature-Input headers, consider
this done */
goto done;
}
result = decode_hex_key(data, hexkey, &keybuf, &keylen);
if(result)
goto fail;
if(alg == HTTPSIG_ALG_ED25519 && keylen != 32) {
failf(data, "httpsig: ed25519 requires a 32-byte key (got %zu)", keylen);
result = CURLE_BAD_FUNCTION_ARGUMENT;
goto fail;
}
Curl_http_method(data, &method, &httpreq);
path = data->state.up.path;
if(!path || !*path)
path = "/";
query = data->state.up.query;
result = httpsig_authority(data, conn, &authority_buf);
if(result)
goto fail;
authority = curlx_dyn_ptr(&authority_buf);
/* Build @query value: RFC 9421 Section 2.2.7 - always starts with "?" */
if(query && *query)
result = curlx_dyn_addf(&query_dyn, "?%s", query);
else
result = curlx_dyn_add(&query_dyn, "?");
if(result)
goto fail;
result = parse_components(data, query, components, &ncomp, &hdrs_copy);
if(result)
goto fail;
created = httpsig_get_created();
result = build_sig_params(&sig_params, components, ncomp,
created, keyid, alg);
if(result)
goto fail;
infof(data, "httpsig: Signature-Input params: %s",
curlx_dyn_ptr(&sig_params));
result = build_sig_base(&sig_base, components, ncomp,
method, authority, path,
curlx_dyn_ptr(&query_dyn),
data, curlx_dyn_ptr(&sig_params));
if(result)
goto fail;
infof(data, "httpsig: Signature base: [%s]",
curlx_dyn_ptr(&sig_base));
result = httpsig_sign_base(data, alg, keybuf, keylen, &sig_base,
raw_sig, &raw_sig_len);
if(result)
goto fail;
result = curlx_dyn_add(&sig_hdr, HTTPSIG_DEFAULT_LABEL "=");
if(result)
goto fail;
result = sf_encode_byte_seq(raw_sig, raw_sig_len, &sig_hdr);
if(result)
goto fail;
result = curlx_dyn_addf(&input_hdr, "%s=%s", HTTPSIG_DEFAULT_LABEL,
curlx_dyn_ptr(&sig_params));
if(result)
goto fail;
auth_headers = curl_maprintf("Signature-Input: %s\r\n"
"Signature: %s\r\n",
curlx_dyn_ptr(&input_hdr),
curlx_dyn_ptr(&sig_hdr));
if(!auth_headers)
goto fail;
infof(data, "httpsig: Signature-Input: %s", curlx_dyn_ptr(&input_hdr));
infof(data, "httpsig: Signature: %s", curlx_dyn_ptr(&sig_hdr));
done:
curlx_free(data->req.hd_auth);
data->req.hd_auth = auth_headers;
data->state.authhost.done = TRUE;
result = CURLE_OK;
fail:
if(keybuf) {
memset(keybuf, 0, keylen);
curlx_free(keybuf);
}
memset(raw_sig, 0, sizeof(raw_sig));
curlx_free(hdrs_copy);
curlx_dyn_free(&sig_params);
curlx_dyn_free(&sig_base);
curlx_dyn_free(&sig_hdr);
curlx_dyn_free(&input_hdr);
curlx_dyn_free(&authority_buf);
curlx_dyn_free(&query_dyn);
return result;
}
#endif /* !CURL_DISABLE_HTTP && !CURL_DISABLE_HTTPSIG */