1<!-- 2Copyright (C) Daniel Stenberg, <daniel@haxx.se>, et al. 3 4SPDX-License-Identifier: curl 5--> 6 7# Building curl with HTTPS-RR and ECH support 8 9We have added support for ECH to curl. It can use HTTPS RRs published in the 10DNS if curl uses DoH, or else can accept the relevant ECHConfigList values 11from the command line. This works with OpenSSL, wolfSSL, BoringSSL or AWS-LC as 12the TLS provider. 13 14This feature is EXPERIMENTAL. DO NOT USE IN PRODUCTION. 15 16This should however provide enough of a proof-of-concept to prompt an informed 17discussion about a good path forward for ECH support in curl. 18 19## OpenSSL Build 20 21To build our ECH-enabled OpenSSL fork: 22 23```bash 24 cd $HOME/code 25 git clone https://github.com/defo-project/openssl 26 cd openssl 27 ./config --libdir=lib --prefix=$HOME/code/openssl-local-inst 28 ...stuff... 29 make -j8 30 ...stuff (maybe go for coffee)... 31 make install_sw 32 ...a little bit of stuff... 33``` 34 35To build curl ECH-enabled, making use of the above: 36 37```bash 38 cd $HOME/code 39 git clone https://github.com/curl/curl 40 cd curl 41 autoreconf -fi 42 LDFLAGS="-Wl,-rpath,$HOME/code/openssl-local-inst/lib/" ./configure --with-ssl=$HOME/code/openssl-local-inst --enable-ech --enable-httpsrr 43 ...lots of output... 44 WARNING: ECH HTTPSRR enabled but marked EXPERIMENTAL... 45 make 46 ...lots more output... 47``` 48 49If you do not get that WARNING at the end of the ``configure`` command, then 50ECH is not enabled, so go back some steps and re-do whatever needs re-doing:-) 51If you want to debug curl then you should add ``--enable-debug`` to the 52``configure`` command. 53 54In a recent (2024-05-20) build on one machine, configure failed to find the 55ECH-enabled SSL library, apparently due to the existence of 56``$HOME/code/openssl-local-inst/lib/pkgconfig`` as a directory containing 57various settings. Deleting that directory worked around the problem but may 58not be the best solution. 59 60## Using ECH and DoH 61 62Curl supports using DoH for A/AAAA lookups so it was relatively easy to add 63retrieval of HTTPS RRs in that situation. To use ECH and DoH together: 64 65```bash 66 cd $HOME/code/curl 67 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 68 ... 69 SSL_ECH_STATUS: success <img src="greentick-small.png" alt="good" /> <br/> 70 ... 71``` 72 73The output snippet above is within the HTML for the webpage, when things work. 74 75The above works for these test sites: 76 77```bash 78 https://defo.ie/ech-check.php 79 https://draft-13.esni.defo.ie:8413/stats 80 https://draft-13.esni.defo.ie:8414/stats 81 https://crypto.cloudflare.com/cdn-cgi/trace 82 https://tls-ech.dev 83``` 84 85The list above has 4 different server technologies, implemented by 3 different 86parties, and includes a case (the port 8414 server) where HelloRetryRequest 87(HRR) is forced. 88 89We currently support the following new curl command line arguments/options: 90 91- ``--ech <config>`` - the ``config`` value can be one of: 92 - ``false`` says to not attempt ECH 93 - ``true`` says to attempt ECH, if possible 94 - ``grease`` if attempting ECH is not possible, then send a GREASE ECH extension 95 - ``hard`` hard-fail the connection if ECH cannot be attempted 96 - ``ecl:<b64value>`` a base64 encoded ECHConfigList, rather than one accessed from the DNS 97 - ``pn:<name>`` override the ``public_name`` from an ECHConfigList 98 99Note that in the above "attempt ECH" means the client emitting a TLS 100ClientHello with a "real" ECH extension, but that does not mean that the 101relevant server can succeed in decrypting, as things can fail for other 102reasons. 103 104## Supplying an ECHConfigList on the command line 105 106To supply the ECHConfigList on the command line, you might need a bit of 107cut-and-paste, e.g.: 108 109```bash 110 dig +short https defo.ie 111 1 . ipv4hint=213.108.108.101 ech=AED+DQA8PAAgACD8WhlS7VwEt5bf3lekhHvXrQBGDrZh03n/LsNtAodbUAAEAAEAAQANY292ZXIuZGVmby5pZQAA ipv6hint=2a00:c6c0:0:116:5::10 112``` 113 114Then paste the base64 encoded ECHConfigList onto the curl command line: 115 116```bash 117 LD_LIBRARY_PATH=$HOME/code/openssl ./src/curl --ech ecl:AED+DQA8PAAgACD8WhlS7VwEt5bf3lekhHvXrQBGDrZh03n/LsNtAodbUAAEAAEAAQANY292ZXIuZGVmby5pZQAA https://defo.ie/ech-check.php 118 ... 119 SSL_ECH_STATUS: success <img src="greentick-small.png" alt="good" /> <br/> 120 ... 121``` 122 123The output snippet above is within the HTML for the webpage. 124 125If you paste in the wrong ECHConfigList (it changes hourly for ``defo.ie``) you 126should get an error like this: 127 128```bash 129 LD_LIBRARY_PATH=$HOME/code/openssl ./src/curl -vvv --ech ecl:AED+DQA8yAAgACDRMQo+qYNsNRNj+vfuQfFIkrrUFmM4vogucxKj/4nzYgAEAAEAAQANY292ZXIuZGVmby5pZQAA https://defo.ie/ech-check.php 130 ... 131 * OpenSSL/3.3.0: error:0A00054B:SSL routines::ech required 132 ... 133``` 134 135There is a reason to want this command line option - for use before publishing 136an ECHConfigList in the DNS as per the Internet-draft [A well-known URI for 137publishing ECHConfigList values](https://datatracker.ietf.org/doc/draft-ietf-tls-wkech/). 138 139If you do use a wrong ECHConfigList value, then the server might return a 140good value, via the ``retry_configs`` mechanism. You can see that value in 141the verbose output, e.g.: 142 143```bash 144 LD_LIBRARY_PATH=$HOME/code/openssl ./src/curl -vvv --ech ecl:AED+DQA8yAAgACDRMQo+qYNsNRNj+vfuQfFIkrrUFmM4vogucxKj/4nzYgAEAAEAAQANY292ZXIuZGVmby5pZQAA https://defo.ie/ech-check.php 145 ... 146* ECH: retry_configs AQD+DQA8DAAgACBvYqJy+Hgk33wh/ZLBzKSPgwxeop7gvojQzfASq7zeZQAEAAEAAQANY292ZXIuZGVmby5pZQAA/g0APEMAIAAgXkT5r4cYs8z19q5rdittyIX8gfQ3ENW4wj1fVoiJZBoABAABAAEADWNvdmVyLmRlZm8uaWUAAP4NADw2ACAAINXSE9EdXzEQIJZA7vpwCIQsWqsFohZARXChgPsnfI1kAAQAAQABAA1jb3Zlci5kZWZvLmllAAD+DQA8cQAgACASeiD5F+UoSnVoHvA2l1EifUVMFtbVZ76xwDqmMPraHQAEAAEAAQANY292ZXIuZGVmby5pZQAA 147* ECH: retry_configs for defo.ie from cover.defo.ie, 319 148 ... 149``` 150 151At that point, you could copy the base64 encoded value above and try again. 152For now, this only works for the OpenSSL and BoringSSL/AWS-LC builds. 153 154## Default settings 155 156Curl has various ways to configure default settings, e.g. in ``$HOME/.curlrc``, 157so one can set the DoH URL and enable ECH that way: 158 159```bash 160 cat ~/.curlrc 161 doh-url=https://one.one.one.one/dns-query 162 silent 163 ech=true 164``` 165 166Note that when you use the system's curl command (rather than our ECH-enabled 167build), it is liable to warn that ``ech`` is an unknown option. If that is an 168issue (e.g. if some script re-directs stdout and stderr somewhere) then adding 169the ``silent`` line above seems to be a good enough fix. (Though of 170course, yet another script could depend on non-silent behavior, so you may have 171to figure out what you prefer yourself.) That seems to have changed with the 172latest build, previously ``silent=TRUE`` was what I used in ``~/.curlrc`` but 173now that seems to cause a problem, so that the following line(s) are ignored. 174 175If you want to always use our OpenSSL build you can set ``LD_LIBRARY_PATH`` 176in the environment: 177 178```bash 179 export LD_LIBRARY_PATH=$HOME/code/openssl 180``` 181 182When you do the above, there can be a mismatch between OpenSSL versions 183for applications that check that. A ``git push`` for example fails so you 184should unset ``LD_LIBRARY_PATH`` before doing that or use a different shell. 185 186```bash 187 git push 188 OpenSSL version mismatch. Built against 30000080, you have 30200000 189 ... 190``` 191 192With all that setup as above the command line gets simpler: 193 194```bash 195 ./src/curl https://defo.ie/ech-check.php 196 ... 197 SSL_ECH_STATUS: success <img src="greentick-small.png" alt="good" /> <br/> 198 ... 199``` 200 201The ``--ech true`` option is opportunistic, so tries to do ECH but does not fail if 202the client for example cannot find any ECHConfig values. The ``--ech hard`` 203option hard-fails if there is no ECHConfig found in DNS, so for now, that is not 204a good option to set as a default. Once ECH has really been attempted by 205the client, if decryption on the server side fails, then curl fails. 206 207## Code changes for ECH support when using DoH 208 209Code changes are ``#ifdef`` protected via ``USE_ECH`` or ``USE_HTTPSRR``: 210 211- ``USE_HTTPSRR`` is used for HTTPS RR retrieval code that could be generically 212 used should non-ECH uses for HTTPS RRs be identified, e.g. use of ALPN values 213or IP address hints. 214 215- ``USE_ECH`` protects ECH specific code. 216 217There are various obvious code blocks for handling the new command line 218arguments which are not described here, but should be fairly clear. 219 220As shown in the ``configure`` usage above, there are ``configure.ac`` changes 221that allow separately dis/enabling ``USE_HTTPSRR`` and ``USE_ECH``. If ``USE_ECH`` 222is enabled, then ``USE_HTTPSRR`` is forced. In both cases ``USE_DOH`` 223is required. (There may be some configuration conflicts available for the 224determined:-) 225 226The main functional change, as you would expect, is in ``lib/vtls/openssl.c`` 227where an ECHConfig, if available from command line or DNS cache, is fed into 228the OpenSSL library via the new APIs implemented in our OpenSSL fork for that 229purpose. This code also implements the opportunistic (``--ech true``) or hard-fail 230(``--ech hard``) logic. 231 232Other than that, the main additions are in ``lib/doh.c`` 233where we reuse ``dohprobe()`` to retrieve an HTTPS RR value for the target 234domain. If such a value is found, that is stored using a new ``doh_store_https()`` 235function in a new field in the ``dohentry`` structure. 236 237The qname for the DoH query is modified if the port number is not 443, as 238defined in the SVCB specification. 239 240When the DoH process has worked, ``Curl_doh_is_resolved()`` now also returns 241the relevant HTTPS RR value data in the ``Curl_dns_entry`` structure. 242That is later accessed when the TLS session is being established, if ECH is 243enabled (from ``lib/vtls/openssl.c`` as described above). 244 245## Limitations 246 247Things that need fixing, but that can probably be ignored for the 248moment: 249 250- We could easily add code to make use of an ``alpn=`` value found in an HTTPS 251 RR, passing that on to OpenSSL for use as the "inner" ALPN value, but have 252yet to do that. 253 254Current limitations (more interesting than the above): 255 256- Only the first HTTPS RR value retrieved is actually processed as described 257 above, that could be extended in future, though picking the "right" HTTPS RR 258could be non-trivial if multiple RRs are published - matching IP address hints 259versus A/AAAA values might be a good basis for that. Last I checked though, 260browsers supporting ECH did not handle multiple HTTPS RRs well, though that 261needs re-checking as it has been a while. 262 263- It is unclear how one should handle any IP address hints found in an HTTPS RR. 264 It may be that a bit of consideration of how "multi-CDN" deployments might 265emerge would provide good answers there, but for now, it is not clear how best 266curl might handle those values when present in the DNS. 267 268- The SVCB/HTTPS RR specification supports a new "CNAME at apex" indirection 269 ("aliasMode") - the current code takes no account of that at all. One could 270envisage implementing the equivalent of following CNAMEs in such cases, but 271it is not clear if that'd be a good plan. (As of now, chrome browsers do not seem 272to have any support for that "aliasMode" and we have not checked Firefox for that 273recently.) 274 275- We have not investigated what related changes or additions might be needed 276 for applications using libcurl, as opposed to use of curl as a command line 277tool. 278 279- We have not yet implemented tests as part of the usual curl test harness as 280doing so would seem to require re-implementing an ECH-enabled server as part 281of the curl test harness. For now, we have a ``./tests/ech_test.sh`` script 282that attempts ECH with various test servers and with many combinations of the 283allowed command line options. While that is a useful test and has find issues, 284it is not comprehensive and we are not (as yet) sure what would be the right 285level of coverage. When running that script you should not have a 286``$HOME/.curlrc`` file that affects ECH or some of the negative tests could 287produce spurious failures. 288 289## Building with cmake 290 291To build with cmake, assuming our ECH-enabled OpenSSL is as before: 292 293```bash 294 cd $HOME/code 295 git clone https://github.com/curl/curl 296 cd curl 297 mkdir build 298 cd build 299 cmake -DOPENSSL_ROOT_DIR=$HOME/code/openssl -DUSE_ECH=1 -DUSE_HTTPSRR=1 .. 300 ... 301 make 302 ... 303 [100%] Built target curl 304``` 305 306The binary produced by the cmake build does not need any ECH-specific 307``LD_LIBRARY_PATH`` setting. 308 309## BoringSSL build 310 311BoringSSL is also supported by curl and also supports ECH, so to build 312with that, instead of our ECH-enabled OpenSSL: 313 314```bash 315 cd $HOME/code 316 git clone https://boringssl.googlesource.com/boringssl 317 cd boringssl 318 cmake -DCMAKE_INSTALL_PREFIX:PATH=$HOME/code/boringssl/inst -DBUILD_SHARED_LIBS=1 319 make 320 ... 321 make install 322``` 323 324Then: 325 326```bash 327 cd $HOME/code 328 git clone https://github.com/curl/curl 329 cd curl 330 autoreconf -fi 331 LDFLAGS="-Wl,-rpath,$HOME/code/boringssl/inst/lib" ./configure --with-ssl=$HOME/code/boringssl/inst --enable-ech --enable-httpsrr 332 ...lots of output... 333 WARNING: ECH HTTPSRR enabled but marked EXPERIMENTAL. Use with caution. 334 make 335``` 336 337The BoringSSL/AWS-LC APIs are fairly similar to those in our ECH-enabled 338OpenSSL fork, so code changes are also in ``lib/vtls/openssl.c``, protected 339via ``#ifdef OPENSSL_IS_BORINGSSL`` and are mostly obvious API variations. 340 341The BoringSSL/AWS-LC APIs however do not support the ``--ech pn:`` command 342line variant as of now. 343 344## wolfSSL build 345 346wolfSSL also supports ECH and can be used by curl, so here's how: 347 348```bash 349 cd $HOME/code 350 git clone https://github.com/wolfSSL/wolfssl 351 cd wolfssl 352 ./autogen.sh 353 ./configure --prefix=$HOME/code/wolfssl/inst --enable-ech --enable-debug --enable-opensslextra 354 make 355 make install 356``` 357 358The install prefix (``inst``) in the above causes wolfSSL to be installed there 359and we seem to need that for the curl configure command to work out. The 360``--enable-opensslextra`` turns out (after much faffing about;-) to be 361important or else we get build problems with curl below. 362 363```bash 364 cd $HOME/code 365 git clone https://github.com/curl/curl 366 cd curl 367 autoreconf -fi 368 ./configure --with-wolfssl=$HOME/code/wolfssl/inst --enable-ech --enable-httpsrr 369 make 370``` 371 372There are some known issues with the ECH implementation in wolfSSL: 373 374- The main issue is that the client currently handles HelloRetryRequest 375 incorrectly. [HRR issue](https://github.com/wolfSSL/wolfssl/issues/6802).) 376 The HRR issue means that the client does not work for 377 [this ECH test web site](https://tls-ech.dev) and any other similarly configured 378 sites. 379- There is also an issue related to so-called middlebox compatibility mode. 380 [middlebox compatibility issue](https://github.com/wolfSSL/wolfssl/issues/6774) 381 382### Code changes to support wolfSSL 383 384There are what seem like oddball differences: 385 386- The DoH URL in``$HOME/.curlrc`` can use `1.1.1.1` for OpenSSL but has to be 387 `one.one.one.one` for wolfSSL. The latter works for both, so OK, we us that. 388- There seems to be some difference in CA databases too - the wolfSSL version 389 does not like ``defo.ie``, whereas the system and OpenSSL ones do. We can 390 ignore that for our purposes via ``--insecure``/``-k`` but would need to fix 391 for a real setup. (Browsers do like those certificates though.) 392 393Then there are some functional code changes: 394 395- tweak to ``configure.ac`` to check if wolfSSL has ECH or not 396- added code to ``lib/vtls/wolfssl.c`` mirroring what's done in the 397 OpenSSL equivalent above. 398- wolfSSL does not support ``--ech false`` or the ``--ech pn:`` command line 399 argument. 400 401The lack of support for ``--ech false`` is because wolfSSL has decided to 402always at least GREASE if built to support ECH. In other words, GREASE is 403a compile time choice for wolfSSL, but a runtime choice for OpenSSL or 404BoringSSL/AWS-LC. (Both are reasonable.) 405 406## Additional notes 407 408### Supporting ECH without DoH 409 410All of the above only applies if DoH is being used. There should be a use-case 411for ECH when DoH is not used by curl - if a system stub resolver supports DoT 412or DoH, then, considering only ECH and the network threat model, it would make 413sense for curl to support ECH without curl itself using DoH. The author for 414example uses a combination of stubby+unbound as the system resolver listening 415on localhost:53, so would fit this use-case. That said, it is unclear if 416this is a niche that is worth trying to address. (The author is just as happy to 417let curl use DoH to talk to the same public recursive that stubby might use:-) 418 419Assuming for the moment this is a use-case we would like to support, then if 420DoH is not being used by curl, it is not clear at this time how to provide 421support for ECH. One option would seem to be to extend the ``c-ares`` library 422to support HTTPS RRs, but in that case it is not now clear whether such 423changes would be attractive to the ``c-ares`` maintainers, nor whether the 424"tag=value" extensibility inherent in the HTTPS/SVCB specification is a good 425match for the ``c-ares`` approach of defining structures specific to decoded 426answers for each supported RRtype. We are also not sure how many downstream 427curl deployments actually make use of the ``c-ares`` library, which would 428affect the utility of such changes. Another option might be to consider using 429some other generic DNS library that does support HTTPS RRs, but it is unclear 430if such a library could or would be used by all or almost all curl builds and 431downstream releases of curl. 432 433Our current conclusion is that doing the above is likely best left until we 434have some experience with the "using DoH" approach, so we are going to punt on 435this for now. 436 437### Debugging 438 439Just a note to self as remembering this is a nuisance: 440 441```bash 442LD_LIBRARY_PATH=$HOME/code/openssl:./lib/.libs gdb ./src/.libs/curl 443``` 444 445### Localhost testing 446 447It can be useful to be able to run against a localhost OpenSSL ``s_server`` 448for testing. We have published instructions for such 449[localhost tests](https://github.com/defo-project/ech-dev-utils/blob/main/howtos/localhost-tests.md) 450in another repository. Once you have that set up, you can start a server 451and then run curl against that: 452 453```bash 454 cd $HOME/code/ech-dev-utils 455 ./scripts/echsvr.sh -d 456 ... 457``` 458 459The ``echsvr.sh`` script supports many ECH-related options. Use ``echsvr.sh -h`` 460for details. 461 462In another window: 463 464```bash 465 cd $HOME/code/curl/ 466 ./src/curl -vvv --insecure --connect-to foo.example.com:8443:localhost:8443 --ech ecl:AD7+DQA6uwAgACBix2B78sX+EQhEbxMspDOc8Z3xVS5aQpYP0Cxpc2AWPAAEAAEAAQALZXhhbXBsZS5jb20AAA== 467``` 468 469### Automated use of ``retry_configs`` not supported so far... 470 471As of now we have not added support for using ``retry_config`` handling in the 472application - for a command line tool, one can just use ``dig`` (or ``kdig``) 473to get the HTTPS RR and pass the ECHConfigList from that on the command line, 474if needed, or one can access the value from command line output in verbose more 475and then reuse that in another invocation. 476 477Both our OpenSSL fork and BoringSSL/AWS-LC have APIs for both controlling GREASE 478and accessing and logging ``retry_configs``, it seems wolfSSL has neither. 479