From d4bcc9943e12421ecfa94f0c633f5eb98ddedfe0 Mon Sep 17 00:00:00 2001 From: Stephen Farrell Date: Tue, 6 Aug 2024 23:16:58 +0100 Subject: [PATCH] Documents initial agreed APIs for Encrypted Client Hello (ECH) and includes a minimal demo for some of those APIs. Reviewed-by: Tomas Mraz Reviewed-by: Neil Horman Reviewed-by: Matt Caswell (Merged from https://github.com/openssl/openssl/pull/24738) --- demos/sslecho/echecho.c | 405 +++++++++++++++++++++++ doc/designs/ech-api.md | 703 +++++++++++++++++++++++----------------- 2 files changed, 818 insertions(+), 290 deletions(-) create mode 100644 demos/sslecho/echecho.c diff --git a/demos/sslecho/echecho.c b/demos/sslecho/echecho.c new file mode 100644 index 0000000000000..c273f16a243da --- /dev/null +++ b/demos/sslecho/echecho.c @@ -0,0 +1,405 @@ +/* + * Copyright 2024 The OpenSSL Project Authors. All Rights Reserved. + * + * Licensed under the Apache License 2.0 (the "License"). You may not use + * this file except in compliance with the License. You can obtain a copy + * in the file LICENSE in the source distribution or at + * https://www.openssl.org/source/license.html + */ + +#include +#include +#include +#include +#include +#include +#include + +static const int server_port = 4433; + +static const char echconfig[] = "AD7+DQA65wAgACA8wVN2BtscOl3vQheUzHeIkVmKIiydUhDCliA4iyQRCwAEAAEAAQALZXhhbXBsZS5jb20AAA=="; +static const char echprivbuf[] = + "-----BEGIN PRIVATE KEY-----\n" + "MC4CAQAwBQYDK2VuBCIEICjd4yGRdsoP9gU7YT7My8DHx1Tjme8GYDXrOMCi8v1V\n" + "-----END PRIVATE KEY-----\n" + "-----BEGIN ECHCONFIG-----\n" + "AD7+DQA65wAgACA8wVN2BtscOl3vQheUzHeIkVmKIiydUhDCliA4iyQRCwAEAAEAAQALZXhhbXBsZS5jb20AAA==\n" + "-----END ECHCONFIG-----\n"; + +typedef unsigned char bool; +#define true 1 +#define false 0 + +/* + * This flag won't be useful until both accept/read (TCP & SSL) methods + * can be called with a timeout. TBD. + */ +static volatile bool server_running = true; + +int create_socket(bool isServer) +{ + int s; + int optval = 1; + struct sockaddr_in addr = { 0 }; + + s = socket(AF_INET, SOCK_STREAM, 0); + if (s < 0) { + perror("Unable to create socket"); + exit(EXIT_FAILURE); + } + + if (isServer) { + addr.sin_family = AF_INET; + addr.sin_port = htons(server_port); + addr.sin_addr.s_addr = INADDR_ANY; + + /* Reuse the address; good for quick restarts */ + if (setsockopt(s, SOL_SOCKET, SO_REUSEADDR, &optval, sizeof(optval)) + < 0) { + perror("setsockopt(SO_REUSEADDR) failed"); + exit(EXIT_FAILURE); + } + + if (bind(s, (struct sockaddr*) &addr, sizeof(addr)) < 0) { + perror("Unable to bind"); + exit(EXIT_FAILURE); + } + + if (listen(s, 1) < 0) { + perror("Unable to listen"); + exit(EXIT_FAILURE); + } + } + + return s; +} + +SSL_CTX* create_context(bool isServer) +{ + const SSL_METHOD *method; + SSL_CTX *ctx; + + if (isServer) + method = TLS_server_method(); + else + method = TLS_client_method(); + + ctx = SSL_CTX_new(method); + if (ctx == NULL) { + perror("Unable to create SSL context"); + ERR_print_errors_fp(stderr); + exit(EXIT_FAILURE); + } + + return ctx; +} + +static int configure_ech(SSL_CTX *ctx, int server, + unsigned char *buf, size_t len) +{ + OSSL_ECHSTORE *es = NULL; + BIO *es_in = BIO_new_mem_buf(buf, len); + + if (es_in == NULL || (es = OSSL_ECHSTORE_init(NULL, NULL)) == NULL) + goto err; + if (server && OSSL_ECHSTORE_read_pem(es, es_in, 1) != 1) + goto err; + if (!server && OSSL_ECHSTORE_read_echconfiglist(es, es_in) != 1) + goto err; + if (SSL_CTX_set1_echstore(ctx, es) != 1) + goto err; + BIO_free_all(es_in); + return 1; +err: + OSSL_ECHSTORE_free(es); + BIO_free_all(es_in); + return 0; +} + +void configure_server_context(SSL_CTX *ctx) +{ + /* Set the key and cert */ + if (SSL_CTX_use_certificate_chain_file(ctx, "cert.pem") <= 0) { + ERR_print_errors_fp(stderr); + exit(EXIT_FAILURE); + } + + if (SSL_CTX_use_PrivateKey_file(ctx, "key.pem", SSL_FILETYPE_PEM) <= 0) { + ERR_print_errors_fp(stderr); + exit(EXIT_FAILURE); + } + + if (configure_ech(ctx, 1, (unsigned char*)echprivbuf, + sizeof(echprivbuf) - 1) != 1) { + ERR_print_errors_fp(stderr); + exit(EXIT_FAILURE); + } +} + +void configure_client_context(SSL_CTX *ctx) +{ + /* + * Configure the client to abort the handshake if certificate verification + * fails + */ + SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER, NULL); + /* + * In a real application you would probably just use the default system certificate trust store and call: + * SSL_CTX_set_default_verify_paths(ctx); + * In this demo though we are using a self-signed certificate, so the client must trust it directly. + */ + if (!SSL_CTX_load_verify_locations(ctx, "cert.pem", NULL)) { + ERR_print_errors_fp(stderr); + exit(EXIT_FAILURE); + } + if (configure_ech(ctx, 0, (unsigned char*)echconfig, + sizeof(echconfig) - 1) != 1) { + ERR_print_errors_fp(stderr); + exit(EXIT_FAILURE); + } +} + +void usage() +{ + printf("Usage: echecho s\n"); + printf(" --or--\n"); + printf(" echecho c ip\n"); + printf(" c=client, s=server, ip=dotted ip of server\n"); + exit(1); +} + +int main(int argc, char **argv) +{ + bool isServer; + int result; + + SSL_CTX *ssl_ctx = NULL; + SSL *ssl = NULL; + + int server_skt = -1; + int client_skt = -1; + + /* used by getline relying on realloc, can't be statically allocated */ + char *txbuf = NULL; + size_t txcap = 0; + int txlen; + + char rxbuf[128]; + size_t rxcap = sizeof(rxbuf); + int rxlen; + + char *rem_server_ip = NULL; + + struct sockaddr_in addr = { 0 }; + unsigned int addr_len = sizeof(addr); + + char *outer_sni = NULL, *inner_sni = NULL; + int ech_status; + + /* Splash */ + printf("\nechecho : Simple Echo Client/Server: %s : %s\n\n", __DATE__, + __TIME__); + + /* Need to know if client or server */ + if (argc < 2) { + usage(); + /* NOTREACHED */ + } + isServer = (argv[1][0] == 's') ? true : false; + /* If client get remote server address (could be 127.0.0.1) */ + if (!isServer) { + if (argc != 3) { + usage(); + /* NOTREACHED */ + } + rem_server_ip = argv[2]; + } + + /* Create context used by both client and server */ + ssl_ctx = create_context(isServer); + + /* If server */ + if (isServer) { + + printf("We are the server on port: %d\n\n", server_port); + + /* Configure server context with appropriate key files */ + configure_server_context(ssl_ctx); + + /* Create server socket; will bind with server port and listen */ + server_skt = create_socket(true); + + /* + * Loop to accept clients. + * Need to implement timeouts on TCP & SSL connect/read functions + * before we can catch a CTRL-C and kill the server. + */ + while (server_running) { + /* Wait for TCP connection from client */ + client_skt = accept(server_skt, (struct sockaddr*) &addr, + &addr_len); + if (client_skt < 0) { + perror("Unable to accept"); + exit(EXIT_FAILURE); + } + + printf("Client TCP connection accepted\n"); + + /* Create server SSL structure using newly accepted client socket */ + ssl = SSL_new(ssl_ctx); + SSL_set_fd(ssl, client_skt); + + /* Wait for SSL connection from the client */ + if (SSL_accept(ssl) <= 0) { + ERR_print_errors_fp(stderr); + server_running = false; + } else { + + printf("Client SSL connection accepted\n\n"); + + ech_status = SSL_ech_get1_status(ssl, &inner_sni, &outer_sni); + printf("ECH %s (status: %d, inner: %s, outer: %s)\n", + (ech_status == 1 ? "worked" : "failed/not-tried"), + ech_status, inner_sni, outer_sni); + OPENSSL_free(inner_sni); + OPENSSL_free(outer_sni); + inner_sni = outer_sni = NULL; + + /* Echo loop */ + while (true) { + /* Get message from client; will fail if client closes connection */ + if ((rxlen = SSL_read(ssl, rxbuf, rxcap)) <= 0) { + if (rxlen == 0) { + printf("Client closed connection\n"); + } + ERR_print_errors_fp(stderr); + break; + } + /* Insure null terminated input */ + rxbuf[rxlen] = 0; + /* Look for kill switch */ + if (strcmp(rxbuf, "kill\n") == 0) { + /* Terminate...with extreme prejudice */ + printf("Server received 'kill' command\n"); + server_running = false; + break; + } + /* Show received message */ + printf("Received: %s", rxbuf); + /* Echo it back */ + if (SSL_write(ssl, rxbuf, rxlen) <= 0) { + ERR_print_errors_fp(stderr); + } + } + } + if (server_running) { + /* Cleanup for next client */ + SSL_shutdown(ssl); + SSL_free(ssl); + close(client_skt); + } + } + printf("Server exiting...\n"); + } + /* Else client */ + else { + + printf("We are the client\n\n"); + + /* Configure client context so we verify the server correctly */ + configure_client_context(ssl_ctx); + + /* Create "bare" socket */ + client_skt = create_socket(false); + /* Set up connect address */ + addr.sin_family = AF_INET; + inet_pton(AF_INET, rem_server_ip, &addr.sin_addr.s_addr); + addr.sin_port = htons(server_port); + /* Do TCP connect with server */ + if (connect(client_skt, (struct sockaddr*) &addr, sizeof(addr)) != 0) { + perror("Unable to TCP connect to server"); + goto exit; + } else { + printf("TCP connection to server successful\n"); + } + + /* Create client SSL structure using dedicated client socket */ + ssl = SSL_new(ssl_ctx); + SSL_set_fd(ssl, client_skt); + /* Set hostname for SNI */ + SSL_set_tlsext_host_name(ssl, rem_server_ip); + /* Configure server hostname check */ + SSL_set1_host(ssl, rem_server_ip); + + /* Now do SSL connect with server */ + if (SSL_connect(ssl) == 1) { + + printf("SSL connection to server successful\n\n"); + + ech_status = SSL_ech_get1_status(ssl, &inner_sni, &outer_sni); + printf("ECH %s (status: %d, inner: %s, outer: %s)\n", + (ech_status == 1 ? "worked" : "failed/not-tried"), + ech_status, inner_sni, outer_sni); + OPENSSL_free(inner_sni); + OPENSSL_free(outer_sni); + inner_sni = outer_sni = NULL; + + /* Loop to send input from keyboard */ + while (true) { + /* Get a line of input */ + txlen = getline(&txbuf, &txcap, stdin); + /* Exit loop on error */ + if (txlen < 0 || txbuf == NULL) { + break; + } + /* Exit loop if just a carriage return */ + if (txbuf[0] == '\n') { + break; + } + /* Send it to the server */ + if ((result = SSL_write(ssl, txbuf, txlen)) <= 0) { + printf("Server closed connection\n"); + ERR_print_errors_fp(stderr); + break; + } + + /* Wait for the echo */ + rxlen = SSL_read(ssl, rxbuf, rxcap); + if (rxlen <= 0) { + printf("Server closed connection\n"); + ERR_print_errors_fp(stderr); + break; + } else { + /* Show it */ + rxbuf[rxlen] = 0; + printf("Received: %s", rxbuf); + } + } + printf("Client exiting...\n"); + } else { + + printf("SSL connection to server failed\n\n"); + + ERR_print_errors_fp(stderr); + } + } + exit: + /* Close up */ + if (ssl != NULL) { + SSL_shutdown(ssl); + SSL_free(ssl); + } + SSL_CTX_free(ssl_ctx); + + if (client_skt != -1) + close(client_skt); + if (server_skt != -1) + close(server_skt); + + if (txbuf != NULL && txcap > 0) + free(txbuf); + + printf("echecho exiting\n"); + + return 0; +} diff --git a/doc/designs/ech-api.md b/doc/designs/ech-api.md index 182be2dc29f0e..90fed89172999 100644 --- a/doc/designs/ech-api.md +++ b/doc/designs/ech-api.md @@ -1,13 +1,19 @@ Encrypted ClientHello (ECH) APIs ================================ +TODO(ECH): replace references/links to the [sftcd +ECH-draft-13c](https://github.com/sftcd/openssl/tree/ECH-draft-13c) (the branch +that has good integration and interop) with relative links as files are +migrated into (PRs for) the feature branch. The `OSSL_ECHSTORE` related text +here is based on another [prototype +branch](https://github.com/sftcd/openssl/tree/ECHStore-1) that is new. + There is an [OpenSSL fork](https://github.com/sftcd/openssl/tree/ECH-draft-13c) that has an implementation of Encrypted Client Hello (ECH) and these are design -notes relating to the current APIs for that, and an analysis of how these -differ from those currently in the boringssl library. +notes taking the APIs implemented there as a starting point. -The [plan](https://github.com/openssl/project/issues/659) is to incrementally -get that code reviewed in this feature/ech branch. +The ECH Protocol +---------------- ECH involves creating an "inner" ClientHello (CH) that contains the potentially sensitive content of a CH, primarily the SNI and perhaps the ALPN values. That @@ -20,19 +26,36 @@ ECH makes use of [HPKE](https://datatracker.ietf.org/doc/rfc9180/) for the encryption of the inner CH. HPKE code was merged to the master branch in November 2022. -The current APIs implemented in this fork are also documented +The ECH APIs are also documented [here](https://github.com/sftcd/openssl/blob/ECH-draft-13c/doc/man3/SSL_ech_set1_echconfig.pod). The descriptions here are less formal and provide some justification for the API design. Unless otherwise stated all APIs return 1 in the case of success and 0 for -error. All APIs call ``SSLfatal`` or ``ERR_raise`` macros as appropriate before +error. All APIs call `SSLfatal` or `ERR_raise` macros as appropriate before returning an error. Prototypes are mostly in -[``include/openssl/ech.h``](https://github.com/sftcd/openssl/blob/ECH-draft-13c/include/openssl/ech.h) +[`include/openssl/ech.h`](https://github.com/sftcd/openssl/blob/ECH-draft-13c/include/openssl/ech.h) for now. +General Approach +---------------- + +This ECH implementation has been prototyped via integrations with curl, apache2, +lighttpd, nginx and haproxy. The implementation interoperates with all other +known ECH implementations, including browsers, the libraries they use +(NSS/BoringSSL), a closed-source server implementation (Cloudflare's test +server) and with wolfssl and (reportedly) a rusttls client. + +To date, the approach taken has been to minimise the application layer code +changes required to ECH-enable those applications. There is of course a tension +between that minimisation goal and providing generic and future-proof +interfaces. + +In terms of implementation, it is expected (and welcome) that many details of +the current ECH implementation will change during review. + Specification ------------- @@ -42,9 +65,7 @@ in August 2021. The latest draft can be found [here](https://datatracker.ietf.org/doc/draft-ietf-tls-esni/). Once browsers and others have done sufficient testing the plan is to -proceed to publishing ECH as an RFC. That will likely include a change -of version code-points which have been tracking Internet-Draft version -numbers during the course of spec development. +proceed to publishing ECH as an RFC. The only current ECHConfig version supported is 0xfe0d which will be the value to be used in the eventual RFC when that issues. (We'll replace the @@ -66,28 +87,20 @@ Note that 0xfe0d is also the value of the ECH extension codepoint: The uses of those should be correctly differentiated in the implementation, to more easily avoid problems if/when new versions are defined. -"GREASEing" is defined in -[RFC8701](https://datatracker.ietf.org/doc/html/rfc8701) and is a mechanism -intended to discourage protocol ossification that can be used for ECH. GREASEd -ECH may turn out to be important as an initial step towards widespread -deployment of ECH. - Minimal Sample Code ------------------- -OpenSSL includes code for an -[``sslecho``](https://github.com/sftcd/openssl/tree/ECH-draft-13c/demos/sslecho) -demo. We've added a minimal -[``echecho``](https://github.com/sftcd/openssl/blob/ECH-draft-13c/demos/sslecho/echecho.c) -that shows that adding one new server call -(``SSL_CTX_ech_enable_server_buffer()``) and one new client call -(``SSL_CTX_ech_set1_echconfig()``) is all that's needed to ECH-enable this -demo. +TODO(ECH): This sample code has only been compiled. The `OSSL_ECHSTORE` stuff +doesn't work yet. + +OpenSSL includes code for an [`sslecho`](../../demos/sslecho) demo. We've +added a minimal [`echecho`](../../demos/sslecho/echecho.c) that shows how to +ECH-enable this demo. Handling Custom Extensions -------------------------- -OpenSSL supports custom extensions (via ``SSL_CTX_add_custom_ext()``) so that +OpenSSL supports custom extensions (via `SSL_CTX_add_custom_ext()`) so that extension values are supplied and parsed by client and server applications via a callback. The ECH specification of course doesn't deal with such implementation matters, but comprehensive ECH support for such custom @@ -99,112 +112,310 @@ extension values remain visible to network observers. That could change if some custom value turns out to be sensitive such that we'd prefer to not include it in the outer CH. -Server-side APIs ----------------- +Padding +------- + +The privacy protection provided by ECH benefits from an observer not being able +to differentiate access to different web origins based on TLS handshake +packets. Some TLS handshake messages can however reduce the size of the +anonymity-set due to message-sizes. In particular the Certificate message size +will depend on the name of the SNI from the inner ClientHello. TLS however does +allow for record layer padding which can reduce the impact of underlying +message sizes on the size of the anonymity set. The recently added +`SSL_CTX_record_padding_ex()` and `SSL_record_padding_ex()` APIs allow for +setting separate padding sizes for the handshake messages, (that most affect +ECH), and application data messages (where padding may affect efficiency more). + +ECHConfig Extensions +-------------------- + +The ECH protocol supports extensibility [within the ECHConfig +structure](https://www.ietf.org/archive/id/draft-ietf-tls-esni-18.html#section-4.2) +via a typical TLS type, length, value scheme. However, to date, there are no +extensions defined, nor do other implementations provide APIs for adding or +manipulating ECHConfig extensions. We therefore take the same approach here. + +When running the ECH protocol, implementations are required to skip over +unknown ECHConfig extensions, or to fail for so-called "mandatory" unsupported +ECHConfig extensions. Our library code is compliant in that respect - it will +skip over extensions that are not "mandatory" (extension type high bit clear) +and fail if any "mandatory" ECHConfig extension (extension type high bit set) +is seen. + +For testing purposes, ECHConfigList values that contain ECHConfig extensions +can be produced using external scripts, and used with the library, but there is +no API support for generating such, and the library has no support for any +specific ECHConfig extension type. (Other than skipping over or failing as +described above.) + +In general, the ECHConfig extensibility mechanism seems to have no proven +utility. (If new fields for an ECHConfig are required, a new ECHConfig version +with the proposed changes can just as easily be developed/deployed.) + +The theory for ECHConfig extensions is that such values might be used to +control the outer ClientHello - controls to affect the inner ClientHello, when +ECH is used, are envisaged to be published as SvcParamKey values in SVCB/HTTP +resource records in the DNS. + +To repeat though: after a number of years of the development of ECH, no such +ECHConfig extensions have been proposed. + +Should some useful ECHConfig extensions be defined in future, then the +`OSSL_ECHSTORE` APIs could be extended to enable management of such, or, new +opaque types could be developed enabling further manipulation of ECHConfig and +ECHConfigList values. + +ECH keys versus TLS server keys +------------------------------- + +ECH private keys are similar to, but different from, TLS server private keys +used to authenticate servers. Notably: + +- ECH private keys are expected to be rotated roughly hourly, rather than every + month or two for TLS server private keys. Hourly ECH key rotation is an +attempt to provide better forward secrecy, given ECH implements an +ephemeral-static ECDH scheme. + +- ECH private keys stand alone - there are no hierarchies and there is no +chaining, and no certificates and no defined relationships between current +and older ECH private keys. The expectation is that a "current" ECH public key +will be published in the DNS and that plus approx. 2 "older" ECH private keys +will remain usable for decryption at any given time. This is a way to balance +DNS TTLs versus forward secrecy and robustness. + +- In particular, the above means that we do not see any need to repeatedly +parse or process related ECHConfigList structures - each can be processed +independently for all practical purposes. + +- There are all the usual algorithm variations, and those will likely result in +the same x25519 versus p256 combinatorics. How that plays out has yet to be +seen as FIPS compliance for ECH is not (yet) a thing. For OpenSSL, it seems +wise to be agnostic and support all relevant combinations. (And doing so is not +that hard.) + +ECH Store APIs +-------------- -The main server-side APIs involve generating a key and the related -ECHConfigList structure that ends up published in the DNS, periodically loading -such keys into a server to prepare for ECH decryption and handling so-called -ECH split-mode where a server only does ECH decryption but passes along the -inner CH to another server that does the actual TLS handshake with the client. +We introduce an externally opaque type `OSSL_ECHSTORE` to allow applications +to create and manage ECHConfigList values and associated meta-data. The +external APIs using `OSSL_ECHSTORE` are: -### Key and ECHConfigList Generation +```c +typedef struct ossl_echstore_st OSSL_ECHSTORE; -``ossl_edch_make_echconfig()`` is for use by command line or other key -management tools, for example the ``openssl ech`` command documented -[here](https://github.com/sftcd/openssl/blob/ECH-draft-13c/doc/man1/openssl-ech.pod.in). +/* if a caller wants to index the last entry in the store */ +# define OSSL_ECHSTORE_LAST -1 -The ECHConfigList structure that will eventually be published in the DNS -contains the ECH public value (an ECC public key) and other ECH related -information, mainly the ``public_name`` that will be used as the SNI value in -outer CH messages. +OSSL_ECHSTORE *OSSL_ECHSTORE_init(OSSL_LIB_CTX *libctx, const char *propq); +void OSSL_ECHSTORE_free(OSSL_ECHSTORE *es); +int OSSL_ECHSTORE_new_config(OSSL_ECHSTORE *es, + uint16_t echversion, uint8_t max_name_length, + const char *public_name, OSSL_HPKE_SUITE suite); +int OSSL_ECHSTORE_write_pem(OSSL_ECHSTORE *es, int index, BIO *out); -```c -int OSSL_ech_make_echconfig(unsigned char *echconfig, size_t *echconfiglen, - unsigned char *priv, size_t *privlen, - uint16_t ekversion, uint16_t max_name_length, - const char *public_name, OSSL_HPKE_SUITE suite, - const unsigned char *extvals, size_t extlen); +int OSSL_ECHSTORE_read_echconfiglist(OSSL_ECHSTORE *es, BIO *in); + +int OSSL_ECHSTORE_get1_info(OSSL_ECHSTORE *es, OSSL_ECH_INFO **info, + int *count); +int OSSL_ECHSTORE_downselect(OSSL_ECHSTORE *es, int index); + +int OSSL_ECHSTORE_set1_key_and_read_pem(OSSL_ECHSTORE *es, EVP_PKEY *priv, + BIO *in, int for_retry); +int OSSL_ECHSTORE_read_pem(OSSL_ECHSTORE *es, BIO *in, int for_retry); +int OSSL_ECHSTORE_num_keys(OSSL_ECHSTORE *es, int *numkeys); +int OSSL_ECHSTORE_flush_keys(OSSL_ECHSTORE *es, time_t age); ``` -The ``echconfig`` and ``priv`` buffer outputs are allocated by the caller -with the allocated size on input and the used-size on output. On output, -the ``echconfig`` contains the base64 encoded ECHConfigList and the -``priv`` value contains the PEM encoded PKCS#8 private value. +`OSSL_ECHSTORE_init()` and `OSSL_ECHSTORE_free()` are relatively obvious. + +`OSSL_ECHSTORE_new_config()` allows the caller to create a new private key +value and the related "singleton" ECHConfigList structure. +`OSSL_ECHSTORE_write_pem()` allows the caller to produce a "PEM" data +structure (conforming to the [PEMECH +specification](https://datatracker.ietf.org/doc/draft-farrell-tls-pemesni/)) +from the `OSSL_ECHSTORE` entry identified by the `index`. (An `index` of +`OSSL_ECHSTORE_LAST` will select the last entry.) +These two APIs will typically be used via the `openssl ech` command line tool. + +`OSSL_ECHSTORE_read_echconfiglist()` will typically be used by a client to +ingest the "ech=" SvcParamKey value found in an SVCB or HTTPS RR retrieved from +the DNS. The resulting set of ECHConfig values can then be associated with an +`SSL_CTX` or `SSL` structure for TLS connections. + +Generally, clients will deal with "singleton" ECHConfigList values, but it is +also possible (in multi-CDN or multi-algorithm cases), that a client may need +more fine-grained control of which ECHConfig from a set to use for a particular +TLS connection. Clients that only support a subset of algorithms can +automatically make such decisions, however, a client faced with a set of HTTPS +RR values might (in theory) need to match (in particular) the server IP address +for the connection to the ECHConfig value via the `public_name` field within +the ECHConfig value. To enable this selection, the `OSSL_ECHSTORE_get1_info()` +API presents the client with the information enabling such selection, and the +`OSSL_ECHSTORE_downselect()` API gives the client a way to select one +particular ECHConfig value from the set stored (discarding the rest). + +`OSSL_ECHSTORE_set1_key_and_read_pem()` and `OSSL_ECHSTORE_read_pem()` can be +used to load a private key value and associated "singleton" ECHConfigList. +Those can be used (by servers) to enable ECH for an `SSL_CTX` or `SSL` +connection. In addition to loading those values, the application can also +indicate via `for_retry` which ECHConfig value(s) are to be included in the +`retry_configs` fallback scheme defined by the ECH protocol. + +`OSSL_ECHSTORE_num_keys()` allows a server to see how many usable ECH private +keys are currently in the store, and `OSSL_ECHSTORE_flush_keys()` allows a +server to flush keys that are older than `age` seconds. The general model is +that a server can maintain an `OSSL_ECHSTORE` into which it periodically loads +the "latest" set of keys, e.g. hourly, and also discards the keys that are too +old, e.g. more than 3 hours old. This allows for more robust private key +management even if public key distribution suffers temporary failures. + +The APIs the clients and servers can use to associate an `OSSL_ECHSTORE` +with an `SSL_CTX` or `SSL` structure: -The ``ekversion`` should be ``OSSL_ECH_CURRENT_VERSION`` for the current version. +```c +int SSL_CTX_set1_echstore(SSL_CTX *ctx, OSSL_ECHSTORE *es); +int SSL_set1_echstore(SSL *s, OSSL_ECHSTORE *es); +``` -The ``max_name_length`` is an element of the ECHConfigList that is used -by clients as part of a padding algorithm. (That design is part of the -spec, but isn't necessarily great - the idea is to include the longest -value that might be the length of a DNS name included as an inner CH -SNI.) A value of 0 is perhaps most likely to be used, indicating that -the maximum isn't known. +ECH will be enabled for the relevant `SSL_CTX` or `SSL` connection +when these functions succeed. Any previously associated `OSSL_ECHSTORE` +will be `OSSL_ECHSTORE_free()`ed. -The ECHConfigList structure is extensible, but, to date, no extensions -have been defined. If provided, the ``extvals`` buffer should contain an -already TLS-encoded set of extensions for inclusion in the ECHConfigList. +To access the `OSSL_ECHSTORE` associated with an `SSL_CTX` or +`SSL` connection: -The ``openssl ech`` command can write the private key and the ECHConfigList -values to a file that matches the ECH PEM file format we have proposed to the -IETF -([draft-farrell-tls-pemesni](https://datatracker.ietf.org/doc/draft-farrell-tls-pemesni/)). -Note that that file format is not an "adopted" work item for the IETF TLS WG -(but should be:-). ``openssl ech`` also allows the two values to be output to -two separate files. +```c +OSSL_ECHSTORE *SSL_CTX_get1_echstore(const SSL_CTX *ctx); +OSSL_ECHSTORE *SSL_get1_echstore(const SSL *s); +``` +The resulting `OSSL_ECHSTORE` can be modified and then re-associated +with an `SSL_CTX` or `SSL` connection. + +Finer-grained client control +---------------------------- + +TODO(ECH): revisit this later, when we hopefully have some more information +about ECH deployments. + +Applications that need fine control over which ECHConfigList (from those +available) will be used, can query an `OSSL_ECHSTORE`, retrieving information +about the set of "singleton" ECHConfigList values available, and then, if +desired, down-select to one of those, e.g., based on the `public_name` that +will be used. This would enable a client that selects the server address to use +based on IP address hints that can also be present in an HTTPS/SCVB resource +record to ensure that the correct matching ECH public value is used. The +information is presented to the caller using the `OSSL_ECH_INFO` type, which +provides a simplified view of ECH data, but where each element of an array +corresponds to exactly one ECH public value and set of names. -### Server Key Management +```c +/* + * Application-visible form of ECH information from the DNS, from config + * files, or from earlier API calls. APIs produce/process an array of these. + */ +typedef struct ossl_ech_info_st { + int index; /* externally re-usable reference to this value */ + char *public_name; /* public_name from API or ECHConfig */ + char *inner_name; /* server-name (for inner CH if doing ECH) */ + unsigned char *outer_alpns; /* outer ALPN string */ + size_t outer_alpns_len; + unsigned char *inner_alpns; /* inner ALPN string */ + size_t inner_alpns_len; + char *echconfig; /* a JSON-like version of the associated ECHConfig */ +} OSSL_ECH_INFO; -The APIs here are mainly designed for web servers and have been used in -proof-of-concept (PoC) integrations with nginx, apache, lighttpd and haproxy, -in addition to the ``openssl s_server``. (See [defo.ie](https://defo.ie) for -details and code for those PoC implementations.) +void OSSL_ECH_INFO_free(OSSL_ECH_INFO *info, int count); +int OSSL_ECH_INFO_print(BIO *out, OSSL_ECH_INFO *info, int count); +``` -As ECH is essentially an ephemeral-static DH scheme, it is likely servers will -fairly frequently update the ECH key pairs in use, to provide something more -akin to forward secrecy. So it is a goal to make it easy for web servers to -re-load keys without complicating their configuration file handling. +ECH Store Internals +------------------- -Cloudflare's test ECH service rotates published ECH public keys hourly -(re-verified on 2023-01-26). We expect other services to do similarly (and do -so for some of our test services at defo.ie). +The internal structure of an ECH Store is as described below: ```c -int SSL_CTX_ech_server_enable_file(SSL_CTX *ctx, const char *file, - int for_retry); -int SSL_CTX_ech_server_enable_dir(SSL_CTX *ctx, int *loaded, - const char *echdir, int for_retry); -int SSL_CTX_ech_server_enable_buffer(SSL_CTX *ctx, const unsigned char *buf, - const size_t blen, int for_retry); +typedef struct ossl_echext_st { + uint16_t type; + uint16_t len; + unsigned char *val; +} OSSL_ECHEXT; + +DEFINE_STACK_OF(OSSL_ECHEXT) + +typedef struct ossl_echstore_entry_st { + uint16_t version; /* 0xff0d for draft-13 */ + char *public_name; + size_t pub_len; + unsigned char *pub; + unsigned int nsuites; + OSSL_HPKE_SUITE *suites; + uint8_t max_name_length; + uint8_t config_id; + STACK_OF(OSSL_ECHEXT) *exts; + char *pemfname; /* name of PEM file from which this was loaded */ + time_t loadtime; /* time public and private key were loaded from file */ + EVP_PKEY *keyshare; /* long(ish) term ECH private keyshare on a server */ + int for_retry; /* whether to use this ECHConfigList in a retry */ + size_t encoded_len; /* length of overall encoded content */ + unsigned char *encoded; /* overall encoded content */ +} OSSL_ECHSTORE_entry; + +DEFINE_STACK_OF(OSSL_ECHSTORE_entry) + +typedef struct ossl_echstore_st { + STACK_OF(OSSL_ECHSTORE_entry) *entries; + OSSL_LIB_CTX *libctx; + const char *propq; +} OSSL_ECHSTORE; ``` -The three functions above support loading keys, the first attempts to load a -key based on an individual file name. The second attempts to load all files -from a directory that have a ``.ech`` file extension - this allows web server -configurations to simply name that directory and then trigger a configuration -reload periodically as keys in that directory have been updated by some -external key management process (likely managed via a cronjob). The last -allows the application to load keys from a buffer (that should contain the same -content as a file) and was added for haproxy which prefers not to do disk reads -after initial startup (for resilience reasons apparently). +Some notes on the above ECHConfig fields: -If the ``for_retry`` input has the value 1, then the corresponding ECHConfig -values will be returned to clients that GREASE or use the wrong public value in -the ``retry-config`` that may enable a client to use ECH in a subsequent -connection. +- `version` should be `OSSL_ECH_CURRENT_VERSION` for the current version. -The content of files referred to above must also match the format defined in -[draft-farrell-tls-pemesni](https://datatracker.ietf.org/doc/draft-farrell-tls-pemesni/). +- `public_name` field is the name used in the SNI of the outer ClientHello, and +that a server ought be able to authenticate if using the `retry_configs` +fallback mechanism. -There are also functions to allow a server to see how many keys are currently -loaded, and one to flush keys that are older than ``age`` seconds. +- `config_id` is a one-octet value used by servers to select which private +value to use to attempt ECH decryption. Servers can also do trial decryption +if desired, as clients might use a random value for the `confid_id` as an +anti-fingerprinting mechanism. (The use of one octet for this value was the +result of an extended debate about efficiency versus fingerprinting.) -```c -int SSL_CTX_ech_server_get_key_status(SSL_CTX *ctx, int *numkeys); -int SSL_CTX_ech_server_flush_keys(SSL_CTX *ctx, unsigned int age); -``` +- The `max_name_length` is an element of the ECHConfigList that is used by +clients as part of a padding algorithm. (That design is part of the spec, but +isn't necessarily great - the idea is to include the longest value that might +be the length of a DNS name included as an inner CH SNI.) A value of 0 is +perhaps most likely to be used, indicating that the maximum isn't known. + +Essentially, an ECH store is a set of ECHConfig values, plus optionally +(for servers), relevant private key value information. + +When a non-singleton ECHConfigList is ingested, that is expanded into +a store that is the same as if a set of singleton ECHConfigList values +had been ingested sequentially. + +In addition to the obvious fields from each ECHConfig, we also store: + +- The `encoded` value (and length) of the ECHConfig, as that is used + as an input for the HPKE encapsulation of the inner ClientHello. (Used + by both clients and servers.) + +- The `EVP_PKEY` pointer to the private key value associated with the + relevant ECHConfig, for use by servers. + +- The PEM filename and file modification time from which a private key value + and ECHConfigList were loaded. If those values are loaded from memory, + the filename value is the SHA-256 hash of the encoded ECHConfigList and + the load time is the time of loading. These values assist when servers + periodically re-load sets of files or PEM structures from memory. + +Split-mode handling +------------------- -### Split-mode handling +TODO(ECH): This ECH split-mode API should be considered tentative. It's design +will be revisited as we get to considering the internals. ECH split-mode involves a front-end server that only does ECH decryption and then passes on the decrypted inner CH to a back-end TLS server that negotiates @@ -223,209 +434,100 @@ int SSL_CTX_ech_raw_decrypt(SSL_CTX *ctx, unsigned char **hrrtok, size_t *toklen); ``` -The caller allocates the ``inner_ch`` buffer, on input ``inner_len`` should -contain the size of the ``inner_ch`` buffer, on output the size of the actual +The caller allocates the `inner_ch` buffer, on input `inner_len` should +contain the size of the `inner_ch` buffer, on output the size of the actual inner CH. Note that, when ECH decryption succeeds, the inner CH will always be smaller than the outer CH. If there is no ECH present in the outer CH then this will return 1 (i.e., the -call will succeed) but ``decrypted_ok`` will be zero. The same will result if a +call will succeed) but `decrypted_ok` will be zero. The same will result if a GREASEd ECH is present or decryption fails for some other (indistinguishable) reason. If the caller wishes to support HelloRetryRequest (HRR), then it must supply -the same ``hrrtok`` and ``toklen`` pointers to both calls to -``SSL_CTX_ech_raw_decrypt()`` (for the initial and second ClientHello -messages). When done, the caller must free the ``hrrtok`` using -``OPENSSL_free()``. If the caller doesn't need to support HRR, then it can +the same `hrrtok` and `toklen` pointers to both calls to +`SSL_CTX_ech_raw_decrypt()` (for the initial and second ClientHello +messages). When done, the caller must free the `hrrtok` using +`OPENSSL_free()`. If the caller doesn't need to support HRR, then it can supply NULL values for these parameters. The value of the token is the client's ephemeral public value, which is not sensitive having being sent in clear in the first ClientHello. This value is missing from the second ClientHello but is needed for ECH decryption. -Note that ``SSL_CTX_ech_raw_decrypt()`` only takes a ClientHello as input. If +Note that `SSL_CTX_ech_raw_decrypt()` only takes a ClientHello as input. If the flight containing the ClientHello contains other messages (e.g. a ChangeCipherSuite or Early data), then the caller is responsible for disentangling those, and for assembling a new flight containing the inner ClientHello. -### ECH-specific Padding of server messages - -If a web server were to host a set of web sites, one of which had a much longer -name than the others, the size of some TLS handshake server messages could -expose which web site was being accessed. Similarly, if the TLS server -certificate for one web site were significantly larger or smaller than others, -message sizes could reveal which web site was being visited. For these -reasons, we provide a way to enable additional ECH-specific padding of the -Certifiate, CertificateVerify and EncryptedExtensions messages sent from the -server to the client during the handshake. - -To enable ECH-specific padding, one makes a call to: - -```c - SSL_CTX_set_options(ctx, SSL_OP_ECH_SPECIFIC_PADDING); -``` - -The default padding scheme is to ensure the following sizes for the plaintext -form of these messages: - -| ------------------- | ------------ | ------------------- | -| Message | Minimum Size | Size is multiple of | -| ------------------- | ------------ | ------------------- | -| Certificate | 1792 | 128 | -| CertificateVerify | 480 | 16 | -| EncryptedExtensions | 32 | 16 | -| ------------------- | ------------ | ------------------- | - -The ciphertext form of these messages, as seen on the network in the record -layer protocol, will usually be 16 octets more, due to the AEAD tag that is -added as part of encryption. - -If a server wishes to have finer-grained control of these sizes, then it can -make use of the ``SSL_CTX_ech_set_pad_sizes()`` or ``SSL_ech_set_pad_sizes()`` -APIs. Both involve populating an ``OSSL_ECH_PAD_SIZES`` data structure as -described below in the obvious manner. - -```c -/* - * Fine-grained ECH-spacific padding controls for a server - */ -typedef struct ossl_ech_pad_sizes_st { - size_t cert_min; /* minimum size */ - size_t cert_unit; /* size will be multiple of */ - size_t certver_min; /* minimum size */ - size_t certver_unit; /* size will be multiple of */ - size_t ee_min; /* minimum size */ - size_t ee_unit; /* size will be multiple of */ -} OSSL_ECH_PAD_SIZES; - -int SSL_CTX_ech_set_pad_sizes(SSL_CTX *ctx, OSSL_ECH_PAD_SIZES *sizes); -int SSL_ech_set_pad_sizes(SSL *s, OSSL_ECH_PAD_SIZES *sizes); -``` - -Client-side APIs ----------------- - -ECHConfig values contain a version, algorithm parameters, the public key to use -for HPKE encryption and the ``public_name`` that is by default used for the -outer SNI when ECH is attempted. - -Clients need to provide one or more ECHConfig values in order to enable ECH for -an SSL connection. ``SSL_ech_set1_echconfig()`` and -``SSL_CTX_set1_echconfig()`` allow clients to provide these to the library in -binary, ascii-hex or base64 encoded format. Multiple calls to these functions -will accumulate the set of ECHConfig values available for a connection. If the -input value provided contains no suitable ECHConfig values (e.g. if it only -contains ECHConfig versions that are not supported), then these functions will -fail and return zero. - -```c -int SSL_ech_set1_echconfig(SSL *s, const unsigned char *val, size_t len); -int SSL_CTX_ech_set1_echconfig(SSL_CTX *ctx, const unsigned char *val, - size_t len); -``` +Different encodings +------------------- -ECHConfig values may be provided via a command line argument to the calling +ECHConfigList values may be provided via a command line argument to the calling application or (more likely) have been retrieved from DNS resource records by -the application. ECHConfig values may be provided in various encodings (base64, -ascii hex or binary) each of which may suit different applications. ECHConfig -values may also be provided embedded in the DNS wire encoding of HTTPS or SVCB -resource records or in the equivalent zone file presentation format. +the application. ECHConfigList values may be provided in various encodings +(base64, ascii hex or binary) each of which may suit different applications. +ECHConfigList values may also be provided embedded in the DNS wire encoding of +HTTPS or SVCB resource records or in the equivalent zone file presentation +format. -``OSSL_ech_find_echconfigs()`` attempts to find and return the (possibly empty) -set of ECHConfig values from a buffer containing one of the encoded forms -described above. Each successfully returned ECHConfigList will have -exactly one ECHConfig, i.e., a single public value. +`OSSL_ECHSTORE_find_echconfigs()` attempts to find and return the (possibly empty) +set of ECHConfigList values as an `OSSL_ECHSTORE` from the input `BIO`. ```c -int OSSL_ech_find_echconfigs(int *num_echs, - unsigned char ***echconfigs, size_t **echlens, - const unsigned char *val, size_t len); +OSSL_ECHSTORE *OSSL_ECHSTORE_find_echconfigs(BIO *in); ``` -``OSSL_ech_find_echconfigs()`` returns the number of ECHConfig values from the -input (``val``/``len``) successfully decoded in the ``num_echs`` output. If -no ECHConfig values values are encountered (which can happen for good HTTPS RR -values) then ``num_echs`` will be zero but the function returns 1. If the -input contains more than one (syntactically correct) ECHConfig, then only +If the input contains more than one (syntactically correct) ECHConfigList, then only those that contain locally supported options (e.g. AEAD ciphers) will be -returned. If no ECHConfig found has supported options then none will be -returned and the function will return 0. +returned. If no ECHConfigList found has supported options then none will be +returned and the function will return NULL. -After a call to ``OSSL_ech_find_echconfigs()``, the application can make a -sequence of calls to ``SSL_ech_set1_echconfig()`` for each of the ECHConfig -values found. (The various output buffers must be freed by the client -afterwards, see the example code in -[``test/ech_test.c``](https://github.com/sftcd/openssl/blob/ECH-draft-13c/test/ech_test.c).) +Additional Client Controls +-------------------------- Clients can additionally more directly control the values to be used for inner and outer SNI and ALPN values via specific APIs. This allows a client to -override the ``public_name`` present in an ECHConfigList that will otherwise -be used for the outer SNI. The ``no_outer`` input allows a client to emit an -outer CH with no SNI at all. +override the `public_name` present in an ECHConfigList that will otherwise +be used for the outer SNI. The `no_outer` input allows a client to emit an +outer CH with no SNI at all. Providing a `NULL` for the `outer_name` means +to send the `public_name` provided from the ECHConfigList. ```c -int SSL_ech_set_server_names(SSL *s, const char *inner_name, +int SSL_ech_set1_server_names(SSL *s, const char *inner_name, const char *outer_name, int no_outer); -int SSL_ech_set_outer_server_name(SSL *s, const char *outer_name, int no_outer); -int SSL_ech_set_outer_alpn_protos(SSL *s, const unsigned char *protos, - unsigned int protos_len); -int SSL_CTX_ech_set_outer_alpn_protos(SSL_CTX *s, const unsigned char *protos, - unsigned int protos_len); +int SSL_ech_set1_outer_server_name(SSL *s, const char *outer_name, int no_outer); +int SSL_ech_set1_outer_alpn_protos(SSL *s, const unsigned char *protos, + size_t protos_len); +int SSL_CTX_ech_set1_outer_alpn_protos(SSL_CTX *s, const unsigned char *protos, + size_t protos_len); ``` If a client attempts ECH but that fails, or sends an ECH-GREASEd CH, to an ECH-supporting server, then that server may return an ECH "retry-config" value that the client could choose to use in a subsequent connection. The -client can detect this situation via the ``SSL_ech_get_status()`` API and +client can detect this situation via the `SSL_ech_get1_status()` API and can access the retry config value via: ```c -int SSL_ech_get_retry_config(SSL *s, unsigned char **ec, size_t *eclen); +OSSL_ECHSTORE *SSL_ech_get1_retry_config(SSL *s); ``` -Clients that need fine control over which ECHConfig (from those available) will -be used, can query the SSL connection, retrieving information about the set of -ECHConfig values available, and then, if desired, down-select to one of those, -e.g., based on the ``public_name`` that will be used. This would enable a -client that selects the server address to use based on IP address hints that -can also be present in an HTTPS/SCVB resource record to ensure that the correct -matching ECHConfig is used. The information is presented to the client using -the ``OSSL_ECH_INFO`` type, which provides a simplified view of ECHConfig data, -but where each element of an array corresponds to exactly one ECH public value -and set of names. - -```c -/* - * Application-visible form of ECH information from the DNS, from config - * files, or from earlier API calls. APIs produce/process an array of these. - */ -typedef struct ossl_ech_info_st { - int index; /* externally re-usable reference to this value */ - char *public_name; /* public_name from API or ECHConfig */ - char *inner_name; /* server-name (for inner CH if doing ECH) */ - char *outer_alpns; /* outer ALPN string */ - char *inner_alpns; /* inner ALPN string */ - char *echconfig; /* a JSON-like version of the associated ECHConfig */ -} OSSL_ECH_INFO; - -void OSSL_ECH_INFO_free(OSSL_ECH_INFO *info, int count); -int OSSL_ECH_INFO_print(BIO *out, OSSL_ECH_INFO *info, int count); -int SSL_ech_get_info(SSL *s, OSSL_ECH_INFO **info, int *count); -int SSL_ech_reduce(SSL *s, int index); -``` +GREASEing +--------- -The ``SSL_ech_reduce()`` function allows the caller to reduce the active set of -ECHConfig values down to just the one they prefer, based on the -``OSSL_ECH_INFO`` index value and whatever criteria the caller uses to prefer -one ECHConfig over another (e.g. the ``public_name``). +"GREASEing" is defined in +[RFC8701](https://datatracker.ietf.org/doc/html/rfc8701) and is a mechanism +intended to discourage protocol ossification that can be used for ECH. GREASEd +ECH may turn out to be important as an initial step towards widespread +deployment of ECH. If a client wishes to GREASE ECH using a specific HPKE suite or ECH version (represented by the TLS extension type code-point) then it can set those values via: ```c -int SSL_ech_set_grease_suite(SSL *s, const char *suite); +int SSL_ech_set1_grease_suite(SSL *s, const char *suite); int SSL_ech_set_grease_type(SSL *s, uint16_t type); ``` @@ -436,9 +538,9 @@ Clients and servers can check the status of ECH processing on an SSL connection using this API: ```c -int SSL_ech_get_status(SSL *s, char **inner_sni, char **outer_sni); +int SSL_ech_get1_status(SSL *s, char **inner_sni, char **outer_sni); -/* Return codes from SSL_ech_get_status */ +/* Return codes from SSL_ech_get1_status */ # define SSL_ECH_STATUS_BACKEND 4 /* ECH back-end: saw an ech_is_inner */ # define SSL_ECH_STATUS_GREASE_ECH 3 /* GREASEd and got an ECH in return */ # define SSL_ECH_STATUS_GREASE 2 /* ECH GREASE happened */ @@ -452,8 +554,8 @@ int SSL_ech_get_status(SSL *s, char **inner_sni, char **outer_sni); # define SSL_ECH_STATUS_FAILED_ECH_BAD_NAME -106 /* We tried, failed and got an ECH, from a bad name */ ``` -The ``inner_sni`` and ``outer_sni`` values should be freed by callers -via ``OPENSSL_free()``. +The `inner_sni` and `outer_sni` values should be freed by callers +via `OPENSSL_free()`. The function returns one of the status values above. @@ -462,8 +564,8 @@ Call-backs and options Clients and servers can set a callback that will be triggered when ECH is attempted and the result of ECH processing is known. The callback function can -access a string (``str``) that can be used for logging (but not for branching). -Callback functions might typically call ``SSL_ech_get_status()`` if branching +access a string (`str`) that can be used for logging (but not for branching). +Callback functions might typically call `SSL_ech_get1_status()` if branching is required. ```c @@ -474,7 +576,7 @@ void SSL_CTX_ech_set_callback(SSL_CTX *ctx, SSL_ech_cb_func f); ``` The following options are defined for ECH and may be set via -``SSL_set_options()``: +`SSL_set_options()`: ```c /* set this to tell client to emit greased ECH values when not doing @@ -490,37 +592,58 @@ The following options are defined for ECH and may be set via /* If set, servers will add GREASEy ECHConfig values to those sent * in retry_configs */ #define SSL_OP_ECH_GREASE_RETRY_CONFIG SSL_OP_BIT(39) -/* If set, servers will add ECH-specific padding to Certificate, - * CertificateVerify and EncryptedExtensions messages */ -#define SSL_OP_ECH_SPECIFIC_PADDING SSL_OP_BIT(40) ``` +A Note on `_get_`,`_get0_`,`_get1_`,`_set_`,`_set0_`,`_set1_` +------------------------------------------------------------- + +TODO(ECH): This text will likely disappear as things settle. + +The abstraction behind the `_get_`,`_get0_`,`_get1_`,`_set_`,`_set0_`,`_set1_` +convention used in OpenSSL APIs is somewhat non-obvious, (but is what it is), +so some words of explanation of the function names above may be useful, partly +as a check that those usages are consistent with other APIs: + +- `_set_` is appropriate where the input/output type(s) are basic and involve + no type-specific memory management (e.g. `SSL_set_enable_ech_grease`) +- there are no uses of `_get_` or `_get0_` above +- `_get1_` is appropriate when a pointer to a complex type is being returned + that may be modified and must be free'd by the application, e.g. + `OSSL_ECHSTORE_get1_info`. +- `_set0_` is also unused above, because... +- the `_set1_` variant seems easier to handle for the application ("with ECH + stuff, if you make it then give it to the library, you still need to free + it") and for consistency amongst these APIs, so that is often used, e.g. + `OSSL_ECHSTORE_set1_key_and_read_pem`. + Build Options ------------- -All ECH code is protected via ``#ifndef OPENSSL_NO_ECH`` and there is -a ``no-ech`` option to build without this code. +All ECH code is protected via `#ifndef OPENSSL_NO_ECH` and there is +a `no-ech` option to build without this code. BoringSSL APIs -------------- -Brief descriptions of boringssl APIs are below together with initial comments +Brief descriptions of BoringSSL APIs are below together with initial comments comparing those to the above. (It may be useful to consider the extent to -which it is useful to make OpenSSL and boring APIs resemble one another.) +which it is useful to make OpenSSL and BoringSSL APIs resemble one another.) -Just as our implementation is under development, boring's ``include/openssl/ssl.h`` -says: "ECH support in BoringSSL is still experimental and under development." +Just as our implementation is under development, BoringSSL's +`include/openssl/ssl.h` says: "ECH support in BoringSSL is still experimental +and under development." ### GREASE -Boring uses an API to enable GREASEing rather than an option. +BoringSSL uses an API to enable GREASEing rather than an option. ```c OPENSSL_EXPORT void SSL_set_enable_ech_grease(SSL *ssl, int enable); ``` -This could work as well for our implementation, or boring could probably change -to use an option, unless there's some reason to prefer not adding new options. +This could work as well for our implementation, or BoringSSL could probably +change to use an option, unless there's some reason to prefer not adding new +options. ### Setting an ECHConfigList @@ -534,8 +657,8 @@ This provides a subset of the equivalent client capabilities from our fork. ### Verifying the outer CH rather than inner -Boring seems to use this API to change the DNS name being verified in order to -validate a ``retry_config``. +BoringSSL seems to use this API to change the DNS name being verified in order +to validate a `retry_config`. ```c OPENSSL_EXPORT void SSL_get0_ech_name_override(const SSL *ssl, @@ -548,11 +671,11 @@ I'm not sure how this compares. Need to investigate. ### Create an ECHConfigList The first function below outputs an ECHConfig, the second adds one of those to -an ``SSL_ECH_KEYS`` structure, the last emits an ECHConfigList from that -structure. There are other APIs for managing memory for ``SSL_ECH_KEYS`` +an `SSL_ECH_KEYS` structure, the last emits an ECHConfigList from that +structure. There are other APIs for managing memory for `SSL_ECH_KEYS` -These APIs also expose HPKE to the application via ``EVP_HPKE_KEY`` which is -defined in ``include/openssl/hpke.h``. HPKE handling differs quite a bit from +These APIs also expose HPKE to the application via `EVP_HPKE_KEY` which is +defined in `include/openssl/hpke.h`. HPKE handling differs quite a bit from the HPKE APIs merged to OpenSSL. ```c @@ -571,25 +694,25 @@ OPENSSL_EXPORT int SSL_ECH_KEYS_marshal_retry_configs(const SSL_ECH_KEYS *keys, ``` -Collectively these are similar to ``OSSL_ech_make_echconfig()``. +Collectively these are similar to `OSSL_ECH_make_echconfig()`. ### Setting ECH keys on a server -Again using the ``SSL_ECH_KEYS`` type and APIs, servers can build up a set of +Again using the `SSL_ECH_KEYS` type and APIs, servers can build up a set of ECH keys using: ```c OPENSSL_EXPORT int SSL_CTX_set1_ech_keys(SSL_CTX *ctx, SSL_ECH_KEYS *keys); ``` -This is similar to the ``SSL_CTX_ech_server_enable_*()`` APIs. +This is similar to the `SSL_CTX_ech_server_enable_*()` APIs. ### Getting status -Boring has: +BoringSSL has: ```c OPENSSL_EXPORT int SSL_ech_accepted(const SSL *ssl); ``` -That seems to be a subset of ``SSL_ech_get_status()``. +That seems to be a subset of `SSL_ech_get1_status()`.