TLS: add support for ECH (Encrypted Client Hello)

An EXPERIMENTAL feature used with CURLOPT_ECH and --ech.

Closes #11922
This commit is contained in:
Stephen Farrell 2024-04-04 14:23:35 +01:00 committed by Daniel Stenberg
parent 565d28dc8e
commit a362962b72
No known key found for this signature in database
GPG Key ID: 5CC908FDB71E12C2
40 changed files with 3122 additions and 16 deletions

View File

@ -2,6 +2,7 @@
#
# SPDX-License-Identifier: curl
#
AAAA
ABI
accessor
ACK
@ -10,6 +11,7 @@ AIA
AIX
al
Alessandro
aliasMode
allocator
alnum
ALPN
@ -109,6 +111,7 @@ CLA
CLAs
cleartext
CLI
ClientHello
clientp
cliget
closesocket
@ -116,6 +119,8 @@ CMake
cmake
CMake's
cmake's
CNAME
CNAMEs
CMakeLists
CNA
CodeQL
@ -146,6 +151,7 @@ cURL
CURLcode
curldown
CURLE
CURLECH
CURLH
curlimages
CURLINFO
@ -164,6 +170,7 @@ dbg
Debian
DEBUGBUILD
decrypt
decrypting
deepcode
DELE
DER
@ -190,6 +197,7 @@ DNS
dns
dnsop
DoH
DoT
doxygen
drftpd
dsa
@ -201,6 +209,9 @@ EBCDIC
ECC
ECDHE
ECH
ecl
ECHConfig
ECHConfigList
ECONNREFUSED
eCOS
EFnet
@ -284,6 +295,8 @@ GOST
GPG
GPL
GPLed
GREASE
GREASEing
Greear
groff
gsasl
@ -307,6 +320,7 @@ Hards
Haxx
haxx
Heimdal
HelloRetryRequest
HELO
HH
HMAC
@ -316,6 +330,7 @@ homebrew
hostname
hostnames
Housley
HRR
Hruska
HSTS
hsts
@ -460,6 +475,7 @@ Marek
Mavrogiannopoulos
Mbed
mbedTLS
md
Meglio
memdebug
MesaLink
@ -470,6 +486,7 @@ Michal
Micrium
MicroBlaze
MicroOS
middlebox
mingw
MinGW
MINIX
@ -590,6 +607,7 @@ pkcs
PKGBUILD
PKI
pluggable
pn
PolarSSL
Polhem
pollset
@ -625,6 +643,7 @@ py
pycurl
pytest
Pytest
qname
QNX
QoS
Qubes
@ -668,6 +687,9 @@ Roadmap
Rockbox
roffit
RPG
RR
RRs
RRtype
RSA
RTMP
rtmp
@ -784,6 +806,7 @@ SunSSH
superset
svc
svcb
SVCB
Svyatoslav
Swisscom
sws

1
.gitignore vendored
View File

@ -65,3 +65,4 @@ curl_fuzzer_seed_corpus.zip
libstandaloneengine.a
tests/string
tests/config
tests/ech-log/

View File

@ -37,6 +37,7 @@
# HAVE_GNUTLS_SRP: `gnutls_srp_verifier` present in GnuTLS
# HAVE_SSL_CTX_SET_QUIC_METHOD: `SSL_CTX_set_quic_method` present in OpenSSL/wolfSSL
# HAVE_QUICHE_CONN_SET_QLOG_FD: `quiche_conn_set_qlog_fd` present in QUICHE
# HAVE_ECH: ECH API checks for OpenSSL, boringssl or wolfSSL
#
# For each of the above variables, if the variable is DEFINED (either
# to ON or OFF), the symbol detection will be skipped. If the
@ -654,6 +655,31 @@ if(USE_OPENSSL OR USE_WOLFSSL)
endif()
endif()
option(USE_HTTPSRR "Enable HTTPS RR support for ECH (experimental)" OFF)
option(USE_ECH "Enable ECH support" OFF)
if(USE_ECH)
if(USE_OPENSSL OR USE_WOLFSSL)
# Be sure that the OpenSSL/wolfSSL library actually supports ECH.
if(NOT DEFINED HAVE_ECH)
if(USE_OPENSSL AND HAVE_BORINGSSL)
openssl_check_symbol_exists(SSL_set1_ech_config_list "openssl/ssl.h" HAVE_ECH)
elseif(USE_OPENSSL)
openssl_check_symbol_exists(SSL_ech_set1_echconfig "openssl/ech.h" HAVE_ECH)
elseif(USE_WOLFSSL)
openssl_check_symbol_exists(wolfSSL_CTX_GenerateEchConfig "wolfssl/options.h;wolfssl/ssl.h" HAVE_ECH)
endif()
endif()
if(NOT HAVE_ECH)
message(FATAL_ERROR "ECH support missing in OpenSSL/BoringSSL/wolfSSL")
else()
message("ECH enabled.")
endif()
else()
message(FATAL_ERROR "ECH requires ECH-enablded OpenSSL, BoringSSL or wolfSSL")
endif()
endif()
option(USE_NGHTTP2 "Use nghttp2 library" OFF)
if(USE_NGHTTP2)
find_package(NGHTTP2 REQUIRED)
@ -1590,6 +1616,8 @@ if(NOT CURL_DISABLE_INSTALL)
_add_if("IPFS" NOT CURL_DISABLE_HTTP)
_add_if("IPNS" NOT CURL_DISABLE_HTTP)
_add_if("HTTPS" NOT CURL_DISABLE_HTTP AND SSL_ENABLED)
_add_if("ECH" HAVE_ECH)
_add_if("HTTPSRR" HAVE_ECH)
_add_if("FTP" NOT CURL_DISABLE_FTP)
_add_if("FTPS" NOT CURL_DISABLE_FTP AND SSL_ENABLED)
_add_if("FILE" NOT CURL_DISABLE_FILE)

View File

@ -51,6 +51,7 @@ CURL_CHECK_OPTION_CURLDEBUG
CURL_CHECK_OPTION_SYMBOL_HIDING
CURL_CHECK_OPTION_ARES
CURL_CHECK_OPTION_RT
CURL_CHECK_OPTION_HTTPSRR
CURL_CHECK_OPTION_ECH
XC_CHECK_PATH_SEPARATOR
@ -4538,6 +4539,16 @@ if test "x$hsts" != "xyes"; then
AC_DEFINE(CURL_DISABLE_HSTS, 1, [disable alt-svc])
fi
dnl *************************************************************
dnl check whether HTTPSRR support if desired
dnl
if test "x$want_httpsrr" != "xno"; then
AC_MSG_RESULT([HTTPSRR support is available])
AC_DEFINE(USE_HTTPSRR, 1, [enable HTTPS RR support])
experimental="$experimental HTTPSRR"
fi
dnl *************************************************************
dnl check whether ECH support, if desired, is actually available
dnl
@ -4548,18 +4559,28 @@ if test "x$want_ech" != "xno"; then
ECH_ENABLED=0
ECH_SUPPORT=''
dnl OpenSSL with a chosen ECH function should be enough
dnl so more exhaustive checking seems unnecessary for now
dnl check for OpenSSL
if test "x$OPENSSL_ENABLED" = "x1"; then
AC_CHECK_FUNCS(SSL_get_ech_status,
ECH_SUPPORT="ECH support available (OpenSSL with SSL_get_ech_status)"
AC_CHECK_FUNCS(SSL_ech_set1_echconfig,
ECH_SUPPORT="ECH support available via OpenSSL with SSL_ech_set1_echconfig"
ECH_ENABLED=1)
fi
dnl check for boringssl equivalent
if test "x$OPENSSL_ENABLED" = "x1"; then
AC_CHECK_FUNCS(SSL_set1_ech_config_list,
ECH_SUPPORT="ECH support available via boringssl with SSL_set1_ech_config_list"
ECH_ENABLED=1)
fi
if test "x$WOLFSSL_ENABLED" = "x1"; then
AC_CHECK_FUNCS(wolfSSL_CTX_GenerateEchConfig,
ECH_SUPPORT="ECH support available via WolfSSL with wolfSSL_CTX_GenerateEchConfig"
ECH_ENABLED=1)
dnl add 'elif' chain here for additional implementations
fi
dnl now deal with whatever we found
if test "x$ECH_ENABLED" = "x1"; then
dnl force pre-requisites for ECH
AC_DEFINE(USE_HTTPSRR, 1, [force HTTPS RR support for ECH])
AC_DEFINE(USE_ECH, 1, [if ECH support is available])
AC_MSG_RESULT($ECH_SUPPORT)
experimental="$experimental ECH"
@ -4777,10 +4798,6 @@ else
AC_MSG_RESULT([no])
fi
if test "x$ECH_ENABLED" = "x1"; then
SUPPORT_FEATURES="$SUPPORT_FEATURES ECH"
fi
if test ${ac_cv_sizeof_curl_off_t} -gt 4; then
if test ${ac_cv_sizeof_off_t} -gt 4 -o \
"$curl_win32_file_api" = "win32_large_files"; then

479
docs/ECH.md Normal file
View File

@ -0,0 +1,479 @@
<!--
Copyright (C) Daniel Stenberg, <daniel@haxx.se>, et al.
SPDX-License-Identifier: curl
-->
# Building curl with HTTPS-RR and ECH support
We've added support for ECH to in this curl build. That can use HTTPS RRs
published in the DNS, if curl is using DoH, or else can accept the relevant
ECHConfigList values from the command line. That works with OpenSSL,
WolfSSL or boringssl as the TLS provider, depending on how you build curl.
This feature is EXPERIMENTAL. DO NOT USE IN PRODUCTION.
This should however provide enough of a proof-of-concept to prompt an informed
discussion about a good path forward for ECH support in curl, when using
OpenSSL, or other TLS libraries, as those add ECH support.
## OpenSSL Build
To build our ECH-enabled OpenSSL fork:
```bash
cd $HOME/code
git clone https://github.com/defo-project/openssl
cd openssl
./config --libdir=lib --prefix=$HOME/code/openssl-local-inst
...stuff...
make -j8
...stuff (maybe go for coffee)...
make install_sw
...a little bit of stuff...
```
To build curl ECH-enabled, making use of the above:
```bash
cd $HOME/code
git clone https://github.com/curl/curl
cd curl
autoreconf -fi
LDFLAGS="-Wl,-rpath,$HOME/code/openss-local-inst/lib/" ./configure --with-ssl=$HOME/code/openssl-local-inst --enable-ech --enable-httpsrr
...lots of output...
WARNING: ech ECH HTTPSRR enabled but marked EXPERIMENTAL...
make
...lots more output...
```
If you do not get that WARNING at the end of the ``configure`` command, then ECH
is not enabled, so go back some steps and re-do whatever needs re-doing:-) If you
want to debug curl then you should add ``--enable-debug`` to the ``configure``
command.
With the above build, I still need to set ``LD_LIBRARY_PATH`` to run the
version of curl built against OpenSSL in my development environment (Ubuntu
23.10).
## Using ECH and DoH
Curl supports using DoH for A/AAAA lookups so it was relatively easy to add
retrieval of HTTPS RRs in that situation. To use ECH and DoH together:
```bash
cd $HOME/code/curl
LD_LIBRARY_PATH=$HOME/code/openssl ./src/curl --ech true --doh-url https://one.one.one.one/dns-query https://defo.ie/ech-check.php
...
SSL_ECH_STATUS: success <img src="greentick-small.png" alt="good" /> <br/>
...
```
The output snippet above is within the HTML for the webpage, when things work.
The above works for these test sites:
```bash
https://defo.ie/ech-check.php
https://draft-13.esni.defo.ie:8413/stats
https://draft-13.esni.defo.ie:8414/stats
https://crypto.cloudflare.com/cdn-cgi/trace
https://tls-ech.dev
```
The list above has 4 different server technologies, implemented by 3 different
parties, and includes a case (the port 8414 server) where HelloRetryRequest
(HRR) is forced.
We currently support the following new curl command line arguments/options:
- ``--ech <config>`` - the ``config`` value can be one of:
- ``false`` says to not attempt ECH
- ``true`` says to attempt ECH, if possible
- ``grease`` if attempting ECH is not possible, then send a GREASE ECH extension
- ``hard`` hard-fail the connection if ECH cannot be attempted
- ``ecl:<b64value>`` a base64 encoded ECHConfigList, rather than one accessed from the DNS
- ``pn:<name>`` over-ride the ``public_name`` from an ECHConfigList
Note that in the above "attempt ECH" means the client emitting a TLS
ClientHello with a "real" ECH extension, but that does not mean that the
relevant server can succeed in decrypting, as things can fail for other
reasons.
## Supplying an ECHConfigList on the command line
To supply the ECHConfigList on the command line, you might need a bit of
cut-and-paste, e.g.:
```bash
dig +short https defo.ie
1 . ipv4hint=213.108.108.101 ech=AED+DQA8PAAgACD8WhlS7VwEt5bf3lekhHvXrQBGDrZh03n/LsNtAodbUAAEAAEAAQANY292ZXIuZGVmby5pZQAA ipv6hint=2a00:c6c0:0:116:5::10
```
Then paste the base64 encoded ECHConfigList onto the curl command line:
```bash
LD_LIBRARY_PATH=$HOME/code/openssl ./src/curl --ech ecl:AED+DQA8PAAgACD8WhlS7VwEt5bf3lekhHvXrQBGDrZh03n/LsNtAodbUAAEAAEAAQANY292ZXIuZGVmby5pZQAA https://defo.ie/ech-check.php
...
SSL_ECH_STATUS: success <img src="greentick-small.png" alt="good" /> <br/>
...
```
The output snippet above is within the HTML for the webpage.
If you paste in the wrong ECHConfigList (it changes hourly for ``defo.ie``) you
should get an error like this:
```bash
LD_LIBRARY_PATH=$HOME/code/openssl ./src/curl -vvv --ech ecl:AED+DQA8yAAgACDRMQo+qYNsNRNj+vfuQfFIkrrUFmM4vogucxKj/4nzYgAEAAEAAQANY292ZXIuZGVmby5pZQAA https://defo.ie/ech-check.php
...
* OpenSSL/3.3.0: error:0A00054B:SSL routines::ech required
...
```
There is a reason to want this command line option - for use before publishing
an ECHConfigList in the DNS as per the Internet-draft [A well-known URI for
publishing ECHConfigList values](https://datatracker.ietf.org/doc/draft-ietf-tls-wkech/).
If you do use a wrong ECHConfigList value, then the server might return a
good value, via the ``retry_configs`` mechanism. You can see that value in
the verbose output, e.g.:
```bash
LD_LIBRARY_PATH=$HOME/code/openssl ./src/curl -vvv --ech ecl:AED+DQA8yAAgACDRMQo+qYNsNRNj+vfuQfFIkrrUFmM4vogucxKj/4nzYgAEAAEAAQANY292ZXIuZGVmby5pZQAA https://defo.ie/ech-check.php
...
* ECH: retry_configs AQD+DQA8DAAgACBvYqJy+Hgk33wh/ZLBzKSPgwxeop7gvojQzfASq7zeZQAEAAEAAQANY292ZXIuZGVmby5pZQAA/g0APEMAIAAgXkT5r4cYs8z19q5rdittyIX8gfQ3ENW4wj1fVoiJZBoABAABAAEADWNvdmVyLmRlZm8uaWUAAP4NADw2ACAAINXSE9EdXzEQIJZA7vpwCIQsWqsFohZARXChgPsnfI1kAAQAAQABAA1jb3Zlci5kZWZvLmllAAD+DQA8cQAgACASeiD5F+UoSnVoHvA2l1EifUVMFtbVZ76xwDqmMPraHQAEAAEAAQANY292ZXIuZGVmby5pZQAA
* ECH: retry_configs for defo.ie from cover.defo.ie, 319
...
```
At that point, you could copy the base64 encoded value above and try again.
For now, this only works for the OpenSSL and boringssl builds.
## Default settings
Curl has various ways to configure default settings, e.g. in ``$HOME/.curlrc``,
so one can set the DoH URL and enable ECH that way:
```bash
cat ~/.curlrc
doh-url=https://one.one.one.one/dns-query
silent
ech=true
```
Note that when you use the system's curl command (rather than our ECH-enabled
build), it is liable to warn that ``ech`` is an unknown option. If that is an
issue (e.g. if some script re-directs stdout and stderr somewhere) then adding
the ``silent`` line above seems to be a good enough fix. (Though of
course, yet another script could depend on non-silent behavior, so you may have
to figure out what you prefer yourself.) That seems to have changed with the
latest build, previously ``silent=TRUE`` was what I used in ``~/.curlrc`` but
now that seems to cause a problem, so that the following line(s) are ignored.
If you want to always use our OpenSSL build you can set ``LD_LIBRARY_PATH``
in the environment:
```bash
export LD_LIBRARY_PATH=$HOME/code/openssl
```
When you do the above, there can be a mismatch between OpenSSL versions
for applications that check that. A ``git push`` for example fails so you
should unset ``LD_LIBRARY_PATH`` before doing that or use a different shell.
```bash
git push
OpenSSL version mismatch. Built against 30000080, you have 30200000
...
```
With all that setup as above the command line gets simpler:
```bash
./src/curl https://defo.ie/ech-check.php
...
SSL_ECH_STATUS: success <img src="greentick-small.png" alt="good" /> <br/>
...
```
The ``--ech true`` option is opportunistic, so tries to do ECH but does not fail if
the client for example cannot find any ECHConfig values. The ``--ech hard``
option hard-fails if there is no ECHConfig found in DNS, so for now, that is not
a good option to set as a default. Once ECH has really been attempted by
the client, if decryption on the server side fails, then curl fails.
## Code changes for ECH support when using DoH
Code changes are ``#ifdef`` protected via ``USE_ECH`` or ``USE_HTTPSRR``:
- ``USE_HTTPSRR`` is used for HTTPS RR retrieval code that could be generically
used should non-ECH uses for HTTPS RRs be identified, e.g. use of ALPN values
or IP address hints.
- ``USE_ECH`` protects ECH specific code.
There are various obvious code blocks for handling the new command line
arguments which aren't described here, but should be fairly clear.
As shown in the ``configure`` usage above, there are ``configure.ac`` changes
that allow separately dis/enabling ``USE_HTTPSRR`` and ``USE_ECH``. If ``USE_ECH``
is enabled, then ``USE_HTTPSRR`` is forced. In both cases ``USE_DOH``
is required. (There may be some configuration conflicts available for the
determined:-)
The main functional change, as you would expect, is in ``lib/vtls/openssl.c``
where an ECHConfig, if available from command line or DNS cache, is fed into
the OpenSSL library via the new APIs implemented in our OpenSSL fork for that
purpose. This code also implements the opportunistic (``--ech true``) or hard-fail
(``--ech hard``) logic.
Other than that, the main additions are in ``lib/doh.c``
where we re-use ``dohprobe()`` to retrieve an HTTPS RR value for the target
domain. If such a value is found, that is stored using a new ``store_https()``
function in a new field in the ``dohentry`` structure.
The qname for the DoH query is modified if the port number is not 443, as
defined in the SVCB specification.
When the DoH process has worked, ``Curl_doh_is_resolved()`` now also returns
the relevant HTTPS RR value data in the ``Curl_dns_entry`` structure.
That is later accessed when the TLS session is being established, if ECH is
enabled (from ``lib/vtls/openssl.c`` as described above).
## Limitations
Things that need fixing, but that can probably be ignored for the
moment:
- We could easily add code to make use of an ``alpn=`` value found in an HTTPS
RR, passing that on to OpenSSL for use as the "inner" ALPN value, but have
yet to do that.
Current limitations (more interesting than the above):
- Only the first HTTPS RR value retrieved is actually processed as described
above, that could be extended in future, though picking the "right" HTTPS RR
could be non-trivial if multiple RRs are published - matching IP address hints
versus A/AAAA values might be a good basis for that. Last I checked though,
browsers supporting ECH did not handle multiple HTTPS RRs well, though that
needs re-checking as it has been a while.
- It is unclear how one should handle any IP address hints found in an HTTPS RR.
It may be that a bit of consideration of how "multi-CDN" deployments might
emerge would provide good answers there, but for now, it is not clear how best
curl might handle those values when present in the DNS.
- The SVCB/HTTPS RR specification supports a new "CNAME at apex" indirection
("aliasMode") - the current code takes no account of that at all. One could
envisage implementing the equivalent of following CNAMEs in such cases, but
it is not clear if that'd be a good plan. (As of now, chrome browsers do not seem
to have any support for that "aliasMode" and we've not checked Firefox for that
recently.)
- We have not investigated what related changes or additions might be needed
for applications using libcurl, as opposed to use of curl as a command line
tool.
- We have not yet implemented tests as part of the usual curl test harness as
doing so would seem to require re-implementing an ECH-enabled server as part
of the curl test harness. For now, we have a ``./tests/ech_test.sh`` script
that attempts ECH with various test servers and with many combinations of the
allowed command line options. While that is a useful test and has find issues,
it is not comprehensive and we're not (as yet) sure what would be the right
level of coverage. When running that script you should not have a
``$HOME/.curlrc`` file that affects ECH or some of the negative tests could
produce spurious failures.
## Building with cmake
To build with cmake, assuming our ECH-enabled OpenSSL is as before:
```bash
cd $HOME/code
git clone https://github.com/curl/curl
cd curl
mkdir build
cd build
cmake -DOPENSSL_ROOT_DIR=$HOME/code/openssl -DUSE_ECH=1 -DUSE_HTTPSRR=1 ..
...
make
...
[100%] Built target curl
```
The binary produced by the cmake build does not need any ECH-specific
``LD_LIBRARY_PATH`` setting.
## boringssl build
BoringSSL is also supported by curl and also supports ECH, so to build
with that, instead of our ECH-enabled OpenSSL:
```bash
cd $HOME/code
git clone https://boringssl.googlesource.com/boringssl
cd boringssl
cmake -DCMAKE_INSTALL_PREFIX:PATH=$HOME/code/boringssl/inst -DBUILD_SHARED_LIBS=1
make
...
make install
```
Then:
```bash
cd $HOME/code
git clone https://github.com/curl/curl
cd curl
autoreconf -fi
LDFLAGS="-Wl,-rpath,$HOME/code/boringssl/inst/lib" ./configure --with-ssl=$HOME/code/boringssl/inst --enable-ech --enable-httpsrr
...lots of output...
WARNING: ech ECH HTTPSRR enabled but marked EXPERIMENTAL. Use with caution!
make
```
The boringssl APIs are fairly similar to those in our ECH-enabled OpenSSL
fork, so code changes are also in ``lib/vtls/openssl.c``, protected
via ``#ifdef OPENSSL_IS_BORINGSSL`` and are mostly obvious API variations.
The boringssl APIs however do not support the ``--ech pn:`` command line
variant as of now.
## WolfSSL build
WolfSSL also supports ECH and can be used by curl, so here's how:
```bash
cd $HOME/code
git clone https://github.com/wolfSSL/wolfssl
cd wolfssl
./autogen.sh
./configure --prefix=$HOME/code/wolfssl/inst --enable-ech --enable-debug --enable-opensslextra
make
make install
```
The install prefix (``inst``) in the above causes WolfSSL to be installed there
and we seem to need that for the curl configure command to work out. The
``--enable-opensslextra`` turns out (after much faffing about;-) to be
important or else we get build problems with curl below.
```bash
cd $HOME/code
git clone https://github.com/curl/curl
cd curl
autoreconf -fi
./configure --with-wolfssl=$HOME/code/wolfssl/inst --enable-ech --enable-httpsrr
make
```
There are some known issues with the ECH implementation in WolfSSL:
- The main issue is that the client currently handles HelloRetryRequest
incorrectly. [HRR issue](https://github.com/wolfSSL/wolfssl/issues/6802).)
The HRR issue means that the client does not work for
[this ECH test web site](https://tls-ech.dev) and any other similarly configured
sites.
- There is also an issue related to so-called middlebox compatibility mode.
[middlebox compatibility issue](https://github.com/wolfSSL/wolfssl/issues/6774)
### Code changes to support WolfSSL
There are what seem like oddball differences:
- The DoH URL in``$HOME/.curlrc`` can use "1.1.1.1" for OpenSSL but has to be
"one.one.one.one" for WolfSSL. The latter works for both, so OK, we'll change
to that.
- There seems to be some difference in CA databases too - the WolfSSL version
does not like ``defo.ie``, whereas the system and OpenSSL ones do. We can ignore
that for our purposes via ``--insecure``/``-k`` but would need to fix for a
real setup. (Browsers do like those certificates though.)
Then there are some functional code changes:
- tweak to ``configure.ac`` to check if WolfSSL has ECH or not
- added code to ``lib/vtls/wolfssl.c`` mirroring what's done in the
OpenSSL equivalent above.
- WolfSSL does not support ``--ech false`` or the ``--ech pn:`` command line
argument.
The lack of support for ``--ech false`` is because wolfSSL has decided to
always at least GREASE if built to support ECH. In other words, GREASE is
a compile time choice for wolfSSL, but a runtime choice for OpenSSL or
boringssl. (Both are reasonable.)
## Additional notes
### Supporting ECH without DoH
All of the above only applies if DoH is being used. There should be a use-case
for ECH when DoH is not used by curl - if a system stub resolver supports DoT
or DoH, then, considering only ECH and the network threat model, it would make
sense for curl to support ECH without curl itself using DoH. The author for
example uses a combination of stubby+unbound as the system resolver listening
on localhost:53, so would fit this use-case. That said, it is unclear if
this is a niche that is worth trying to address. (The author is just as happy to
let curl use DoH to talk to the same public recursive that stubby might use:-)
Assuming for the moment this is a use-case we'd like to support, then
if DoH is not being used by curl, it is not clear at this time how to provide
support for ECH. One option would seem to be to extend the ``c-ares`` library
to support HTTPS RRs, but in that case it is not now clear whether such changes
would be attractive to the ``c-ares`` maintainers, nor whether the "tag=value"
extensibility inherent in the HTTPS/SVCB specification is a good match for the
``c-ares`` approach of defining structures specific to decoded answers for each
supported RRtype. We're also not sure how many downstream curl deployments
actually make use of the ``c-ares`` library, which would affect the utility of
such changes. Another option might be to consider using some other generic DNS
library that does support HTTPS RRs, but it is unclear if such a library could
or would be used by all or almost all curl builds and downstream releases of
curl.
Our current conclusion is that doing the above is likely best left until we
have some experience with the "using DoH" approach, so we're going to punt on
this for now.
### Debugging
Just a note to self as remembering this is a nuisance:
```bash
LD_LIBRARY_PATH=$HOME/code/openssl:./lib/.libs gdb ./src/.libs/curl
```
### Localhost testing
It can be useful to be able to run against a localhost OpenSSL ``s_server``
for testing. We have published instructions for such
[localhost tests](https://github.com/defo-project/ech-dev-utils/blob/main/howtos/localhost-tests.md)
in another repository. Once you have that set up, you can start a server
and then run curl against that:
```bash
cd $HOME/code/ech-dev-utils
./scripts/echsvr.sh -d
...
```
The ``echsvr.sh`` script supports many ECH-related options. Use ``echsvr.sh -h``
for details.
In another window:
```bash
cd $HOME/code/curl/
./src/curl -vvv --insecure --connect-to foo.example.com:8443:localhost:8443 --ech ecl:AD7+DQA6uwAgACBix2B78sX+EQhEbxMspDOc8Z3xVS5aQpYP0Cxpc2AWPAAEAAEAAQALZXhhbXBsZS5jb20AAA==
```
### Automated use of ``retry_configs`` not supported so far...
As of now we have not added support for using ``retry_config`` handling in the
application - for a command line tool, one can just use ``dig`` (or ``kdig``)
to get the HTTPS RR and pass the ECHConfigList from that on the command line,
if needed, or one can access the value from command line output in verbose more
and then re-use that in another invocation.
Both our OpenSSL fork and boringssl have APIs for both controlling GREASE and
accessing and logging ``retry_configs``, it seems WolfSSL has neither.

View File

@ -28,3 +28,4 @@ Experimental support in curl means:
- HTTP/3 support (using the quiche or msh3 backends)
- The rustls backend
- WebSocket
- Use of the HTTPS resource record and Encrypted Client Hello (ECH) when using DoH

View File

@ -90,6 +90,7 @@ DPAGES = \
doh-insecure.md \
doh-url.md \
dump-header.md \
ech.md \
egd-file.md \
engine.md \
etag-compare.md \

54
docs/cmdline-opts/ech.md Normal file
View File

@ -0,0 +1,54 @@
---
c: Copyright (C) Daniel Stenberg, <daniel@haxx.se>, et al.
SPDX-License-Identifier: curl
Long: ech
Arg: <config>
Help: Configure Encrypted Client Hello (ECH) for use with the TLS session
Added: 8.8.0
Category: tls ECH
Protocols: HTTPS
Multi: single
See-also:
- doh-url
Example:
- --ech true $URL
---
# `--ech`
Specifies how to do ECH (Encrypted Client Hello).
The values allowed for \<config\> can be:
## "false"
Do not attempt ECH
## "grease"
Send a GREASE ECH extension
## "true"
Attempt ECH if possible, but do not fail if ECH is not attempted.
(The connection fails if ECH is attempted but fails.)
## "hard"
Attempt ECH and fail if that is not possible.
ECH only works with TLS 1.3 and also requires using
DoH or providing an ECHConfigList on the command line.
## "ecl:<b64val>"
A base64 encoded ECHConfigList that is used for ECH.
## "pn:<name>"
A name to use to over-ride the `public_name` field of an ECHConfigList
(only available with OpenSSL TLS support)
## Errors
Most errors cause error
*CURLE_ECH_REQUIRED* (101).

View File

@ -1361,6 +1361,12 @@ int main(void)
}
~~~
# ENCRYPTED CLIENT HELLO OPTIONS
## CURLOPT_ECH
Set the configuration for ECH. See CURLOPT_ECH(3)
# AVAILABILITY
Always

View File

@ -487,6 +487,10 @@ An internal call to poll() or select() returned error that is not recoverable.
A value or data field grew larger than allowed.
## CURLE_ECH_REQUIRED (101)"
ECH was attempted but failed.
# CURLMcode
This is the generic return code used by functions in the libcurl multi

View File

@ -0,0 +1,83 @@
---
c: Copyright (C) Daniel Stenberg, <daniel.se>, et al.
SPDX-License-Identifier: curl
Title: CURLOPT_ECH
Section: 3
Source: libcurl
See-also:
- (3)
Protocol:
- TLS
TLS-backend:
- OpenSSL
- wolfSSL
---
# NAME
CURLOPT_ECH - configuration for Encrypted Client Hello
# SYNOPSIS
~~~c
#include <curl/curl.h>
CURLcode curl_easy_setopt(CURL *handle, CURLOPT_ECH, char *config);
~~~
# DESCRIPTION
ECH is only compatible with TLSv1.3.
This experimental feature requires a special build of OpenSSL, as ECH is not
yet supported in OpenSSL releases. In contrast ECH is supported by the latest
BoringSSL and wolfSSL releases. See [ECH.md](../../ECH.md) for details of how
to build such an OpenSSL library.
There is also a known issue with using wolfSSL which does not support ECH
when the HelloRetryRequest mechanism is used.
Pass a string that specifies configuration details for ECH.
In all cases, if ECH is attempted, it may fail for various reasons.
The keywords supported are:
## false
Turns off ECH.
## grease
Instructs client to emit a GREASE ECH extension.
(The connection fails if ECH is attempted but fails.)
## true
Instructs client to attempt ECH, if possible, but to not fail if attempting ECH is not possible.
## hard
Instructs client to attempt ECH and fail if if attempting ECH is not possible.
## ecl:\<base64-value\>
If the string starts with "ecl:" then the remainder of the string should be a base64-encoded
ECHConfigList that is used for ECH rather than attempting to download such a value from
the DNS.
## pn:\<name\>
If the string starts with "pn:" then the remainder of the string should be a DNS/hostname
that is used to over-ride the public_name field of the ECHConfigList that is used
for ECH.
# DEFAULT
NULL, meaning ECH is disabled.
# EXAMPLE
~~~c
CURL *curl = curl_easy_init();
const char *config ="ecl:AED+DQA87wAgACB/RuzUCsW3uBbSFI7mzD63TUXpI8sGDTnFTbFCDpa+CAAEAAEAAQANY292ZXIuZGVmby5pZQAA";
if(curl) {
curl_easy_setopt(curl, CURLOPT_ECH, config);
curl_easy_perform(curl);
}
~~~
# AVAILABILITY
Added in 8.8.0
# RETURN VALUE
Returns CURLE_OK on success or CURLE_OUT_OF_MEMORY if there was insufficient heap space.

View File

@ -166,6 +166,7 @@ man_MANS = \
CURLOPT_DOH_SSL_VERIFYPEER.3 \
CURLOPT_DOH_SSL_VERIFYSTATUS.3 \
CURLOPT_DOH_URL.3 \
CURLOPT_ECH.3 \
CURLOPT_EGDSOCKET.3 \
CURLOPT_ERRORBUFFER.3 \
CURLOPT_EXPECT_100_TIMEOUT_MS.3 \

View File

@ -340,6 +340,7 @@ CURLE_URL_MALFORMAT_USER 7.1 7.17.0
CURLE_USE_SSL_FAILED 7.17.0
CURLE_WEIRD_SERVER_REPLY 7.51.0
CURLE_WRITE_ERROR 7.1
CURLE_ECH_REQUIRED 8.8.0
CURLFILETYPE_DEVICE_BLOCK 7.21.0
CURLFILETYPE_DEVICE_CHAR 7.21.0
CURLFILETYPE_DIRECTORY 7.21.0
@ -617,6 +618,7 @@ CURLOPT_DOH_SSL_VERIFYHOST 7.76.0
CURLOPT_DOH_SSL_VERIFYPEER 7.76.0
CURLOPT_DOH_SSL_VERIFYSTATUS 7.76.0
CURLOPT_DOH_URL 7.62.0
CURLOPT_ECH 8.8.0
CURLOPT_EGDSOCKET 7.7 7.84.0
CURLOPT_ENCODING 7.10 7.21.6
CURLOPT_ERRORBUFFER 7.1

View File

@ -55,6 +55,7 @@
--doh-insecure 7.76.0
--doh-url 7.62.0
--dump-header (-D) 5.7
--ech 8.8.0
--egd-file 7.7
--engine 7.9.3
--etag-compare 7.68.0

View File

@ -632,6 +632,7 @@ typedef enum {
CURLE_SSL_CLIENTCERT, /* 98 - client-side certificate required */
CURLE_UNRECOVERABLE_POLL, /* 99 - poll/select returned fatal error */
CURLE_TOO_LARGE, /* 100 - a value/data met its maximum */
CURLE_ECH_REQUIRED, /* 101 - ECH tried but failed */
CURL_LAST /* never use! */
} CURLcode;
@ -2209,6 +2210,9 @@ typedef enum {
/* millisecond version */
CURLOPT(CURLOPT_SERVER_RESPONSE_TIMEOUT_MS, CURLOPTTYPE_LONG, 324),
/* set ECH configuration */
CURLOPT(CURLOPT_ECH, CURLOPTTYPE_STRINGPOINT, 325),
CURLOPT_LASTENTRY /* the last unused */
} CURLoption;
@ -3161,7 +3165,7 @@ typedef struct curl_version_info_data curl_version_info_data;
#define CURL_VERSION_GSASL (1<<29) /* libgsasl is supported */
#define CURL_VERSION_THREADSAFE (1<<30) /* libcurl API is thread-safe */
/*
/*
* NAME curl_version_info()
*
* DESCRIPTION

View File

@ -275,6 +275,7 @@ CURLWARNING(_curl_easy_getinfo_err_curl_off_t,
(option) == CURLOPT_DNS_LOCAL_IP6 || \
(option) == CURLOPT_DNS_SERVERS || \
(option) == CURLOPT_DOH_URL || \
(option) == CURLOPT_ECH || \
(option) == CURLOPT_EGDSOCKET || \
(option) == CURLOPT_FTP_ACCOUNT || \
(option) == CURLOPT_FTP_ALTERNATIVE_TO_USER || \

View File

@ -805,3 +805,9 @@ ${SIZEOF_TIME_T_CODE}
/* Define to 1 to enable TLS-SRP support. */
#cmakedefine USE_TLS_SRP 1
/* Define to 1 to query for HTTPSRR when using DoH */
#cmakedefine USE_HTTPSRR 1
/* if ECH support is available */
#cmakedefine USE_ECH 1

413
lib/doh.c
View File

@ -42,9 +42,13 @@
#include "curl_printf.h"
#include "curl_memory.h"
#include "memdebug.h"
#include "escape.h"
#define DNS_CLASS_IN 0x01
/* local_print_buf truncates if the hex string will be more than this */
#define LOCAL_PB_HEXMAX 400
#ifndef CURL_DISABLE_VERBOSE_STRINGS
static const char * const errors[]={
"",
@ -187,6 +191,26 @@ doh_write_cb(const void *contents, size_t size, size_t nmemb, void *userp)
return realsize;
}
#if defined(USE_HTTPSRR) && defined(CURLDEBUG)
static void local_print_buf(struct Curl_easy *data,
const char *prefix,
unsigned char *buf, size_t len)
{
unsigned char hexstr[LOCAL_PB_HEXMAX];
size_t hlen = LOCAL_PB_HEXMAX;
bool truncated = false;
if(len > (LOCAL_PB_HEXMAX / 2))
truncated = true;
Curl_hexencode(buf, len, hexstr, hlen);
if(!truncated)
infof(data, "%s: len=%d, val=%s", prefix, (int)len, hexstr);
else
infof(data, "%s: len=%d (truncated)val=%s", prefix, (int)len, hexstr);
return;
}
#endif
/* called from multi.c when this DoH transfer is complete */
static int doh_done(struct Curl_easy *doh, CURLcode result)
{
@ -379,6 +403,12 @@ struct Curl_addrinfo *Curl_doh(struct Curl_easy *data,
int slot;
struct dohdata *dohp;
struct connectdata *conn = data->conn;
#ifdef USE_HTTPSRR
/* for now, this is only used when ECH is enabled */
# ifdef USE_ECH
char *qname = NULL;
# endif
#endif
*waitp = FALSE;
(void)hostname;
(void)port;
@ -418,6 +448,37 @@ struct Curl_addrinfo *Curl_doh(struct Curl_easy *data,
goto error;
dohp->pending++;
}
#endif
#ifdef USE_HTTPSRR
/*
* TODO: Figure out the conditions under which we want to make
* a request for an HTTPS RR when we are not doing ECH. For now,
* making this request breaks a bunch of DoH tests, e.g. test2100,
* where the addiitonal request doesn't match the pre-cooked data
* files, so there's a bit of work attached to making the request
* in a non-ECH use-case. For the present, we'll only make the
* request when ECH is enabled in the build and is being used for
* the curl operation.
*/
# ifdef USE_ECH
if(data->set.tls_ech & CURLECH_ENABLE
|| data->set.tls_ech & CURLECH_HARD) {
if(port == 443)
qname = strdup(hostname);
else
qname = aprintf("_%d._https.%s", port, hostname);
if(!qname)
goto error;
result = dohprobe(data, &dohp->probe[DOH_PROBE_SLOT_HTTPS],
DNS_TYPE_HTTPS, qname, data->set.str[STRING_DOH],
data->multi, dohp->headers);
free(qname);
if(result)
goto error;
dohp->pending++;
}
# endif
#endif
*waitp = TRUE; /* this never returns synchronously */
return NULL;
@ -501,6 +562,25 @@ static DOHcode store_aaaa(const unsigned char *doh,
return DOH_OK;
}
#ifdef USE_HTTPSRR
static DOHcode store_https(const unsigned char *doh,
int index,
struct dohentry *d,
uint16_t len)
{
/* silently ignore RRs over the limit */
if(d->numhttps_rrs < DOH_MAX_HTTPS) {
struct dohhttps_rr *h = &d->https_rrs[d->numhttps_rrs];
h->val = Curl_memdup(&doh[index], len);
if(!h->val)
return DOH_OUT_OF_MEM;
h->len = len;
d->numhttps_rrs++;
}
return DOH_OK;
}
#endif
static DOHcode store_cname(const unsigned char *doh,
size_t dohlen,
unsigned int index,
@ -563,7 +643,8 @@ static DOHcode rdata(const unsigned char *doh,
/* RDATA
- A (TYPE 1): 4 bytes
- AAAA (TYPE 28): 16 bytes
- NS (TYPE 2): N bytes */
- NS (TYPE 2): N bytes
- HTTPS (TYPE 65): N bytes */
DOHcode rc;
switch(type) {
@ -581,6 +662,13 @@ static DOHcode rdata(const unsigned char *doh,
if(rc)
return rc;
break;
#ifdef USE_HTTPSRR
case DNS_TYPE_HTTPS:
rc = store_https(doh, index, d, rdlength);
if(rc)
return rc;
break;
#endif
case DNS_TYPE_CNAME:
rc = store_cname(doh, dohlen, index, d);
if(rc)
@ -737,7 +825,11 @@ UNITTEST DOHcode doh_decode(const unsigned char *doh,
if(index != dohlen)
return DOH_DNS_MALFORMAT; /* something is wrong */
#ifdef USE_HTTTPS
if((type != DNS_TYPE_NS) && !d->numcname && !d->numaddr && !d->numhttps_rrs)
#else
if((type != DNS_TYPE_NS) && !d->numcname && !d->numaddr)
#endif
/* nothing stored! */
return DOH_NO_CONTENT;
@ -776,6 +868,16 @@ static void showdoh(struct Curl_easy *data,
infof(data, "%s", buffer);
}
}
#ifdef USE_HTTPSRR
for(i = 0; i < d->numhttps_rrs; i++) {
# ifdef CURLDEBUG
local_print_buf(data, "DoH HTTPS",
d->https_rrs[i].val, d->https_rrs[i].len);
# else
infof(data, "DoH HTTPS RR: length %d", d->https_rrs[i].len);
# endif
}
#endif
for(i = 0; i < d->numcname; i++) {
infof(data, "CNAME: %s", Curl_dyn_ptr(&d->cname[i]));
}
@ -895,7 +997,18 @@ static CURLcode doh2ai(const struct dohentry *de, const char *hostname,
#ifndef CURL_DISABLE_VERBOSE_STRINGS
static const char *type2name(DNStype dnstype)
{
return (dnstype == DNS_TYPE_A)?"A":"AAAA";
switch(dnstype) {
case DNS_TYPE_A:
return "A";
case DNS_TYPE_AAAA:
return "AAAA";
#ifdef USE_HTTPSRR
case DNS_TYPE_HTTPS:
return "HTTPS";
#endif
default:
return "unknown";
}
}
#endif
@ -905,8 +1018,282 @@ UNITTEST void de_cleanup(struct dohentry *d)
for(i = 0; i < d->numcname; i++) {
Curl_dyn_free(&d->cname[i]);
}
#ifdef USE_HTTPSRR
for(i = 0; i < d->numhttps_rrs; i++)
free(d->https_rrs[i].val);
#endif
}
#ifdef USE_HTTPSRR
/*
* @brief decode the DNS name in a binary RRData
* @param buf points to the buffer (in/out)
* @param remaining points to the remaining buffer length (in/out)
* @param dnsname returns the string form name on success
* @return is 1 for success, error otherwise
*
* The encoding here is defined in
* https://tools.ietf.org/html/rfc1035#section-3.1
*
* The input buffer pointer will be modified so it points to
* just after the end of the DNS name encoding on output. (And
* that's why it's an "unsigned char **" :-)
*/
static CURLcode local_decode_rdata_name(unsigned char **buf, size_t *remaining,
char **dnsname)
{
unsigned char *cp = NULL;
int rem = 0;
char *thename = NULL, *tp = NULL;
unsigned char clen = 0; /* chunk len */
if(!buf || !remaining || !dnsname)
return CURLE_OUT_OF_MEMORY;
rem = (int)*remaining;
thename = calloc(1, CURL_MAXLEN_host_name);
if(!thename)
return CURLE_OUT_OF_MEMORY;
cp = *buf;
tp = thename;
clen = *cp++;
if(clen == 0) {
/* special case - return "." as name */
thename[0] = '.';
thename[1] = 0x00;
}
while(clen) {
if(clen >= rem) {
free(thename);
return CURLE_OUT_OF_MEMORY;
}
if(((tp - thename) + clen) > CURL_MAXLEN_host_name) {
free(thename);
return CURLE_OUT_OF_MEMORY;
}
memcpy(tp, cp, clen);
tp += clen;
*tp++ = '.';
cp += clen;
rem -= (clen + 1);
if(rem <= 0) {
free(thename);
return CURLE_OUT_OF_MEMORY;
}
clen = *cp++;
}
*buf = cp;
if(rem <= 0) {
free(thename);
return CURLE_OUT_OF_MEMORY;
}
*remaining = rem - 1;
*dnsname = thename;
return CURLE_OK;
}
static CURLcode local_decode_rdata_alpn(unsigned char *rrval, size_t len,
char **alpns)
{
/*
* spec here is as per draft-ietf-dnsop-svcb-https, section-7.1.1
* encoding is catenated list of strings each preceded by a one
* octet length
* output is comma-sep list of the strings
* implementations may or may not handle quoting of comma within
* string values, so we might see a comma within the wire format
* version of a string, in which case we'll precede that by a
* backslash - same goes for a backslash character, and of course
* we need to use two backslashes in strings when we mean one;-)
*/
int remaining = (int) len;
char *oval;
size_t olen = 0, i;
unsigned char *cp = rrval;
struct dynbuf dval;
if(!alpns)
return CURLE_OUT_OF_MEMORY;
Curl_dyn_init(&dval, DYN_DOH_RESPONSE);
remaining = (int)len;
cp = rrval;
while(remaining > 0) {
size_t tlen = (size_t) *cp++;
/* if not 1st time, add comma */
if(remaining != (int)len && Curl_dyn_addn(&dval, ",", 1))
goto err;
remaining--;
if(tlen > (size_t)remaining)
goto err;
/* add escape char if needed, clunky but easier to read */
for(i = 0; i != tlen; i++) {
if('\\' == *cp || ',' == *cp) {
if(Curl_dyn_addn(&dval, "\\", 1))
goto err;
}
if(Curl_dyn_addn(&dval, cp++, 1))
goto err;
}
remaining -= (int)tlen;
}
olen = Curl_dyn_len(&dval);
/* I think the + 1 here is ok but it could trigger a read error */
oval = (char *)Curl_memdup(Curl_dyn_ptr(&dval), olen + 1);
if(!oval)
goto err;
Curl_dyn_free(&dval);
oval[olen]='\0';
*alpns = oval;
return CURLE_OK;
err:
Curl_dyn_free(&dval);
return CURLE_BAD_CONTENT_ENCODING;
}
#ifdef CURLDEBUG
static CURLcode test_alpn_escapes(void)
{
/* we'll use an example from draft-ietf-dnsop-svcb, figure 10 */
static unsigned char example[] = {
0x08, /* length 8 */
0x66, 0x5c, 0x6f, 0x6f, 0x2c, 0x62, 0x61, 0x72, /* value "f\\oo,bar" */
0x02, /* length 2 */
0x68, 0x32 /* value "h2" */
};
size_t example_len = sizeof(example);
char *aval = NULL;
static const char *expected = "f\\\\oo\\,bar,h2";
if(local_decode_rdata_alpn(example, example_len, &aval) != CURLE_OK)
return CURLE_BAD_CONTENT_ENCODING;
if(strlen(aval) != strlen(expected))
return CURLE_BAD_CONTENT_ENCODING;
if(memcmp(aval, expected, strlen(aval)))
return CURLE_BAD_CONTENT_ENCODING;
return CURLE_OK;
}
#endif
static CURLcode Curl_doh_decode_httpsrr(unsigned char *rrval, size_t len,
struct Curl_https_rrinfo **hrr)
{
size_t remaining = len;
unsigned char *cp = rrval;
uint16_t pcode = 0, plen = 0;
struct Curl_https_rrinfo *lhrr = NULL;
char *dnsname = NULL;
#ifdef CURLDEBUG
/* a few tests of escaping, shouldn't be here but ok for now */
if(test_alpn_escapes() != CURLE_OK)
return CURLE_OUT_OF_MEMORY;
#endif
lhrr = calloc(1, sizeof(struct Curl_https_rrinfo));
if(!lhrr)
return CURLE_OUT_OF_MEMORY;
lhrr->val = calloc(1, len);
if(!lhrr->val)
goto err;
lhrr->len = len;
memcpy(lhrr->val, rrval, len);
if(remaining <= 2)
goto err;
lhrr->priority = (uint16_t)((cp[0] << 8) + cp[1]);
cp += 2;
remaining -= (uint16_t)2;
if(local_decode_rdata_name(&cp, &remaining, &dnsname) != CURLE_OK)
goto err;
lhrr->target = dnsname;
while(remaining >= 4) {
pcode = (uint16_t)((*cp << 8) + (*(cp + 1)));
cp += 2;
plen = (uint16_t)((*cp << 8) + (*(cp + 1)));
cp += 2;
remaining -= 4;
if(pcode == HTTPS_RR_CODE_ALPN) {
if(local_decode_rdata_alpn(cp, plen, &lhrr->alpns) != CURLE_OK)
goto err;
}
if(pcode == HTTPS_RR_CODE_NO_DEF_ALPN)
lhrr->no_def_alpn = TRUE;
else if(pcode == HTTPS_RR_CODE_IPV4) {
lhrr->ipv4hints = Curl_memdup(cp, plen);
if(!lhrr->ipv4hints)
goto err;
lhrr->ipv4hints_len = (size_t)plen;
}
else if(pcode == HTTPS_RR_CODE_ECH) {
lhrr->echconfiglist = Curl_memdup(cp, plen);
if(!lhrr->echconfiglist)
goto err;
lhrr->echconfiglist_len = (size_t)plen;
}
else if(pcode == HTTPS_RR_CODE_IPV6) {
lhrr->ipv6hints = Curl_memdup(cp, plen);
if(!lhrr->ipv6hints)
goto err;
lhrr->ipv6hints_len = (size_t)plen;
}
if(plen > 0 && plen <= remaining) {
cp += plen;
remaining -= plen;
}
}
DEBUGASSERT(!remaining);
*hrr = lhrr;
return CURLE_OK;
err:
if(lhrr) {
if(lhrr->target)
free(lhrr->target);
if(lhrr->echconfiglist)
free(lhrr->echconfiglist);
if(lhrr->val)
free(lhrr->val);
free(lhrr);
}
return CURLE_OUT_OF_MEMORY;
}
# ifdef CURLDEBUG
static void local_print_httpsrr(struct Curl_easy *data,
struct Curl_https_rrinfo *hrr)
{
DEBUGASSERT(hrr);
infof(data, "HTTPS RR: priority %d, target: %s",
hrr->priority, hrr->target);
if(hrr->alpns)
infof(data, "HTTPS RR: alpns %s", hrr->alpns);
else
infof(data, "HTTPS RR: no alpns");
if(hrr->no_def_alpn)
infof(data, "HTTPS RR: no_def_alpn set");
else
infof(data, "HTTPS RR: no_def_alpn not set");
if(hrr->ipv4hints) {
local_print_buf(data, "HTTPS RR: ipv4hints",
hrr->ipv4hints, hrr->ipv4hints_len);
}
else
infof(data, "HTTPS RR: no ipv4hints");
if(hrr->echconfiglist) {
local_print_buf(data, "HTTPS RR: ECHConfigList",
hrr->echconfiglist, hrr->echconfiglist_len);
}
else
infof(data, "HTTPS RR: no ECHConfigList");
if(hrr->ipv6hints) {
local_print_buf(data, "HTTPS RR: ipv6hint",
hrr->ipv6hints, hrr->ipv6hints_len);
}
else
infof(data, "HTTPS RR: no ipv6hints");
return;
}
# endif
#endif
CURLcode Curl_doh_is_resolved(struct Curl_easy *data,
struct Curl_dns_entry **dnsp)
{
@ -923,9 +1310,15 @@ CURLcode Curl_doh_is_resolved(struct Curl_easy *data,
CURLE_COULDNT_RESOLVE_HOST;
}
else if(!dohp->pending) {
#ifndef USE_HTTPSRR
DOHcode rc[DOH_PROBE_SLOTS] = {
DOH_OK, DOH_OK
};
#else
DOHcode rc[DOH_PROBE_SLOTS] = {
DOH_OK, DOH_OK, DOH_OK
};
#endif
struct dohentry de;
int slot;
/* remove DoH handles from multi handle and close them */
@ -991,6 +1384,22 @@ CURLcode Curl_doh_is_resolved(struct Curl_easy *data,
} /* address processing done */
/* Now process any build-specific attributes retrieved from DNS */
#ifdef USE_HTTPSRR
if(de.numhttps_rrs > 0 && result == CURLE_OK && *dnsp) {
struct Curl_https_rrinfo *hrr = NULL;
result = Curl_doh_decode_httpsrr(de.https_rrs->val, de.https_rrs->len,
&hrr);
if(result) {
infof(data, "Failed to decode HTTPS RR");
return result;
}
infof(data, "Some HTTPS RR to process");
# ifdef CURLDEBUG
local_print_httpsrr(data, hrr);
# endif
(*dnsp)->hinfo = hrr;
}
#endif
/* All done */
de_cleanup(&de);

View File

@ -26,6 +26,9 @@
#include "urldata.h"
#include "curl_addrinfo.h"
#ifdef USE_HTTPSRR
# include <stdint.h>
#endif
#ifndef CURL_DISABLE_DOH
@ -51,7 +54,8 @@ typedef enum {
DNS_TYPE_NS = 2,
DNS_TYPE_CNAME = 5,
DNS_TYPE_AAAA = 28,
DNS_TYPE_DNAME = 39 /* RFC6672 */
DNS_TYPE_DNAME = 39, /* RFC6672 */
DNS_TYPE_HTTPS = 65
} DNStype;
/* one of these for each DoH request */
@ -88,6 +92,7 @@ int Curl_doh_getsock(struct connectdata *conn, curl_socket_t *socks);
#define DOH_MAX_ADDR 24
#define DOH_MAX_CNAME 4
#define DOH_MAX_HTTPS 4
struct dohaddr {
int type;
@ -97,12 +102,44 @@ struct dohaddr {
} ip;
};
#ifdef USE_HTTPSRR
/*
* These are the code points for DNS wire format SvcParams as
* per draft-ietf-dnsop-svcb-https
* Not all are supported now, and even those that are may need
* more work in future to fully support the spec.
*/
#define HTTPS_RR_CODE_ALPN 0x01
#define HTTPS_RR_CODE_NO_DEF_ALPN 0x02
#define HTTPS_RR_CODE_PORT 0x03
#define HTTPS_RR_CODE_IPV4 0x04
#define HTTPS_RR_CODE_ECH 0x05
#define HTTPS_RR_CODE_IPV6 0x06
/*
* These may need escaping when found within an alpn string
* value.
*/
#define COMMA_CHAR ','
#define BACKSLASH_CHAR '\\'
struct dohhttps_rr {
uint16_t len; /* raw encoded length */
unsigned char *val; /* raw encoded octets */
};
#endif
struct dohentry {
struct dynbuf cname[DOH_MAX_CNAME];
struct dohaddr addr[DOH_MAX_ADDR];
int numaddr;
unsigned int ttl;
int numcname;
#ifdef USE_HTTPSRR
struct dohhttps_rr https_rrs[DOH_MAX_HTTPS];
int numhttps_rrs;
#endif
};

View File

@ -86,6 +86,7 @@ struct curl_easyoption Curl_easyopts[] = {
{"DOH_SSL_VERIFYPEER", CURLOPT_DOH_SSL_VERIFYPEER, CURLOT_LONG, 0},
{"DOH_SSL_VERIFYSTATUS", CURLOPT_DOH_SSL_VERIFYSTATUS, CURLOT_LONG, 0},
{"DOH_URL", CURLOPT_DOH_URL, CURLOT_STRING, 0},
{"ECH", CURLOPT_ECH, CURLOT_STRING, 0},
{"EGDSOCKET", CURLOPT_EGDSOCKET, CURLOT_STRING, 0},
{"ENCODING", CURLOPT_ACCEPT_ENCODING, CURLOT_STRING, CURLOT_FLAG_ALIAS},
{"ERRORBUFFER", CURLOPT_ERRORBUFFER, CURLOT_OBJECT, 0},
@ -375,6 +376,6 @@ struct curl_easyoption Curl_easyopts[] = {
*/
int Curl_easyopts_check(void)
{
return ((CURLOPT_LASTENTRY%10000) != (324 + 1));
return ((CURLOPT_LASTENTRY%10000) != (325 + 1));
}
#endif

View File

@ -1070,6 +1070,23 @@ static void freednsentry(void *freethis)
dns->inuse--;
if(dns->inuse == 0) {
Curl_freeaddrinfo(dns->addr);
#ifdef USE_HTTPSRR
if(dns->hinfo) {
if(dns->hinfo->target)
free(dns->hinfo->target);
if(dns->hinfo->alpns)
free(dns->hinfo->alpns);
if(dns->hinfo->ipv4hints)
free(dns->hinfo->ipv4hints);
if(dns->hinfo->echconfiglist)
free(dns->hinfo->echconfiglist);
if(dns->hinfo->ipv6hints)
free(dns->hinfo->ipv6hints);
if(dns->hinfo->val)
free(dns->hinfo->val);
free(dns->hinfo);
}
#endif
free(dns);
}
}

View File

@ -32,6 +32,10 @@
#include <setjmp.h>
#ifdef USE_HTTPSRR
# include <stdint.h>
#endif
/* Allocate enough memory to hold the full name information structs and
* everything. OSF1 is known to require at least 8872 bytes. The buffer
* required for storing all possible aliases and IP numbers is according to
@ -58,8 +62,41 @@ struct connectdata;
*/
struct Curl_hash *Curl_global_host_cache_init(void);
#ifdef USE_HTTPSRR
#define CURL_MAXLEN_host_name 253
struct Curl_https_rrinfo {
size_t len; /* raw encoded length */
unsigned char *val; /* raw encoded octets */
/*
* fields from HTTPS RR, with the mandatory fields
* first (priority, target), then the others in the
* order of the keytag numbers defined at
* https://datatracker.ietf.org/doc/html/rfc9460#section-14.3.2
*/
uint16_t priority;
char *target;
char *alpns; /* keytag = 1 */
bool no_def_alpn; /* keytag = 2 */
/*
* we don't support ports (keytag = 3) as we don't support
* port-switching yet
*/
unsigned char *ipv4hints; /* keytag = 4 */
size_t ipv4hints_len;
unsigned char *echconfiglist; /* keytag = 5 */
size_t echconfiglist_len;
unsigned char *ipv6hints; /* keytag = 6 */
size_t ipv6hints_len;
};
#endif
struct Curl_dns_entry {
struct Curl_addrinfo *addr;
#ifdef USE_HTTPSRR
struct Curl_https_rrinfo *hinfo;
#endif
/* timestamp == 0 -- permanent CURLOPT_RESOLVE entry (doesn't time out) */
time_t timestamp;
/* use-counter, use Curl_resolv_unlock to release reference */

View File

@ -3141,6 +3141,49 @@ CURLcode Curl_vsetopt(struct Curl_easy *data, CURLoption option, va_list param)
data->set.ws_raw_mode = raw;
break;
}
#endif
#ifdef USE_ECH
case CURLOPT_ECH: {
size_t plen = 0;
argptr = va_arg(param, char *);
if(!argptr) {
data->set.tls_ech = CURLECH_DISABLE;
result = CURLE_BAD_FUNCTION_ARGUMENT;
return result;
}
plen = strlen(argptr);
if(plen > CURL_MAX_INPUT_LENGTH) {
data->set.tls_ech = CURLECH_DISABLE;
result = CURLE_BAD_FUNCTION_ARGUMENT;
return result;
}
/* set tls_ech flag value, preserving CLA_CFG bit */
if(plen == 5 && !strcmp(argptr, "false"))
data->set.tls_ech = CURLECH_DISABLE
| (data->set.tls_ech & CURLECH_CLA_CFG);
else if(plen == 6 && !strcmp(argptr, "grease"))
data->set.tls_ech = CURLECH_GREASE
| (data->set.tls_ech & CURLECH_CLA_CFG);
else if(plen == 4 && !strcmp(argptr, "true"))
data->set.tls_ech = CURLECH_ENABLE
| (data->set.tls_ech & CURLECH_CLA_CFG);
else if(plen == 4 && !strcmp(argptr, "hard"))
data->set.tls_ech = CURLECH_HARD
| (data->set.tls_ech & CURLECH_CLA_CFG);
else if(plen > 5 && !strncmp(argptr, "ecl:", 4)) {
result = Curl_setstropt(&data->set.str[STRING_ECH_CONFIG], argptr + 4);
if(result)
return result;
data->set.tls_ech |= CURLECH_CLA_CFG;
}
else if(plen > 4 && !strncmp(argptr, "pn:", 3)) {
result = Curl_setstropt(&data->set.str[STRING_ECH_PUBLIC], argptr + 3);
if(result)
return result;
}
break;
}
#endif
case CURLOPT_QUICK_EXIT:
data->set.quick_exit = (0 != va_arg(param, long)) ? 1L:0L;

View File

@ -322,6 +322,9 @@ curl_easy_strerror(CURLcode error)
case CURLE_TOO_LARGE:
return "A value or data field grew larger than allowed";
case CURLE_ECH_REQUIRED:
return "ECH attempted but failed";
/* error codes not used by current libcurl */
case CURLE_OBSOLETE20:
case CURLE_OBSOLETE24:

View File

@ -55,6 +55,15 @@
struct curl_trc_featt;
#ifdef USE_ECH
/* CURLECH_ bits for the tls_ech option */
# define CURLECH_DISABLE (1<<0)
# define CURLECH_GREASE (1<<1)
# define CURLECH_ENABLE (1<<2)
# define CURLECH_HARD (1<<3)
# define CURLECH_CLA_CFG (1<<4)
#endif
#ifdef USE_WEBSOCKETS
/* CURLPROTO_GOPHERS (29) is the highest publicly used protocol bit number,
* the rest are internal information. If we use higher bits we only do this on
@ -627,6 +636,9 @@ enum doh_slots {
DOH_PROBE_SLOT_IPADDR_V6 = 1, /* 'V6' likewise */
/* Space here for (possibly build-specific) additional slot definitions */
#ifdef USE_HTTPSRR
DOH_PROBE_SLOT_HTTPS = 2, /* for HTTPS RR */
#endif
/* for example */
/* #ifdef WANT_DOH_FOOBAR_TXT */
@ -1532,6 +1544,8 @@ enum dupstring {
#ifndef CURL_DISABLE_PROXY
STRING_HAPROXY_CLIENT_IP, /* CURLOPT_HAPROXY_CLIENT_IP */
#endif
STRING_ECH_CONFIG, /* CURLOPT_ECH_CONFIG */
STRING_ECH_PUBLIC, /* CURLOPT_ECH_PUBLIC */
/* -- end of null-terminated strings -- */
@ -1859,6 +1873,9 @@ struct UserDefined {
#ifdef USE_WEBSOCKETS
BIT(ws_raw_mode);
#endif
#ifdef USE_ECH
int tls_ech; /* TLS ECH configuration */
#endif
};
#ifndef CURL_DISABLE_MIME

View File

@ -82,6 +82,17 @@
#include <openssl/tls1.h>
#include <openssl/evp.h>
#ifdef USE_ECH
# ifndef OPENSSL_IS_BORINGSSL
# include <openssl/ech.h>
# endif
# include "curl_base64.h"
# define ECH_ENABLED(__data__) \
(__data__->set.tls_ech && \
!(__data__->set.tls_ech & CURLECH_DISABLE)\
)
#endif /* USE_ECH */
#if (OPENSSL_VERSION_NUMBER >= 0x0090808fL) && !defined(OPENSSL_NO_OCSP)
#include <openssl/ocsp.h>
#endif
@ -3508,6 +3519,9 @@ CURLcode Curl_ossl_ctx_init(struct ossl_ctx *octx,
const char * const ssl_cert_type = ssl_config->cert_type;
const bool verifypeer = conn_config->verifypeer;
char error_buffer[256];
#ifdef USE_ECH
struct ssl_connect_data *connssl = cf->ctx;
#endif
/* Make funny stuff to get random input */
result = ossl_seed(data);
@ -3843,6 +3857,135 @@ CURLcode Curl_ossl_ctx_init(struct ossl_ctx *octx,
return CURLE_SSL_CONNECT_ERROR;
}
}
#ifdef USE_ECH
if(ECH_ENABLED(data)) {
unsigned char *ech_config = NULL;
size_t ech_config_len = 0;
char *outername = data->set.str[STRING_ECH_PUBLIC];
int trying_ech_now = 0;
if(data->set.tls_ech & CURLECH_GREASE) {
infof(data, "ECH: will GREASE ClientHello");
# ifdef OPENSSL_IS_BORINGSSL
SSL_set_enable_ech_grease(octx->ssl, 1);
# else
SSL_set_options(octx->ssl, SSL_OP_ECH_GREASE);
# endif
}
else if(data->set.tls_ech & CURLECH_CLA_CFG) {
# ifdef OPENSSL_IS_BORINGSSL
/* have to do base64 decode here for boring */
const char *b64 = data->set.str[STRING_ECH_CONFIG];
if(!b64) {
infof(data, "ECH: ECHConfig from command line empty");
return CURLE_SSL_CONNECT_ERROR;
}
ech_config_len = 2 * strlen(b64);
result = Curl_base64_decode(b64, &ech_config, &ech_config_len);
if(result || !ech_config) {
infof(data, "ECH: can't base64 decode ECHConfig from command line");
if(data->set.tls_ech & CURLECH_HARD)
return result;
}
if(SSL_set1_ech_config_list(octx->ssl, ech_config,
ech_config_len) != 1) {
infof(data, "ECH: SSL_ECH_set1_echconfig failed");
if(data->set.tls_ech & CURLECH_HARD) {
free(ech_config);
return CURLE_SSL_CONNECT_ERROR;
}
}
free(ech_config);
trying_ech_now = 1;
# else
ech_config = (unsigned char *) data->set.str[STRING_ECH_CONFIG];
if(!ech_config) {
infof(data, "ECH: ECHConfig from command line empty");
return CURLE_SSL_CONNECT_ERROR;
}
ech_config_len = strlen(data->set.str[STRING_ECH_CONFIG]);
if(SSL_ech_set1_echconfig(octx->ssl, ech_config, ech_config_len) != 1) {
infof(data, "ECH: SSL_ECH_set1_echconfig failed");
if(data->set.tls_ech & CURLECH_HARD)
return CURLE_SSL_CONNECT_ERROR;
}
else
trying_ech_now = 1;
# endif
infof(data, "ECH: ECHConfig from command line");
}
else {
struct Curl_dns_entry *dns = NULL;
dns = Curl_fetch_addr(data, connssl->peer.hostname, connssl->peer.port);
if(!dns) {
infof(data, "ECH: requested but no DNS info available");
if(data->set.tls_ech & CURLECH_HARD)
return CURLE_SSL_CONNECT_ERROR;
}
else {
struct Curl_https_rrinfo *rinfo = NULL;
rinfo = dns->hinfo;
if(rinfo && rinfo->echconfiglist) {
unsigned char *ecl = rinfo->echconfiglist;
size_t elen = rinfo->echconfiglist_len;
infof(data, "ECH: ECHConfig from DoH HTTPS RR");
# ifndef OPENSSL_IS_BORINGSSL
if(SSL_ech_set1_echconfig(octx->ssl, ecl, elen) != 1) {
infof(data, "ECH: SSL_ECH_set1_echconfig failed");
if(data->set.tls_ech & CURLECH_HARD)
return CURLE_SSL_CONNECT_ERROR;
}
# else
if(SSL_set1_ech_config_list(octx->ssl, ecl, elen) != 1) {
infof(data, "ECH: SSL_set1_ech_config_list failed (boring)");
if(data->set.tls_ech & CURLECH_HARD)
return CURLE_SSL_CONNECT_ERROR;
}
# endif
else {
trying_ech_now = 1;
infof(data, "ECH: imported ECHConfigList of length %ld", elen);
}
}
else {
infof(data, "ECH: requested but no ECHConfig available");
if(data->set.tls_ech & CURLECH_HARD)
return CURLE_SSL_CONNECT_ERROR;
}
Curl_resolv_unlock(data, dns);
}
}
# ifdef OPENSSL_IS_BORINGSSL
if(trying_ech_now && outername) {
infof(data, "ECH: setting public_name not supported with boringssl");
return CURLE_SSL_CONNECT_ERROR;
}
# else
if(trying_ech_now && outername) {
infof(data, "ECH: inner: '%s', outer: '%s'",
connssl->peer.hostname, outername);
result = SSL_ech_set_server_names(octx->ssl,
connssl->peer.hostname, outername,
0 /* do send outer */);
if(result != 1) {
infof(data, "ECH: rv failed to set server name(s) %d [ERROR]", result);
return CURLE_SSL_CONNECT_ERROR;
}
}
# endif /* not BORING */
if(trying_ech_now
&& SSL_set_min_proto_version(octx->ssl, TLS1_3_VERSION) != 1) {
infof(data, "ECH: Can't force TLSv1.3 [ERROR]");
return CURLE_SSL_CONNECT_ERROR;
}
}
#endif /* USE_ECH */
#endif
octx->reused_session = FALSE;
@ -3926,6 +4069,70 @@ static CURLcode ossl_connect_step1(struct Curl_cfilter *cf,
return CURLE_OK;
}
#ifdef USE_ECH
/* If we have retry configs, then trace those out */
static void ossl_trace_ech_retry_configs(struct Curl_easy *data, SSL* ssl,
int reason)
{
CURLcode result = CURLE_OK;
size_t rcl = 0;
int rv = 1;
# ifndef OPENSSL_IS_BORINGSSL
char *inner = NULL;
unsigned char *rcs = NULL;
char *outer = NULL;
# else
const char *inner = NULL;
const uint8_t *rcs = NULL;
const char *outer = NULL;
size_t out_name_len = 0;
int servername_type = 0;
# endif
/* nothing to trace if not doing ECH */
if(!ECH_ENABLED(data))
return;
# ifndef OPENSSL_IS_BORINGSSL
rv = SSL_ech_get_retry_config(ssl, &rcs, &rcl);
# else
SSL_get0_ech_retry_configs(ssl, &rcs, &rcl);
rv = (int)rcl;
# endif
if(rv && rcs) {
# define HEXSTR_MAX 800
char *b64str = NULL;
size_t blen = 0;
result = Curl_base64_encode((const char *)rcs, rcl,
&b64str, &blen);
if(!result && b64str)
infof(data, "ECH: retry_configs %s", b64str);
free(b64str);
# ifndef OPENSSL_IS_BORINGSSL
rv = SSL_ech_get_status(ssl, &inner, &outer);
infof(data, "ECH: retry_configs for %s from %s, %d %d",
inner ? inner : "NULL", outer ? outer : "NULL", reason, rv);
#else
rv = SSL_ech_accepted(ssl);
servername_type = SSL_get_servername_type(ssl);
inner = SSL_get_servername(ssl, servername_type);
SSL_get0_ech_name_override(ssl, &outer, &out_name_len);
/* TODO: get the inner from boring */
infof(data, "ECH: retry_configs for %s from %s, %d %d",
inner ? inner : "NULL", outer ? outer : "NULL", reason, rv);
#endif
}
else
infof(data, "ECH: no retry_configs (rv = %d)", rv);
# ifndef OPENSSL_IS_BORINGSSL
OPENSSL_free((void *)rcs);
# endif
return;
}
#endif
static CURLcode ossl_connect_step2(struct Curl_cfilter *cf,
struct Curl_easy *data)
{
@ -4038,6 +4245,21 @@ static CURLcode ossl_connect_step2(struct Curl_cfilter *cf,
result = CURLE_SSL_CLIENTCERT;
ossl_strerror(errdetail, error_buffer, sizeof(error_buffer));
}
#endif
#ifdef USE_ECH
else if((lib == ERR_LIB_SSL) &&
# ifndef OPENSSL_IS_BORINGSSL
(reason == SSL_R_ECH_REQUIRED)) {
# else
(reason == SSL_R_ECH_REJECTED)) {
# endif
/* trace retry_configs if we got some */
ossl_trace_ech_retry_configs(data, octx->ssl, reason);
result = CURLE_ECH_REQUIRED;
ossl_strerror(errdetail, error_buffer, sizeof(error_buffer));
}
#endif
else {
result = CURLE_SSL_CONNECT_ERROR;
@ -4092,6 +4314,68 @@ static CURLcode ossl_connect_step2(struct Curl_cfilter *cf,
negotiated_group_name? negotiated_group_name : "[blank]",
OBJ_nid2sn(psigtype_nid));
#ifdef USE_ECH
# ifndef OPENSSL_IS_BORINGSSL
if(ECH_ENABLED(data)) {
char *inner = NULL, *outer = NULL;
const char *status = NULL;
int rv;
rv = SSL_ech_get_status(octx->ssl, &inner, &outer);
switch(rv) {
case SSL_ECH_STATUS_SUCCESS:
status = "succeeded";
break;
case SSL_ECH_STATUS_GREASE_ECH:
status = "sent GREASE, got retry-configs";
break;
case SSL_ECH_STATUS_GREASE:
status = "sent GREASE";
break;
case SSL_ECH_STATUS_NOT_TRIED:
status = "not attempted";
break;
case SSL_ECH_STATUS_NOT_CONFIGURED:
status = "not configured";
break;
case SSL_ECH_STATUS_BACKEND:
status = "backend (unexpected)";
break;
case SSL_ECH_STATUS_FAILED:
status = "failed";
break;
case SSL_ECH_STATUS_BAD_CALL:
status = "bad call (unexpected)";
break;
case SSL_ECH_STATUS_BAD_NAME:
status = "bad name (unexpected)";
break;
default:
status = "unexpected status";
infof(data, "ECH: unexpected status %d",rv);
}
infof(data, "ECH: result: status is %s, inner is %s, outer is %s",
(status?status:"NULL"),
(inner?inner:"NULL"),
(outer?outer:"NULL"));
OPENSSL_free(inner);
OPENSSL_free(outer);
if(rv == SSL_ECH_STATUS_GREASE_ECH) {
/* trace retry_configs if we got some */
ossl_trace_ech_retry_configs(data, octx->ssl, 0);
}
if(rv != SSL_ECH_STATUS_SUCCESS
&& data->set.tls_ech & CURLECH_HARD) {
infof(data, "ECH: ech-hard failed");
return CURLE_SSL_CONNECT_ERROR;
}
}
else {
infof(data, "ECH: result: status is not attempted");
}
# endif /* BORING */
#endif /* USE_ECH */
#ifdef HAS_ALPN
/* Sets data and len to negotiated protocol, len is 0 if no protocol was
* negotiated

View File

@ -74,6 +74,14 @@
#include "curl_memory.h"
#include "memdebug.h"
#ifdef USE_ECH
# include "curl_base64.h"
# define ECH_ENABLED(__data__) \
(__data__->set.tls_ech && \
!(__data__->set.tls_ech & CURLECH_DISABLE)\
)
#endif /* USE_ECH */
/* KEEP_PEER_CERT is a product of the presence of build time symbol
OPENSSL_EXTRA without NO_CERTS, depending on the version. KEEP_PEER_CERT is
in wolfSSL's settings.h, and the latter two are build time symbols in
@ -725,6 +733,82 @@ wolfssl_connect_step1(struct Curl_cfilter *cf, struct Curl_easy *data)
Curl_ssl_sessionid_unlock(data);
}
#ifdef USE_ECH
if(ECH_ENABLED(data)) {
int trying_ech_now = 0;
if(data->set.str[STRING_ECH_PUBLIC]) {
infof(data, "ECH: outername not (yet) supported with WolfSSL");
return CURLE_SSL_CONNECT_ERROR;
}
if(data->set.tls_ech == CURLECH_GREASE) {
infof(data, "ECH: GREASE'd ECH not yet supported for wolfSSL");
return CURLE_SSL_CONNECT_ERROR;
}
if(data->set.tls_ech & CURLECH_CLA_CFG
&& data->set.str[STRING_ECH_CONFIG]) {
char *b64val = data->set.str[STRING_ECH_CONFIG];
word32 b64len = 0;
b64len = (word32) strlen(b64val);
if(b64len
&& wolfSSL_SetEchConfigsBase64(backend->handle, b64val, b64len)
!= WOLFSSL_SUCCESS) {
if(data->set.tls_ech & CURLECH_HARD)
return CURLE_SSL_CONNECT_ERROR;
}
else {
trying_ech_now = 1;
infof(data, "ECH: ECHConfig from command line");
}
}
else {
struct Curl_dns_entry *dns = NULL;
dns = Curl_fetch_addr(data, connssl->peer.hostname, connssl->peer.port);
if(!dns) {
infof(data, "ECH: requested but no DNS info available");
if(data->set.tls_ech & CURLECH_HARD)
return CURLE_SSL_CONNECT_ERROR;
}
else {
struct Curl_https_rrinfo *rinfo = NULL;
rinfo = dns->hinfo;
if(rinfo && rinfo->echconfiglist) {
unsigned char *ecl = rinfo->echconfiglist;
size_t elen = rinfo->echconfiglist_len;
infof(data, "ECH: ECHConfig from DoH HTTPS RR");
if(wolfSSL_SetEchConfigs(backend->handle, ecl, (word32) elen) !=
WOLFSSL_SUCCESS) {
infof(data, "ECH: wolfSSL_SetEchConfigs failed");
if(data->set.tls_ech & CURLECH_HARD)
return CURLE_SSL_CONNECT_ERROR;
}
else {
trying_ech_now = 1;
infof(data, "ECH: imported ECHConfigList of length %ld", elen);
}
}
else {
infof(data, "ECH: requested but no ECHConfig available");
if(data->set.tls_ech & CURLECH_HARD)
return CURLE_SSL_CONNECT_ERROR;
}
Curl_resolv_unlock(data, dns);
}
}
if(trying_ech_now
&& SSL_set_min_proto_version(backend->handle, TLS1_3_VERSION) != 1) {
infof(data, "ECH: Can't force TLSv1.3 [ERROR]");
return CURLE_SSL_CONNECT_ERROR;
}
}
#endif /* USE_ECH */
#ifdef USE_BIO_CHAIN
{
WOLFSSL_BIO *bio;
@ -858,6 +942,31 @@ wolfssl_connect_step2(struct Curl_cfilter *cf, struct Curl_easy *data)
"continuing anyway");
}
}
#endif
#ifdef USE_ECH
else if(-1 == detail) {
/* try access a retry_config ECHConfigList for tracing */
byte echConfigs[1000];
word32 echConfigsLen = 1000;
int rv = 0;
/* this currently doesn't produce the retry_configs */
rv = wolfSSL_GetEchConfigs(backend->handle, echConfigs,
&echConfigsLen);
if(rv != WOLFSSL_SUCCESS) {
infof(data, "Failed to get ECHConfigs");
}
else {
char *b64str = NULL;
size_t blen = 0;
rv = Curl_base64_encode((const char *)echConfigs, echConfigsLen,
&b64str, &blen);
if(!rv && b64str)
infof(data, "ECH: (not yet) retry_configs %s", b64str);
free(b64str);
}
}
#endif
else if(backend->io_result == CURLE_AGAIN) {
return CURLE_OK;

View File

@ -568,6 +568,105 @@ AC_DEFUN([CURL_CHECK_LIB_ARES], [
fi
])
dnl CURL_CHECK_OPTION_NTLM_WB
dnl -------------------------------------------------
dnl Verify if configure has been invoked with option
dnl --enable-ntlm-wb or --disable-ntlm-wb, and set
dnl shell variable want_ntlm_wb and want_ntlm_wb_file
dnl as appropriate.
AC_DEFUN([CURL_CHECK_OPTION_NTLM_WB], [
AC_BEFORE([$0],[CURL_CHECK_NTLM_WB])dnl
OPT_NTLM_WB="default"
AC_ARG_ENABLE(ntlm-wb,
AS_HELP_STRING([--enable-ntlm-wb@<:@=FILE@:>@],[Enable NTLM delegation to winbind's ntlm_auth helper, where FILE is ntlm_auth's absolute filename (default: /usr/bin/ntlm_auth)])
AS_HELP_STRING([--disable-ntlm-wb],[Disable NTLM delegation to winbind's ntlm_auth helper]),
OPT_NTLM_WB=$enableval)
want_ntlm_wb_file="/usr/bin/ntlm_auth"
case "$OPT_NTLM_WB" in
no)
dnl --disable-ntlm-wb option used
want_ntlm_wb="no"
;;
default)
dnl configure option not specified
want_ntlm_wb="yes"
;;
*)
dnl --enable-ntlm-wb option used
want_ntlm_wb="yes"
if test -n "$enableval" && test "$enableval" != "yes"; then
want_ntlm_wb_file="$enableval"
fi
;;
esac
])
dnl CURL_CHECK_NTLM_WB
dnl -------------------------------------------------
dnl Check if support for NTLM delegation to winbind's
dnl ntlm_auth helper will finally be enabled depending
dnl on given configure options and target platform.
AC_DEFUN([CURL_CHECK_NTLM_WB], [
AC_REQUIRE([CURL_CHECK_OPTION_NTLM_WB])dnl
AC_REQUIRE([CURL_CHECK_NATIVE_WINDOWS])dnl
AC_MSG_CHECKING([whether to enable NTLM delegation to winbind's helper])
if test "$curl_cv_native_windows" = "yes" ||
test "x$SSL_ENABLED" = "x"; then
want_ntlm_wb_file=""
want_ntlm_wb="no"
elif test "x$ac_cv_func_fork" != "xyes"; then
dnl ntlm_wb requires fork
want_ntlm_wb="no"
fi
AC_MSG_RESULT([$want_ntlm_wb])
if test "$want_ntlm_wb" = "yes"; then
AC_DEFINE(NTLM_WB_ENABLED, 1,
[Define to enable NTLM delegation to winbind's ntlm_auth helper.])
AC_DEFINE_UNQUOTED(NTLM_WB_FILE, "$want_ntlm_wb_file",
[Define absolute filename for winbind's ntlm_auth helper.])
NTLM_WB_ENABLED=1
fi
])
dnl CURL_CHECK_OPTION_HTTPSRR
dnl -----------------------------------------------------
dnl Verify whether configure has been invoked with option
dnl --enable-httpsrr or --disable-httpsrr, and set
dnl shell variable want_httpsrr as appropriate.
AC_DEFUN([CURL_CHECK_OPTION_HTTPSRR], [
AC_MSG_CHECKING([whether to enable HTTPSRR support])
OPT_HTTPSRR="default"
AC_ARG_ENABLE(httpsrr,
AS_HELP_STRING([--enable-httpsrr],[Enable HTTPSRR support])
AS_HELP_STRING([--disable-httpsrr],[Disable HTTPSRR support]),
OPT_HTTPSRR=$enableval)
case "$OPT_HTTPSRR" in
no)
dnl --disable-httpsrr option used
want_httpsrr="no"
curl_httpsrr_msg="no (--enable-httpsrr)"
AC_MSG_RESULT([no])
;;
default)
dnl configure option not specified
want_httpsrr="no"
curl_httpsrr_msg="no (--enable-httpsrr)"
AC_MSG_RESULT([no])
;;
*)
dnl --enable-httpsrr option used
want_httpsrr="yes"
curl_httpsrr_msg="enabled (--disable-httpsrr)"
experimental="httpsrr"
AC_MSG_RESULT([yes])
;;
esac
])
dnl CURL_CHECK_OPTION_ECH
dnl -----------------------------------------------------
dnl Verify whether configure has been invoked with option
@ -603,3 +702,4 @@ AS_HELP_STRING([--disable-ech],[Disable ECH support]),
;;
esac
])
])

View File

@ -1097,6 +1097,9 @@ curl_easy_setopt_ccsid(CURL *easy, CURLoption tag, ...)
case CURLOPT_DNS_LOCAL_IP6:
case CURLOPT_DNS_SERVERS:
case CURLOPT_DOH_URL:
#ifdef USE_ECH
case CURLOPT_ECH:
#endif
case CURLOPT_EGDSOCKET:
case CURLOPT_FTPPORT:
case CURLOPT_FTP_ACCOUNT:

View File

@ -176,6 +176,14 @@ static void free_config_fields(struct OperationConfig *config)
Curl_safefree(config->aws_sigv4);
Curl_safefree(config->proto_str);
Curl_safefree(config->proto_redir_str);
#ifdef USE_ECH
Curl_safefree(config->ech);
config->ech = NULL;
Curl_safefree(config->ech_config);
config->ech_config = NULL;
Curl_safefree(config->ech_public);
config->ech_public = NULL;
#endif
}
void config_free(struct OperationConfig *config)

View File

@ -298,6 +298,12 @@ struct OperationConfig {
struct State state; /* for create_transfer() */
bool rm_partial; /* on error, remove partially written output
files */
#ifdef USE_ECH
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 */
#endif
};
struct GlobalConfig {

View File

@ -123,6 +123,7 @@ typedef enum {
C_DOH_INSECURE,
C_DOH_URL,
C_DUMP_HEADER,
C_ECH,
C_EGD_FILE,
C_ENGINE,
C_EPRT,
@ -404,6 +405,7 @@ static const struct LongShort aliases[]= {
{"doh-insecure", ARG_BOOL, ' ', C_DOH_INSECURE},
{"doh-url" , ARG_STRG, ' ', C_DOH_URL},
{"dump-header", ARG_FILE, 'D', C_DUMP_HEADER},
{"ech", ARG_STRG, ' ', C_ECH},
{"egd-file", ARG_STRG, ' ', C_EGD_FILE},
{"engine", ARG_STRG, ' ', C_ENGINE},
{"eprt", ARG_BOOL, ' ', C_EPRT},
@ -2079,6 +2081,57 @@ ParameterError getparameter(const char *flag, /* f or -long-flag */
err = PARAM_ENGINES_REQUESTED;
}
break;
#ifndef USE_ECH
case C_ECH: /* --ech, not implemented by default */
err = PARAM_LIBCURL_DOESNT_SUPPORT;
break;
#else
case C_ECH: /* --ech */
if(strlen(nextarg) > 4 && strncasecompare("pn:", nextarg, 3)) {
/* a public_name */
err = getstr(&config->ech_public, nextarg, DENY_BLANK);
}
else if(strlen(nextarg) > 5 && strncasecompare("ecl:", nextarg, 4)) {
/* an ECHConfigList */
if('@' != *(nextarg + 4)) {
err = getstr(&config->ech_config, nextarg, DENY_BLANK);
}
else {
/* Indirect case: @filename or @- for stdin */
char *tmpcfg = NULL;
FILE *file;
nextarg++; /* skip over '@' */
if(!strcmp("-", nextarg)) {
file = stdin;
}
else {
file = fopen(nextarg, FOPEN_READTEXT);
}
if(!file) {
warnf(global,
"Couldn't read file \"%s\" "
"specified for \"--ech ecl:\" option",
nextarg);
return PARAM_BAD_USE; /* */
}
err = file2string(&tmpcfg, file);
if(file != stdin)
fclose(file);
if(err)
return err;
config->ech_config = aprintf("ecl:%s",tmpcfg);
if(!config->ech_config)
return PARAM_NO_MEM;
free(tmpcfg);
} /* file done */
}
else {
/* Simple case: just a string, with a keyword */
err = getstr(&config->ech, nextarg, DENY_BLANK);
}
break;
#endif
case C_CAPATH: /* --capath */
err = getstr(&config->capath, nextarg, DENY_BLANK);
break;

View File

@ -67,6 +67,7 @@ static const struct category_descriptors categories[] = {
{"telnet", "TELNET protocol options", CURLHELP_TELNET},
{"tftp", "TFTP protocol options", CURLHELP_TFTP},
{"tls", "All TLS/SSL related options", CURLHELP_TLS},
{"ech", "All Encrypted Client Hello (ECH) options", CURLHELP_ECH},
{"upload", "All options for uploads",
CURLHELP_UPLOAD},
{"verbose", "Options related to any kind of command line output of curl",

View File

@ -68,6 +68,7 @@ struct helptxt {
#define CURLHELP_TLS 1u << 22u
#define CURLHELP_UPLOAD 1u << 23u
#define CURLHELP_VERBOSE 1u << 24u
#define CURLHELP_ECH 1u << 25u
extern const struct helptxt helptext[];

View File

@ -168,6 +168,9 @@ const struct helptxt helptext[] = {
{"-D, --dump-header <filename>",
"Write the received headers to <filename>",
CURLHELP_HTTP | CURLHELP_FTP},
{" --ech <config>",
"Configure Encrypted Client Hello (ECH) for use with the TLS session",
CURLHELP_TLS | CURLHELP_ECH},
{" --egd-file <file>",
"EGD socket path for random data",
CURLHELP_TLS},

View File

@ -2187,6 +2187,16 @@ static CURLcode single_transfer(struct GlobalConfig *global,
if(config->hsts)
my_setopt_str(curl, CURLOPT_HSTS, config->hsts);
#ifdef USE_ECH
/* only if enabled in configure */
if(config->ech) /* only if set (optional) */
my_setopt_str(curl, CURLOPT_ECH, config->ech);
if(config->ech_public) /* only if set (optional) */
my_setopt_str(curl, CURLOPT_ECH, config->ech_public);
if(config->ech_config) /* only if set (optional) */
my_setopt_str(curl, CURLOPT_ECH, config->ech_config);
#endif
/* initialize retry vars for loop below */
per->retry_sleep_default = (config->retry_delay) ?
config->retry_delay*1000L : RETRY_SLEEP_DEFAULT; /* ms */

View File

@ -54,6 +54,7 @@ Invalid category provided, here is a list of all categories:
telnet TELNET protocol options
tftp TFTP protocol options
tls All TLS/SSL related options
ech All Encrypted Client Hello (ECH) options
upload All options for uploads
verbose Options related to any kind of command line output of curl
</stdout>

View File

@ -133,7 +133,8 @@ e97: proxy handshake error
e98: SSL Client Certificate required
e99: Unrecoverable error in select/poll
e100: A value or data field grew larger than allowed
e101: Unknown error
e101: ECH attempted but failed
e102: Unknown error
m-1: Please call curl_multi_perform() soon
m0: No error
m1: Invalid multi handle

99
tests/ech_combos.py Executable file
View File

@ -0,0 +1,99 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#***************************************************************************
# _ _ ____ _
# Project ___| | | | _ \| |
# / __| | | | |_) | |
# | (__| |_| | _ <| |___
# \___|\___/|_| \_\_____|
#
# Copyright (C) Daniel Stenberg, <daniel@haxx.se>, et al.
#
# This software is licensed as described in the file COPYING, which
# you should have received as part of this distribution. The terms
# are also available at https://curl.se/docs/copyright.html.
#
# You may opt to use, copy, modify, merge, publish, distribute and/or sell
# copies of the Software, and permit persons to whom the Software is
# furnished to do so, under the terms of the COPYING file.
#
# This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY
# KIND, either express or implied.
#
# SPDX-License-Identifier: curl
#
###########################################################################
#
# Python3 program to print all combination of size r in an array of size n.
# This is used to generate test lines in tests/ech_test.sh.
# This will be discarded in the process of moving from experimental,
# but is worth preserving for the moment in case of changes to the
# ECH command line args
def CombinationRepetitionUtil(chosen, arr, badarr, index,
r, start, end):
# Current combination is ready,
# print it
if index == r:
# figure out if result should be good or bad and
# print prefix, assuming $turl does support ECH so
# should work if given "positive" parameters
res = 1
j = len(chosen) - 1
while res and j >= 0:
if chosen[j] in badarr:
res = 0
j = j - 1
print("cli_test $turl 1", res, end = " ")
# print combination but eliminating any runs of
# two identical params
for j in range(r):
if j != 0 and chosen[j] != chosen[j-1]:
print(chosen[j], end = " ")
print()
return
# When no more elements are
# there to put in chosen[]
if start > n:
return
# Current is included, put
# next at next location
chosen[index] = arr[start]
# Current is excluded, replace it
# with next (Note that i+1 is passed,
# but index is not changed)
CombinationRepetitionUtil(chosen, arr, badarr, index + 1,
r, start, end)
CombinationRepetitionUtil(chosen, arr, badarr, index,
r, start + 1, end)
# The main function that prints all
# combinations of size r in arr[] of
# size n. This function mainly uses
# CombinationRepetitionUtil()
def CombinationRepetition(arr, badarr, n, r):
# A temporary array to store
# all combination one by one
chosen = [0] * r
# Print all combination using
# temporary array 'chosen[]'
CombinationRepetitionUtil(chosen, arr, badarr, 0, r, 0, n)
# Driver code
badarr = [ '--ech grease', '--ech false', '--ech ecl:$badecl', '--ech pn:$badpn' ]
goodarr = [ '--ech hard', '--ech true', '--ech ecl:$goodecl', '--ech pn:$goodpn' ]
arr = badarr + goodarr
r = 8
n = len(arr) - 1
CombinationRepetition(arr, badarr, n, r)
# This code is contributed by Vaibhav Kumar 12.

1151
tests/ech_tests.sh Executable file

File diff suppressed because it is too large Load Diff