httpsig: add RFC 9421 HTTP Message Signatures support

Add support for signing outgoing HTTP requests per RFC 9421 using
Ed25519 or HMAC-SHA256 algorithms.

New libcurl options:
 - CURLOPT_HTTPSIG: signing algorithm ("ed25519" or "hmac-sha256")
 - CURLOPT_HTTPSIG_KEY: path to hex-encoded key file
 - CURLOPT_HTTPSIG_KEYID: key identifier for Signature-Input
 - CURLOPT_HTTPSIG_HEADERS: space-separated components to sign

New CLI flags: --httpsig, --httpsig-key, --httpsig-keyid,
--httpsig-headers

The crypto layer follows the sha256.c multi-backend pattern with
implementations for OpenSSL (EVP_DigestSign) and wolfSSL
(wc_ed25519_sign_msg). HMAC-SHA256 uses the existing Curl_hmacit()
infrastructure which works on all backends.

Verified by test 5000 to 5021

Assisted-by: Daniel Stenberg
Signed-off-by: Sameeh Jubran <sameeh@wolfssl.com>
Closes #22386
Closes #21239
This commit is contained in:
Sameeh Jubran 2026-07-24 22:49:52 +02:00 committed by Daniel Stenberg
parent ebc5212dac
commit a55731050e
No known key found for this signature in database
GPG key ID: 5CC908FDB71E12C2
78 changed files with 3192 additions and 10 deletions

View file

@ -368,6 +368,9 @@ httpget
HttpGet
HTTPS
https
httpsig
HTTPSIG
CURLHTTPSIG
HTTPSRR
hyper's
IANA
@ -428,6 +431,7 @@ KDE
keepalive
Keil
kerberos
keyid
Keychain
keychain
KiB
@ -996,6 +1000,7 @@ WinIDN
WinLDAP
winsock
Wireshark
wolfCrypt
wolfSSH
wolfSSL
ws

View file

@ -200,7 +200,7 @@ jobs:
install_steps: wolfssl-opensslextra-arm
tflags: '--min=815 1 to 1000'
LDFLAGS: -Wl,-rpath,/home/runner/wolfssl-opensslextra/lib
configure: --with-wolfssl=/home/runner/wolfssl-opensslextra --enable-ech --enable-debug
configure: --with-wolfssl=/home/runner/wolfssl-opensslextra --enable-ech --enable-debug --enable-httpsig
- name: 'wolfssl-opensslextra valgrind 2'
image: ubuntu-26.04-arm
@ -208,7 +208,7 @@ jobs:
install_steps: wolfssl-opensslextra-arm
tflags: '--min=835 1001 to 9999'
LDFLAGS: -Wl,-rpath,/home/runner/wolfssl-opensslextra/lib
configure: --with-wolfssl=/home/runner/wolfssl-opensslextra --enable-ech --enable-debug
configure: --with-wolfssl=/home/runner/wolfssl-opensslextra --enable-ech --enable-debug --enable-httpsig
- name: 'openssl default'
install_steps: pytest

View file

@ -435,6 +435,8 @@ option(CURL_DISABLE_NEGOTIATE_AUTH "Disable negotiate authentication" OFF)
mark_as_advanced(CURL_DISABLE_NEGOTIATE_AUTH)
option(CURL_DISABLE_AWS "Disable aws-sigv4" OFF)
mark_as_advanced(CURL_DISABLE_AWS)
option(CURL_DISABLE_HTTPSIG "Disable HTTP Message Signatures (RFC 9421)" ON)
mark_as_advanced(CURL_DISABLE_HTTPSIG)
option(CURL_DISABLE_DICT "Disable DICT" OFF)
mark_as_advanced(CURL_DISABLE_DICT)
option(CURL_DISABLE_DOH "Disable DNS-over-HTTPS" OFF)
@ -2037,6 +2039,7 @@ curl_add_if("SSPI" USE_WINDOWS_SSPI)
curl_add_if("GSS-API" HAVE_GSSAPI)
curl_add_if("alt-svc" NOT CURL_DISABLE_ALTSVC)
curl_add_if("HSTS" NOT CURL_DISABLE_HSTS)
curl_add_if("HTTPSIG" NOT CURL_DISABLE_HTTPSIG)
curl_add_if("SPNEGO" NOT CURL_DISABLE_NEGOTIATE_AUTH AND
(HAVE_GSSAPI OR USE_WINDOWS_SSPI))
curl_add_if("Kerberos" NOT CURL_DISABLE_KERBEROS_AUTH AND

View file

@ -4566,6 +4566,29 @@ AS_HELP_STRING([--disable-aws],[Disable AWS sig support]),
AC_MSG_RESULT(yes)
)
dnl ************************************************************
dnl disable httpsig (RFC 9421)
dnl
AC_MSG_CHECKING([whether to enable HTTP Message Signatures (RFC 9421)])
AC_ARG_ENABLE(httpsig,
AS_HELP_STRING([--enable-httpsig],[Enable HTTP Message Signatures support])
AS_HELP_STRING([--disable-httpsig],[Disable HTTP Message Signatures support (default)]),
[ case "$enableval" in
no)
AC_MSG_RESULT(no)
AC_DEFINE(CURL_DISABLE_HTTPSIG, 1, [to disable HTTP Message Signatures support])
want_httpsig="no"
;;
*)
AC_MSG_RESULT(yes)
want_httpsig="yes"
;;
esac ],
AC_MSG_RESULT(no)
AC_DEFINE(CURL_DISABLE_HTTPSIG, 1, [to disable HTTP Message Signatures support])
want_httpsig="no"
)
dnl ************************************************************
dnl disable NTLM support
dnl
@ -5313,6 +5336,11 @@ if test "$want_httpsrr" != "no"; then
SUPPORT_FEATURES="$SUPPORT_FEATURES HTTPSRR"
fi
if test "$want_httpsig" = "yes"; then
SUPPORT_FEATURES="$SUPPORT_FEATURES HTTPSIG"
experimental="$experimental HTTPSIG"
fi
if test "$SSLS_EXPORT_ENABLED" = "1"; then
SUPPORT_FEATURES="$SUPPORT_FEATURES SSLS-EXPORT"
fi

View file

@ -42,6 +42,10 @@ Disable support for the negotiate authentication methods.
Disable **aws-sigv4** support.
## `CURL_DISABLE_HTTPSIG`
Disable RFC 9421 HTTP Message Signatures support.
## `CURL_DISABLE_CA_SEARCH`
Disable unsafe CA bundle search in PATH on Windows.

View file

@ -105,3 +105,22 @@ Graduation requirements:
- HTTPS records can control ALPN and port number, at least
- There are options to control HTTPS use
## HTTP Message Signatures (RFC 9421)
Sign outgoing HTTP requests according to RFC 9421 using the
`--httpsig-algo`, `--httpsig-key`, `--httpsig-keyid` and
`--httpsig-headers` command line options, or the corresponding
`CURLOPT_HTTPSIG_*` libcurl options. Built only when configured with
`--enable-httpsig`.
Graduation requirements:
- the option set (names, arguments, defaults) is settled
- interoperability has been verified against at least two independent
RFC 9421 implementations
- no test cases are disabled for the feature
- feedback from users saying the API works for their use cases

View file

@ -3,3 +3,4 @@
# SPDX-License-Identifier: curl
curl.txt
asciipage.tmp.*

View file

@ -134,6 +134,10 @@ DPAGES = \
http2.md \
http3.md \
http3-only.md \
httpsig-algo.md \
httpsig-headers.md \
httpsig-key.md \
httpsig-keyid.md \
ignore-content-length.md \
insecure.md \
interface.md \

View file

@ -0,0 +1,36 @@
---
c: Copyright (C) Daniel Stenberg, <daniel@haxx.se>, et al.
SPDX-License-Identifier: curl
Long: httpsig-algo
Protocols: HTTP
Arg: <algorithm>
Help: Algorithm for HTTP Message Signatures
Category: auth http
Added: 8.22.0
Multi: single
Experimental: yes
See-also:
- httpsig-key
- httpsig-keyid
- httpsig-headers
Example:
- --httpsig-key key.hex --httpsig-keyid "my-key" $URL
- --httpsig-algo hmac-sha256 --httpsig-key secret.hex --httpsig-keyid "shared" $URL
---
# `--httpsig-algo`
Sign outgoing HTTP requests using RFC 9421 HTTP Message Signatures.
This option specifies which signing algorithm to use. Supported values are
**ed25519** and **hmac-sha256**. If not specified, **ed25519** is used. Any
other value causes curl to exit with an error.
HTTP Message Signatures are enabled when any of --httpsig-algo,
--httpsig-key, --httpsig-keyid or --httpsig-headers is given. When enabled,
--httpsig-key and --httpsig-keyid are required. Without any of these options
no signing is performed.
By default, the signed components are `method`, `authority`, `path`, and
`query` (when a query string is present). Use --httpsig-headers to override
the set of components included in the signature.

View file

@ -0,0 +1,45 @@
---
c: Copyright (C) Daniel Stenberg, <daniel@haxx.se>, et al.
SPDX-License-Identifier: curl
Long: httpsig-headers
Protocols: HTTP
Arg: <components>
Help: Components to sign for HTTP Message Signatures
Category: auth http
Added: 8.22.0
Multi: single
Experimental: yes
See-also:
- httpsig-algo
- httpsig-key
- httpsig-keyid
Example:
- --httpsig-algo ed25519 --httpsig-key key.hex --httpsig-keyid "my-key" --httpsig-headers "method authority content-type:" $URL
---
# `--httpsig-headers`
Space-separated list of components to include in the RFC 9421 HTTP Message
Signature. Derived components are given as bare names: `method`, `authority`,
`path`, and `query`. HTTP header fields are given with a trailing colon, for
example `content-type:` and `content-digest:`.
If not specified, the default set is `method authority path` (plus `query`
when a query string is present in the URL).
## Signing request headers
Header components are taken from `-H` / `--header` options only. Headers curl
adds by default (such as `User-Agent`) are not signed unless you set them
explicitly, for example:
curl --httpsig-algo ed25519 \
--httpsig-key k.hex \
--httpsig-keyid mykey \
-H "User-Agent: MyApp/1.0" \
--httpsig-headers \
"method authority path user-agent:" \
$URL
Each component may appear only once. Duplicate identifiers in
`--httpsig-headers` cause curl to exit with an error.

View file

@ -0,0 +1,35 @@
---
c: Copyright (C) Daniel Stenberg, <daniel@haxx.se>, et al.
SPDX-License-Identifier: curl
Long: httpsig-key
Protocols: HTTP
Arg: <file>
Help: Key file for HTTP Message Signatures
Category: auth http
Added: 8.22.0
Multi: single
Experimental: yes
See-also:
- httpsig-algo
- httpsig-keyid
Example:
- --httpsig-algo ed25519 --httpsig-key key.hex --httpsig-keyid "my-key" $URL
---
# `--httpsig-key`
Path to the key file used for RFC 9421 HTTP Message Signatures.
The file must contain a hex-encoded key on its first line. For **ed25519**,
this is the 32-byte private seed (64 hex characters). For **hmac-sha256**,
this is the shared secret. PEM files are not supported.
## Generating Ed25519 keys
With OpenSSL 3:
openssl genpkey -algorithm ED25519 -out k.pem
openssl pkey -in k.pem -outform RAW -out k.raw
xxd -p -c 64 k.raw | tr -d '\n' > k.hex
Use `k.hex` with `--httpsig-key`.

View file

@ -0,0 +1,23 @@
---
c: Copyright (C) Daniel Stenberg, <daniel@haxx.se>, et al.
SPDX-License-Identifier: curl
Long: httpsig-keyid
Protocols: HTTP
Arg: <id>
Help: Key identifier for HTTP Message Signatures
Category: auth http
Added: 8.22.0
Multi: single
Experimental: yes
See-also:
- httpsig-algo
- httpsig-key
Example:
- --httpsig-algo ed25519 --httpsig-key key.hex --httpsig-keyid "my-key" $URL
---
# `--httpsig-keyid`
The key identifier to include in the `Signature-Input` header when using RFC
9421 HTTP Message Signatures. This value appears as the `keyid` parameter and
allows the server to look up the correct verification key.

View file

@ -474,6 +474,22 @@ See CURLOPT_HTTPPOST(3)
Tunnel through the HTTP proxy. CURLOPT_HTTPPROXYTUNNEL(3)
## CURLOPT_HTTPSIG_ALGORITHM
RFC 9421 HTTP Message Signatures algorithm. See CURLOPT_HTTPSIG_ALGORITHM(3)
## CURLOPT_HTTPSIG_HEADERS
Components to sign for HTTP Message Signatures. See CURLOPT_HTTPSIG_HEADERS(3)
## CURLOPT_HTTPSIG_KEY
Hex-encoded key for HTTP Message Signatures. See CURLOPT_HTTPSIG_KEY(3)
## CURLOPT_HTTPSIG_KEYID
Key identifier for HTTP Message Signatures. See CURLOPT_HTTPSIG_KEYID(3)
## CURLOPT_HTTP_CONTENT_DECODING
Disable Content decoding. See CURLOPT_HTTP_CONTENT_DECODING(3)

View file

@ -232,6 +232,13 @@ HTTP/3 and QUIC support are built-in (Added in 7.66.0)
libcurl was built with support for HTTPS-proxy.
## `HTTPSIG`
*features* mask bit: non-existent
libcurl was built with support for RFC 9421 HTTP Message Signatures (Added in
8.22.0)
## `HTTPSRR`
*features* mask bit: non-existent

View file

@ -118,6 +118,11 @@ single auth algorithm is acceptable.
provides AWS V4 signature authentication on HTTPS header
see CURLOPT_AWS_SIGV4(3).
## CURLAUTH_HTTPSIG
provides RFC 9421 HTTP Message Signatures on outgoing requests,
see CURLOPT_HTTPSIG_ALGORITHM(3).
# DEFAULT
CURLAUTH_BASIC
@ -158,6 +163,8 @@ CURLAUTH_AWS_SIGV4 was added in 7.74.0
CURLAUTH_DIGEST_IE does nothing since 8.21.0
CURLAUTH_HTTPSIG was added in 8.22.0
# %AVAILABILITY%
# RETURN VALUE

View file

@ -0,0 +1,89 @@
---
c: Copyright (C) Daniel Stenberg, <daniel@haxx.se>, et al.
SPDX-License-Identifier: curl
Title: CURLOPT_HTTPSIG_ALGORITHM
Section: 3
Source: libcurl
See-also:
- CURLOPT_HTTPSIG_HEADERS (3)
- CURLOPT_HTTPSIG_KEY (3)
- CURLOPT_HTTPSIG_KEYID (3)
- CURLOPT_HTTPAUTH (3)
Protocol:
- HTTP
Added-in: 8.22.0
---
# NAME
CURLOPT_HTTPSIG_ALGORITHM - RFC 9421 HTTP Message Signatures algorithm
# SYNOPSIS
~~~c
#include <curl/curl.h>
CURLcode curl_easy_setopt(CURL *handle, CURLOPT_HTTPSIG_ALGORITHM,
long algorithm);
~~~
# DESCRIPTION
This feature is **experimental** and may change before it is considered
stable. We advise against using it in production.
Enable RFC 9421 HTTP Message Signatures on outgoing requests. Pass a long
set to one of the values below to select the signing algorithm.
## CURLHTTPSIG_NONE (0)
Disable HTTP Message Signatures.
## CURLHTTPSIG_ED25519 (1)
Sign with Ed25519 (RFC 8032). Requires a TLS backend with Ed25519 support.
## CURLHTTPSIG_HMAC_SHA256 (2)
Sign with HMAC-SHA256.
##
Setting this option to a non-zero value also sets CURLOPT_HTTPAUTH(3) to
CURLAUTH_HTTPSIG. The options CURLOPT_HTTPSIG_KEY(3) and
CURLOPT_HTTPSIG_KEYID(3) must also be set.
# DEFAULT
CURLHTTPSIG_NONE (0)
# %PROTOCOLS%
# EXAMPLE
~~~c
int main(void)
{
CURL *curl = curl_easy_init();
if(curl) {
curl_easy_setopt(curl, CURLOPT_URL, "https://example.com/api");
curl_easy_setopt(curl, CURLOPT_HTTPSIG_ALGORITHM,
CURLHTTPSIG_ED25519);
curl_easy_setopt(curl, CURLOPT_HTTPSIG_KEY,
"9f8362f87a484a954e6e740c5b4c0e84"
"229139a20aa8ab56ff66586f6a7d29c5");
curl_easy_setopt(curl, CURLOPT_HTTPSIG_KEYID, "my-key-id");
curl_easy_perform(curl);
}
}
~~~
# %AVAILABILITY%
# RETURN VALUE
curl_easy_setopt(3) returns a CURLcode indicating success or error.
CURLE_OK (0) means everything was OK, non-zero means an error occurred, see
libcurl-errors(3).

View file

@ -0,0 +1,108 @@
---
c: Copyright (C) Daniel Stenberg, <daniel@haxx.se>, et al.
SPDX-License-Identifier: curl
Title: CURLOPT_HTTPSIG_HEADERS
Section: 3
Source: libcurl
See-also:
- CURLOPT_HTTPSIG_ALGORITHM (3)
- CURLOPT_HTTPSIG_KEY (3)
- CURLOPT_HTTPSIG_KEYID (3)
Protocol:
- HTTP
Added-in: 8.22.0
---
# NAME
CURLOPT_HTTPSIG_HEADERS - components to sign for HTTP Message Signatures
# SYNOPSIS
~~~c
#include <curl/curl.h>
CURLcode curl_easy_setopt(CURL *handle, CURLOPT_HTTPSIG_HEADERS,
char *components);
~~~
# DESCRIPTION
This feature is **experimental** and may change before it is considered
stable. We advise against using it in production.
Pass a space-separated list of component identifiers to include in the
RFC 9421 HTTP Message Signature.
Derived components are given as bare names:
- **method** - the HTTP method (GET, POST, etc.)
- **authority** - the host and optional port
- **path** - the request path
- **query** - the query string including the leading `?`
HTTP header fields are given with a trailing colon, for example `content-type:`
or `content-digest:`. This mirrors how a header looks and keeps a leading `@`
free for its usual curl meaning (read the value from a file).
If this option is not set, the default components are **method**, **authority**,
**path** (plus **query** when a query string is present).
## Signing request headers
Header components are resolved from the list set with CURLOPT_HTTPHEADER(3)
only. Headers that libcurl adds later (such as the default `User-Agent`) are
**not** visible to the signer unless the application supplies them explicitly.
To sign `User-Agent`, supply it via CURLOPT_HTTPHEADER(3) together with this
option before the transfer; see EXAMPLE.
Each component identifier may appear at most once (RFC 9421 Section 2).
Listing the same component twice returns `CURLE_BAD_FUNCTION_ARGUMENT`.
At most 16 components are accepted; supplying more returns
`CURLE_BAD_FUNCTION_ARGUMENT`.
The application does not have to keep the string around after setting this
option.
# DEFAULT
NULL (uses the default component set)
# %PROTOCOLS%
# EXAMPLE
~~~c
int main(void)
{
CURL *curl = curl_easy_init();
struct curl_slist *headers = NULL;
if(curl) {
headers = curl_slist_append(headers, "User-Agent: MyApp/1.0");
curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
curl_easy_setopt(curl, CURLOPT_URL, "https://example.com/api");
curl_easy_setopt(curl, CURLOPT_HTTPSIG_ALGORITHM,
CURLHTTPSIG_ED25519);
curl_easy_setopt(curl, CURLOPT_HTTPSIG_KEY,
"9f8362f87a484a954e6e740c5b4c0e84"
"229139a20aa8ab56ff66586f6a7d29c5");
curl_easy_setopt(curl, CURLOPT_HTTPSIG_KEYID, "my-key-id");
curl_easy_setopt(curl, CURLOPT_HTTPSIG_HEADERS,
"method authority path content-type: user-agent:");
curl_easy_perform(curl);
curl_slist_free_all(headers);
}
}
~~~
# %AVAILABILITY%
# RETURN VALUE
curl_easy_setopt(3) returns a CURLcode indicating success or error.
CURLE_OK (0) means everything was OK, non-zero means an error occurred, see
libcurl-errors(3).

View file

@ -0,0 +1,90 @@
---
c: Copyright (C) Daniel Stenberg, <daniel@haxx.se>, et al.
SPDX-License-Identifier: curl
Title: CURLOPT_HTTPSIG_KEY
Section: 3
Source: libcurl
See-also:
- CURLOPT_HTTPSIG_ALGORITHM (3)
- CURLOPT_HTTPSIG_KEYID (3)
Protocol:
- HTTP
Added-in: 8.22.0
---
# NAME
CURLOPT_HTTPSIG_KEY - hex-encoded key for HTTP Message Signatures
# SYNOPSIS
~~~c
#include <curl/curl.h>
CURLcode curl_easy_setopt(CURL *handle, CURLOPT_HTTPSIG_KEY, char *key);
~~~
# DESCRIPTION
This feature is **experimental** and may change before it is considered
stable. We advise against using it in production.
Pass a null-terminated string containing the hex-encoded private key or
shared secret used for RFC 9421 HTTP Message Signatures.
For **ed25519**, this is the 32-byte private seed (64 hex characters). For
**hmac-sha256**, this is the shared secret as hex; the decoded length is half
the number of hex digits, up to `CURL_MAX_INPUT_LENGTH / 2` bytes (the same
upper bound as other libcurl string options).
PEM and other encodings are not supported; pass the raw key material as hex.
## Generating Ed25519 keys
With OpenSSL 3:
openssl genpkey -algorithm ED25519 -out ed25519.pem
openssl pkey -in ed25519.pem -outform RAW | xxd -p -c 64 | tr -d '\n' > key.hex
The `key.hex` file is one line of 64 hexadecimal digits.
The application does not have to keep the string around after setting this
option.
Using this option multiple times makes the last set string override the
previous ones. Set it to NULL to disable its use again.
# DEFAULT
NULL
# %PROTOCOLS%
# EXAMPLE
~~~c
int main(void)
{
CURL *curl = curl_easy_init();
if(curl) {
curl_easy_setopt(curl, CURLOPT_URL, "https://example.com/api");
curl_easy_setopt(curl, CURLOPT_HTTPSIG_ALGORITHM,
CURLHTTPSIG_ED25519);
curl_easy_setopt(curl, CURLOPT_HTTPSIG_KEY,
"9f8362f87a484a954e6e740c5b4c0e84"
"229139a20aa8ab56ff66586f6a7d29c5");
curl_easy_setopt(curl, CURLOPT_HTTPSIG_KEYID, "my-key-id");
curl_easy_perform(curl);
}
}
~~~
# %AVAILABILITY%
# RETURN VALUE
curl_easy_setopt(3) returns a CURLcode indicating success or error.
CURLE_OK (0) means everything was OK, non-zero means an error occurred, see
libcurl-errors(3).

View file

@ -0,0 +1,76 @@
---
c: Copyright (C) Daniel Stenberg, <daniel@haxx.se>, et al.
SPDX-License-Identifier: curl
Title: CURLOPT_HTTPSIG_KEYID
Section: 3
Source: libcurl
See-also:
- CURLOPT_HTTPSIG_ALGORITHM (3)
- CURLOPT_HTTPSIG_KEY (3)
Protocol:
- HTTP
Added-in: 8.22.0
---
# NAME
CURLOPT_HTTPSIG_KEYID - key identifier for HTTP Message Signatures
# SYNOPSIS
~~~c
#include <curl/curl.h>
CURLcode curl_easy_setopt(CURL *handle, CURLOPT_HTTPSIG_KEYID, char *keyid);
~~~
# DESCRIPTION
This feature is **experimental** and may change before it is considered
stable. We advise against using it in production.
Pass a null-terminated string that identifies the key used for RFC 9421 HTTP
Message Signatures. This value is included as the `keyid` parameter in the
`Signature-Input` header, allowing the server to look up the corresponding
verification key.
The application does not have to keep the string around after setting this
option.
Using this option multiple times makes the last set string override the
previous ones. Set it to NULL to disable its use again.
# DEFAULT
NULL
# %PROTOCOLS%
# EXAMPLE
~~~c
int main(void)
{
CURL *curl = curl_easy_init();
if(curl) {
curl_easy_setopt(curl, CURLOPT_URL, "https://example.com/api");
curl_easy_setopt(curl, CURLOPT_HTTPSIG_ALGORITHM,
CURLHTTPSIG_ED25519);
curl_easy_setopt(curl, CURLOPT_HTTPSIG_KEY,
"9f8362f87a484a954e6e740c5b4c0e84"
"229139a20aa8ab56ff66586f6a7d29c5");
curl_easy_setopt(curl, CURLOPT_HTTPSIG_KEYID, "my-key-id");
curl_easy_perform(curl);
}
}
~~~
# %AVAILABILITY%
# RETURN VALUE
curl_easy_setopt(3) returns a CURLcode indicating success or error.
CURLE_OK (0) means everything was OK, non-zero means an error occurred, see
libcurl-errors(3).

View file

@ -227,6 +227,10 @@ man_MANS = \
CURLOPT_HTTPHEADER.3 \
CURLOPT_HTTPPOST.3 \
CURLOPT_HTTPPROXYTUNNEL.3 \
CURLOPT_HTTPSIG_ALGORITHM.3 \
CURLOPT_HTTPSIG_HEADERS.3 \
CURLOPT_HTTPSIG_KEY.3 \
CURLOPT_HTTPSIG_KEYID.3 \
CURLOPT_IGNORE_CONTENT_LENGTH.3 \
CURLOPT_INFILESIZE.3 \
CURLOPT_INFILESIZE_LARGE.3 \

View file

@ -202,6 +202,7 @@ CURLALTSVC_READONLYFILE 7.64.1
CURLAUTH_ANY 7.10.6
CURLAUTH_ANYSAFE 7.10.6
CURLAUTH_AWS_SIGV4 7.75.0
CURLAUTH_HTTPSIG 8.22.0
CURLAUTH_BASIC 7.10.6
CURLAUTH_BEARER 7.61.0
CURLAUTH_DIGEST 7.10.6
@ -420,6 +421,9 @@ CURLHEADER_SEPARATE 7.37.0
CURLHEADER_UNIFIED 7.37.0
CURLHSTS_ENABLE 7.74.0
CURLHSTS_READONLYFILE 7.74.0
CURLHTTPSIG_ED25519 8.22.0
CURLHTTPSIG_HMAC_SHA256 8.22.0
CURLHTTPSIG_NONE 8.22.0
CURLINFO_ACTIVESOCKET 7.45.0
CURLINFO_APPCONNECT_TIME 7.19.0
CURLINFO_APPCONNECT_TIME_T 7.61.0
@ -697,6 +701,10 @@ CURLOPT_HTTPHEADER 7.1
CURLOPT_HTTPPOST 7.1 7.56.0
CURLOPT_HTTPPROXYTUNNEL 7.3
CURLOPT_HTTPREQUEST 7.1 - 7.15.5
CURLOPT_HTTPSIG_ALGORITHM 8.22.0
CURLOPT_HTTPSIG_HEADERS 8.22.0
CURLOPT_HTTPSIG_KEY 8.22.0
CURLOPT_HTTPSIG_KEYID 8.22.0
CURLOPT_IGNORE_CONTENT_LENGTH 7.14.1
CURLOPT_INFILE 7.1 7.9.7
CURLOPT_INFILESIZE 7.1

View file

@ -98,6 +98,10 @@
--http2-prior-knowledge 7.49.0
--http3 7.66.0
--http3-only 7.88.0
--httpsig-algo 8.22.0
--httpsig-headers 8.22.0
--httpsig-key 8.22.0
--httpsig-keyid 8.22.0
--ignore-content-length 7.14.1
--ip-tos 8.9.0
--ipfs-gateway 8.4.0

View file

@ -843,12 +843,20 @@ typedef enum {
#endif
#define CURLAUTH_BEARER (((unsigned long)1) << 6)
#define CURLAUTH_AWS_SIGV4 (((unsigned long)1) << 7)
#define CURLAUTH_HTTPSIG (((unsigned long)1) << 8)
#define CURLAUTH_ONLY (((unsigned long)1) << 31)
#define CURLAUTH_ANY ((~CURLAUTH_DIGEST_IE) & \
#define CURLAUTH_ANY ((~(CURLAUTH_DIGEST_IE | \
CURLAUTH_HTTPSIG)) & \
((unsigned long)0xffffffff))
#define CURLAUTH_ANYSAFE ((~(CURLAUTH_BASIC | CURLAUTH_DIGEST_IE)) & \
#define CURLAUTH_ANYSAFE ((~(CURLAUTH_BASIC | CURLAUTH_DIGEST_IE | \
CURLAUTH_HTTPSIG)) & \
((unsigned long)0xffffffff))
/* constants for CURLOPT_HTTPSIG_ALGORITHM */
#define CURLHTTPSIG_NONE 0L
#define CURLHTTPSIG_ED25519 1L
#define CURLHTTPSIG_HMAC_SHA256 2L
/* all types supported by server */
#define CURLSSH_AUTH_ANY ((unsigned long)0xffffffff)
#define CURLSSH_AUTH_NONE 0L /* none allowed, silly but complete */
@ -2266,6 +2274,18 @@ typedef enum {
/* set TLS supported signature algorithms */
CURLOPT(CURLOPT_SSL_SIGNATURE_ALGORITHMS, CURLOPTTYPE_STRINGPOINT, 328),
/* RFC 9421 HTTP Message Signatures algorithm */
CURLOPT(CURLOPT_HTTPSIG_ALGORITHM, CURLOPTTYPE_VALUES, 329),
/* Hex-encoded key for HTTP Message Signatures */
CURLOPT(CURLOPT_HTTPSIG_KEY, CURLOPTTYPE_STRINGPOINT, 330),
/* Key identifier for HTTP Message Signatures */
CURLOPT(CURLOPT_HTTPSIG_KEYID, CURLOPTTYPE_STRINGPOINT, 331),
/* Space-separated list of components to sign for HTTP Message Signatures */
CURLOPT(CURLOPT_HTTPSIG_HEADERS, CURLOPTTYPE_STRINGPOINT, 332),
CURLOPT_LASTENTRY /* the last unused */
} CURLoption;

View file

@ -496,6 +496,9 @@ CURLWARNING(Wcurl_easy_getinfo_err_curl_off_t,
(option) == CURLOPT_USERAGENT || \
(option) == CURLOPT_USERNAME || \
(option) == CURLOPT_AWS_SIGV4 || \
(option) == CURLOPT_HTTPSIG_HEADERS || \
(option) == CURLOPT_HTTPSIG_KEY || \
(option) == CURLOPT_HTTPSIG_KEYID || \
(option) == CURLOPT_USERPWD || \
(option) == CURLOPT_XOAUTH2_BEARER || \
0)

View file

@ -222,6 +222,8 @@ LIB_CFILES = \
http1.c \
http2.c \
http_aws_sigv4.c \
http_httpsig.c \
curl_ed25519.c \
http_chunks.c \
http_digest.c \
http_negotiate.c \
@ -363,6 +365,8 @@ LIB_HFILES = \
http1.h \
http2.h \
http_aws_sigv4.h \
http_httpsig.h \
curl_ed25519.h \
http_chunks.h \
http_digest.h \
http_negotiate.h \

View file

@ -61,6 +61,9 @@
/* disables aws-sigv4 */
#cmakedefine CURL_DISABLE_AWS 1
/* disables HTTP Message Signatures (RFC 9421) */
#cmakedefine CURL_DISABLE_HTTPSIG 1
/* disables DICT */
#cmakedefine CURL_DISABLE_DICT 1

170
lib/curl_ed25519.c Normal file
View file

@ -0,0 +1,170 @@
/***************************************************************************
* _ _ ____ _
* 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)
/* Please keep the SSL backend-specific #if branches in this order:
*
* 1. USE_OPENSSL
* 2. USE_WOLFSSL
* 3. USE_GNUTLS
* 4. USE_MBEDTLS
*/
#include "curl_ed25519.h"
#ifdef USE_OPENSSL
#include <openssl/evp.h>
CURLcode Curl_ed25519_sign(const unsigned char *key, size_t keylen,
const unsigned char *msg, size_t msglen,
unsigned char *sig, size_t *siglen)
{
EVP_PKEY *pkey;
EVP_MD_CTX *mdctx;
size_t slen;
int rc;
if(keylen != CURL_ED25519_KEYLEN)
return CURLE_BAD_FUNCTION_ARGUMENT;
pkey = EVP_PKEY_new_raw_private_key(EVP_PKEY_ED25519, NULL, key, keylen);
if(!pkey)
return CURLE_AUTH_ERROR;
mdctx = EVP_MD_CTX_new();
if(!mdctx) {
EVP_PKEY_free(pkey);
return CURLE_OUT_OF_MEMORY;
}
rc = EVP_DigestSignInit(mdctx, NULL, NULL, NULL, pkey);
if(rc != 1) {
EVP_MD_CTX_free(mdctx);
EVP_PKEY_free(pkey);
return CURLE_AUTH_ERROR;
}
slen = CURL_ED25519_SIGLEN;
rc = EVP_DigestSign(mdctx, sig, &slen, msg, msglen);
EVP_MD_CTX_free(mdctx);
EVP_PKEY_free(pkey);
if(rc != 1)
return CURLE_AUTH_ERROR;
*siglen = slen;
return CURLE_OK;
}
#elif defined(USE_WOLFSSL)
#include <wolfssl/options.h>
#include <wolfssl/wolfcrypt/settings.h>
#if (defined(HAVE_ED25519) || defined(WOLFSSL_CURVE25519_USE_ED25519)) && \
defined(HAVE_ED25519_KEY_IMPORT) && defined(HAVE_ED25519_SIGN)
#include <wolfssl/wolfcrypt/ed25519.h>
#include <wolfssl/wolfcrypt/error-crypt.h>
#include <wolfssl/wolfcrypt/random.h>
CURLcode Curl_ed25519_sign(const unsigned char *key, size_t keylen,
const unsigned char *msg, size_t msglen,
unsigned char *sig, size_t *siglen)
{
int ret;
WC_RNG rng;
ed25519_key edkey;
word32 outlen;
unsigned char pubkey[ED25519_PUB_KEY_SIZE];
if(keylen != ED25519_KEY_SIZE)
return CURLE_BAD_FUNCTION_ARGUMENT;
ret = wc_InitRng(&rng);
if(ret)
return CURLE_AUTH_ERROR;
ret = wc_ed25519_init(&edkey);
if(ret) {
wc_FreeRng(&rng);
return CURLE_AUTH_ERROR;
}
ret = wc_ed25519_import_private_only(key, ED25519_KEY_SIZE, &edkey);
if(ret)
goto fail;
ret = wc_ed25519_make_public(&edkey, pubkey, ED25519_PUB_KEY_SIZE);
if(ret)
goto fail;
ret = wc_ed25519_import_private_key(key, ED25519_KEY_SIZE,
pubkey, ED25519_PUB_KEY_SIZE, &edkey);
if(ret)
goto fail;
outlen = ED25519_SIG_SIZE;
ret = wc_ed25519_sign_msg(msg, (word32)msglen, sig, &outlen, &edkey);
if(ret)
goto fail;
*siglen = (size_t)outlen;
wc_ed25519_free(&edkey);
wc_FreeRng(&rng);
return CURLE_OK;
fail:
wc_ed25519_free(&edkey);
wc_FreeRng(&rng);
return CURLE_AUTH_ERROR;
}
#else /* USE_WOLFSSL but no Ed25519 sign/import in this wolfSSL build */
CURLcode Curl_ed25519_sign(const unsigned char *key, size_t keylen,
const unsigned char *msg, size_t msglen,
unsigned char *sig, size_t *siglen)
{
(void)key; (void)keylen; (void)msg; (void)msglen;
(void)sig; (void)siglen;
return CURLE_NOT_BUILT_IN;
}
#endif /* wolfSSL Ed25519 */
#else /* no Ed25519-capable backend */
CURLcode Curl_ed25519_sign(const unsigned char *key, size_t keylen,
const unsigned char *msg, size_t msglen,
unsigned char *sig, size_t *siglen)
{
(void)key; (void)keylen; (void)msg; (void)msglen;
(void)sig; (void)siglen;
return CURLE_NOT_BUILT_IN;
}
#endif /* Ed25519 backends */
#endif /* !CURL_DISABLE_HTTP && !CURL_DISABLE_HTTPSIG */

44
lib/curl_ed25519.h Normal file
View file

@ -0,0 +1,44 @@
#ifndef HEADER_CURL_ED25519_H
#define HEADER_CURL_ED25519_H
/***************************************************************************
* _ _ ____ _
* 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)
#define CURL_ED25519_SIGLEN 64
#define CURL_ED25519_KEYLEN 32
/* Sign with Ed25519 (RFC 8032).
* key/keylen: raw 32-byte private seed
* msg/msglen: data to sign
* sig: output buffer (at least CURL_ED25519_SIGLEN bytes)
* siglen: out - actual signature length on success
* Returns CURLE_OK or CURLE_NOT_BUILT_IN if no backend supports Ed25519. */
CURLcode Curl_ed25519_sign(const unsigned char *key, size_t keylen,
const unsigned char *msg, size_t msglen,
unsigned char *sig, size_t *siglen);
#endif /* !CURL_DISABLE_HTTP && !CURL_DISABLE_HTTPSIG */
#endif /* HEADER_CURL_ED25519_H */

View file

@ -26,6 +26,7 @@
#if (defined(USE_CURL_NTLM_CORE) && !defined(USE_WINDOWS_SSPI)) || \
!defined(CURL_DISABLE_AWS) || !defined(CURL_DISABLE_DIGEST_AUTH) || \
!defined(CURL_DISABLE_HTTPSIG) || \
defined(USE_LIBSSH2) || defined(USE_SSL)
#define HMAC_MD5_LENGTH 16

View file

@ -298,6 +298,9 @@
# ifndef CURL_DISABLE_AWS
# define CURL_DISABLE_AWS
# endif
# ifndef CURL_DISABLE_HTTPSIG
# define CURL_DISABLE_HTTPSIG
# endif
# ifndef CURL_DISABLE_DOH
# define CURL_DISABLE_DOH
# endif

View file

@ -26,7 +26,8 @@
***************************************************************************/
#include "curl_setup.h"
#if !defined(CURL_DISABLE_AWS) || !defined(CURL_DISABLE_DIGEST_AUTH) || \
#if !defined(CURL_DISABLE_AWS) || !defined(CURL_DISABLE_HTTPSIG) || \
!defined(CURL_DISABLE_DIGEST_AUTH) || \
defined(USE_LIBSSH2) || defined(USE_SSL)
#include "curl_hmac.h"

View file

@ -140,6 +140,10 @@ const struct curl_easyoption Curl_easyopts[] = {
{ "HTTPHEADER", CURLOPT_HTTPHEADER, CURLOT_SLIST, 0 },
{ "HTTPPOST", CURLOPT_HTTPPOST, CURLOT_OBJECT, 0 },
{ "HTTPPROXYTUNNEL", CURLOPT_HTTPPROXYTUNNEL, CURLOT_LONG, 0 },
{ "HTTPSIG_ALGORITHM", CURLOPT_HTTPSIG_ALGORITHM, CURLOT_VALUES, 0 },
{ "HTTPSIG_HEADERS", CURLOPT_HTTPSIG_HEADERS, CURLOT_STRING, 0 },
{ "HTTPSIG_KEY", CURLOPT_HTTPSIG_KEY, CURLOT_STRING, 0 },
{ "HTTPSIG_KEYID", CURLOPT_HTTPSIG_KEYID, CURLOT_STRING, 0 },
{ "HTTP_CONTENT_DECODING", CURLOPT_HTTP_CONTENT_DECODING, CURLOT_LONG, 0 },
{ "HTTP_TRANSFER_DECODING", CURLOPT_HTTP_TRANSFER_DECODING,
CURLOT_LONG, 0 },
@ -385,6 +389,6 @@ const struct curl_easyoption Curl_easyopts[] = {
*/
int Curl_easyopts_check(void)
{
return (CURLOPT_LASTENTRY % 10000) != (328 + 1);
return (CURLOPT_LASTENTRY % 10000) != (332 + 1);
}
#endif

View file

@ -61,6 +61,7 @@
#include "http_ntlm.h"
#include "http_negotiate.h"
#include "http_aws_sigv4.h"
#include "http_httpsig.h"
#include "url.h"
#include "urlapi-int.h"
#include "curl_share.h"
@ -376,6 +377,10 @@ static bool pickoneauth(struct auth *pick, unsigned long mask,
#ifndef CURL_DISABLE_AWS
else if(avail & CURLAUTH_AWS_SIGV4)
pick->picked = CURLAUTH_AWS_SIGV4;
#endif
#ifndef CURL_DISABLE_HTTPSIG
else if(avail & CURLAUTH_HTTPSIG)
pick->picked = CURLAUTH_HTTPSIG;
#endif
else {
pick->picked = CURLAUTH_PICKNONE; /* we select to use nothing */
@ -665,6 +670,15 @@ static CURLcode output_auth_headers(struct Curl_easy *data,
}
else
#endif
#ifndef CURL_DISABLE_HTTPSIG
if((authstatus->picked == CURLAUTH_HTTPSIG) && !proxy) {
auth = "HTTPSIG";
result = Curl_output_httpsig(data);
if(result)
return result;
}
else
#endif
#ifdef USE_SPNEGO
if(authstatus->picked == CURLAUTH_NEGOTIATE) {
auth = "Negotiate";
@ -792,6 +806,9 @@ CURLcode Curl_http_output_auth(struct Curl_easy *data,
#ifdef USE_SPNEGO
!(authhost->want & CURLAUTH_NEGOTIATE) &&
!(authproxy->want & CURLAUTH_NEGOTIATE) &&
#endif
#ifndef CURL_DISABLE_HTTPSIG
!(authhost->want & CURLAUTH_HTTPSIG) &&
#endif
!data->state.creds) {
/* no authentication with no user or password */

692
lib/http_httpsig.c Normal file
View file

@ -0,0 +1,692 @@
/***************************************************************************
* _ _ ____ _
* 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.aptr.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.aptr.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 = data->set.str[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;
const char *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;
}
hexkey = data->set.str[STRING_HTTPSIG_KEY];
keyid = data->set.str[STRING_HTTPSIG_KEYID];
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 */

35
lib/http_httpsig.h Normal file
View file

@ -0,0 +1,35 @@
#ifndef HEADER_CURL_HTTP_HTTPSIG_H
#define HEADER_CURL_HTTP_HTTPSIG_H
/***************************************************************************
* _ _ ____ _
* 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"
CURLcode Curl_output_httpsig(struct Curl_easy *data);
#endif /* !CURL_DISABLE_HTTP && !CURL_DISABLE_HTTPSIG */
#endif /* HEADER_CURL_HTTP_HTTPSIG_H */

View file

@ -1103,6 +1103,19 @@ static CURLcode setopt_long_http(struct Curl_easy *data, CURLoption option,
#else
result = CURLE_NOT_BUILT_IN;
break;
#endif
#ifndef CURL_DISABLE_HTTPSIG
case CURLOPT_HTTPSIG_ALGORITHM:
if(arg != CURLHTTPSIG_NONE &&
arg != CURLHTTPSIG_ED25519 &&
arg != CURLHTTPSIG_HMAC_SHA256)
return CURLE_BAD_FUNCTION_ARGUMENT;
s->httpsig_algorithm = (uint8_t)arg;
if(arg)
s->httpauth = (uint32_t)CURLAUTH_HTTPSIG;
else
s->httpauth &= ~(uint32_t)CURLAUTH_HTTPSIG;
break;
#endif
default:
return CURLE_UNKNOWN_OPTION;
@ -2081,6 +2094,17 @@ static CURLcode setopt_cptr_http_mqtt(struct Curl_easy *data,
if(s->str[STRING_AWS_SIGV4])
s->httpauth = CURLAUTH_AWS_SIGV4;
break;
#endif
#ifndef CURL_DISABLE_HTTPSIG
case CURLOPT_HTTPSIG_KEY:
result = Curl_setstropt(&s->str[STRING_HTTPSIG_KEY], ptr);
break;
case CURLOPT_HTTPSIG_KEYID:
result = Curl_setstropt(&s->str[STRING_HTTPSIG_KEYID], ptr);
break;
case CURLOPT_HTTPSIG_HEADERS:
result = Curl_setstropt(&s->str[STRING_HTTPSIG_HEADERS], ptr);
break;
#endif
case CURLOPT_REFERER:
/*

View file

@ -849,6 +849,11 @@ enum dupstring {
#ifndef CURL_DISABLE_AWS
STRING_AWS_SIGV4, /* Parameters for V4 signature */
#endif
#ifndef CURL_DISABLE_HTTPSIG
STRING_HTTPSIG_KEY, /* hex-encoded key data */
STRING_HTTPSIG_KEYID, /* key identifier */
STRING_HTTPSIG_HEADERS, /* space-separated components to sign */
#endif
#ifndef CURL_DISABLE_PROXY
STRING_HAPROXY_CLIENT_IP, /* CURLOPT_HAPROXY_CLIENT_IP */
#endif
@ -890,6 +895,7 @@ struct UserDefined {
void *writeheader; /* write the header to this if non-NULL */
uint32_t httpauth; /* kind of HTTP authentication to use (bitmask) */
uint32_t proxyauth; /* kind of proxy authentication to use (bitmask) */
uint8_t httpsig_algorithm; /* CURLHTTPSIG_* algorithm for RFC 9421 */
void *postfields; /* if POST, set the fields' values here */
curl_seek_callback seek_func; /* function that seeks the input */
curl_off_t postfieldsize; /* if POST, this might have a size to use instead

View file

@ -469,6 +469,9 @@ static const struct feat features_table[] = {
!defined(CURL_DISABLE_HTTP)
FEATURE("HTTPS-proxy", https_proxy_present, CURL_VERSION_HTTPS_PROXY),
#endif
#ifndef CURL_DISABLE_HTTPSIG
FEATURE("HTTPSIG", NULL, 0),
#endif
#ifdef USE_HTTPSRR
FEATURE("HTTPSRR", NULL, 0),
#endif

View file

@ -71,6 +71,9 @@ options:
CURLOPT_FTP_ALTERNATIVE_TO_USER
CURLOPT_HAPROXY_CLIENT_IP
CURLOPT_HSTS
CURLOPT_HTTPSIG_HEADERS
CURLOPT_HTTPSIG_KEY
CURLOPT_HTTPSIG_KEYID
CURLOPT_INTERFACE
CURLOPT_ISSUERCERT
CURLOPT_KEYPASSWD

View file

@ -1060,6 +1060,9 @@ CURLcode curl_easy_setopt_ccsid(CURL *easy, CURLoption tag, ...)
case CURLOPT_FTP_ALTERNATIVE_TO_USER:
case CURLOPT_HAPROXY_CLIENT_IP:
case CURLOPT_HSTS:
case CURLOPT_HTTPSIG_HEADERS:
case CURLOPT_HTTPSIG_KEY:
case CURLOPT_HTTPSIG_KEYID:
case CURLOPT_INTERFACE:
case CURLOPT_ISSUERCERT:
case CURLOPT_KEYPASSWD:

View file

@ -1143,7 +1143,14 @@ HEAD
elsif(length($desc) > 78) {
print STDERR "WARN: the --$long description is too long\n";
}
print $line;
my $ifdef = "";
my $endif = "";
if($long =~ /^(ipfs|httpsig)/) {
$ifdef = sprintf "#ifndef CURL_DISABLE_%s\n", uc($1);
$endif = "#endif\n";
}
print $ifdef.$line.$endif;
}
print <<FOOT
{ NULL, NULL, 0 }

View file

@ -38,6 +38,7 @@
#include "tool_cb_see.h"
#include "tool_cb_dbg.h"
#include "tool_helpers.h"
#include "tool_paramhlp.h"
#include "tool_version.h"
#ifdef HAVE_NETINET_IN_H
@ -564,6 +565,80 @@ static CURLcode cookie_setopts(struct OperationConfig *config, CURL *curl)
return result;
}
/* only --httpsig-* options */
#ifndef CURL_DISABLE_HTTPSIG
static CURLcode httpsig_setopts(struct OperationConfig *config, CURL *curl)
{
CURLcode result = CURLE_OK;
/* HTTP Message Signatures are enabled when any of the --httpsig-* options
is given. --httpsig-key and --httpsig-keyid are then required, while
--httpsig-algo is optional and defaults to ed25519. */
if(config->httpsig_algorithm || config->httpsig_key ||
config->httpsig_keyid || config->httpsig_headers) {
long httpsig_alg = CURLHTTPSIG_NONE;
if(!config->httpsig_key) {
errorf("--httpsig-key is required");
return CURLE_FAILED_INIT;
}
if(!config->httpsig_keyid) {
errorf("--httpsig-keyid is required");
return CURLE_FAILED_INIT;
}
if(!config->httpsig_algorithm ||
curl_strequal(config->httpsig_algorithm, "ed25519"))
httpsig_alg = CURLHTTPSIG_ED25519;
else if(curl_strequal(config->httpsig_algorithm, "hmac-sha256"))
httpsig_alg = CURLHTTPSIG_HMAC_SHA256;
else {
errorf("--httpsig-algo: unsupported algorithm '%s'",
config->httpsig_algorithm);
return CURLE_FAILED_INIT;
}
my_setopt_long(curl, CURLOPT_HTTPSIG_ALGORITHM, httpsig_alg);
MY_SETOPT_STR(curl, CURLOPT_HTTPSIG_HEADERS, config->httpsig_headers);
MY_SETOPT_STR(curl, CURLOPT_HTTPSIG_KEYID, config->httpsig_keyid);
{
FILE *keyf = curlx_fopen(config->httpsig_key, FOPEN_READTEXT);
if(keyf) {
char *hexdata = NULL;
ParameterError pe = file2string(&hexdata, keyf);
curlx_fclose(keyf);
if(pe == PARAM_NO_MEM) {
curlx_safefree(hexdata);
return CURLE_OUT_OF_MEMORY;
}
if(pe == PARAM_READ_ERROR) {
curlx_safefree(hexdata);
errorf("httpsig: cannot read key file '%s'", config->httpsig_key);
return CURLE_READ_ERROR;
}
if(!hexdata || !*hexdata) {
curlx_safefree(hexdata);
errorf("httpsig: key file '%s' is empty", config->httpsig_key);
return CURLE_BAD_FUNCTION_ARGUMENT;
}
/* can't use the MY_SETOPT_STR() macro here since it returns on error
and we must free the hexdata */
result = my_setopt_str(curl, CURLOPT_HTTPSIG_KEY, hexdata);
curlx_safefree(hexdata);
if(setopt_bad(result))
return result;
}
else {
errorf("httpsig: cannot open key file '%s'", config->httpsig_key);
return CURLE_READ_ERROR;
}
}
}
return CURLE_OK;
}
#else
#define httpsig_setopts(x,y) CURLE_OK
#endif
/* only for HTTP transfers */
static CURLcode http_setopts(struct OperationConfig *config, CURL *curl,
const char *use_proto)
@ -579,6 +654,9 @@ static CURLcode http_setopts(struct OperationConfig *config, CURL *curl,
#ifndef CURL_DISABLE_AWS
MY_SETOPT_STR(curl, CURLOPT_AWS_SIGV4, config->aws_sigv4);
#endif
result = httpsig_setopts(config, curl);
if(result)
return result;
my_setopt_long(curl, CURLOPT_AUTOREFERER, config->autoreferer);
if(config->proxyheaders) {

View file

@ -93,6 +93,13 @@ static const char *disabled[] = {
"OFF"
#else
"ON"
#endif
,
"httpsig: "
#ifdef CURL_DISABLE_HTTPSIG
"OFF"
#else
"ON"
#endif
,
"DoH: "

View file

@ -181,6 +181,10 @@ static void free_config_fields(struct OperationConfig *config)
curlx_safefree(config->ftp_account);
curlx_safefree(config->ftp_alternative_to_user);
curlx_safefree(config->aws_sigv4);
curlx_safefree(config->httpsig_algorithm);
curlx_safefree(config->httpsig_headers);
curlx_safefree(config->httpsig_key);
curlx_safefree(config->httpsig_keyid);
curlx_safefree(config->ech);
curlx_safefree(config->ech_config);
curlx_safefree(config->ech_public);

View file

@ -153,6 +153,10 @@ struct OperationConfig {
char *unix_socket_path; /* path to Unix domain socket */
char *haproxy_clientip; /* client IP for HAProxy protocol */
char *aws_sigv4;
char *httpsig_algorithm;
char *httpsig_headers;
char *httpsig_key;
char *httpsig_keyid;
char *ech; /* Config set by --ech keywords */
char *ech_config; /* Config set by "--ech esl:" option */
char *ech_public; /* Config set by "--ech pn:" option */

View file

@ -170,6 +170,12 @@ static const struct LongShort aliases[] = {
{"http2-prior-knowledge", ARG_NONE, ' ', C_HTTP2_PRIOR_KNOWLEDGE},
{"http3", ARG_NONE|ARG_TLS, ' ', C_HTTP3},
{"http3-only", ARG_NONE|ARG_TLS, ' ', C_HTTP3_ONLY},
#ifndef CURL_DISABLE_HTTPSIG
{"httpsig-algo", ARG_STRG, ' ', C_HTTPSIG_ALGORITHM},
{"httpsig-headers", ARG_STRG, ' ', C_HTTPSIG_HEADERS},
{"httpsig-key", ARG_FILE, ' ', C_HTTPSIG_KEY},
{"httpsig-keyid", ARG_STRG, ' ', C_HTTPSIG_KEYID},
#endif
{"ignore-content-length", ARG_BOOL, ' ', C_IGNORE_CONTENT_LENGTH},
{"include", ARG_BOOL, ' ', C_INCLUDE},
{"insecure", ARG_BOOL, 'k', C_INSECURE},
@ -2363,6 +2369,12 @@ static ParameterError opt_file(struct OperationConfig *config,
case C_UPLOAD_FILE: /* --upload-file */
err = parse_upload_file(config, nextarg);
break;
case C_HTTPSIG_KEY: /* --httpsig-key */
if(!feature_httpsig)
err = PARAM_LIBCURL_DOESNT_SUPPORT;
else
err = getstr(&config->httpsig_key, nextarg, DENY_BLANK);
break;
}
return err;
}
@ -2557,6 +2569,26 @@ static ParameterError opt_string(struct OperationConfig *config,
config->authtype |= CURLAUTH_AWS_SIGV4;
err = getstr(&config->aws_sigv4, nextarg, ALLOW_BLANK);
break;
case C_HTTPSIG_ALGORITHM: /* --httpsig-algo */
if(!feature_httpsig)
err = PARAM_LIBCURL_DOESNT_SUPPORT;
else {
config->authtype |= CURLAUTH_HTTPSIG;
err = getstr(&config->httpsig_algorithm, nextarg, DENY_BLANK);
}
break;
case C_HTTPSIG_KEYID: /* --httpsig-keyid */
if(!feature_httpsig)
err = PARAM_LIBCURL_DOESNT_SUPPORT;
else
err = getstr(&config->httpsig_keyid, nextarg, DENY_BLANK);
break;
case C_HTTPSIG_HEADERS: /* --httpsig-headers */
if(!feature_httpsig)
err = PARAM_LIBCURL_DOESNT_SUPPORT;
else
err = getstr(&config->httpsig_headers, nextarg, DENY_BLANK);
break;
case C_INTERFACE: /* --interface */
/* interface */
err = getstr(&config->iface, nextarg, DENY_BLANK);

View file

@ -125,6 +125,10 @@ typedef enum {
C_HTTP2_PRIOR_KNOWLEDGE,
C_HTTP3,
C_HTTP3_ONLY,
C_HTTPSIG_ALGORITHM,
C_HTTPSIG_HEADERS,
C_HTTPSIG_KEY,
C_HTTPSIG_KEYID,
C_IGNORE_CONTENT_LENGTH,
C_INCLUDE,
C_INSECURE,

View file

@ -69,6 +69,7 @@ bool feature_brotli = FALSE;
bool feature_hsts = FALSE;
bool feature_http2 = FALSE;
bool feature_http3 = FALSE;
bool feature_httpsig = FALSE;
bool feature_httpsproxy = FALSE;
bool feature_libz = FALSE;
bool feature_ntlm = FALSE;
@ -97,6 +98,7 @@ static struct feature_name_presentp {
{ "HTTP2", &feature_http2, CURL_VERSION_HTTP2 },
{ "HTTP3", &feature_http3, CURL_VERSION_HTTP3 },
{ "HTTPS-proxy", &feature_httpsproxy, CURL_VERSION_HTTPS_PROXY },
{ "HTTPSIG", &feature_httpsig, 0 },
{ "IDN", NULL, CURL_VERSION_IDN },
{ "IPv6", NULL, CURL_VERSION_IPV6 },
{ "Kerberos", NULL, CURL_VERSION_KERBEROS5 },

View file

@ -52,6 +52,7 @@ extern bool feature_brotli;
extern bool feature_hsts;
extern bool feature_http2;
extern bool feature_http3;
extern bool feature_httpsig;
extern bool feature_httpsproxy;
extern bool feature_libz;
extern bool feature_ntlm;

View file

@ -303,6 +303,26 @@ const struct helptxt helptext[] = {
{ " --http3-only",
"Use HTTP/3 only",
CURLHELP_HTTP },
#ifndef CURL_DISABLE_HTTPSIG
{ " --httpsig-algo <algorithm>",
"Algorithm for HTTP Message Signatures",
CURLHELP_AUTH | CURLHELP_HTTP },
#endif
#ifndef CURL_DISABLE_HTTPSIG
{ " --httpsig-headers <components>",
"Components to sign for HTTP Message Signatures",
CURLHELP_AUTH | CURLHELP_HTTP },
#endif
#ifndef CURL_DISABLE_HTTPSIG
{ " --httpsig-key <file>",
"Key file for HTTP Message Signatures",
CURLHELP_AUTH | CURLHELP_HTTP },
#endif
#ifndef CURL_DISABLE_HTTPSIG
{ " --httpsig-keyid <id>",
"Key identifier for HTTP Message Signatures",
CURLHELP_AUTH | CURLHELP_HTTP },
#endif
{ " --ignore-content-length",
"Ignore the size of the remote resource",
CURLHELP_HTTP | CURLHELP_FTP },
@ -315,9 +335,11 @@ const struct helptxt helptext[] = {
{ " --ip-tos <string>",
"Set IP Type of Service or Traffic Class",
CURLHELP_CONNECTION },
#ifndef CURL_DISABLE_IPFS
{ " --ipfs-gateway <URL>",
"Gateway for IPFS",
CURLHELP_CURL },
#endif
{ "-4, --ipv4",
"Resolve names to IPv4 addresses",
CURLHELP_CONNECTION | CURLHELP_DNS },

View file

@ -290,11 +290,16 @@ test3300 test3301 test3302 test3303 test3304 test3305 test3306 \
\
test3400 test3401 \
\
test4000 test4001
test4000 test4001 \
\
test5000 test5001 test5002 test5003 test5004 test5005 test5006 test5007 \
test5008 test5009 test5010 test5011 test5012 test5013 test5014 test5015 \
test5016 test5017 test5018 test5019 test5020 test5021
EXTRA_DIST = $(TESTCASES) DISABLED data-xml1 \
data1461.txt data1463.txt \
data1400.c data1401.c data1402.c data1403.c data1404.c data1405.c data1406.c \
data1407.c data1420.c data1465.c data1481.c \
data1705-1.md data1705-2.md data1705-3.md data1705-4.md data1705-stdout.1 \
data1706-1.md data1706-2.md data1706-3.md data1706-4.md data1706-stdout.txt
data1706-1.md data1706-2.md data1706-3.md data1706-4.md data1706-stdout.txt \
data-httpsig-ed25519.key data-httpsig-hmac-sha256.key

View file

@ -0,0 +1 @@
9f8362f87a484a954e6e740c5b4c0e84229139a20aa8ab56ff66586f6a7d29c5

View file

@ -0,0 +1 @@
0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef

57
tests/data/test5000 Normal file
View file

@ -0,0 +1,57 @@
<?xml version="1.0" encoding="US-ASCII"?>
<testcase>
<info>
<keywords>
HTTP
CURLOPT_HTTPSIG_ALGORITHM
RFC9421
</keywords>
</info>
# Server-side
<reply>
<data nocheck="yes">
HTTP/1.1 200 OK
Date: Tue, 09 Nov 2010 14:49:00 GMT
Server: test-server/fake
Content-Type: text/html
Content-Length: 0
</data>
</reply>
# Client-side
<client>
<server>
http
</server>
# this relies on the Debug feature which allows tests to set the time
<features>
Debug
httpsig
</features>
<name>
HTTP RFC 9421 Message Signatures: basic GET with Ed25519
</name>
<tool>
lib%TESTNUMBER
</tool>
<command>
http://example.com:9000/%TESTNUMBER/resource example.com:9000:%HOSTIP:%HTTPPORT
</command>
</client>
# Verify data after the test has been "shot"
<verify>
<protocol crlf="headers">
GET /%TESTNUMBER/resource HTTP/1.1
Host: example.com:9000
Signature-Input: sig1=("@method" "@authority" "@path");created=0;keyid="test-key-ed25519";alg="ed25519"
Signature: sig1=:Olp3ugYSVwc47PTNBMrkV2P+8p5w2VPFzURxNaG44UA2On6OFG2FzFLEl8QEN1khiOdRNlFDouZsOwpJoPHvBA==:
Accept: */*
</protocol>
</verify>
</testcase>

55
tests/data/test5001 Normal file
View file

@ -0,0 +1,55 @@
<?xml version="1.0" encoding="US-ASCII"?>
<testcase>
<info>
<keywords>
HTTP
httpsig
RFC9421
</keywords>
</info>
# Server-side
<reply>
<data crlf="headers">
HTTP/1.1 200 OK
Date: Tue, 09 Nov 2010 14:49:00 GMT
Server: test-server/fake
Last-Modified: Tue, 13 Jun 2000 12:10:00 GMT
Content-Length: 6
Connection: close
Content-Type: text/html
-foo-
</data>
</reply>
# Client-side
<client>
<server>
http
</server>
<features>
Debug
httpsig
</features>
<name>
HTTP RFC 9421 Message Signatures: CLI GET with query
</name>
<command>
"http://example.com:8000/%TESTNUMBER/resource?action=read" --httpsig-algo "ed25519" --httpsig-key %SRCDIR/data/data-httpsig-ed25519.key --httpsig-keyid "my-key-1" --connect-to example.com:8000:%HOSTIP:%HTTPPORT
</command>
</client>
# Verify data after the test has been "shot"
<verify>
<protocol crlf="headers">
GET /%TESTNUMBER/resource?action=read HTTP/1.1
Host: example.com:8000
Signature-Input: sig1=("@method" "@authority" "@path" "@query");created=0;keyid="my-key-1";alg="ed25519"
Signature: sig1=:RQniOeqmwdRzGvoDIMJ8XJha75evJWgqo5/66EeuJeEGczZtnP2U/F52Lzd/y7Vd1DCb8oUcCKHrKi2VJI7lBA==:
User-Agent: curl/%VERSION
Accept: */*
</protocol>
</verify>
</testcase>

55
tests/data/test5002 Normal file
View file

@ -0,0 +1,55 @@
<?xml version="1.0" encoding="US-ASCII"?>
<testcase>
<info>
<keywords>
HTTP
httpsig
RFC9421
hmac-sha256
</keywords>
</info>
# Server-side
<reply>
<data crlf="headers">
HTTP/1.1 200 OK
Date: Tue, 09 Nov 2010 14:49:00 GMT
Server: test-server/fake
Content-Length: 6
Connection: close
Content-Type: text/html
-foo-
</data>
</reply>
# Client-side
<client>
<server>
http
</server>
<features>
Debug
httpsig
</features>
<name>
HTTP RFC 9421 Message Signatures: CLI GET with HMAC-SHA256
</name>
<command>
"http://example.com:7000/%TESTNUMBER/resource" --httpsig-algo "hmac-sha256" --httpsig-key %SRCDIR/data/data-httpsig-hmac-sha256.key --httpsig-keyid "shared-key-1" --connect-to example.com:7000:%HOSTIP:%HTTPPORT
</command>
</client>
# Verify data after the test has been "shot"
<verify>
<protocol crlf="headers">
GET /%TESTNUMBER/resource HTTP/1.1
Host: example.com:7000
Signature-Input: sig1=("@method" "@authority" "@path");created=0;keyid="shared-key-1";alg="hmac-sha256"
Signature: sig1=:jvajJheXE4H/TqWfAn8m0d0Rx0XWYKADN/1P0qn+FLw=:
User-Agent: curl/%VERSION
Accept: */*
</protocol>
</verify>
</testcase>

55
tests/data/test5003 Normal file
View file

@ -0,0 +1,55 @@
<?xml version="1.0" encoding="US-ASCII"?>
<testcase>
<info>
<keywords>
HTTP
httpsig
RFC9421
httpsig-headers
</keywords>
</info>
# Server-side
<reply>
<data crlf="headers">
HTTP/1.1 200 OK
Date: Tue, 09 Nov 2010 14:49:00 GMT
Server: test-server/fake
Content-Length: 6
Connection: close
Content-Type: text/html
-foo-
</data>
</reply>
# Client-side
<client>
<server>
http
</server>
<features>
Debug
httpsig
</features>
<name>
HTTP RFC 9421 Message Signatures: custom --httpsig-headers
</name>
<command>
"http://example.com:6000/%TESTNUMBER/resource" --httpsig-algo "ed25519" --httpsig-key %SRCDIR/data/data-httpsig-ed25519.key --httpsig-keyid "test-key-ed25519" --httpsig-headers "method authority" --connect-to example.com:6000:%HOSTIP:%HTTPPORT
</command>
</client>
# Verify data after the test has been "shot"
<verify>
<protocol crlf="headers">
GET /%TESTNUMBER/resource HTTP/1.1
Host: example.com:6000
Signature-Input: sig1=("@method" "@authority");created=0;keyid="test-key-ed25519";alg="ed25519"
Signature: sig1=:q+8VHrGBOhkJm9d9dxKv3BfhyVWKIFiYXqdLZVx8yGrTzZO6Q6zJicFFuLvwJex6DS+Pw6YEMQzmtyj7dKOCCw==:
User-Agent: curl/%VERSION
Accept: */*
</protocol>
</verify>
</testcase>

66
tests/data/test5004 Normal file
View file

@ -0,0 +1,66 @@
<?xml version="1.0" encoding="US-ASCII"?>
<testcase>
<info>
<keywords>
HTTP
CURLOPT_HTTPSIG_ALGORITHM
RFC9421
ed25519
B.2.6
</keywords>
</info>
# Server-side
<reply>
<data nocheck="yes">
HTTP/1.1 200 OK
Date: Tue, 09 Nov 2010 14:49:00 GMT
Server: test-server/fake
Content-Type: text/html
Content-Length: 0
</data>
</reply>
# Client-side
<client>
<server>
http
</server>
<features>
Debug
httpsig
</features>
<name>
HTTP RFC 9421 B.2.6: Ed25519 POST with headers (RFC test vector)
</name>
<tool>
lib%TESTNUMBER
</tool>
<setenv>
CURL_HTTPSIG_CREATED=1618884473
</setenv>
<command>
http://example.com/%TESTNUMBER/foo example.com:80:%HOSTIP:%HTTPPORT
</command>
</client>
# Verify data after the test has been "shot"
<verify>
<protocol crlf="headers" nonewline="yes">
POST /%TESTNUMBER/foo HTTP/1.1
Host: example.com
Signature-Input: sig1=("date" "@method" "@path" "@authority" "content-type" "content-length");created=1618884473;keyid="test-key-ed25519";alg="ed25519"
Signature: sig1=:6QriGerNuao/A02UMre01lQOwlVQ0L9Cx3WJPQonQpUdKxSNg/XXXqjKdcC9PELzmAKJ10loLIq10yYLTzmzCA==:
Accept: */*
Date: Tue, 20 Apr 2021 02:07:55 GMT
Content-Type: application/json
Content-Length: 18
{"hello": "world"}
</protocol>
</verify>
</testcase>

56
tests/data/test5005 Normal file
View file

@ -0,0 +1,56 @@
<?xml version="1.0" encoding="US-ASCII"?>
<testcase>
<info>
<keywords>
HTTP
httpsig
RFC9421
header-signing
</keywords>
</info>
# Server-side
<reply>
<data crlf="headers">
HTTP/1.1 200 OK
Date: Tue, 09 Nov 2010 14:49:00 GMT
Server: test-server/fake
Content-Length: 6
Connection: close
Content-Type: text/html
-foo-
</data>
</reply>
# Client-side
<client>
<server>
http
</server>
<features>
Debug
httpsig
</features>
<name>
HTTP RFC 9421 Message Signatures: sign with a regular HTTP header
</name>
<command>
"http://example.com:5000/%TESTNUMBER/resource" --httpsig-algo "ed25519" --httpsig-key %SRCDIR/data/data-httpsig-ed25519.key --httpsig-keyid "test-key-ed25519" --httpsig-headers "method authority content-type:" -H "Content-Type: application/json" --connect-to example.com:5000:%HOSTIP:%HTTPPORT
</command>
</client>
# Verify data after the test has been "shot"
<verify>
<protocol crlf="headers">
GET /%TESTNUMBER/resource HTTP/1.1
Host: example.com:5000
Signature-Input: sig1=("@method" "@authority" "content-type");created=0;keyid="test-key-ed25519";alg="ed25519"
Signature: sig1=:lyo8O1BjxYmPJchwmIO6ZwOEycijlfsLsqVpVVMiuPFAWrZ7zm0eoIF0X0wET8bsO9jiDRWjDx0b0dPK7FHPCA==:
User-Agent: curl/%VERSION
Accept: */*
Content-Type: application/json
</protocol>
</verify>
</testcase>

56
tests/data/test5006 Normal file
View file

@ -0,0 +1,56 @@
<?xml version="1.0" encoding="US-ASCII"?>
<testcase>
<info>
<keywords>
HTTP
httpsig
RFC9421
case-normalization
</keywords>
</info>
# Server-side
<reply>
<data crlf="headers">
HTTP/1.1 200 OK
Date: Tue, 09 Nov 2010 14:49:00 GMT
Server: test-server/fake
Content-Length: 6
Connection: close
Content-Type: text/html
-foo-
</data>
</reply>
# Client-side
<client>
<server>
http
</server>
<features>
Debug
httpsig
</features>
<name>
HTTP RFC 9421 Message Signatures: component name lowercasing
</name>
<command>
"http://example.com:5100/%TESTNUMBER/resource" --httpsig-algo "ed25519" --httpsig-key %SRCDIR/data/data-httpsig-ed25519.key --httpsig-keyid "test-key-ed25519" --httpsig-headers "method Content-Type:" -H "Content-Type: text/plain" --connect-to example.com:5100:%HOSTIP:%HTTPPORT
</command>
</client>
# Verify data after the test has been "shot"
<verify>
<protocol crlf="headers">
GET /%TESTNUMBER/resource HTTP/1.1
Host: example.com:5100
Signature-Input: sig1=("@method" "content-type");created=0;keyid="test-key-ed25519";alg="ed25519"
Signature: sig1=:tu3WEN25K59evFpiHY5GZurVRtbaex5QM1W9b+P1rPCgNDsEEOepNZap/+nm48QZSensuc4tOpy1gbvAj25gAg==:
User-Agent: curl/%VERSION
Accept: */*
Content-Type: text/plain
</protocol>
</verify>
</testcase>

58
tests/data/test5007 Normal file
View file

@ -0,0 +1,58 @@
<?xml version="1.0" encoding="US-ASCII"?>
<testcase>
<info>
<keywords>
HTTP
HTTP POST
httpsig
RFC9421
</keywords>
</info>
# Server-side
<reply>
<data crlf="headers">
HTTP/1.1 200 OK
Date: Tue, 09 Nov 2010 14:49:00 GMT
Server: test-server/fake
Content-Length: 6
Connection: close
Content-Type: text/html
-foo-
</data>
</reply>
# Client-side
<client>
<server>
http
</server>
<features>
Debug
httpsig
</features>
<name>
HTTP RFC 9421 Message Signatures: POST method
</name>
<command>
"http://example.com:5200/%TESTNUMBER/resource" --httpsig-algo "ed25519" --httpsig-key %SRCDIR/data/data-httpsig-ed25519.key --httpsig-keyid "test-key-ed25519" -d "postbody" --connect-to example.com:5200:%HOSTIP:%HTTPPORT
</command>
</client>
# Verify data after the test has been "shot"
<verify>
<protocol crlf="headers" nonewline="yes">
POST /%TESTNUMBER/resource HTTP/1.1
Host: example.com:5200
Signature-Input: sig1=("@method" "@authority" "@path");created=0;keyid="test-key-ed25519";alg="ed25519"
Signature: sig1=:fqvzBFSBFu9oys7DawypZIrt2VS+nOnN1TQ7aTLcPUbEv7Zqm4leYd0q5r9FwW+kvabAjMj8J8N8F7FmPpIWCA==:
User-Agent: curl/%VERSION
Accept: */*
Content-Length: 8
Content-Type: application/x-www-form-urlencoded
postbody
</protocol>
</verify>
</testcase>

46
tests/data/test5008 Normal file
View file

@ -0,0 +1,46 @@
<?xml version="1.0" encoding="US-ASCII"?>
<testcase>
<info>
<keywords>
HTTP
httpsig
RFC9421
error
</keywords>
</info>
# Server-side
<reply>
<data nocheck="yes">
HTTP/1.1 200 OK
Date: Tue, 09 Nov 2010 14:49:00 GMT
Server: test-server/fake
Content-Length: 0
</data>
</reply>
# Client-side
<client>
<server>
http
</server>
<features>
Debug
httpsig
</features>
<name>
HTTP RFC 9421 Message Signatures: error - missing --httpsig-key
</name>
<command>
"http://example.com/%TESTNUMBER/resource" --httpsig-algo "ed25519" --httpsig-keyid "my-key" --connect-to example.com::%HOSTIP:%HTTPPORT
</command>
</client>
# Verify data after the test has been "shot"
<verify>
<errorcode>
2
</errorcode>
</verify>
</testcase>

46
tests/data/test5009 Normal file
View file

@ -0,0 +1,46 @@
<?xml version="1.0" encoding="US-ASCII"?>
<testcase>
<info>
<keywords>
HTTP
httpsig
RFC9421
error
</keywords>
</info>
# Server-side
<reply>
<data nocheck="yes">
HTTP/1.1 200 OK
Date: Tue, 09 Nov 2010 14:49:00 GMT
Server: test-server/fake
Content-Length: 0
</data>
</reply>
# Client-side
<client>
<server>
http
</server>
<features>
Debug
httpsig
</features>
<name>
HTTP RFC 9421 Message Signatures: error - unsupported algorithm
</name>
<command>
"http://example.com/%TESTNUMBER/resource" --httpsig-algo "rsa-pss-sha512" --httpsig-key %SRCDIR/data/data-httpsig-ed25519.key --httpsig-keyid "my-key" --connect-to example.com::%HOSTIP:%HTTPPORT
</command>
</client>
# Verify data after the test has been "shot"
<verify>
<errorcode>
2
</errorcode>
</verify>
</testcase>

46
tests/data/test5010 Normal file
View file

@ -0,0 +1,46 @@
<?xml version="1.0" encoding="US-ASCII"?>
<testcase>
<info>
<keywords>
HTTP
httpsig
RFC9421
error
</keywords>
</info>
# Server-side
<reply>
<data nocheck="yes">
HTTP/1.1 200 OK
Date: Tue, 09 Nov 2010 14:49:00 GMT
Server: test-server/fake
Content-Length: 0
</data>
</reply>
# Client-side
<client>
<server>
http
</server>
<features>
Debug
httpsig
</features>
<name>
HTTP RFC 9421 Message Signatures: error - non-existent key file
</name>
<command>
"http://example.com/%TESTNUMBER/resource" --httpsig-algo "ed25519" --httpsig-key /nonexistent/key.hex --httpsig-keyid "my-key" --connect-to example.com::%HOSTIP:%HTTPPORT
</command>
</client>
# Verify data after the test has been "shot"
<verify>
<errorcode>
26
</errorcode>
</verify>
</testcase>

55
tests/data/test5011 Normal file
View file

@ -0,0 +1,55 @@
<?xml version="1.0" encoding="US-ASCII"?>
<testcase>
<info>
<keywords>
HTTP
httpsig
RFC9421
query-special-chars
</keywords>
</info>
# Server-side
<reply>
<data crlf="headers">
HTTP/1.1 200 OK
Date: Tue, 09 Nov 2010 14:49:00 GMT
Server: test-server/fake
Content-Length: 6
Connection: close
Content-Type: text/html
-foo-
</data>
</reply>
# Client-side
<client>
<server>
http
</server>
<features>
Debug
httpsig
</features>
<name>
HTTP RFC 9421 Message Signatures: query string with special characters
</name>
<command>
"http://example.com:5300/%TESTNUMBER/resource?name=me%AMPnoval%AMPaim=b%25ad" --httpsig-algo "ed25519" --httpsig-key %SRCDIR/data/data-httpsig-ed25519.key --httpsig-keyid "test-key-ed25519" --connect-to example.com:5300:%HOSTIP:%HTTPPORT
</command>
</client>
# Verify data after the test has been "shot"
<verify>
<protocol crlf="headers">
GET /%TESTNUMBER/resource?name=me%AMPnoval%AMPaim=b%25ad HTTP/1.1
Host: example.com:5300
Signature-Input: sig1=("@method" "@authority" "@path" "@query");created=0;keyid="test-key-ed25519";alg="ed25519"
Signature: sig1=:ZFaJcDMTJEBbr90c0FaqupdJYDhNSz1i/eVCpMTDJ+1phLql2U4ROLiO26SGL6D0Nd5rr/3VXbDcWJ2vnV6IBA==:
User-Agent: curl/%VERSION
Accept: */*
</protocol>
</verify>
</testcase>

58
tests/data/test5012 Normal file
View file

@ -0,0 +1,58 @@
<?xml version="1.0" encoding="US-ASCII"?>
<testcase>
<info>
<keywords>
HTTP
httpsig
RFC9421
default-port
</keywords>
</info>
# Server-side
<reply>
<data crlf="headers">
HTTP/1.1 200 OK
Date: Tue, 09 Nov 2010 14:49:00 GMT
Server: test-server/fake
Content-Length: 6
Connection: close
Content-Type: text/html
-foo-
</data>
</reply>
# Client-side
<client>
<server>
http
</server>
<features>
Debug
httpsig
</features>
<name>
HTTP RFC 9421 Message Signatures: default port omitted from @authority
</name>
<command>
"http://example.com/%TESTNUMBER/resource" --httpsig-algo "ed25519" --httpsig-key %SRCDIR/data/data-httpsig-ed25519.key --httpsig-keyid "test-key-ed25519" --connect-to example.com::%HOSTIP:%HTTPPORT
</command>
</client>
# Verify data after the test has been "shot"
<verify>
<strippart>
s/Signature: sig1=:.*:/Signature: sig1=:STRIPPED:/
</strippart>
<protocol crlf="headers">
GET /%TESTNUMBER/resource HTTP/1.1
Host: example.com
Signature-Input: sig1=("@method" "@authority" "@path");created=0;keyid="test-key-ed25519";alg="ed25519"
Signature: sig1=:STRIPPED:
User-Agent: curl/%VERSION
Accept: */*
</protocol>
</verify>
</testcase>

58
tests/data/test5013 Normal file
View file

@ -0,0 +1,58 @@
<?xml version="1.0" encoding="US-ASCII"?>
<testcase>
<info>
<keywords>
HTTP
httpsig
RFC9421
non-default-port
</keywords>
</info>
# Server-side
<reply>
<data crlf="headers">
HTTP/1.1 200 OK
Date: Tue, 09 Nov 2010 14:49:00 GMT
Server: test-server/fake
Content-Length: 6
Connection: close
Content-Type: text/html
-foo-
</data>
</reply>
# Client-side
<client>
<server>
http
</server>
<features>
Debug
httpsig
</features>
<name>
HTTP RFC 9421 Message Signatures: non-default port in @authority
</name>
<command>
"http://example.com:9000/%TESTNUMBER/resource" --httpsig-algo "ed25519" --httpsig-key %SRCDIR/data/data-httpsig-ed25519.key --httpsig-keyid "test-key-ed25519" --connect-to example.com:9000:%HOSTIP:%HTTPPORT
</command>
</client>
# Verify data after the test has been "shot"
<verify>
<strippart>
s/Signature: sig1=:.*:/Signature: sig1=:STRIPPED:/
</strippart>
<protocol crlf="headers">
GET /%TESTNUMBER/resource HTTP/1.1
Host: example.com:9000
Signature-Input: sig1=("@method" "@authority" "@path");created=0;keyid="test-key-ed25519";alg="ed25519"
Signature: sig1=:STRIPPED:
User-Agent: curl/%VERSION
Accept: */*
</protocol>
</verify>
</testcase>

58
tests/data/test5014 Normal file
View file

@ -0,0 +1,58 @@
<?xml version="1.0" encoding="US-ASCII"?>
<testcase>
<info>
<keywords>
HTTP
httpsig
RFC9421
empty-query
</keywords>
</info>
# Server-side
<reply>
<data crlf="headers">
HTTP/1.1 200 OK
Date: Tue, 09 Nov 2010 14:49:00 GMT
Server: test-server/fake
Content-Length: 6
Connection: close
Content-Type: text/html
-foo-
</data>
</reply>
# Client-side
<client>
<server>
http
</server>
<features>
Debug
httpsig
</features>
<name>
HTTP RFC 9421 Message Signatures: @query explicitly requested, no query in URL
</name>
<command>
"http://example.com:5400/%TESTNUMBER/resource" --httpsig-algo "ed25519" --httpsig-key %SRCDIR/data/data-httpsig-ed25519.key --httpsig-keyid "test-key-ed25519" --httpsig-headers "method authority query" --connect-to example.com:5400:%HOSTIP:%HTTPPORT
</command>
</client>
# Verify data after the test has been "shot"
<verify>
<strippart>
s/Signature: sig1=:.*:/Signature: sig1=:STRIPPED:/
</strippart>
<protocol crlf="headers">
GET /%TESTNUMBER/resource HTTP/1.1
Host: example.com:5400
Signature-Input: sig1=("@method" "@authority" "@query");created=0;keyid="test-key-ed25519";alg="ed25519"
Signature: sig1=:STRIPPED:
User-Agent: curl/%VERSION
Accept: */*
</protocol>
</verify>
</testcase>

62
tests/data/test5015 Normal file
View file

@ -0,0 +1,62 @@
<?xml version="1.0" encoding="US-ASCII"?>
<testcase>
<info>
<keywords>
HTTP
HTTP POST
httpsig
RFC9421
hmac-sha256
</keywords>
</info>
# Server-side
<reply>
<data crlf="headers">
HTTP/1.1 200 OK
Date: Tue, 09 Nov 2010 14:49:00 GMT
Server: test-server/fake
Content-Length: 6
Connection: close
Content-Type: text/html
-foo-
</data>
</reply>
# Client-side
<client>
<server>
http
</server>
<features>
Debug
httpsig
</features>
<name>
HTTP RFC 9421 Message Signatures: POST with HMAC-SHA256
</name>
<command>
"http://example.com:5500/%TESTNUMBER/resource" --httpsig-algo "hmac-sha256" --httpsig-key %SRCDIR/data/data-httpsig-hmac-sha256.key --httpsig-keyid "shared-key-1" -d "postbody" --connect-to example.com:5500:%HOSTIP:%HTTPPORT
</command>
</client>
# Verify data after the test has been "shot"
<verify>
<strippart>
s/Signature: sig1=:.*:/Signature: sig1=:STRIPPED:/
</strippart>
<protocol crlf="headers" nonewline="yes">
POST /%TESTNUMBER/resource HTTP/1.1
Host: example.com:5500
Signature-Input: sig1=("@method" "@authority" "@path");created=0;keyid="shared-key-1";alg="hmac-sha256"
Signature: sig1=:STRIPPED:
User-Agent: curl/%VERSION
Accept: */*
Content-Length: 8
Content-Type: application/x-www-form-urlencoded
postbody
</protocol>
</verify>
</testcase>

60
tests/data/test5016 Normal file
View file

@ -0,0 +1,60 @@
<?xml version="1.0" encoding="US-ASCII"?>
<testcase>
<info>
<keywords>
HTTP
httpsig
RFC9421
hmac-sha256
header-signing
</keywords>
</info>
# Server-side
<reply>
<data crlf="headers">
HTTP/1.1 200 OK
Date: Tue, 09 Nov 2010 14:49:00 GMT
Server: test-server/fake
Content-Length: 6
Connection: close
Content-Type: text/html
-foo-
</data>
</reply>
# Client-side
<client>
<server>
http
</server>
<features>
Debug
httpsig
</features>
<name>
HTTP RFC 9421 Message Signatures: HMAC-SHA256 with custom headers
</name>
<command>
"http://example.com:5600/%TESTNUMBER/resource" --httpsig-algo "hmac-sha256" --httpsig-key %SRCDIR/data/data-httpsig-hmac-sha256.key --httpsig-keyid "shared-key-1" --httpsig-headers "method authority content-type:" -H "Content-Type: application/json" --connect-to example.com:5600:%HOSTIP:%HTTPPORT
</command>
</client>
# Verify data after the test has been "shot"
<verify>
<strippart>
s/Signature: sig1=:.*:/Signature: sig1=:STRIPPED:/
</strippart>
<protocol crlf="headers">
GET /%TESTNUMBER/resource HTTP/1.1
Host: example.com:5600
Signature-Input: sig1=("@method" "@authority" "content-type");created=0;keyid="shared-key-1";alg="hmac-sha256"
Signature: sig1=:STRIPPED:
User-Agent: curl/%VERSION
Accept: */*
Content-Type: application/json
</protocol>
</verify>
</testcase>

55
tests/data/test5017 Normal file
View file

@ -0,0 +1,55 @@
<?xml version="1.0" encoding="US-ASCII"?>
<testcase>
<info>
<keywords>
HTTP
httpsig
RFC9421
preexisting-signature
</keywords>
</info>
# Server-side
<reply>
<data crlf="headers">
HTTP/1.1 200 OK
Date: Tue, 09 Nov 2010 14:49:00 GMT
Server: test-server/fake
Content-Length: 6
Connection: close
Content-Type: text/html
-foo-
</data>
</reply>
# Client-side
<client>
<server>
http
</server>
<features>
Debug
httpsig
</features>
<name>
HTTP RFC 9421 Message Signatures: user-supplied Signature header skips signing
</name>
<command>
"http://example.com:5700/%TESTNUMBER/resource" --httpsig-algo "ed25519" --httpsig-key %SRCDIR/data/data-httpsig-ed25519.key --httpsig-keyid "test-key-ed25519" -H "Signature: preexisting" -H "Signature-Input: preexisting" --connect-to example.com:5700:%HOSTIP:%HTTPPORT
</command>
</client>
# Verify data after the test has been "shot"
<verify>
<protocol crlf="headers">
GET /%TESTNUMBER/resource HTTP/1.1
Host: example.com:5700
User-Agent: curl/%VERSION
Accept: */*
Signature: preexisting
Signature-Input: preexisting
</protocol>
</verify>
</testcase>

63
tests/data/test5018 Normal file
View file

@ -0,0 +1,63 @@
<?xml version="1.0" encoding="US-ASCII"?>
<testcase>
<info>
<keywords>
HTTP
HTTP PUT
httpsig
RFC9421
</keywords>
</info>
# Server-side
<reply>
<data crlf="headers">
HTTP/1.1 200 OK
Date: Tue, 09 Nov 2010 14:49:00 GMT
Server: test-server/fake
Content-Length: 6
Connection: close
Content-Type: text/html
-foo-
</data>
</reply>
# Client-side
<client>
<server>
http
</server>
<features>
Debug
httpsig
</features>
<file name="log/upload5018">
putdata
</file>
<name>
HTTP RFC 9421 Message Signatures: PUT upload
</name>
<command>
"http://example.com:5800/%TESTNUMBER/resource" --httpsig-algo "ed25519" --httpsig-key %SRCDIR/data/data-httpsig-ed25519.key --httpsig-keyid "test-key-ed25519" -T log/upload5018 --connect-to example.com:5800:%HOSTIP:%HTTPPORT
</command>
</client>
# Verify data after the test has been "shot"
<verify>
<strippart>
s/Signature: sig1=:.*:/Signature: sig1=:STRIPPED:/
</strippart>
<protocol crlf="headers">
PUT /%TESTNUMBER/resource HTTP/1.1
Host: example.com:5800
Signature-Input: sig1=("@method" "@authority" "@path");created=0;keyid="test-key-ed25519";alg="ed25519"
Signature: sig1=:STRIPPED:
User-Agent: curl/%VERSION
Accept: */*
Content-Length: 8
putdata
</protocol>
</verify>
</testcase>

46
tests/data/test5019 Normal file
View file

@ -0,0 +1,46 @@
<?xml version="1.0" encoding="US-ASCII"?>
<testcase>
<info>
<keywords>
HTTP
httpsig
RFC9421
error
</keywords>
</info>
# Server-side
<reply>
<data nocheck="yes">
HTTP/1.1 200 OK
Date: Tue, 09 Nov 2010 14:49:00 GMT
Server: test-server/fake
Content-Length: 0
</data>
</reply>
# Client-side
<client>
<server>
http
</server>
<features>
Debug
httpsig
</features>
<name>
HTTP RFC 9421 Message Signatures: error - duplicate component
</name>
<command>
"http://example.com/%TESTNUMBER/resource" --httpsig-algo "ed25519" --httpsig-key %SRCDIR/data/data-httpsig-ed25519.key --httpsig-keyid "my-key" --httpsig-headers "method method" --connect-to example.com::%HOSTIP:%HTTPPORT
</command>
</client>
# Verify data after the test has been "shot"
<verify>
<errorcode>
43
</errorcode>
</verify>
</testcase>

46
tests/data/test5020 Normal file
View file

@ -0,0 +1,46 @@
<?xml version="1.0" encoding="US-ASCII"?>
<testcase>
<info>
<keywords>
HTTP
httpsig
RFC9421
error
</keywords>
</info>
# Server-side
<reply>
<data nocheck="yes">
HTTP/1.1 200 OK
Date: Tue, 09 Nov 2010 14:49:00 GMT
Server: test-server/fake
Content-Length: 0
</data>
</reply>
# Client-side
<client>
<server>
http
</server>
<features>
Debug
httpsig
</features>
<name>
HTTP RFC 9421 Message Signatures: error - rejects legacy '@' component syntax
</name>
<command>
"http://example.com/%TESTNUMBER/resource" --httpsig-algo "ed25519" --httpsig-key %SRCDIR/data/data-httpsig-ed25519.key --httpsig-keyid "my-key" --httpsig-headers "@method authority" --connect-to example.com::%HOSTIP:%HTTPPORT
</command>
</client>
# Verify data after the test has been "shot"
<verify>
<errorcode>
43
</errorcode>
</verify>
</testcase>

57
tests/data/test5021 Normal file
View file

@ -0,0 +1,57 @@
<?xml version="1.0" encoding="US-ASCII"?>
<testcase>
<info>
<keywords>
HTTP
httpsig
RFC9421
</keywords>
</info>
# Server-side
<reply>
<data crlf="headers">
HTTP/1.1 200 OK
Date: Tue, 09 Nov 2010 14:49:00 GMT
Server: test-server/fake
Content-Length: 6
Connection: close
Content-Type: text/html
-foo-
</data>
</reply>
# Client-side
<client>
<server>
http
</server>
<features>
Debug
httpsig
</features>
<name>
HTTP RFC 9421 Message Signatures: default algorithm (ed25519) when --httpsig-algo omitted
</name>
<command>
"http://example.com:6100/%TESTNUMBER/resource" --httpsig-key %SRCDIR/data/data-httpsig-ed25519.key --httpsig-keyid "test-key-ed25519" --connect-to example.com:6100:%HOSTIP:%HTTPPORT
</command>
</client>
# Verify data after the test has been "shot"
<verify>
<strippart>
s/Signature: sig1=:.*:/Signature: sig1=:STRIPPED:/
</strippart>
<protocol crlf="headers">
GET /%TESTNUMBER/resource HTTP/1.1
Host: example.com:6100
Signature-Input: sig1=("@method" "@authority" "@path");created=0;keyid="test-key-ed25519";alg="ed25519"
Signature: sig1=:STRIPPED:
User-Agent: curl/%VERSION
Accept: */*
</protocol>
</verify>
</testcase>

View file

@ -124,4 +124,5 @@ TESTS_C = \
lib2700.c \
lib3010.c lib3025.c lib3026.c lib3027.c lib3033.c lib3034.c \
lib3100.c lib3101.c lib3102.c lib3103.c lib3104.c lib3105.c \
lib3207.c lib3208.c
lib3207.c lib3208.c \
lib5000.c lib5004.c

66
tests/libtest/lib5000.c Normal file
View file

@ -0,0 +1,66 @@
/***************************************************************************
* _ _ ____ _
* 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 "first.h"
static CURLcode test_lib5000(const char *URL)
{
CURL *curl;
CURLcode result = TEST_ERR_MAJOR_BAD;
struct curl_slist *connect_to = NULL;
if(curl_global_init(CURL_GLOBAL_ALL) != CURLE_OK) {
curl_mfprintf(stderr, "curl_global_init() failed\n");
return TEST_ERR_MAJOR_BAD;
}
curl = curl_easy_init();
if(!curl) {
curl_mfprintf(stderr, "curl_easy_init() failed\n");
curl_global_cleanup();
return TEST_ERR_MAJOR_BAD;
}
easy_setopt(curl, CURLOPT_VERBOSE, 1L);
easy_setopt(curl, CURLOPT_HTTPSIG_ALGORITHM, (long)CURLHTTPSIG_ED25519);
easy_setopt(curl, CURLOPT_HTTPSIG_KEY,
"9f8362f87a484a954e6e740c5b4c0e84"
"229139a20aa8ab56ff66586f6a7d29c5");
easy_setopt(curl, CURLOPT_HTTPSIG_KEYID, "test-key-ed25519");
easy_setopt(curl, CURLOPT_HEADER, 0L);
easy_setopt(curl, CURLOPT_URL, URL);
if(libtest_arg2) {
connect_to = curl_slist_append(connect_to, libtest_arg2);
}
easy_setopt(curl, CURLOPT_CONNECT_TO, connect_to);
result = curl_easy_perform(curl);
test_cleanup:
curl_slist_free_all(connect_to);
curl_easy_cleanup(curl);
curl_global_cleanup();
return result;
}

85
tests/libtest/lib5004.c Normal file
View file

@ -0,0 +1,85 @@
/***************************************************************************
* _ _ ____ _
* 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
*
***************************************************************************/
/* RFC 9421 Appendix B.2.6 test vector: Ed25519 signing of a POST request
* with headers. Uses CURL_HTTPSIG_CREATED=1618884473 to match the RFC
* expected timestamp. */
#include "first.h"
static CURLcode test_lib5004(const char *URL)
{
CURL *curl;
CURLcode result = TEST_ERR_MAJOR_BAD;
struct curl_slist *connect_to = NULL;
struct curl_slist *headers = NULL;
if(curl_global_init(CURL_GLOBAL_ALL) != CURLE_OK) {
curl_mfprintf(stderr, "curl_global_init() failed\n");
return TEST_ERR_MAJOR_BAD;
}
curl = curl_easy_init();
if(!curl) {
curl_mfprintf(stderr, "curl_easy_init() failed\n");
curl_global_cleanup();
return TEST_ERR_MAJOR_BAD;
}
easy_setopt(curl, CURLOPT_VERBOSE, 1L);
easy_setopt(curl, CURLOPT_HTTPSIG_ALGORITHM, (long)CURLHTTPSIG_ED25519);
easy_setopt(curl, CURLOPT_HTTPSIG_KEY,
"9f8362f87a484a954e6e740c5b4c0e84"
"229139a20aa8ab56ff66586f6a7d29c5");
easy_setopt(curl, CURLOPT_HTTPSIG_KEYID, "test-key-ed25519");
easy_setopt(curl, CURLOPT_HTTPSIG_HEADERS,
"date: method path authority content-type: content-length:");
easy_setopt(curl, CURLOPT_POSTFIELDS, "{\"hello\": \"world\"}");
headers = curl_slist_append(headers,
"Date: Tue, 20 Apr 2021 02:07:55 GMT");
headers = curl_slist_append(headers,
"Content-Type: application/json");
headers = curl_slist_append(headers,
"Content-Length: 18");
easy_setopt(curl, CURLOPT_HTTPHEADER, headers);
easy_setopt(curl, CURLOPT_HEADER, 0L);
easy_setopt(curl, CURLOPT_URL, URL);
if(libtest_arg2) {
connect_to = curl_slist_append(connect_to, libtest_arg2);
}
easy_setopt(curl, CURLOPT_CONNECT_TO, connect_to);
result = curl_easy_perform(curl);
test_cleanup:
curl_slist_free_all(headers);
curl_slist_free_all(connect_to);
curl_easy_cleanup(curl);
curl_global_cleanup();
return result;
}