← Commit list

a55731050e

Sameeh Jubran
Sameeh Jubran
2026-07-25
SUCCESS Duration: 195s

Subject

httpsig: add RFC 9421 HTTP Message Signatures support

Binary

No binary (build failed)

Architecture & Compiler Flags

Architecture: x86_64-linux-gnu Compiler: gcc (Ubuntu 11.4.0-1ubuntu1~22.04.3) 11.4.0

—

Build Log

(log yok)

Commit Diff GitHub →

.github/scripts/pyspelling.words +5 -0.github/workflows/linux.yml +2 -2CMakeLists.txt +3 -0configure.ac +28 -0docs/CURL-DISABLE.md +4 -0docs/EXPERIMENTAL.md +19 -0docs/cmdline-opts/.gitignore +1 -0docs/cmdline-opts/Makefile.inc +4 -0docs/cmdline-opts/httpsig-algo.md +36 -0docs/cmdline-opts/httpsig-headers.md +45 -0docs/cmdline-opts/httpsig-key.md +35 -0docs/cmdline-opts/httpsig-keyid.md +23 -0docs/libcurl/curl_easy_setopt.md +16 -0docs/libcurl/curl_version_info.md +7 -0docs/libcurl/opts/CURLOPT_HTTPAUTH.md +7 -0docs/libcurl/opts/CURLOPT_HTTPSIG_ALGORITHM.md +89 -0docs/libcurl/opts/CURLOPT_HTTPSIG_HEADERS.md +108 -0docs/libcurl/opts/CURLOPT_HTTPSIG_KEY.md +90 -0docs/libcurl/opts/CURLOPT_HTTPSIG_KEYID.md +76 -0docs/libcurl/opts/Makefile.inc +4 -0docs/libcurl/symbols-in-versions +8 -0docs/options-in-versions +4 -0include/curl/curl.h +22 -2include/curl/typecheck-gcc.h +3 -0lib/Makefile.inc +4 -0lib/curl_config-cmake.h.in +3 -0lib/curl_ed25519.c +170 -0lib/curl_ed25519.h +44 -0lib/curl_hmac.h +1 -0lib/curl_setup.h +3 -0lib/curl_sha256.h +2 -1lib/easyoptions.c +5 -1lib/http.c +17 -0lib/http_httpsig.c +692 -0lib/http_httpsig.h +35 -0lib/setopt.c +24 -0lib/urldata.h +6 -0lib/version.c +3 -0projects/OS400/README.OS400 +3 -0projects/OS400/ccsidcurl.c +3 -0scripts/managen +8 -1src/config2setopts.c +78 -0src/curlinfo.c +7 -0src/tool_cfgable.c +4 -0src/tool_cfgable.h +4 -0src/tool_getparam.c +32 -0src/tool_getparam.h +4 -0src/tool_libinfo.c +2 -0src/tool_libinfo.h +1 -0src/tool_listhelp.c +22 -0tests/data/Makefile.am +7 -2tests/data/data-httpsig-ed25519.key +1 -0tests/data/data-httpsig-hmac-sha256.key +1 -0tests/data/test5000 +57 -0tests/data/test5001 +55 -0tests/data/test5002 +55 -0tests/data/test5003 +55 -0tests/data/test5004 +66 -0tests/data/test5005 +56 -0tests/data/test5006 +56 -0tests/data/test5007 +58 -0tests/data/test5008 +46 -0tests/data/test5009 +46 -0tests/data/test5010 +46 -0tests/data/test5011 +55 -0tests/data/test5012 +58 -0tests/data/test5013 +58 -0tests/data/test5014 +58 -0tests/data/test5015 +62 -0tests/data/test5016 +60 -0tests/data/test5017 +55 -0tests/data/test5018 +63 -0tests/data/test5019 +46 -0tests/data/test5020 +46 -0tests/data/test5021 +57 -0tests/libtest/Makefile.inc +2 -1tests/libtest/lib5000.c +66 -0tests/libtest/lib5004.c +85 -0
.github/scripts/pyspelling.words+5 -0
@@ -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
.github/workflows/linux.yml+2 -2
@@ -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
CMakeLists.txt+3 -0
@@ -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
configure.ac+28 -0
@@ -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
docs/CURL-DISABLE.md+4 -0
@@ -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.
docs/EXPERIMENTAL.md+19 -0
@@ -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
docs/cmdline-opts/.gitignore+1 -0
@@ -3,3 +3,4 @@
# SPDX-License-Identifier: curl
curl.txt
+asciipage.tmp.*
docs/cmdline-opts/Makefile.inc+4 -0
@@ -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 \
docs/cmdline-opts/httpsig-algo.md+36 -0
@@ -0,0 +1,36 @@
+---
+c: Copyright (C) Daniel Stenberg, <[email protected]>, 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.
docs/cmdline-opts/httpsig-headers.md+45 -0
@@ -0,0 +1,45 @@
+---
+c: Copyright (C) Daniel Stenberg, <[email protected]>, 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.
docs/cmdline-opts/httpsig-key.md+35 -0
@@ -0,0 +1,35 @@
+---
+c: Copyright (C) Daniel Stenberg, <[email protected]>, 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`.
docs/cmdline-opts/httpsig-keyid.md+23 -0
@@ -0,0 +1,23 @@
+---
+c: Copyright (C) Daniel Stenberg, <[email protected]>, 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.
docs/libcurl/curl_easy_setopt.md+16 -0
@@ -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)
docs/libcurl/curl_version_info.md+7 -0
@@ -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
docs/libcurl/opts/CURLOPT_HTTPAUTH.md+7 -0
@@ -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
docs/libcurl/opts/CURLOPT_HTTPSIG_ALGORITHM.md+89 -0
@@ -0,0 +1,89 @@
+---
+c: Copyright (C) Daniel Stenberg, <[email protected]>, 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).
docs/libcurl/opts/CURLOPT_HTTPSIG_HEADERS.md+108 -0
@@ -0,0 +1,108 @@
+---
+c: Copyright (C) Daniel Stenberg, <[email protected]>, 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).
docs/libcurl/opts/CURLOPT_HTTPSIG_KEY.md+90 -0
@@ -0,0 +1,90 @@
+---
+c: Copyright (C) Daniel Stenberg, <[email protected]>, 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).
docs/libcurl/opts/CURLOPT_HTTPSIG_KEYID.md+76 -0
@@ -0,0 +1,76 @@
+---
+c: Copyright (C) Daniel Stenberg, <[email protected]>, 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).
docs/libcurl/opts/Makefile.inc+4 -0
@@ -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 \
docs/libcurl/symbols-in-versions+8 -0
@@ -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
docs/options-in-versions+4 -0
@@ -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
include/curl/curl.h+22 -2
@@ -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;
include/curl/typecheck-gcc.h+3 -0
@@ -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)
lib/Makefile.inc+4 -0
@@ -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 \
lib/curl_config-cmake.h.in+3 -0
@@ -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
lib/curl_ed25519.c+170 -0
@@ -0,0 +1,170 @@
+/***************************************************************************
+ * _ _ ____ _
+ * Project ___| | | | _ \| |
+ * / __| | | | |_) | |
+ * | (__| |_| | _ <| |___
+ * \___|\___/|_| \_\_____|
+ *
+ * Copyright (C) Daniel Stenberg, <[email protected]>, 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 */
lib/curl_ed25519.h+44 -0
@@ -0,0 +1,44 @@
+#ifndef HEADER_CURL_ED25519_H
+#define HEADER_CURL_ED25519_H
+/***************************************************************************
+ * _ _ ____ _
+ * Project ___| | | | _ \| |
+ * / __| | | | |_) | |
+ * | (__| |_| | _ <| |___
+ * \___|\___/|_| \_\_____|
+ *
+ * Copyright (C) Daniel Stenberg, <[email protected]>, 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 */
lib/curl_hmac.h+1 -0
@@ -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
lib/curl_setup.h+3 -0
@@ -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
lib/curl_sha256.h+2 -1
@@ -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"
lib/easyoptions.c+5 -1
@@ -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
lib/http.c+17 -0
@@ -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 */
lib/http_httpsig.c+692 -0
@@ -0,0 +1,692 @@
+/***************************************************************************
+ * _ _ ____ _
+ * Project ___| | | | _ \| |
+ * / __| | | | |_) | |
+ * | (__| |_| | _ <| |___
+ * \___|\___/|_| \_\_____|
+ *
+ * Copyright (C) Daniel Stenberg, <[email protected]>, 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 */
lib/http_httpsig.h+35 -0
@@ -0,0 +1,35 @@
+#ifndef HEADER_CURL_HTTP_HTTPSIG_H
+#define HEADER_CURL_HTTP_HTTPSIG_H
+/***************************************************************************
+ * _ _ ____ _
+ * Project ___| | | | _ \| |
+ * / __| | | | |_) | |
+ * | (__| |_| | _ <| |___
+ * \___|\___/|_| \_\_____|
+ *
+ * Copyright (C) Daniel Stenberg, <[email protected]>, 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 */
lib/setopt.c+24 -0
@@ -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:
/*
lib/urldata.h+6 -0
@@ -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
lib/version.c+3 -0
@@ -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
projects/OS400/README.OS400+3 -0
@@ -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
projects/OS400/ccsidcurl.c+3 -0
@@ -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:
scripts/managen+8 -1
@@ -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 }
src/config2setopts.c+78 -0
@@ -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) {
src/curlinfo.c+7 -0
@@ -93,6 +93,13 @@ static const char *disabled[] = {
"OFF"
#else
"ON"
+#endif
+ ,
+ "httpsig: "
+#ifdef CURL_DISABLE_HTTPSIG
+ "OFF"
+#else
+ "ON"
#endif
,
"DoH: "
src/tool_cfgable.c+4 -0
@@ -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);
src/tool_cfgable.h+4 -0
@@ -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 */
src/tool_getparam.c+32 -0
@@ -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);
src/tool_getparam.h+4 -0
@@ -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,
src/tool_libinfo.c+2 -0
@@ -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 },
src/tool_libinfo.h+1 -0
@@ -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;
src/tool_listhelp.c+22 -0
@@ -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 },
tests/data/Makefile.am+7 -2
@@ -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
tests/data/data-httpsig-ed25519.key+1 -0
@@ -0,0 +1 @@
+9f8362f87a484a954e6e740c5b4c0e84229139a20aa8ab56ff66586f6a7d29c5
tests/data/data-httpsig-hmac-sha256.key+1 -0
@@ -0,0 +1 @@
+0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
tests/data/test5000+57 -0
@@ -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>
tests/data/test5001+55 -0
@@ -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>
tests/data/test5002+55 -0
@@ -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>
tests/data/test5003+55 -0
@@ -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>
tests/data/test5004+66 -0
@@ -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>
tests/data/test5005+56 -0
@@ -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>
tests/data/test5006+56 -0
@@ -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>
tests/data/test5007+58 -0
@@ -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>
tests/data/test5008+46 -0
@@ -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>
tests/data/test5009+46 -0
@@ -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>
tests/data/test5010+46 -0
@@ -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>
tests/data/test5011+55 -0
@@ -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>
tests/data/test5012+58 -0
@@ -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>
tests/data/test5013+58 -0
@@ -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>
tests/data/test5014+58 -0
@@ -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>
tests/data/test5015+62 -0
@@ -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>
tests/data/test5016+60 -0
@@ -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>
tests/data/test5017+55 -0
@@ -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>
tests/data/test5018+63 -0
@@ -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>
tests/data/test5019+46 -0
@@ -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>
tests/data/test5020+46 -0
@@ -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>
tests/data/test5021+57 -0
@@ -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>
tests/libtest/Makefile.inc+2 -1
@@ -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
tests/libtest/lib5000.c+66 -0
@@ -0,0 +1,66 @@
+/***************************************************************************
+ * _ _ ____ _
+ * Project ___| | | | _ \| |
+ * / __| | | | |_) | |
+ * | (__| |_| | _ <| |___
+ * \___|\___/|_| \_\_____|
+ *
+ * Copyright (C) Daniel Stenberg, <[email protected]>, 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;
+}
tests/libtest/lib5004.c+85 -0
@@ -0,0 +1,85 @@
+/***************************************************************************
+ * _ _ ____ _
+ * Project ___| | | | _ \| |
+ * / __| | | | |_) | |
+ * | (__| |_| | _ <| |___
+ * \___|\___/|_| \_\_____|
+ *
+ * Copyright (C) Daniel Stenberg, <[email protected]>, 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;
+}