From a55731050e8c3dbea0b96205cf916443118f6acb Mon Sep 17 00:00:00 2001 From: Sameeh Jubran Date: Fri, 24 Jul 2026 22:49:52 +0200 Subject: [PATCH] httpsig: add RFC 9421 HTTP Message Signatures support Add support for signing outgoing HTTP requests per RFC 9421 using Ed25519 or HMAC-SHA256 algorithms. New libcurl options: - CURLOPT_HTTPSIG: signing algorithm ("ed25519" or "hmac-sha256") - CURLOPT_HTTPSIG_KEY: path to hex-encoded key file - CURLOPT_HTTPSIG_KEYID: key identifier for Signature-Input - CURLOPT_HTTPSIG_HEADERS: space-separated components to sign New CLI flags: --httpsig, --httpsig-key, --httpsig-keyid, --httpsig-headers The crypto layer follows the sha256.c multi-backend pattern with implementations for OpenSSL (EVP_DigestSign) and wolfSSL (wc_ed25519_sign_msg). HMAC-SHA256 uses the existing Curl_hmacit() infrastructure which works on all backends. Verified by test 5000 to 5021 Assisted-by: Daniel Stenberg Signed-off-by: Sameeh Jubran Closes #22386 Closes #21239 --- .github/scripts/pyspelling.words | 5 + .github/workflows/linux.yml | 4 +- CMakeLists.txt | 3 + configure.ac | 28 + docs/CURL-DISABLE.md | 4 + docs/EXPERIMENTAL.md | 19 + docs/cmdline-opts/.gitignore | 1 + docs/cmdline-opts/Makefile.inc | 4 + docs/cmdline-opts/httpsig-algo.md | 36 + docs/cmdline-opts/httpsig-headers.md | 45 ++ docs/cmdline-opts/httpsig-key.md | 35 + docs/cmdline-opts/httpsig-keyid.md | 23 + docs/libcurl/curl_easy_setopt.md | 16 + docs/libcurl/curl_version_info.md | 7 + docs/libcurl/opts/CURLOPT_HTTPAUTH.md | 7 + .../libcurl/opts/CURLOPT_HTTPSIG_ALGORITHM.md | 89 +++ docs/libcurl/opts/CURLOPT_HTTPSIG_HEADERS.md | 108 +++ docs/libcurl/opts/CURLOPT_HTTPSIG_KEY.md | 90 +++ docs/libcurl/opts/CURLOPT_HTTPSIG_KEYID.md | 76 ++ docs/libcurl/opts/Makefile.inc | 4 + docs/libcurl/symbols-in-versions | 8 + docs/options-in-versions | 4 + include/curl/curl.h | 24 +- include/curl/typecheck-gcc.h | 3 + lib/Makefile.inc | 4 + lib/curl_config-cmake.h.in | 3 + lib/curl_ed25519.c | 170 +++++ lib/curl_ed25519.h | 44 ++ lib/curl_hmac.h | 1 + lib/curl_setup.h | 3 + lib/curl_sha256.h | 3 +- lib/easyoptions.c | 6 +- lib/http.c | 17 + lib/http_httpsig.c | 692 ++++++++++++++++++ lib/http_httpsig.h | 35 + lib/setopt.c | 24 + lib/urldata.h | 6 + lib/version.c | 3 + projects/OS400/README.OS400 | 3 + projects/OS400/ccsidcurl.c | 3 + scripts/managen | 9 +- src/config2setopts.c | 78 ++ src/curlinfo.c | 7 + src/tool_cfgable.c | 4 + src/tool_cfgable.h | 4 + src/tool_getparam.c | 32 + src/tool_getparam.h | 4 + src/tool_libinfo.c | 2 + src/tool_libinfo.h | 1 + src/tool_listhelp.c | 22 + tests/data/Makefile.am | 9 +- tests/data/data-httpsig-ed25519.key | 1 + tests/data/data-httpsig-hmac-sha256.key | 1 + tests/data/test5000 | 57 ++ tests/data/test5001 | 55 ++ tests/data/test5002 | 55 ++ tests/data/test5003 | 55 ++ tests/data/test5004 | 66 ++ tests/data/test5005 | 56 ++ tests/data/test5006 | 56 ++ tests/data/test5007 | 58 ++ tests/data/test5008 | 46 ++ tests/data/test5009 | 46 ++ tests/data/test5010 | 46 ++ tests/data/test5011 | 55 ++ tests/data/test5012 | 58 ++ tests/data/test5013 | 58 ++ tests/data/test5014 | 58 ++ tests/data/test5015 | 62 ++ tests/data/test5016 | 60 ++ tests/data/test5017 | 55 ++ tests/data/test5018 | 63 ++ tests/data/test5019 | 46 ++ tests/data/test5020 | 46 ++ tests/data/test5021 | 57 ++ tests/libtest/Makefile.inc | 3 +- tests/libtest/lib5000.c | 66 ++ tests/libtest/lib5004.c | 85 +++ 78 files changed, 3192 insertions(+), 10 deletions(-) create mode 100644 docs/cmdline-opts/httpsig-algo.md create mode 100644 docs/cmdline-opts/httpsig-headers.md create mode 100644 docs/cmdline-opts/httpsig-key.md create mode 100644 docs/cmdline-opts/httpsig-keyid.md create mode 100644 docs/libcurl/opts/CURLOPT_HTTPSIG_ALGORITHM.md create mode 100644 docs/libcurl/opts/CURLOPT_HTTPSIG_HEADERS.md create mode 100644 docs/libcurl/opts/CURLOPT_HTTPSIG_KEY.md create mode 100644 docs/libcurl/opts/CURLOPT_HTTPSIG_KEYID.md create mode 100644 lib/curl_ed25519.c create mode 100644 lib/curl_ed25519.h create mode 100644 lib/http_httpsig.c create mode 100644 lib/http_httpsig.h create mode 100644 tests/data/data-httpsig-ed25519.key create mode 100644 tests/data/data-httpsig-hmac-sha256.key create mode 100644 tests/data/test5000 create mode 100644 tests/data/test5001 create mode 100644 tests/data/test5002 create mode 100644 tests/data/test5003 create mode 100644 tests/data/test5004 create mode 100644 tests/data/test5005 create mode 100644 tests/data/test5006 create mode 100644 tests/data/test5007 create mode 100644 tests/data/test5008 create mode 100644 tests/data/test5009 create mode 100644 tests/data/test5010 create mode 100644 tests/data/test5011 create mode 100644 tests/data/test5012 create mode 100644 tests/data/test5013 create mode 100644 tests/data/test5014 create mode 100644 tests/data/test5015 create mode 100644 tests/data/test5016 create mode 100644 tests/data/test5017 create mode 100644 tests/data/test5018 create mode 100644 tests/data/test5019 create mode 100644 tests/data/test5020 create mode 100644 tests/data/test5021 create mode 100644 tests/libtest/lib5000.c create mode 100644 tests/libtest/lib5004.c diff --git a/.github/scripts/pyspelling.words b/.github/scripts/pyspelling.words index 7563fcd6c7..ce0f4cbfca 100644 --- a/.github/scripts/pyspelling.words +++ b/.github/scripts/pyspelling.words @@ -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 diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index 86383de691..47c887651c 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -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 diff --git a/CMakeLists.txt b/CMakeLists.txt index 54032401a2..f36b49034e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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 diff --git a/configure.ac b/configure.ac index 2456589b61..eed980f0eb 100644 --- a/configure.ac +++ b/configure.ac @@ -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 diff --git a/docs/CURL-DISABLE.md b/docs/CURL-DISABLE.md index a6a1ea1661..83494cf18b 100644 --- a/docs/CURL-DISABLE.md +++ b/docs/CURL-DISABLE.md @@ -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. diff --git a/docs/EXPERIMENTAL.md b/docs/EXPERIMENTAL.md index b1b472c5ba..ff37422172 100644 --- a/docs/EXPERIMENTAL.md +++ b/docs/EXPERIMENTAL.md @@ -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 diff --git a/docs/cmdline-opts/.gitignore b/docs/cmdline-opts/.gitignore index 8d42e2c53d..b903c3bf77 100644 --- a/docs/cmdline-opts/.gitignore +++ b/docs/cmdline-opts/.gitignore @@ -3,3 +3,4 @@ # SPDX-License-Identifier: curl curl.txt +asciipage.tmp.* diff --git a/docs/cmdline-opts/Makefile.inc b/docs/cmdline-opts/Makefile.inc index f8fd01ccd6..c6ffc06a5e 100644 --- a/docs/cmdline-opts/Makefile.inc +++ b/docs/cmdline-opts/Makefile.inc @@ -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 \ diff --git a/docs/cmdline-opts/httpsig-algo.md b/docs/cmdline-opts/httpsig-algo.md new file mode 100644 index 0000000000..c97814738e --- /dev/null +++ b/docs/cmdline-opts/httpsig-algo.md @@ -0,0 +1,36 @@ +--- +c: Copyright (C) Daniel Stenberg, , et al. +SPDX-License-Identifier: curl +Long: httpsig-algo +Protocols: HTTP +Arg: +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. diff --git a/docs/cmdline-opts/httpsig-headers.md b/docs/cmdline-opts/httpsig-headers.md new file mode 100644 index 0000000000..7b590c9854 --- /dev/null +++ b/docs/cmdline-opts/httpsig-headers.md @@ -0,0 +1,45 @@ +--- +c: Copyright (C) Daniel Stenberg, , et al. +SPDX-License-Identifier: curl +Long: httpsig-headers +Protocols: HTTP +Arg: +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. diff --git a/docs/cmdline-opts/httpsig-key.md b/docs/cmdline-opts/httpsig-key.md new file mode 100644 index 0000000000..a2d8f611d8 --- /dev/null +++ b/docs/cmdline-opts/httpsig-key.md @@ -0,0 +1,35 @@ +--- +c: Copyright (C) Daniel Stenberg, , et al. +SPDX-License-Identifier: curl +Long: httpsig-key +Protocols: HTTP +Arg: +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`. diff --git a/docs/cmdline-opts/httpsig-keyid.md b/docs/cmdline-opts/httpsig-keyid.md new file mode 100644 index 0000000000..2dc305fcfb --- /dev/null +++ b/docs/cmdline-opts/httpsig-keyid.md @@ -0,0 +1,23 @@ +--- +c: Copyright (C) Daniel Stenberg, , et al. +SPDX-License-Identifier: curl +Long: httpsig-keyid +Protocols: HTTP +Arg: +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. diff --git a/docs/libcurl/curl_easy_setopt.md b/docs/libcurl/curl_easy_setopt.md index 44d2132577..23eda35203 100644 --- a/docs/libcurl/curl_easy_setopt.md +++ b/docs/libcurl/curl_easy_setopt.md @@ -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) diff --git a/docs/libcurl/curl_version_info.md b/docs/libcurl/curl_version_info.md index 5b18222169..6b1d61121b 100644 --- a/docs/libcurl/curl_version_info.md +++ b/docs/libcurl/curl_version_info.md @@ -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 diff --git a/docs/libcurl/opts/CURLOPT_HTTPAUTH.md b/docs/libcurl/opts/CURLOPT_HTTPAUTH.md index 692178c72e..925033281f 100644 --- a/docs/libcurl/opts/CURLOPT_HTTPAUTH.md +++ b/docs/libcurl/opts/CURLOPT_HTTPAUTH.md @@ -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 diff --git a/docs/libcurl/opts/CURLOPT_HTTPSIG_ALGORITHM.md b/docs/libcurl/opts/CURLOPT_HTTPSIG_ALGORITHM.md new file mode 100644 index 0000000000..2501ec33eb --- /dev/null +++ b/docs/libcurl/opts/CURLOPT_HTTPSIG_ALGORITHM.md @@ -0,0 +1,89 @@ +--- +c: Copyright (C) Daniel Stenberg, , 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 + +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). diff --git a/docs/libcurl/opts/CURLOPT_HTTPSIG_HEADERS.md b/docs/libcurl/opts/CURLOPT_HTTPSIG_HEADERS.md new file mode 100644 index 0000000000..2e2cc4f77e --- /dev/null +++ b/docs/libcurl/opts/CURLOPT_HTTPSIG_HEADERS.md @@ -0,0 +1,108 @@ +--- +c: Copyright (C) Daniel Stenberg, , 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 + +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). diff --git a/docs/libcurl/opts/CURLOPT_HTTPSIG_KEY.md b/docs/libcurl/opts/CURLOPT_HTTPSIG_KEY.md new file mode 100644 index 0000000000..e81378f9cc --- /dev/null +++ b/docs/libcurl/opts/CURLOPT_HTTPSIG_KEY.md @@ -0,0 +1,90 @@ +--- +c: Copyright (C) Daniel Stenberg, , 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 + +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). diff --git a/docs/libcurl/opts/CURLOPT_HTTPSIG_KEYID.md b/docs/libcurl/opts/CURLOPT_HTTPSIG_KEYID.md new file mode 100644 index 0000000000..d921db89a4 --- /dev/null +++ b/docs/libcurl/opts/CURLOPT_HTTPSIG_KEYID.md @@ -0,0 +1,76 @@ +--- +c: Copyright (C) Daniel Stenberg, , 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 + +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). diff --git a/docs/libcurl/opts/Makefile.inc b/docs/libcurl/opts/Makefile.inc index 1eb628b800..f0dc077a94 100644 --- a/docs/libcurl/opts/Makefile.inc +++ b/docs/libcurl/opts/Makefile.inc @@ -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 \ diff --git a/docs/libcurl/symbols-in-versions b/docs/libcurl/symbols-in-versions index faa3b54946..a3d92a2637 100644 --- a/docs/libcurl/symbols-in-versions +++ b/docs/libcurl/symbols-in-versions @@ -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 diff --git a/docs/options-in-versions b/docs/options-in-versions index fa20b2dd29..595cb05caa 100644 --- a/docs/options-in-versions +++ b/docs/options-in-versions @@ -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 diff --git a/include/curl/curl.h b/include/curl/curl.h index 326804eb18..0797c92cad 100644 --- a/include/curl/curl.h +++ b/include/curl/curl.h @@ -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; diff --git a/include/curl/typecheck-gcc.h b/include/curl/typecheck-gcc.h index d600c73cdc..c70bd17cb8 100644 --- a/include/curl/typecheck-gcc.h +++ b/include/curl/typecheck-gcc.h @@ -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) diff --git a/lib/Makefile.inc b/lib/Makefile.inc index aff75e28a5..e00e599d6c 100644 --- a/lib/Makefile.inc +++ b/lib/Makefile.inc @@ -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 \ diff --git a/lib/curl_config-cmake.h.in b/lib/curl_config-cmake.h.in index 97d8f54f5a..ffcce8273c 100644 --- a/lib/curl_config-cmake.h.in +++ b/lib/curl_config-cmake.h.in @@ -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 diff --git a/lib/curl_ed25519.c b/lib/curl_ed25519.c new file mode 100644 index 0000000000..318c70ed21 --- /dev/null +++ b/lib/curl_ed25519.c @@ -0,0 +1,170 @@ +/*************************************************************************** + * _ _ ____ _ + * Project ___| | | | _ \| | + * / __| | | | |_) | | + * | (__| |_| | _ <| |___ + * \___|\___/|_| \_\_____| + * + * Copyright (C) Daniel Stenberg, , et al. + * + * This software is licensed as described in the file COPYING, which + * you should have received as part of this distribution. The terms + * are also available at https://curl.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 + +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 +#include +#if (defined(HAVE_ED25519) || defined(WOLFSSL_CURVE25519_USE_ED25519)) && \ + defined(HAVE_ED25519_KEY_IMPORT) && defined(HAVE_ED25519_SIGN) +#include +#include +#include + +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 */ diff --git a/lib/curl_ed25519.h b/lib/curl_ed25519.h new file mode 100644 index 0000000000..381b6511eb --- /dev/null +++ b/lib/curl_ed25519.h @@ -0,0 +1,44 @@ +#ifndef HEADER_CURL_ED25519_H +#define HEADER_CURL_ED25519_H +/*************************************************************************** + * _ _ ____ _ + * Project ___| | | | _ \| | + * / __| | | | |_) | | + * | (__| |_| | _ <| |___ + * \___|\___/|_| \_\_____| + * + * Copyright (C) Daniel Stenberg, , et al. + * + * This software is licensed as described in the file COPYING, which + * you should have received as part of this distribution. The terms + * are also available at https://curl.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 */ diff --git a/lib/curl_hmac.h b/lib/curl_hmac.h index d9a697a0a5..4fd8ec47bb 100644 --- a/lib/curl_hmac.h +++ b/lib/curl_hmac.h @@ -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 diff --git a/lib/curl_setup.h b/lib/curl_setup.h index c8743108bd..a91b912f3e 100644 --- a/lib/curl_setup.h +++ b/lib/curl_setup.h @@ -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 diff --git a/lib/curl_sha256.h b/lib/curl_sha256.h index 6c48a8e176..fb823169f7 100644 --- a/lib/curl_sha256.h +++ b/lib/curl_sha256.h @@ -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" diff --git a/lib/easyoptions.c b/lib/easyoptions.c index 4c0b4f0ce4..1643ec3256 100644 --- a/lib/easyoptions.c +++ b/lib/easyoptions.c @@ -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 diff --git a/lib/http.c b/lib/http.c index 91dcdb9716..d8bac2e5cc 100644 --- a/lib/http.c +++ b/lib/http.c @@ -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 */ diff --git a/lib/http_httpsig.c b/lib/http_httpsig.c new file mode 100644 index 0000000000..bcf796900f --- /dev/null +++ b/lib/http_httpsig.c @@ -0,0 +1,692 @@ +/*************************************************************************** + * _ _ ____ _ + * Project ___| | | | _ \| | + * / __| | | | |_) | | + * | (__| |_| | _ <| |___ + * \___|\___/|_| \_\_____| + * + * Copyright (C) Daniel Stenberg, , et al. + * + * This software is licensed as described in the file COPYING, which + * you should have received as part of this distribution. The terms + * are also available at https://curl.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 + +#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 */ diff --git a/lib/http_httpsig.h b/lib/http_httpsig.h new file mode 100644 index 0000000000..410755d7e0 --- /dev/null +++ b/lib/http_httpsig.h @@ -0,0 +1,35 @@ +#ifndef HEADER_CURL_HTTP_HTTPSIG_H +#define HEADER_CURL_HTTP_HTTPSIG_H +/*************************************************************************** + * _ _ ____ _ + * Project ___| | | | _ \| | + * / __| | | | |_) | | + * | (__| |_| | _ <| |___ + * \___|\___/|_| \_\_____| + * + * Copyright (C) Daniel Stenberg, , et al. + * + * This software is licensed as described in the file COPYING, which + * you should have received as part of this distribution. The terms + * are also available at https://curl.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 */ diff --git a/lib/setopt.c b/lib/setopt.c index 51cd58a7f9..a030b5489e 100644 --- a/lib/setopt.c +++ b/lib/setopt.c @@ -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: /* diff --git a/lib/urldata.h b/lib/urldata.h index 5d6b3c2fdf..1be8da2dd6 100644 --- a/lib/urldata.h +++ b/lib/urldata.h @@ -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 diff --git a/lib/version.c b/lib/version.c index 33dbe3c436..c4cdde6377 100644 --- a/lib/version.c +++ b/lib/version.c @@ -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 diff --git a/projects/OS400/README.OS400 b/projects/OS400/README.OS400 index 5d4b643a5e..71a78990f3 100644 --- a/projects/OS400/README.OS400 +++ b/projects/OS400/README.OS400 @@ -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 diff --git a/projects/OS400/ccsidcurl.c b/projects/OS400/ccsidcurl.c index cba89f8642..1dcf9543f9 100644 --- a/projects/OS400/ccsidcurl.c +++ b/projects/OS400/ccsidcurl.c @@ -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: diff --git a/scripts/managen b/scripts/managen index 267b4cae3f..4e21474ea0 100755 --- a/scripts/managen +++ b/scripts/managen @@ -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 <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) { diff --git a/src/curlinfo.c b/src/curlinfo.c index 94a02e965f..36eabeb78f 100644 --- a/src/curlinfo.c +++ b/src/curlinfo.c @@ -93,6 +93,13 @@ static const char *disabled[] = { "OFF" #else "ON" +#endif + , + "httpsig: " +#ifdef CURL_DISABLE_HTTPSIG + "OFF" +#else + "ON" #endif , "DoH: " diff --git a/src/tool_cfgable.c b/src/tool_cfgable.c index b0a5990177..5423944a29 100644 --- a/src/tool_cfgable.c +++ b/src/tool_cfgable.c @@ -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); diff --git a/src/tool_cfgable.h b/src/tool_cfgable.h index e21c3c5b2d..7a587f6675 100644 --- a/src/tool_cfgable.h +++ b/src/tool_cfgable.h @@ -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 */ diff --git a/src/tool_getparam.c b/src/tool_getparam.c index 5499b1ff36..9364c86839 100644 --- a/src/tool_getparam.c +++ b/src/tool_getparam.c @@ -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); diff --git a/src/tool_getparam.h b/src/tool_getparam.h index 32476d3776..92495a4de6 100644 --- a/src/tool_getparam.h +++ b/src/tool_getparam.h @@ -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, diff --git a/src/tool_libinfo.c b/src/tool_libinfo.c index 2eec25f083..bce6bb7d6e 100644 --- a/src/tool_libinfo.c +++ b/src/tool_libinfo.c @@ -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 }, diff --git a/src/tool_libinfo.h b/src/tool_libinfo.h index 481db1e58e..4ca507ad66 100644 --- a/src/tool_libinfo.h +++ b/src/tool_libinfo.h @@ -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; diff --git a/src/tool_listhelp.c b/src/tool_listhelp.c index c0b0af792f..63cc990622 100644 --- a/src/tool_listhelp.c +++ b/src/tool_listhelp.c @@ -303,6 +303,26 @@ const struct helptxt helptext[] = { { " --http3-only", "Use HTTP/3 only", CURLHELP_HTTP }, +#ifndef CURL_DISABLE_HTTPSIG + { " --httpsig-algo ", + "Algorithm for HTTP Message Signatures", + CURLHELP_AUTH | CURLHELP_HTTP }, +#endif +#ifndef CURL_DISABLE_HTTPSIG + { " --httpsig-headers ", + "Components to sign for HTTP Message Signatures", + CURLHELP_AUTH | CURLHELP_HTTP }, +#endif +#ifndef CURL_DISABLE_HTTPSIG + { " --httpsig-key ", + "Key file for HTTP Message Signatures", + CURLHELP_AUTH | CURLHELP_HTTP }, +#endif +#ifndef CURL_DISABLE_HTTPSIG + { " --httpsig-keyid ", + "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 ", "Set IP Type of Service or Traffic Class", CURLHELP_CONNECTION }, +#ifndef CURL_DISABLE_IPFS { " --ipfs-gateway ", "Gateway for IPFS", CURLHELP_CURL }, +#endif { "-4, --ipv4", "Resolve names to IPv4 addresses", CURLHELP_CONNECTION | CURLHELP_DNS }, diff --git a/tests/data/Makefile.am b/tests/data/Makefile.am index 2e001ec3c8..f012cc58c0 100644 --- a/tests/data/Makefile.am +++ b/tests/data/Makefile.am @@ -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 diff --git a/tests/data/data-httpsig-ed25519.key b/tests/data/data-httpsig-ed25519.key new file mode 100644 index 0000000000..f0b6688b67 --- /dev/null +++ b/tests/data/data-httpsig-ed25519.key @@ -0,0 +1 @@ +9f8362f87a484a954e6e740c5b4c0e84229139a20aa8ab56ff66586f6a7d29c5 diff --git a/tests/data/data-httpsig-hmac-sha256.key b/tests/data/data-httpsig-hmac-sha256.key new file mode 100644 index 0000000000..eef9161772 --- /dev/null +++ b/tests/data/data-httpsig-hmac-sha256.key @@ -0,0 +1 @@ +0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef diff --git a/tests/data/test5000 b/tests/data/test5000 new file mode 100644 index 0000000000..b935ecdf8a --- /dev/null +++ b/tests/data/test5000 @@ -0,0 +1,57 @@ + + + + +HTTP +CURLOPT_HTTPSIG_ALGORITHM +RFC9421 + + + +# Server-side + + +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 + + + + +# Client-side + + +http + +# this relies on the Debug feature which allows tests to set the time + +Debug +httpsig + + + +HTTP RFC 9421 Message Signatures: basic GET with Ed25519 + + +lib%TESTNUMBER + + + +http://example.com:9000/%TESTNUMBER/resource example.com:9000:%HOSTIP:%HTTPPORT + + + +# Verify data after the test has been "shot" + + +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: */* + + + + diff --git a/tests/data/test5001 b/tests/data/test5001 new file mode 100644 index 0000000000..705289019e --- /dev/null +++ b/tests/data/test5001 @@ -0,0 +1,55 @@ + + + + +HTTP +httpsig +RFC9421 + + + +# Server-side + + +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- + + + +# Client-side + + +http + + +Debug +httpsig + + +HTTP RFC 9421 Message Signatures: CLI GET with query + + +"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 + + + +# Verify data after the test has been "shot" + + +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: */* + + + + diff --git a/tests/data/test5002 b/tests/data/test5002 new file mode 100644 index 0000000000..0d002f831f --- /dev/null +++ b/tests/data/test5002 @@ -0,0 +1,55 @@ + + + + +HTTP +httpsig +RFC9421 +hmac-sha256 + + + +# Server-side + + +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- + + + +# Client-side + + +http + + +Debug +httpsig + + +HTTP RFC 9421 Message Signatures: CLI GET with HMAC-SHA256 + + +"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 + + + +# Verify data after the test has been "shot" + + +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: */* + + + + diff --git a/tests/data/test5003 b/tests/data/test5003 new file mode 100644 index 0000000000..9d8bbfb06f --- /dev/null +++ b/tests/data/test5003 @@ -0,0 +1,55 @@ + + + + +HTTP +httpsig +RFC9421 +httpsig-headers + + + +# Server-side + + +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- + + + +# Client-side + + +http + + +Debug +httpsig + + +HTTP RFC 9421 Message Signatures: custom --httpsig-headers + + +"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 + + + +# Verify data after the test has been "shot" + + +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: */* + + + + diff --git a/tests/data/test5004 b/tests/data/test5004 new file mode 100644 index 0000000000..c916131762 --- /dev/null +++ b/tests/data/test5004 @@ -0,0 +1,66 @@ + + + + +HTTP +CURLOPT_HTTPSIG_ALGORITHM +RFC9421 +ed25519 +B.2.6 + + + +# Server-side + + +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 + + + + +# Client-side + + +http + + +Debug +httpsig + + + +HTTP RFC 9421 B.2.6: Ed25519 POST with headers (RFC test vector) + + +lib%TESTNUMBER + + + +CURL_HTTPSIG_CREATED=1618884473 + + + +http://example.com/%TESTNUMBER/foo example.com:80:%HOSTIP:%HTTPPORT + + + +# Verify data after the test has been "shot" + + +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"} + + + diff --git a/tests/data/test5005 b/tests/data/test5005 new file mode 100644 index 0000000000..f52c94d8d5 --- /dev/null +++ b/tests/data/test5005 @@ -0,0 +1,56 @@ + + + + +HTTP +httpsig +RFC9421 +header-signing + + + +# Server-side + + +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- + + + +# Client-side + + +http + + +Debug +httpsig + + +HTTP RFC 9421 Message Signatures: sign with a regular HTTP header + + +"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 + + + +# Verify data after the test has been "shot" + + +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 + + + + diff --git a/tests/data/test5006 b/tests/data/test5006 new file mode 100644 index 0000000000..fee77488ea --- /dev/null +++ b/tests/data/test5006 @@ -0,0 +1,56 @@ + + + + +HTTP +httpsig +RFC9421 +case-normalization + + + +# Server-side + + +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- + + + +# Client-side + + +http + + +Debug +httpsig + + +HTTP RFC 9421 Message Signatures: component name lowercasing + + +"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 + + + +# Verify data after the test has been "shot" + + +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 + + + + diff --git a/tests/data/test5007 b/tests/data/test5007 new file mode 100644 index 0000000000..42d72bfd1b --- /dev/null +++ b/tests/data/test5007 @@ -0,0 +1,58 @@ + + + + +HTTP +HTTP POST +httpsig +RFC9421 + + + +# Server-side + + +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- + + + +# Client-side + + +http + + +Debug +httpsig + + +HTTP RFC 9421 Message Signatures: POST method + + +"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 + + + +# Verify data after the test has been "shot" + + +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 + + + diff --git a/tests/data/test5008 b/tests/data/test5008 new file mode 100644 index 0000000000..01e6283224 --- /dev/null +++ b/tests/data/test5008 @@ -0,0 +1,46 @@ + + + + +HTTP +httpsig +RFC9421 +error + + + +# Server-side + + +HTTP/1.1 200 OK +Date: Tue, 09 Nov 2010 14:49:00 GMT +Server: test-server/fake +Content-Length: 0 + + + + +# Client-side + + +http + + +Debug +httpsig + + +HTTP RFC 9421 Message Signatures: error - missing --httpsig-key + + +"http://example.com/%TESTNUMBER/resource" --httpsig-algo "ed25519" --httpsig-keyid "my-key" --connect-to example.com::%HOSTIP:%HTTPPORT + + + +# Verify data after the test has been "shot" + + +2 + + + diff --git a/tests/data/test5009 b/tests/data/test5009 new file mode 100644 index 0000000000..ef7bc5e6a7 --- /dev/null +++ b/tests/data/test5009 @@ -0,0 +1,46 @@ + + + + +HTTP +httpsig +RFC9421 +error + + + +# Server-side + + +HTTP/1.1 200 OK +Date: Tue, 09 Nov 2010 14:49:00 GMT +Server: test-server/fake +Content-Length: 0 + + + + +# Client-side + + +http + + +Debug +httpsig + + +HTTP RFC 9421 Message Signatures: error - unsupported algorithm + + +"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 + + + +# Verify data after the test has been "shot" + + +2 + + + diff --git a/tests/data/test5010 b/tests/data/test5010 new file mode 100644 index 0000000000..ff77e59ceb --- /dev/null +++ b/tests/data/test5010 @@ -0,0 +1,46 @@ + + + + +HTTP +httpsig +RFC9421 +error + + + +# Server-side + + +HTTP/1.1 200 OK +Date: Tue, 09 Nov 2010 14:49:00 GMT +Server: test-server/fake +Content-Length: 0 + + + + +# Client-side + + +http + + +Debug +httpsig + + +HTTP RFC 9421 Message Signatures: error - non-existent key file + + +"http://example.com/%TESTNUMBER/resource" --httpsig-algo "ed25519" --httpsig-key /nonexistent/key.hex --httpsig-keyid "my-key" --connect-to example.com::%HOSTIP:%HTTPPORT + + + +# Verify data after the test has been "shot" + + +26 + + + diff --git a/tests/data/test5011 b/tests/data/test5011 new file mode 100644 index 0000000000..94206854a2 --- /dev/null +++ b/tests/data/test5011 @@ -0,0 +1,55 @@ + + + + +HTTP +httpsig +RFC9421 +query-special-chars + + + +# Server-side + + +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- + + + +# Client-side + + +http + + +Debug +httpsig + + +HTTP RFC 9421 Message Signatures: query string with special characters + + +"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 + + + +# Verify data after the test has been "shot" + + +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: */* + + + + diff --git a/tests/data/test5012 b/tests/data/test5012 new file mode 100644 index 0000000000..e119c29d58 --- /dev/null +++ b/tests/data/test5012 @@ -0,0 +1,58 @@ + + + + +HTTP +httpsig +RFC9421 +default-port + + + +# Server-side + + +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- + + + +# Client-side + + +http + + +Debug +httpsig + + +HTTP RFC 9421 Message Signatures: default port omitted from @authority + + +"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 + + + +# Verify data after the test has been "shot" + + +s/Signature: sig1=:.*:/Signature: sig1=:STRIPPED:/ + + +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: */* + + + + diff --git a/tests/data/test5013 b/tests/data/test5013 new file mode 100644 index 0000000000..9cad55190c --- /dev/null +++ b/tests/data/test5013 @@ -0,0 +1,58 @@ + + + + +HTTP +httpsig +RFC9421 +non-default-port + + + +# Server-side + + +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- + + + +# Client-side + + +http + + +Debug +httpsig + + +HTTP RFC 9421 Message Signatures: non-default port in @authority + + +"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 + + + +# Verify data after the test has been "shot" + + +s/Signature: sig1=:.*:/Signature: sig1=:STRIPPED:/ + + +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: */* + + + + diff --git a/tests/data/test5014 b/tests/data/test5014 new file mode 100644 index 0000000000..6033e40bec --- /dev/null +++ b/tests/data/test5014 @@ -0,0 +1,58 @@ + + + + +HTTP +httpsig +RFC9421 +empty-query + + + +# Server-side + + +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- + + + +# Client-side + + +http + + +Debug +httpsig + + +HTTP RFC 9421 Message Signatures: @query explicitly requested, no query in URL + + +"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 + + + +# Verify data after the test has been "shot" + + +s/Signature: sig1=:.*:/Signature: sig1=:STRIPPED:/ + + +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: */* + + + + diff --git a/tests/data/test5015 b/tests/data/test5015 new file mode 100644 index 0000000000..f9459be37a --- /dev/null +++ b/tests/data/test5015 @@ -0,0 +1,62 @@ + + + + +HTTP +HTTP POST +httpsig +RFC9421 +hmac-sha256 + + + +# Server-side + + +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- + + + +# Client-side + + +http + + +Debug +httpsig + + +HTTP RFC 9421 Message Signatures: POST with HMAC-SHA256 + + +"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 + + + +# Verify data after the test has been "shot" + + +s/Signature: sig1=:.*:/Signature: sig1=:STRIPPED:/ + + +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 + + + diff --git a/tests/data/test5016 b/tests/data/test5016 new file mode 100644 index 0000000000..f6fbe2b013 --- /dev/null +++ b/tests/data/test5016 @@ -0,0 +1,60 @@ + + + + +HTTP +httpsig +RFC9421 +hmac-sha256 +header-signing + + + +# Server-side + + +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- + + + +# Client-side + + +http + + +Debug +httpsig + + +HTTP RFC 9421 Message Signatures: HMAC-SHA256 with custom headers + + +"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 + + + +# Verify data after the test has been "shot" + + +s/Signature: sig1=:.*:/Signature: sig1=:STRIPPED:/ + + +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 + + + + diff --git a/tests/data/test5017 b/tests/data/test5017 new file mode 100644 index 0000000000..04a582e0d3 --- /dev/null +++ b/tests/data/test5017 @@ -0,0 +1,55 @@ + + + + +HTTP +httpsig +RFC9421 +preexisting-signature + + + +# Server-side + + +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- + + + +# Client-side + + +http + + +Debug +httpsig + + +HTTP RFC 9421 Message Signatures: user-supplied Signature header skips signing + + +"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 + + + +# Verify data after the test has been "shot" + + +GET /%TESTNUMBER/resource HTTP/1.1 +Host: example.com:5700 +User-Agent: curl/%VERSION +Accept: */* +Signature: preexisting +Signature-Input: preexisting + + + + diff --git a/tests/data/test5018 b/tests/data/test5018 new file mode 100644 index 0000000000..417c1f0b5b --- /dev/null +++ b/tests/data/test5018 @@ -0,0 +1,63 @@ + + + + +HTTP +HTTP PUT +httpsig +RFC9421 + + + +# Server-side + + +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- + + + +# Client-side + + +http + + +Debug +httpsig + + +putdata + + +HTTP RFC 9421 Message Signatures: PUT upload + + +"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 + + + +# Verify data after the test has been "shot" + + +s/Signature: sig1=:.*:/Signature: sig1=:STRIPPED:/ + + +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 + + + diff --git a/tests/data/test5019 b/tests/data/test5019 new file mode 100644 index 0000000000..b07df9aa18 --- /dev/null +++ b/tests/data/test5019 @@ -0,0 +1,46 @@ + + + + +HTTP +httpsig +RFC9421 +error + + + +# Server-side + + +HTTP/1.1 200 OK +Date: Tue, 09 Nov 2010 14:49:00 GMT +Server: test-server/fake +Content-Length: 0 + + + + +# Client-side + + +http + + +Debug +httpsig + + +HTTP RFC 9421 Message Signatures: error - duplicate component + + +"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 + + + +# Verify data after the test has been "shot" + + +43 + + + diff --git a/tests/data/test5020 b/tests/data/test5020 new file mode 100644 index 0000000000..f6c27e877d --- /dev/null +++ b/tests/data/test5020 @@ -0,0 +1,46 @@ + + + + +HTTP +httpsig +RFC9421 +error + + + +# Server-side + + +HTTP/1.1 200 OK +Date: Tue, 09 Nov 2010 14:49:00 GMT +Server: test-server/fake +Content-Length: 0 + + + + +# Client-side + + +http + + +Debug +httpsig + + +HTTP RFC 9421 Message Signatures: error - rejects legacy '@' component syntax + + +"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 + + + +# Verify data after the test has been "shot" + + +43 + + + diff --git a/tests/data/test5021 b/tests/data/test5021 new file mode 100644 index 0000000000..da5f5af672 --- /dev/null +++ b/tests/data/test5021 @@ -0,0 +1,57 @@ + + + + +HTTP +httpsig +RFC9421 + + + +# Server-side + + +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- + + + +# Client-side + + +http + + +Debug +httpsig + + +HTTP RFC 9421 Message Signatures: default algorithm (ed25519) when --httpsig-algo omitted + + +"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 + + + +# Verify data after the test has been "shot" + + +s/Signature: sig1=:.*:/Signature: sig1=:STRIPPED:/ + + +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: */* + + + + diff --git a/tests/libtest/Makefile.inc b/tests/libtest/Makefile.inc index 7b959e0df4..89f6a82546 100644 --- a/tests/libtest/Makefile.inc +++ b/tests/libtest/Makefile.inc @@ -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 diff --git a/tests/libtest/lib5000.c b/tests/libtest/lib5000.c new file mode 100644 index 0000000000..a0eb92f0ae --- /dev/null +++ b/tests/libtest/lib5000.c @@ -0,0 +1,66 @@ +/*************************************************************************** + * _ _ ____ _ + * Project ___| | | | _ \| | + * / __| | | | |_) | | + * | (__| |_| | _ <| |___ + * \___|\___/|_| \_\_____| + * + * Copyright (C) Daniel Stenberg, , et al. + * + * This software is licensed as described in the file COPYING, which + * you should have received as part of this distribution. The terms + * are also available at https://curl.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; +} diff --git a/tests/libtest/lib5004.c b/tests/libtest/lib5004.c new file mode 100644 index 0000000000..1f6fb1a223 --- /dev/null +++ b/tests/libtest/lib5004.c @@ -0,0 +1,85 @@ +/*************************************************************************** + * _ _ ____ _ + * Project ___| | | | _ \| | + * / __| | | | |_) | | + * | (__| |_| | _ <| |___ + * \___|\___/|_| \_\_____| + * + * Copyright (C) Daniel Stenberg, , et al. + * + * This software is licensed as described in the file COPYING, which + * you should have received as part of this distribution. The terms + * are also available at https://curl.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; +}