From a362962b7289ec02b412890c9515657cf0ed50ac Mon Sep 17 00:00:00 2001 From: Stephen Farrell Date: Thu, 4 Apr 2024 14:23:35 +0100 Subject: [PATCH] TLS: add support for ECH (Encrypted Client Hello) An EXPERIMENTAL feature used with CURLOPT_ECH and --ech. Closes #11922 --- .github/scripts/spellcheck.words | 23 + .gitignore | 1 + CMakeLists.txt | 28 + configure.ac | 37 +- docs/ECH.md | 479 +++++++++++++ docs/EXPERIMENTAL.md | 1 + docs/cmdline-opts/Makefile.inc | 1 + docs/cmdline-opts/ech.md | 54 ++ docs/libcurl/curl_easy_setopt.md | 6 + docs/libcurl/libcurl-errors.md | 4 + docs/libcurl/opts/CURLOPT_ECH.md | 83 +++ docs/libcurl/opts/Makefile.inc | 1 + docs/libcurl/symbols-in-versions | 2 + docs/options-in-versions | 1 + include/curl/curl.h | 6 +- include/curl/typecheck-gcc.h | 1 + lib/curl_config.h.cmake | 6 + lib/doh.c | 413 ++++++++++- lib/doh.h | 39 +- lib/easyoptions.c | 3 +- lib/hostip.c | 17 + lib/hostip.h | 37 + lib/setopt.c | 43 ++ lib/strerror.c | 3 + lib/urldata.h | 17 + lib/vtls/openssl.c | 284 ++++++++ lib/vtls/wolfssl.c | 109 +++ m4/curl-confopts.m4 | 100 +++ packages/OS400/ccsidcurl.c | 3 + src/tool_cfgable.c | 8 + src/tool_cfgable.h | 6 + src/tool_getparam.c | 53 ++ src/tool_help.c | 1 + src/tool_help.h | 1 + src/tool_listhelp.c | 3 + src/tool_operate.c | 10 + tests/data/test1462 | 1 + tests/data/test1538 | 3 +- tests/ech_combos.py | 99 +++ tests/ech_tests.sh | 1151 ++++++++++++++++++++++++++++++ 40 files changed, 3122 insertions(+), 16 deletions(-) create mode 100644 docs/ECH.md create mode 100644 docs/cmdline-opts/ech.md create mode 100644 docs/libcurl/opts/CURLOPT_ECH.md create mode 100755 tests/ech_combos.py create mode 100755 tests/ech_tests.sh diff --git a/.github/scripts/spellcheck.words b/.github/scripts/spellcheck.words index 050513c76f..5e534846c8 100644 --- a/.github/scripts/spellcheck.words +++ b/.github/scripts/spellcheck.words @@ -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 diff --git a/.gitignore b/.gitignore index 2d5c292325..6d1e69ed04 100644 --- a/.gitignore +++ b/.gitignore @@ -65,3 +65,4 @@ curl_fuzzer_seed_corpus.zip libstandaloneengine.a tests/string tests/config +tests/ech-log/ diff --git a/CMakeLists.txt b/CMakeLists.txt index 4607c51d13..d5c778bb04 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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) diff --git a/configure.ac b/configure.ac index c31870ded5..21a3debbdc 100644 --- a/configure.ac +++ b/configure.ac @@ -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 diff --git a/docs/ECH.md b/docs/ECH.md new file mode 100644 index 0000000000..cfb897d4e5 --- /dev/null +++ b/docs/ECH.md @@ -0,0 +1,479 @@ + + +# 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 good
+ ... +``` + +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 `` - 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:`` a base64 encoded ECHConfigList, rather than one accessed from the DNS + - ``pn:`` 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 good
+ ... +``` + +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 good
+ ... +``` + +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. + diff --git a/docs/EXPERIMENTAL.md b/docs/EXPERIMENTAL.md index 4e781ecba7..09a0d166ec 100644 --- a/docs/EXPERIMENTAL.md +++ b/docs/EXPERIMENTAL.md @@ -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 diff --git a/docs/cmdline-opts/Makefile.inc b/docs/cmdline-opts/Makefile.inc index 428cc3bab2..deb4c7c326 100644 --- a/docs/cmdline-opts/Makefile.inc +++ b/docs/cmdline-opts/Makefile.inc @@ -90,6 +90,7 @@ DPAGES = \ doh-insecure.md \ doh-url.md \ dump-header.md \ + ech.md \ egd-file.md \ engine.md \ etag-compare.md \ diff --git a/docs/cmdline-opts/ech.md b/docs/cmdline-opts/ech.md new file mode 100644 index 0000000000..f8a9a0dbd2 --- /dev/null +++ b/docs/cmdline-opts/ech.md @@ -0,0 +1,54 @@ +--- +c: Copyright (C) Daniel Stenberg, , et al. +SPDX-License-Identifier: curl +Long: ech +Arg: +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 \ 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:" + +A base64 encoded ECHConfigList that is used for ECH. + +## "pn:" + +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). + diff --git a/docs/libcurl/curl_easy_setopt.md b/docs/libcurl/curl_easy_setopt.md index 770fdc91bc..b80d0b6640 100644 --- a/docs/libcurl/curl_easy_setopt.md +++ b/docs/libcurl/curl_easy_setopt.md @@ -1361,6 +1361,12 @@ int main(void) } ~~~ +# ENCRYPTED CLIENT HELLO OPTIONS + +## CURLOPT_ECH + +Set the configuration for ECH. See CURLOPT_ECH(3) + # AVAILABILITY Always diff --git a/docs/libcurl/libcurl-errors.md b/docs/libcurl/libcurl-errors.md index af16b99dea..ef31b2fc93 100644 --- a/docs/libcurl/libcurl-errors.md +++ b/docs/libcurl/libcurl-errors.md @@ -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 diff --git a/docs/libcurl/opts/CURLOPT_ECH.md b/docs/libcurl/opts/CURLOPT_ECH.md new file mode 100644 index 0000000000..d8f768db59 --- /dev/null +++ b/docs/libcurl/opts/CURLOPT_ECH.md @@ -0,0 +1,83 @@ +--- +c: Copyright (C) Daniel Stenberg, , 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 + +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:\ +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:\ +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. diff --git a/docs/libcurl/opts/Makefile.inc b/docs/libcurl/opts/Makefile.inc index 7a292b81d5..bf5ea7645f 100644 --- a/docs/libcurl/opts/Makefile.inc +++ b/docs/libcurl/opts/Makefile.inc @@ -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 \ diff --git a/docs/libcurl/symbols-in-versions b/docs/libcurl/symbols-in-versions index 5f810b8028..6b37e52317 100644 --- a/docs/libcurl/symbols-in-versions +++ b/docs/libcurl/symbols-in-versions @@ -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 diff --git a/docs/options-in-versions b/docs/options-in-versions index 0905809439..d3513ff119 100644 --- a/docs/options-in-versions +++ b/docs/options-in-versions @@ -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 diff --git a/include/curl/curl.h b/include/curl/curl.h index bb9a423baf..25b3836b4e 100644 --- a/include/curl/curl.h +++ b/include/curl/curl.h @@ -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 diff --git a/include/curl/typecheck-gcc.h b/include/curl/typecheck-gcc.h index b880f3dc60..873a49e020 100644 --- a/include/curl/typecheck-gcc.h +++ b/include/curl/typecheck-gcc.h @@ -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 || \ diff --git a/lib/curl_config.h.cmake b/lib/curl_config.h.cmake index f3904d2675..11b0cb54fd 100644 --- a/lib/curl_config.h.cmake +++ b/lib/curl_config.h.cmake @@ -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 diff --git a/lib/doh.c b/lib/doh.c index 363c1fb6f6..f3c69ca4bb 100644 --- a/lib/doh.c +++ b/lib/doh.c @@ -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); diff --git a/lib/doh.h b/lib/doh.h index ffcf7a0335..9c6e0767c2 100644 --- a/lib/doh.h +++ b/lib/doh.h @@ -26,6 +26,9 @@ #include "urldata.h" #include "curl_addrinfo.h" +#ifdef USE_HTTPSRR +# include +#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 }; diff --git a/lib/easyoptions.c b/lib/easyoptions.c index 9c4438a100..c79d136707 100644 --- a/lib/easyoptions.c +++ b/lib/easyoptions.c @@ -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 diff --git a/lib/hostip.c b/lib/hostip.c index 8f13a7c73e..801de5ce43 100644 --- a/lib/hostip.c +++ b/lib/hostip.c @@ -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); } } diff --git a/lib/hostip.h b/lib/hostip.h index eee86bdf85..3ccf4354fe 100644 --- a/lib/hostip.h +++ b/lib/hostip.h @@ -32,6 +32,10 @@ #include +#ifdef USE_HTTPSRR +# include +#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 */ diff --git a/lib/setopt.c b/lib/setopt.c index 1ca10401ab..f719cc43dd 100644 --- a/lib/setopt.c +++ b/lib/setopt.c @@ -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; diff --git a/lib/strerror.c b/lib/strerror.c index a900e78d15..f142cf181d 100644 --- a/lib/strerror.c +++ b/lib/strerror.c @@ -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: diff --git a/lib/urldata.h b/lib/urldata.h index aa3f44fc2f..8bccffb0f5 100644 --- a/lib/urldata.h +++ b/lib/urldata.h @@ -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 diff --git a/lib/vtls/openssl.c b/lib/vtls/openssl.c index eada3c398f..7839ab4ab6 100644 --- a/lib/vtls/openssl.c +++ b/lib/vtls/openssl.c @@ -82,6 +82,17 @@ #include #include +#ifdef USE_ECH +# ifndef OPENSSL_IS_BORINGSSL +# include +# 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 #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 diff --git a/lib/vtls/wolfssl.c b/lib/vtls/wolfssl.c index 48cf132a37..82593f301b 100644 --- a/lib/vtls/wolfssl.c +++ b/lib/vtls/wolfssl.c @@ -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; diff --git a/m4/curl-confopts.m4 b/m4/curl-confopts.m4 index 1ee5d50c46..5c307fc819 100644 --- a/m4/curl-confopts.m4 +++ b/m4/curl-confopts.m4 @@ -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 ]) +]) diff --git a/packages/OS400/ccsidcurl.c b/packages/OS400/ccsidcurl.c index 596c1f1e15..4d1e2b52ad 100644 --- a/packages/OS400/ccsidcurl.c +++ b/packages/OS400/ccsidcurl.c @@ -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: diff --git a/src/tool_cfgable.c b/src/tool_cfgable.c index 3259bc7a5f..bb27158326 100644 --- a/src/tool_cfgable.c +++ b/src/tool_cfgable.c @@ -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) diff --git a/src/tool_cfgable.h b/src/tool_cfgable.h index dfa74d81ff..74d0c45f2e 100644 --- a/src/tool_cfgable.h +++ b/src/tool_cfgable.h @@ -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 { diff --git a/src/tool_getparam.c b/src/tool_getparam.c index f50992f8a6..f56981a743 100644 --- a/src/tool_getparam.c +++ b/src/tool_getparam.c @@ -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; diff --git a/src/tool_help.c b/src/tool_help.c index fd16af85f3..443e6b492b 100644 --- a/src/tool_help.c +++ b/src/tool_help.c @@ -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", diff --git a/src/tool_help.h b/src/tool_help.h index a05cd84581..894e39f852 100644 --- a/src/tool_help.h +++ b/src/tool_help.h @@ -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[]; diff --git a/src/tool_listhelp.c b/src/tool_listhelp.c index adb01b3bed..8429322045 100644 --- a/src/tool_listhelp.c +++ b/src/tool_listhelp.c @@ -168,6 +168,9 @@ const struct helptxt helptext[] = { {"-D, --dump-header ", "Write the received headers to ", CURLHELP_HTTP | CURLHELP_FTP}, + {" --ech ", + "Configure Encrypted Client Hello (ECH) for use with the TLS session", + CURLHELP_TLS | CURLHELP_ECH}, {" --egd-file ", "EGD socket path for random data", CURLHELP_TLS}, diff --git a/src/tool_operate.c b/src/tool_operate.c index 49ec7d835c..9f14b3e586 100644 --- a/src/tool_operate.c +++ b/src/tool_operate.c @@ -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 */ diff --git a/tests/data/test1462 b/tests/data/test1462 index 654e443444..a914f305a0 100644 --- a/tests/data/test1462 +++ b/tests/data/test1462 @@ -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 diff --git a/tests/data/test1538 b/tests/data/test1538 index c0f038be40..7abbc555b4 100644 --- a/tests/data/test1538 +++ b/tests/data/test1538 @@ -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 diff --git a/tests/ech_combos.py b/tests/ech_combos.py new file mode 100755 index 0000000000..7f41198671 --- /dev/null +++ b/tests/ech_combos.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +#*************************************************************************** +# _ _ ____ _ +# 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 +# +########################################################################### +# +# 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. + diff --git a/tests/ech_tests.sh b/tests/ech_tests.sh new file mode 100755 index 0000000000..df8e45b12b --- /dev/null +++ b/tests/ech_tests.sh @@ -0,0 +1,1151 @@ +#!/bin/bash +#*************************************************************************** +# _ _ ____ _ +# 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 +# +########################################################################### +# + +# Run some tests against servers we know to support ECH (CF, defo.ie, etc.). +# as well as some we know don't do ECH but have an HTTPS RR, and finally some +# for which neither is the case. + +# TODO: Translate this into something that approximates a valid curl test:-) +# Should be useful though even before such translation and a pile less work +# to do this than that. The pile of work required would include making an +# ECH-enabled server and a DoH server. For now, this is just run manually. +# + +# set -x + +# Exit with an error if there's an active ech stanza in ~/.curlrc +# as that'd likely skew some results (e.g. turning a fail into a +# success or vice versa) +: "${CURL_CFG_FILE=$HOME/.curlrc}" +active_ech=$(grep ech "$CURL_CFG_FILE" | grep -v "#.*ech") +if [[ "$active_ech" != "" ]] +then + echo "You seem to have an active ECH setting in $CURL_CFG_FILE" + echo "That might affect results so please remove that or comment" + echo "it out - exiting." + exit 1 +fi + + +# Targets we expect to be ECH-enabled servers +# for which an HTTPS RR is published. +# structure is host:port mapped to pathname +# TODO: add negative tests for these +declare -A ech_targets=( + [my-own.net]="ech-check.php" + [my-own.net:8443]="ech-check.php" + [defo.ie]="ech-check.php" + [cover.defo.ie]="" + [draft-13.esni.defo.ie:8413]="stats" + [draft-13.esni.defo.ie:8414]="stats" + [draft-13.esni.defo.ie:9413]="" + [draft-13.esni.defo.ie:10413]="" + [draft-13.esni.defo.ie:11413]="" + [draft-13.esni.defo.ie:12413]="" + [draft-13.esni.defo.ie:12414]="" + [crypto.cloudflare.com]="cdn-cgi/trace" + [tls-ech.dev]="" + [epochbelt.com]="" +) + +# Targets we expect not to be ECH-enabled servers +# but for which an HTTPS RR is published. +declare -A httpsrr_targets=( + [ietf.org]="" + [rte.ie]="" +) + +# Targets we expect not to be ECH-enabled servers +# and for which no HTTPS RR is published. +declare -A neither_targets=( + [www.tcd.ie]="" + [jell.ie]="" +) + +# +# Variables that can be over-ridden from environment +# + +# Top of curl test tree, assume we're there +: "${CTOP:=.}" + +# Plase to put test log output +: "${LTOP:=$CTOP/tests/ech-log/}" + +# place to stash outputs when things go wrong +: "${BTOP:=$LTOP}" + +# time to wait for a remote access to work, 10 seconds +: "${tout:=10s}" + +# Where we find OpenSSL .so's +: "${OSSL:=$HOME/code/openssl}" + +# Where we find WolfSSL .so's +: "${WSSL:=$HOME/code/wolfssl/inst/lib}" + +# Where we find boringssl .so's +: "${BSSL:=$HOME/code/boringssl/inst/lib}" + +# Where we send DoH queries when using kdig or curl +: "${DOHSERVER:=one.one.one.one}" +: "${DOHPATH:=dns-query}" + +# Whether to send mail when bad things happen (mostly for cronjob) +: "${DOMAIL:=no}" + +# Misc vars and functions + +DEFPORT=443 + +function whenisitagain() +{ + /bin/date -u +%Y%m%d-%H%M%S +} + +function fileage() +{ + echo $(($(date +%s) - $(date +%s -r "$1"))) +} + +function hostport2host() +{ + case $1 in + *:*) host=${1%:*} port=${1##*:};; + *) host=$1 port=$DEFPORT;; + esac + echo "$host" +} + +function hostport2port() +{ + case $1 in + *:*) host=${1%:*} port=${1##*:};; + *) host=$1 port=$DEFPORT;; + esac + echo "$port" +} + +function cli_test() +{ + # 1st param is target URL + turl=$1 + # 2nd param is 0 if we expect curl to not work or 1 if we expect it + # to have worked + curl_winorlose=$2 + # 3rd param is 0 if we expect ECH to not work or 1 if we expect it + # to have worked + ech_winorlose=$3 + # remaining params are passed to command line + # echparms=(${@:4}) + IFS=" " read -r -a echparms <<< "${@:4}" + + TMPF=$(mktemp) + cmd="timeout $tout $CURL ${CURL_PARAMS[*]} ${echparms[*]} $turl >$TMPF 2>&1" + echo "cli_test: $cmd " >> "$logfile" + timeout "$tout" "$CURL" "${CURL_PARAMS[@]}" "${echparms[@]}" "$turl" >"$TMPF" 2>&1 + eres=$? + if [[ "$eres" == "124" ]] + then + allgood="no" + echo "cli_test: Timeout running $cmd" + cat "$TMPF" >> "$logfile" + echo "cli_test: Timeout running $cmd" >> "$logfile" + fi + if [[ "$eres" != "0" && "$curl_winorlose" == "1" ]] + then + allgood="no" + echo "cli_test: curl failure running $cmd" + cat "$TMPF" >> "$logfile" + echo "cli_test: curl failure running $cmd" >> "$logfile" + fi + ech_success=$(grep -c "ECH: result: status is succeeded" "$TMPF") + if [[ "$ech_success" == "$ech_winorlose" ]] + then + echo "cli_test ok for ${echparms[*]}" + else + allgood="no" + echo "cli_test: ECH failure running $cmd" + cat "$TMPF" >> "$logfile" + echo "cli_test: ECH failure running $cmd" >> "$logfile" + fi + rm -f "$TMPF" +} + +function get_ech_configlist() +{ + domain=$1 + ecl=$(dig +short https "$domain" | grep "ech=" | sed -e 's/^.*ech=//' | sed -e 's/ .*//') + echo "$ecl" +} + +# start of main script + +# start by assuming we have nothing we need... +have_ossl="no" +have_wolf="no" +have_bssl="no" +using_ossl="no" +using_wolf="no" +using_bssl="no" +have_curl="no" +have_dig="no" +have_kdig="no" +have_presout="no" +have_portsblocked="no" + +# setup logging +NOW=$(whenisitagain) +BINNAME=$(basename "$0" .sh) +if [ ! -d "$LTOP" ] +then + mkdir -p "$LTOP" +fi +if [ ! -d "$LTOP" ] +then + echo "Can't see $LTOP for logs - exiting" + exit 1 +fi +logfile=$LTOP/${BINNAME}_$NOW.log + +echo "-----" > "$logfile" +echo "Running $0 at $NOW" >> "$logfile" +echo "Running $0 at $NOW" + +# check we have the binaries needed and which TLS library we'll be using +if [ -f "$OSSL"/libssl.so ] +then + have_ossl="yes" +fi +if [ -f "$WSSL"/libwolfssl.so ] +then + have_wolf="yes" +fi +if [ -f "$BSSL"/libssl.so ] +then + have_bssl="yes" +fi +CURL="$CTOP/src/curl" +CURL_PARAMS=(-vvv --doh-url https://one.one.one.one/dns-query) +if [ -f "$CTOP"/src/curl ] +then + have_curl="yes" +fi +ossl_cnt=$(LD_LIBRARY_PATH=$OSSL $CURL "${CURL_PARAMS[@]}" -V 2> /dev/null | grep -c OpenSSL) +if ((ossl_cnt == 1)) +then + using_ossl="yes" + # setup access to our .so + export LD_LIBRARY_PATH=$OSSL +fi +bssl_cnt=$(LD_LIBRARY_PATH=$BSSL $CURL "${CURL_PARAMS[@]}" -V 2> /dev/null | grep -c BoringSSL) +if ((bssl_cnt == 1)) +then + using_bssl="yes" + # setup access to our .so + export LD_LIBRARY_PATH=$BSSL +fi +wolf_cnt=$($CURL "${CURL_PARAMS[@]}" -V 2> /dev/null | grep -c wolfSSL) +if ((wolf_cnt == 1)) +then + using_wolf="yes" + # for some reason curl+wolfSSL dislikes certs that are ok + # for browsers, so we'll test using "insecure" mode (-k) + # but that's ok here as we're only interested in ECH testing + CURL_PARAMS+=(-k) +fi +# check if we have dig and it knows https or not +digcmd="dig +short" +wdig=$(type -p dig) +if [[ "$wdig" != "" ]] +then + have_dig="yes" +fi +wkdig=$(type -p kdig) +if [[ "$wkdig" != "" ]] +then + have_kdig="yes" + digcmd="kdig @$DOHSERVER +https +short" +fi +# see if our dig version knows HTTPS +dout=$($digcmd https defo.ie) +if [[ $dout != "1 . "* ]] +then + dout=$($digcmd -t TYPE65 defo.ie) + if [[ $dout == "1 . "* ]] + then + # we're good + have_presout="yes" + fi +else + have_presout="yes" +fi + +# Check if ports other than 443 are blocked from this +# vantage point (I run tests in a n/w where that's +# sadly true sometimes;-) +# echo "Checking if ports other than 443 are maybe blocked" +not443testurl="https://draft-13.esni.defo.ie:9413/" +timeout "$tout" "$CURL" "${CURL_PARAMS[@]}" "$not443testurl" >/dev/null 2>&1 +eres=$? +if [[ "$eres" == "124" ]] +then + echo "Timeout running curl for $not443testurl" >> "$logfile" + echo "Timeout running curl for $not443testurl" + have_portsblocked="yes" +fi + +{ + echo "have_ossl: $have_ossl" + echo "have_wolf: $have_wolf" + echo "have_bssl: $have_bssl" + echo "using_ossl: $using_ossl" + echo "using_wolf: $using_wolf" + echo "using_bssl: $using_bssl" + echo "have_curl: $have_curl" + echo "have_dig: $have_dig" + echo "have_kdig: $have_kdig" + echo "have_presout: $have_presout" + echo "have_portsblocked: $have_portsblocked" +} >> "$logfile" + +echo "curl: have $have_curl, cURL command: |$CURL ${CURL_PARAMS[*]}|" +echo "ossl: have: $have_ossl, using: $using_ossl" +echo "wolf: have: $have_wolf, using: $using_wolf" +echo "bssl: have: $have_bssl, using: $using_bssl" +echo "dig: $have_dig, kdig: $have_kdig, HTTPS pres format: $have_presout" +echo "dig command: |$digcmd|" +echo "ports != 443 blocked: $have_portsblocked" + +if [[ "$have_curl" == "no" ]] +then + echo "Can't proceed without curl - exiting" + exit 32 +fi + +allgood="yes" + +skip="false" + +if [[ "$skip" != "true" ]] +then + +# basic ECH good/bad +for targ in "${!ech_targets[@]}" +do + if [[ "$using_wolf" == "yes" ]] + then + case $targ in + "draft-13.esni.defo.ie:8414" | "tls-ech.dev" | \ + "crypto.cloudflare.com" | "epochbelt.com") + echo "Skipping $targ 'cause wolf"; continue;; + *) + ;; + esac + fi + host=$(hostport2host "$targ") + port=$(hostport2port "$targ") + if [[ "$port" != "443" && "$have_portsblocked" == "yes" ]] + then + echo "Skipping $targ as ports != 443 seem blocked" + continue + fi + path=${ech_targets[$targ]} + turl="https://$host:$port/$path" + echo "ECH check for $turl" + { + echo "" + echo "ECH check for $turl" + } >> "$logfile" + timeout "$tout" "$CURL" "${CURL_PARAMS[@]}" --ech hard "$turl" >> "$logfile" 2>&1 + eres=$? + if [[ "$eres" == "124" ]] + then + allgood="no" + { + echo "Timeout for $turl" + echo -e "\tTimeout for $turl" + echo "Timeout running curl for $host:$port/$path" + } >> "$logfile" + fi + if [[ "$eres" != "0" ]] + then + allgood="no" + echo "Error ($eres) for $turl" >> "$logfile" + echo -e "\tError ($eres) for $turl" + fi + echo "" >> "$logfile" +done + +# check if public_name override works (OpenSSL only) +if [[ "$using_ossl" == "yes" ]] +then + for targ in "${!ech_targets[@]}" + do + host=$(hostport2host "$targ") + port=$(hostport2port "$targ") + if [[ "$port" != "443" && "$have_portsblocked" == "yes" ]] + then + echo "Skipping $targ as ports != 443 seem blocked" + continue + fi + path=${ech_targets[$targ]} + turl="https://$host:$port/$path" + echo "PN override check for $turl" + { + echo "" + echo "PN override check for $turl" + } >> "$logfile" + timeout "$tout" "$CURL" "${CURL_PARAMS[@]}" --ech pn:override --ech hard "$turl" >> "$logfile" 2>&1 + eres=$? + if [[ "$eres" == "124" ]] + then + allgood="no" + { + echo "Timeout for $turl" + echo -e "\tTimeout for $turl" + echo "Timeout running curl for $host:$port/$path" + } >> "$logfile" + fi + if [[ "$eres" != "0" ]] + then + allgood="no" + echo "PN override Error ($eres) for $turl" >> "$logfile" + echo -e "\tPN override Error ($eres) for $turl" + fi + echo "" >> "$logfile" + done +fi + +for targ in "${!httpsrr_targets[@]}" +do + host=$(hostport2host "$targ") + port=$(hostport2port "$targ") + if [[ "$port" != "443" && "$have_portsblocked" == "yes" ]] + then + echo "Skipping $targ as ports != 443 seem blocked" + continue + fi + path=${httpsrr_targets[$targ]} + turl="https://$host:$port/$path" + echo "HTTPS RR but no ECHConfig check for $turl" + { + echo "" + echo "HTTPS RR but no ECHConfig check for $turl" + } >> "$logfile" + timeout "$tout" "$CURL" "${CURL_PARAMS[@]}" --ech true "$turl" >> "$logfile" 2>&1 + eres=$? + if [[ "$eres" == "124" ]] + then + allgood="no" + { + echo "Timeout for $turl" + echo -e "\tTimeout for $turl" + echo "Timeout running curl for $host:$port/$path" + } >> "$logfile" + fi + if [[ "$eres" != "0" ]] + then + allgood="no" + echo "Error ($eres) for $turl" >> "$logfile" + echo -e "\tError ($eres) for $turl" + fi + echo "" >> "$logfile" +done + +for targ in "${!neither_targets[@]}" +do + host=$(hostport2host "$targ") + port=$(hostport2port "$targ") + if [[ "$port" != "443" && "$have_portsblocked" == "yes" ]] + then + echo "Skipping $targ as ports != 443 seem blocked" + continue + fi + path=${neither_targets[$targ]} + turl="https://$host:$port/$path" + echo "Neither HTTPS nor ECHConfig check for $turl" + { + echo "" + echo "Neither HTTPS nor ECHConfig check for $turl" + } >> "$logfile" + timeout "$tout" "$CURL" "${CURL_PARAMS[@]}" --ech true "$turl" >> "$logfile" 2>&1 + eres=$? + if [[ "$eres" == "124" ]] + then + allgood="no" + { + echo "Timeout for $turl" + echo -e "\tTimeout for $turl" + echo "Timeout running curl for $host:$port/$path" + } >> "$logfile" + fi + if [[ "$eres" != "0" ]] + then + allgood="no" + echo "Error ($eres) for $turl" >> "$logfile" + echo -e "\tError ($eres) for $turl" + fi + echo "" >> "$logfile" +done + + +# Check various command line options, if we're good so far +if [[ "$using_ossl" == "yes" && "$allgood" == "yes" ]] +then + # use this test URL as it'll tell us if things worked + turl="https://defo.ie/ech-check.php" + echo "cli_test with $turl" + echo "cli_test with $turl" >> "$logfile" + cli_test "$turl" 1 1 --ech true + cli_test "$turl" 1 0 --ech false + cli_test "$turl" 1 1 --ech false --ech true + cli_test "$turl" 1 1 --ech false --ech true --ech pn:foobar + cli_test "$turl" 1 1 --ech false --ech pn:foobar --ech true + echconfiglist=$(get_ech_configlist defo.ie) + cli_test "$turl" 1 1 --ech ecl:"$echconfiglist" + cli_test "$turl" 1 0 --ech ecl: +fi + +fi # skip + +# Check combinations of command line options, if we're good so far +# Most of this only works for openssl, which is ok, as we're checking +# the argument handling here, not the ECH protocol +if [[ "$using_ossl" == "yes" && "$allgood" == "yes" ]] +then + # ech can be hard, true, grease or false + # ecl:ecl can be correct, incorrect or missing + # ech:pn can be correct, incorrect or missing + # in all cases the "last" argument provided should "win" + # but only one of hard, true, grease or false will apply + turl="https://defo.ie/ech-check.php" + echconfiglist=$(get_ech_configlist defo.ie) + goodecl=$echconfiglist + echconfiglist=$(get_ech_configlist hidden.hoba.ie) + badecl=$echconfiglist + goodpn="cover.defo.ie" + badpn="hoba.ie" + echo "more cli_test with $turl" + echo "more cli_test with $turl" >> "$logfile" + + # The combinatorics here are handled via the tests/ech_combos.py script + # which produces all the relevant combinations or inputs and orders + # thereof. We have to manually assess whether or not ECH is expected to + # work for each case. + cli_test "$turl" 0 0 + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech ecl:"$badecl" + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 1 1 --ech ecl:"$badecl" --ech ecl:"$goodecl" + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 1 1 --ech ecl:"$badecl" --ech ecl:"$goodecl" --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech ecl:"$badecl" --ech hard + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 1 1 --ech ecl:"$badecl" --ech hard --ech ecl:"$goodecl" + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 1 1 --ech ecl:"$badecl" --ech hard --ech ecl:"$goodecl" --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech ecl:"$badecl" --ech hard --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech ecl:"$badecl" --ech hard --ech true + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 1 1 --ech ecl:"$badecl" --ech hard --ech true --ech ecl:"$goodecl" + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 1 1 --ech ecl:"$badecl" --ech hard --ech true --ech ecl:"$goodecl" --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech ecl:"$badecl" --ech hard --ech true --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech ecl:"$badecl" --ech pn:$badpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 1 1 --ech ecl:"$badecl" --ech pn:$badpn --ech ecl:"$goodecl" + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 1 1 --ech ecl:"$badecl" --ech pn:$badpn --ech ecl:"$goodecl" --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech ecl:"$badecl" --ech pn:$badpn --ech hard + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 1 1 --ech ecl:"$badecl" --ech pn:$badpn --ech hard --ech ecl:"$goodecl" + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 1 1 --ech ecl:"$badecl" --ech pn:$badpn --ech hard --ech ecl:"$goodecl" --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech ecl:"$badecl" --ech pn:$badpn --ech hard --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech ecl:"$badecl" --ech pn:$badpn --ech hard --ech true + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 1 1 --ech ecl:"$badecl" --ech pn:$badpn --ech hard --ech true --ech ecl:"$goodecl" + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 1 1 --ech ecl:"$badecl" --ech pn:$badpn --ech hard --ech true --ech ecl:"$goodecl" --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech ecl:"$badecl" --ech pn:$badpn --ech hard --ech true --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech ecl:"$badecl" --ech pn:$badpn --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech ecl:"$badecl" --ech pn:$badpn --ech true + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 1 1 --ech ecl:"$badecl" --ech pn:$badpn --ech true --ech ecl:"$goodecl" + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 1 1 --ech ecl:"$badecl" --ech pn:$badpn --ech true --ech ecl:"$goodecl" --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" - 0 --ech ecl:"$badecl" --ech pn:$badpn --ech true --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech ecl:"$badecl" --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech ecl:"$badecl" --ech true + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 1 1 --ech ecl:"$badecl" --ech true --ech ecl:"$goodecl" + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 1 1 --ech ecl:"$badecl" --ech true --ech ecl:"$goodecl" --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech ecl:"$badecl" --ech true --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 1 1 --ech ecl:"$goodecl" + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 1 1 --ech ecl:"$goodecl" --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 1 0 --ech false + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 1 0 --ech false --ech ecl:"$badecl" + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 1 0 --ech false --ech ecl:"$badecl" --ech ecl:"$goodecl" + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 1 0 --ech false --ech ecl:"$badecl" --ech ecl:"$goodecl" --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech false --ech ecl:"$badecl" --ech hard + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 1 1 --ech false --ech ecl:"$badecl" --ech hard --ech ecl:"$goodecl" + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 1 1 --ech false --ech ecl:"$badecl" --ech hard --ech ecl:"$goodecl" --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech false --ech ecl:"$badecl" --ech hard --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech false --ech ecl:"$badecl" --ech hard --ech true + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 1 1 --ech false --ech ecl:"$badecl" --ech hard --ech true --ech ecl:"$goodecl" + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 1 1 --ech false --ech ecl:"$badecl" --ech hard --ech true --ech ecl:"$goodecl" --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech false --ech ecl:"$badecl" --ech hard --ech true --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 1 0 --ech false --ech ecl:"$badecl" --ech pn:$badpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 1 0 --ech false --ech ecl:"$badecl" --ech pn:$badpn --ech ecl:"$goodecl" + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 1 0 --ech false --ech ecl:"$badecl" --ech pn:$badpn --ech ecl:"$goodecl" --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech false --ech ecl:"$badecl" --ech pn:$badpn --ech hard + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 1 1 --ech false --ech ecl:"$badecl" --ech pn:$badpn --ech hard --ech ecl:"$goodecl" + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 1 1 --ech false --ech ecl:"$badecl" --ech pn:$badpn --ech hard --ech ecl:"$goodecl" --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech false --ech ecl:"$badecl" --ech pn:$badpn --ech hard --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech false --ech ecl:"$badecl" --ech pn:$badpn --ech hard --ech true + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 1 1 --ech false --ech ecl:"$badecl" --ech pn:$badpn --ech hard --ech true --ech ecl:"$goodecl" + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 1 1 --ech false --ech ecl:"$badecl" --ech pn:$badpn --ech hard --ech true --ech ecl:"$goodecl" --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech false --ech ecl:"$badecl" --ech pn:$badpn --ech hard --ech true --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 1 0 --ech false --ech ecl:"$badecl" --ech pn:$badpn --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech false --ech ecl:"$badecl" --ech pn:$badpn --ech true + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 1 1 --ech false --ech ecl:"$badecl" --ech pn:$badpn --ech true --ech ecl:"$goodecl" + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 1 1 --ech false --ech ecl:"$badecl" --ech pn:$badpn --ech true --ech ecl:"$goodecl" --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech false --ech ecl:"$badecl" --ech pn:$badpn --ech true --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 1 0 --ech false --ech ecl:"$badecl" --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech false --ech ecl:"$badecl" --ech true + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 1 1 --ech false --ech ecl:"$badecl" --ech true --ech ecl:"$goodecl" + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 1 1 --ech false --ech ecl:"$badecl" --ech true --ech ecl:"$goodecl" --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech false --ech ecl:"$badecl" --ech true --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 1 0 --ech false --ech ecl:"$goodecl" + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 1 0 --ech false --ech ecl:"$goodecl" --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 1 1 --ech false --ech hard + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 1 1 --ech false --ech hard --ech ecl:"$goodecl" + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 1 1 --ech false --ech hard --ech ecl:"$goodecl" --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 1 1 --ech false --ech hard --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 1 1 --ech false --ech hard --ech true + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 1 1 --ech false --ech hard --ech true --ech ecl:"$goodecl" + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 1 1 --ech false --ech hard --ech true --ech ecl:"$goodecl" --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 1 1 --ech false --ech hard --ech true --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 1 0 --ech false --ech pn:$badpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 1 0 --ech false --ech pn:$badpn --ech ecl:"$goodecl" + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 1 0 --ech false --ech pn:$badpn --ech ecl:"$goodecl" --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 1 1 --ech false --ech pn:$badpn --ech hard + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 1 1 --ech false --ech pn:$badpn --ech hard --ech ecl:"$goodecl" + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 1 1 --ech false --ech pn:$badpn --ech hard --ech ecl:"$goodecl" --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 1 1 --ech false --ech pn:$badpn --ech hard --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 1 1 --ech false --ech pn:$badpn --ech hard --ech true + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 1 1 --ech false --ech pn:$badpn --ech hard --ech true --ech ecl:"$goodecl" + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 1 1 --ech false --ech pn:$badpn --ech hard --ech true --ech ecl:"$goodecl" --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 1 1 --ech false --ech pn:$badpn --ech hard --ech true --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 1 0 --ech false --ech pn:$badpn --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 1 1 --ech false --ech pn:$badpn --ech true + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 1 1 --ech false --ech pn:$badpn --ech true --ech ecl:"$goodecl" + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 1 1 --ech false --ech pn:$badpn --ech true --ech ecl:"$goodecl" --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 1 1 --ech false --ech pn:$badpn --ech true --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 1 0 --ech false --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 1 1 --ech false --ech true + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 1 1 --ech false --ech true --ech ecl:"$goodecl" + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 1 1 --ech false --ech true --ech ecl:"$goodecl" --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 1 1 --ech false --ech true --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 1 1 --ech hard + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 1 1 --ech hard --ech ecl:"$goodecl" + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 1 1 --ech hard --ech ecl:"$goodecl" --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 1 1 --ech hard --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 1 1 --ech hard --ech true + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 1 1 --ech hard --ech true --ech ecl:"$goodecl" + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 1 1 --ech hard --ech true --ech ecl:"$goodecl" --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 1 1 --ech hard --ech true --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 1 0 --ech pn:$badpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 1 1 --ech pn:$badpn --ech ecl:"$goodecl" + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 1 1 --ech pn:$badpn --ech ecl:"$goodecl" --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 1 1 --ech pn:$badpn --ech hard + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 1 1 --ech pn:$badpn --ech hard --ech ecl:"$goodecl" + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 1 1 --ech pn:$badpn --ech hard --ech ecl:"$goodecl" --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 1 1 --ech pn:$badpn --ech hard --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 1 1 --ech pn:$badpn --ech hard --ech true + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 1 1 --ech pn:$badpn --ech hard --ech true --ech ecl:"$goodecl" + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 1 1 --ech pn:$badpn --ech hard --ech true --ech ecl:"$goodecl" --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 1 1 --ech pn:$badpn --ech hard --ech true --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 1 0 --ech pn:$badpn --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 1 1 --ech pn:$badpn --ech true + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 1 1 --ech pn:$badpn --ech true --ech ecl:"$goodecl" + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 1 1 --ech pn:$badpn --ech true --ech ecl:"$goodecl" --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 1 1 --ech pn:$badpn --ech true --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 1 0 --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 1 1 --ech true + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 1 1 --ech true --ech ecl:"$goodecl" + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 1 1 --ech true --ech ecl:"$goodecl" --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 1 1 --ech true --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 1 0 + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 1 1 --ech ecl:"$goodecl" + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 1 1 --ech ecl:"$goodecl" --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 1 0 --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 1 1 --ech true + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 1 1 --ech true --ech ecl:"$goodecl" + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 1 1 --ech true --ech ecl:"$goodecl" --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 1 1 --ech true --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + + # a target URL that doesn't support ECH + turl="https://tcd.ie" + echo "cli_test with $turl" + echo "cli_test with $turl" >> "$logfile" + # the params below don't matter much here as we'll fail anyway + echconfiglist=$(get_ech_configlist defo.ie) + goodecl=$echconfiglist + badecl="$goodecl" + goodpn="tcd.ie" + badpn="tcd.ie" + cli_test "$turl" 1 0 + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech ecl:"$badecl" + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech ecl:"$badecl" --ech ecl:"$goodecl" + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech ecl:"$badecl" --ech ecl:"$goodecl" --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech ecl:"$badecl" --ech hard + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech ecl:"$badecl" --ech hard --ech ecl:"$goodecl" + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech ecl:"$badecl" --ech hard --ech ecl:"$goodecl" --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech ecl:"$badecl" --ech hard --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech ecl:"$badecl" --ech hard --ech true + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech ecl:"$badecl" --ech hard --ech true --ech ecl:"$goodecl" + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech ecl:"$badecl" --ech hard --ech true --ech ecl:"$goodecl" --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech ecl:"$badecl" --ech hard --ech true --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech ecl:"$badecl" --ech pn:$badpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech ecl:"$badecl" --ech pn:$badpn --ech ecl:"$goodecl" + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech ecl:"$badecl" --ech pn:$badpn --ech ecl:"$goodecl" --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech ecl:"$badecl" --ech pn:$badpn --ech hard + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech ecl:"$badecl" --ech pn:$badpn --ech hard --ech ecl:"$goodecl" + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech ecl:"$badecl" --ech pn:$badpn --ech hard --ech ecl:"$goodecl" --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech ecl:"$badecl" --ech pn:$badpn --ech hard --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech ecl:"$badecl" --ech pn:$badpn --ech hard --ech true + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech ecl:"$badecl" --ech pn:$badpn --ech hard --ech true --ech ecl:"$goodecl" + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech ecl:"$badecl" --ech pn:$badpn --ech hard --ech true --ech ecl:"$goodecl" --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech ecl:"$badecl" --ech pn:$badpn --ech hard --ech true --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech ecl:"$badecl" --ech pn:$badpn --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech ecl:"$badecl" --ech pn:$badpn --ech true + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech ecl:"$badecl" --ech pn:$badpn --ech true --ech ecl:"$goodecl" + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech ecl:"$badecl" --ech pn:$badpn --ech true --ech ecl:"$goodecl" --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech ecl:"$badecl" --ech pn:$badpn --ech true --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech ecl:"$badecl" --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech ecl:"$badecl" --ech true + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech ecl:"$badecl" --ech true --ech ecl:"$goodecl" + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech ecl:"$badecl" --ech true --ech ecl:"$goodecl" --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech ecl:"$badecl" --ech true --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech ecl:"$goodecl" + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech ecl:"$goodecl" --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech false + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech false --ech ecl:"$badecl" + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech false --ech ecl:"$badecl" --ech ecl:"$goodecl" + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech false --ech ecl:"$badecl" --ech ecl:"$goodecl" --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech false --ech ecl:"$badecl" --ech hard + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech false --ech ecl:"$badecl" --ech hard --ech ecl:"$goodecl" + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech false --ech ecl:"$badecl" --ech hard --ech ecl:"$goodecl" --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech false --ech ecl:"$badecl" --ech hard --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech false --ech ecl:"$badecl" --ech hard --ech true + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech false --ech ecl:"$badecl" --ech hard --ech true --ech ecl:"$goodecl" + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech false --ech ecl:"$badecl" --ech hard --ech true --ech ecl:"$goodecl" --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech false --ech ecl:"$badecl" --ech hard --ech true --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech false --ech ecl:"$badecl" --ech pn:$badpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech false --ech ecl:"$badecl" --ech pn:$badpn --ech ecl:"$goodecl" + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech false --ech ecl:"$badecl" --ech pn:$badpn --ech ecl:"$goodecl" --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech false --ech ecl:"$badecl" --ech pn:$badpn --ech hard + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech false --ech ecl:"$badecl" --ech pn:$badpn --ech hard --ech ecl:"$goodecl" + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech false --ech ecl:"$badecl" --ech pn:$badpn --ech hard --ech ecl:"$goodecl" --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech false --ech ecl:"$badecl" --ech pn:$badpn --ech hard --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech false --ech ecl:"$badecl" --ech pn:$badpn --ech hard --ech true + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech false --ech ecl:"$badecl" --ech pn:$badpn --ech hard --ech true --ech ecl:"$goodecl" + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech false --ech ecl:"$badecl" --ech pn:$badpn --ech hard --ech true --ech ecl:"$goodecl" --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech false --ech ecl:"$badecl" --ech pn:$badpn --ech hard --ech true --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech false --ech ecl:"$badecl" --ech pn:$badpn --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech false --ech ecl:"$badecl" --ech pn:$badpn --ech true + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech false --ech ecl:"$badecl" --ech pn:$badpn --ech true --ech ecl:"$goodecl" + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech false --ech ecl:"$badecl" --ech pn:$badpn --ech true --ech ecl:"$goodecl" --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech false --ech ecl:"$badecl" --ech pn:$badpn --ech true --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech false --ech ecl:"$badecl" --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech false --ech ecl:"$badecl" --ech true + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech false --ech ecl:"$badecl" --ech true --ech ecl:"$goodecl" + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech false --ech ecl:"$badecl" --ech true --ech ecl:"$goodecl" --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech false --ech ecl:"$badecl" --ech true --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech false --ech ecl:"$goodecl" + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech false --ech ecl:"$goodecl" --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech false --ech hard + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech false --ech hard --ech ecl:"$goodecl" + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech false --ech hard --ech ecl:"$goodecl" --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech false --ech hard --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech false --ech hard --ech true + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech false --ech hard --ech true --ech ecl:"$goodecl" + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech false --ech hard --ech true --ech ecl:"$goodecl" --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech false --ech hard --ech true --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech false --ech pn:$badpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech false --ech pn:$badpn --ech ecl:"$goodecl" + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech false --ech pn:$badpn --ech ecl:"$goodecl" --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech false --ech pn:$badpn --ech hard + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech false --ech pn:$badpn --ech hard --ech ecl:"$goodecl" + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech false --ech pn:$badpn --ech hard --ech ecl:"$goodecl" --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech false --ech pn:$badpn --ech hard --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech false --ech pn:$badpn --ech hard --ech true + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech false --ech pn:$badpn --ech hard --ech true --ech ecl:"$goodecl" + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech false --ech pn:$badpn --ech hard --ech true --ech ecl:"$goodecl" --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech false --ech pn:$badpn --ech hard --ech true --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech false --ech pn:$badpn --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech false --ech pn:$badpn --ech true + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech false --ech pn:$badpn --ech true --ech ecl:"$goodecl" + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech false --ech pn:$badpn --ech true --ech ecl:"$goodecl" --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech false --ech pn:$badpn --ech true --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech false --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech false --ech true + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech false --ech true --ech ecl:"$goodecl" + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech false --ech true --ech ecl:"$goodecl" --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech false --ech true --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech hard + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech hard --ech ecl:"$goodecl" + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech hard --ech ecl:"$goodecl" --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech hard --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech hard --ech true + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech hard --ech true --ech ecl:"$goodecl" + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech hard --ech true --ech ecl:"$goodecl" --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech hard --ech true --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech pn:$badpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech pn:$badpn --ech ecl:"$goodecl" + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech pn:$badpn --ech ecl:"$goodecl" --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech pn:$badpn --ech hard + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech pn:$badpn --ech hard --ech ecl:"$goodecl" + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech pn:$badpn --ech hard --ech ecl:"$goodecl" --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech pn:$badpn --ech hard --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech pn:$badpn --ech hard --ech true + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech pn:$badpn --ech hard --ech true --ech ecl:"$goodecl" + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech pn:$badpn --ech hard --ech true --ech ecl:"$goodecl" --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech pn:$badpn --ech hard --ech true --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech pn:$badpn --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech pn:$badpn --ech true + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech pn:$badpn --ech true --ech ecl:"$goodecl" + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech pn:$badpn --ech true --ech ecl:"$goodecl" --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech pn:$badpn --ech true --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech true + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech true --ech ecl:"$goodecl" + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech true --ech ecl:"$goodecl" --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech true --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech ecl:"$goodecl" + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech ecl:"$goodecl" --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech true + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech true --ech ecl:"$goodecl" + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech true --ech ecl:"$goodecl" --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi + cli_test "$turl" 0 0 --ech true --ech pn:$goodpn + if [[ "$allgood" != "yes" ]]; then echo $LINENO; fi +fi + + +END=$(whenisitagain) +echo "Finished $0 at $END" >> "$logfile" +echo "-----" >> "$logfile" + +if [[ "$allgood" == "yes" ]] +then + echo "Finished $0 at $END" + echo "All good, log in $logfile" + exit 0 +else + echo "Finished $0 at $END" + echo "NOT all good, log in $logfile" +fi + +# send a mail to root (will be fwd'd) but just once every 24 hours +# 'cause we only really need "new" news +itsnews="yes" +age_of_news=0 +if [ -f "$LTOP"/bad_runs ] +then + age_of_news=$(fileage "$LTOP"/bad_runs) + # only consider news "new" if we haven't mailed today + if ((age_of_news < 24*3600)) + then + itsnews="no" + fi +fi +if [[ "$DOMAIL" == "yes" && "$itsnews" == "yes" ]] +then + echo "ECH badness at $NOW" | mail -s "ECH badness at $NOW" root +fi +# add to list of bad runs (updating file age) +echo "ECH badness at $NOW" >>"$LTOP"/bad_runs +exit 2 + -- 2.47.3