Documents initial agreed APIs for Encrypted Client Hello (ECH)
and includes a minimal demo for some of those APIs. Reviewed-by: Tomas Mraz <tomas@openssl.org> Reviewed-by: Neil Horman <nhorman@openssl.org> Reviewed-by: Matt Caswell <matt@openssl.org> (Merged from https://github.com/openssl/openssl/pull/24738)
This commit is contained in:
parent
cb616bf86c
commit
02e3203e40
2 changed files with 819 additions and 291 deletions
405
demos/sslecho/echecho.c
Normal file
405
demos/sslecho/echecho.c
Normal file
|
@ -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 <stdio.h>
|
||||
#include <unistd.h>
|
||||
#include <string.h>
|
||||
#include <sys/socket.h>
|
||||
#include <arpa/inet.h>
|
||||
#include <openssl/ssl.h>
|
||||
#include <openssl/err.h>
|
||||
|
||||
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;
|
||||
}
|
|
@ -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 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.
|
||||
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).
|
||||
|
||||
### Key and ECHConfigList Generation
|
||||
ECHConfig Extensions
|
||||
--------------------
|
||||
|
||||
``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).
|
||||
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.
|
||||
|
||||
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.
|
||||
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
|
||||
--------------
|
||||
|
||||
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:
|
||||
|
||||
```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);
|
||||
typedef struct ossl_echstore_st OSSL_ECHSTORE;
|
||||
|
||||
/* if a caller wants to index the last entry in the store */
|
||||
# define OSSL_ECHSTORE_LAST -1
|
||||
|
||||
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);
|
||||
|
||||
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.
|
||||
|
||||
The ``ekversion`` should be ``OSSL_ECH_CURRENT_VERSION`` for the current version.
|
||||
`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.
|
||||
|
||||
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.
|
||||
`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.
|
||||
|
||||
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.
|
||||
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).
|
||||
|
||||
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.
|
||||
`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.
|
||||
|
||||
### Server Key Management
|
||||
`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 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.)
|
||||
|
||||
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.
|
||||
|
||||
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 APIs the clients and servers can use to associate an `OSSL_ECHSTORE`
|
||||
with an `SSL_CTX` or `SSL` structure:
|
||||
|
||||
```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);
|
||||
int SSL_CTX_set1_echstore(SSL_CTX *ctx, OSSL_ECHSTORE *es);
|
||||
int SSL_set1_echstore(SSL *s, OSSL_ECHSTORE *es);
|
||||
```
|
||||
|
||||
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).
|
||||
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.
|
||||
|
||||
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.
|
||||
|
||||
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/).
|
||||
|
||||
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.
|
||||
To access the `OSSL_ECHSTORE` associated with an `SSL_CTX` or
|
||||
`SSL` connection:
|
||||
|
||||
```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);
|
||||
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.
|
||||
|
||||
```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;
|
||||
|
||||
void OSSL_ECH_INFO_free(OSSL_ECH_INFO *info, int count);
|
||||
int OSSL_ECH_INFO_print(BIO *out, OSSL_ECH_INFO *info, int count);
|
||||
```
|
||||
|
||||
### Split-mode handling
|
||||
ECH Store Internals
|
||||
-------------------
|
||||
|
||||
The internal structure of an ECH Store is as described below:
|
||||
|
||||
```c
|
||||
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;
|
||||
```
|
||||
|
||||
Some notes on the above ECHConfig fields:
|
||||
|
||||
- `version` should be `OSSL_ECH_CURRENT_VERSION` for the current version.
|
||||
|
||||
- `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.
|
||||
|
||||
- `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.)
|
||||
|
||||
- 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
|
||||
-------------------
|
||||
|
||||
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
|
||||
Different encodings
|
||||
-------------------
|
||||
|
||||
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);
|
||||
```
|
||||
|
||||
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.
|
||||
GREASEing
|
||||
---------
|
||||
|
||||
```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);
|
||||
```
|
||||
|
||||
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()`.
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue