From 1f675f8955ad1980575657ad0b39600521388520 Mon Sep 17 00:00:00 2001 From: Stephen Farrell Date: Wed, 26 Jun 2024 12:55:17 +0100 Subject: [PATCH 01/24] add ech-api.md Reviewed-by: Tomas Mraz Reviewed-by: Neil Horman Reviewed-by: Matt Caswell (Merged from https://github.com/openssl/openssl/pull/24738) --- doc/designs/ech-api.md | 595 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 595 insertions(+) create mode 100644 doc/designs/ech-api.md diff --git a/doc/designs/ech-api.md b/doc/designs/ech-api.md new file mode 100644 index 0000000000000..182be2dc29f0e --- /dev/null +++ b/doc/designs/ech-api.md @@ -0,0 +1,595 @@ +Encrypted ClientHello (ECH) APIs +================================ + +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. + +The [plan](https://github.com/openssl/project/issues/659) is to incrementally +get that code reviewed in this feature/ech branch. + +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 +inner CH is then encrypted and embedded (as a CH extension) in an outer CH that +contains presumably less sensitive values. The spec includes a "compression" +scheme that allows the inner CH to refer to extensions from the outer CH where +the same value would otherwise be present in both. + +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 +[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 +returning an error. + +Prototypes are mostly in +[``include/openssl/ech.h``](https://github.com/sftcd/openssl/blob/ECH-draft-13c/include/openssl/ech.h) +for now. + +Specification +------------- + +ECH is an IETF TLS WG specification. It has been stable since +[draft-13](https://datatracker.ietf.org/doc/draft-ietf-tls-esni/13/), published +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. + +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 +XXXX with the relevant RFC number once that's known.) + +```c +/* version from RFC XXXX */ +# define OSSL_ECH_RFCXXXX_VERSION 0xfe0d +/* latest version from an RFC */ +# define OSSL_ECH_CURRENT_VERSION OSSL_ECH_RFCXXXX_VERSION +``` + +Note that 0xfe0d is also the value of the ECH extension codepoint: + +```c +# define TLSEXT_TYPE_ech 0xfe0d +``` + +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. + +Handling Custom Extensions +-------------------------- + +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 +extensions could quickly become complex. At present, in the absence of evidence +of sensitive custom extension values, we handle all such extensions by using +the ECH compression mechanism. That means we require no API changes, only make +one call to the application callbacks and get interoperability, but that such +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 +---------------- + +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. + +### Key and ECHConfigList Generation + +``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 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. + +```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); +``` + +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. + +The ``ekversion`` should be ``OSSL_ECH_CURRENT_VERSION`` for the current version. + +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. + +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. + +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. + +### Server Key Management + +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). + +```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); +``` + +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). + +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. + +```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); +``` + +### Split-mode handling + +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 +the actual TLS session with the client, based on the inner CH content. The +function to support this simply takes the outer CH, indicates whether +decryption has succeeded or not, and if it has, returns the inner CH and SNI +values (allowing routing to the correct back-end). Both the supplied (outer) +CH and returned (inner) CH here include the record layer header. + +```c +int SSL_CTX_ech_raw_decrypt(SSL_CTX *ctx, + int *decrypted_ok, + char **inner_sni, char **outer_sni, + unsigned char *outer_ch, size_t outer_len, + unsigned char *inner_ch, size_t *inner_len, + 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 +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 +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 +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 +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); +``` + +ECHConfig 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. + +``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. + +```c +int OSSL_ech_find_echconfigs(int *num_echs, + unsigned char ***echconfigs, size_t **echlens, + const unsigned char *val, size_t len); +``` + +``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 +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. + +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).) + +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. + +```c +int SSL_ech_set_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); +``` + +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 +can access the retry config value via: + +```c +int SSL_ech_get_retry_config(SSL *s, unsigned char **ec, size_t *eclen); +``` + +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); +``` + +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``). + +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_set_grease_type(SSL *s, uint16_t type); +``` + +ECH Status API +-------------- + +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); + +/* Return codes from SSL_ech_get_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 */ +# define SSL_ECH_STATUS_SUCCESS 1 /* Success */ +# define SSL_ECH_STATUS_FAILED 0 /* Some internal or protocol error */ +# define SSL_ECH_STATUS_BAD_CALL -100 /* Some in/out arguments were NULL */ +# define SSL_ECH_STATUS_NOT_TRIED -101 /* ECH wasn't attempted */ +# define SSL_ECH_STATUS_BAD_NAME -102 /* ECH ok but server cert bad */ +# define SSL_ECH_STATUS_NOT_CONFIGURED -103 /* ECH wasn't configured */ +# define SSL_ECH_STATUS_FAILED_ECH -105 /* We tried, failed and got an ECH, from a good name */ +# 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 function returns one of the status values above. + +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 +is required. + +```c +typedef unsigned int (*SSL_ech_cb_func)(SSL *s, const char *str); + +void SSL_ech_set_callback(SSL *s, SSL_ech_cb_func f); +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()``: + +```c +/* set this to tell client to emit greased ECH values when not doing + * "real" ECH */ +#define SSL_OP_ECH_GREASE SSL_OP_BIT(36) +/* If this is set then the server side will attempt trial decryption */ +/* of ECHs even if there is no matching record_digest. That's a bit */ +/* inefficient, but more privacy friendly */ +#define SSL_OP_ECH_TRIALDECRYPT SSL_OP_BIT(37) +/* If set, clients will ignore the supplied ECH config_id and replace + * that with a random value */ +#define SSL_OP_ECH_IGNORE_CID SSL_OP_BIT(38) +/* 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) +``` + +Build Options +------------- + +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 +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.) + +Just as our implementation is under development, boring'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. + +```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. + +### Setting an ECHConfigList + +```c +OPENSSL_EXPORT int SSL_set1_ech_config_list(SSL *ssl, + const uint8_t *ech_config_list, + size_t ech_config_list_len); +``` + +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``. + +```c +OPENSSL_EXPORT void SSL_get0_ech_name_override(const SSL *ssl, + const char **out_name, + size_t *out_name_len); +``` + +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`` + +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 +OPENSSL_EXPORT int SSL_marshal_ech_config(uint8_t **out, size_t *out_len, + uint8_t config_id, + const EVP_HPKE_KEY *key, + const char *public_name, + size_t max_name_len); +OPENSSL_EXPORT int SSL_ECH_KEYS_add(SSL_ECH_KEYS *keys, int is_retry_config, + const uint8_t *ech_config, + size_t ech_config_len, + const EVP_HPKE_KEY *key); +OPENSSL_EXPORT int SSL_ECH_KEYS_marshal_retry_configs(const SSL_ECH_KEYS *keys, + uint8_t **out, + size_t *out_len); + +``` + +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 +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. + +### Getting status + +Boring has: + +```c +OPENSSL_EXPORT int SSL_ech_accepted(const SSL *ssl); +``` + +That seems to be a subset of ``SSL_ech_get_status()``. From b1ec1b30bef929adf02213c621081eb14c6d5588 Mon Sep 17 00:00:00 2001 From: Stephen Farrell Date: Tue, 6 Aug 2024 23:16:58 +0100 Subject: [PATCH 02/24] 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()`. From 8d50306edf20205328048fb60b846f5cf46b33fa Mon Sep 17 00:00:00 2001 From: Stephen Farrell Date: Thu, 15 Aug 2024 01:27:24 +0100 Subject: [PATCH 03/24] ECH build artefacts and a bit of code Reviewed-by: Tomas Mraz Reviewed-by: Matt Caswell (Merged from https://github.com/openssl/openssl/pull/25193) --- Configurations/unix-Makefile.tmpl | 3 +- Configure | 3 +- INSTALL.md | 5 + apps/build.info | 1 + apps/ech.c | 199 +++++++++++++ apps/lib/s_cb.c | 4 + apps/list.c | 3 + crypto/err/openssl.txt | 1 + crypto/ssl_err.c | 1 + demos/sslecho/Makefile | 4 +- demos/sslecho/README.md | 45 +++ doc/build.info | 14 + doc/designs/ech-api.md | 51 ++-- doc/man1/build.info | 1 + doc/man1/openssl-ech.pod.in | 94 ++++++ doc/man1/openssl.pod | 4 + doc/man3/SSL_CTX_set_options.pod | 24 ++ doc/man3/SSL_set1_echstore.pod | 187 ++++++++++++ include/openssl/ech.h | 137 +++++++++ include/openssl/pem.h | 1 + include/openssl/ssl.h.in | 28 ++ include/openssl/sslerr.h | 1 + include/openssl/tls1.h | 8 + include/openssl/types.h | 6 + ssl/build.info | 1 + ssl/ech.c | 467 ++++++++++++++++++++++++++++++ ssl/ech_local.h | 102 +++++++ test/build.info | 7 +- test/ech_test.c | 66 +++++ test/recipes/30-test_ech.t | 21 ++ util/libssl.num | 29 +- util/perl/TLSProxy/Message.pm | 2 + 32 files changed, 1490 insertions(+), 30 deletions(-) create mode 100644 apps/ech.c create mode 100644 doc/man1/openssl-ech.pod.in create mode 100644 doc/man3/SSL_set1_echstore.pod create mode 100644 include/openssl/ech.h create mode 100644 ssl/ech.c create mode 100644 ssl/ech_local.h create mode 100644 test/ech_test.c create mode 100644 test/recipes/30-test_ech.t diff --git a/Configurations/unix-Makefile.tmpl b/Configurations/unix-Makefile.tmpl index 7fdb0b86eb87a..3b8fee0171ca8 100644 --- a/Configurations/unix-Makefile.tmpl +++ b/Configurations/unix-Makefile.tmpl @@ -1349,7 +1349,8 @@ errors: include/openssl/dtls1.h include/openssl/srtp.h include/openssl/quic.h - include/openssl/sslerr_legacy.h ); + include/openssl/sslerr_legacy.h + include/openssl/ech.h); my @cryptoheaders_tmpl = qw( include/internal/dso.h include/internal/o_dir.h diff --git a/Configure b/Configure index 2d89667176d16..0127e3e55dd18 100755 --- a/Configure +++ b/Configure @@ -472,6 +472,7 @@ my @disablables = ( "ecdh", "ecdsa", "ecx", + "ech", "egd", "engine", "err", @@ -635,7 +636,7 @@ my @disable_cascades = ( "blake2", "bf", "camellia", "cast", "chacha", "cmac", "cms", "cmp", "comp", "ct", "des", "dgram", "dh", "dsa", - "ec", "engine", + "ec", "ech", "engine", "filenames", "idea", "ktls", "lms", "md4", "ml-dsa", "ml-kem", "multiblock", diff --git a/INSTALL.md b/INSTALL.md index f90b937cef682..b167e078fcbb1 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -800,6 +800,11 @@ Disable legacy TLS EC groups that were deprecated in RFC8422. These are the Koblitz curves, B, B, B, B, and the binary Elliptic curves that would also be disabled by C. +### no-ech + +Don't build support for Encrypted Client Hello (ECH) extension (draft-ietf-tls-esni) +TODO(ECH) update link to RFC. + ### enable-ec_nistp_64_gcc_128 Enable support for optimised implementations of some commonly used NIST diff --git a/apps/build.info b/apps/build.info index 345f7079584a5..d6835829e4bac 100644 --- a/apps/build.info +++ b/apps/build.info @@ -18,6 +18,7 @@ $OPENSSLSRC=\ pkcs8.c pkey.c pkeyparam.c pkeyutl.c prime.c rand.c req.c \ s_client.c s_server.c s_time.c sess_id.c skeyutl.c smime.c speed.c \ spkac.c verify.c version.c x509.c rehash.c storeutl.c \ + ech.c \ list.c info.c fipsinstall.c pkcs12.c IF[{- !$disabled{'ec'} -}] $OPENSSLSRC=$OPENSSLSRC ec.c ecparam.c diff --git a/apps/ech.c b/apps/ech.c new file mode 100644 index 0000000000000..06f123bca682c --- /dev/null +++ b/apps/ech.c @@ -0,0 +1,199 @@ +/* + * 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 "apps.h" +#include "progs.h" +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#ifndef OPENSSL_NO_ECH + +# define OSSL_ECH_KEYGEN_MODE 0 /* default: generate a key pair/ECHConfig */ +# define OSSL_ECH_SELPRINT_MODE 1 /* we can print/down-select ECHConfigList */ + +# define PEM_SELECT_ALL -1 /* to indicate we're not downselecting another */ + +typedef enum OPTION_choice { + /* standard openssl options */ + OPT_ERR = -1, OPT_EOF = 0, OPT_HELP, OPT_VERBOSE, + OPT_PEMOUT, + /* ECHConfig specifics */ + OPT_PUBLICNAME, OPT_ECHVERSION, + OPT_MAXNAMELENGTH, OPT_HPKESUITE +} OPTION_CHOICE; + +const OPTIONS ech_options[] = { + OPT_SECTION("General options"), + {"help", OPT_HELP, '-', "Display this summary"}, + {"verbose", OPT_VERBOSE, '-', "Provide additional output"}, + OPT_SECTION("Key generation"), + {"pemout", OPT_PEMOUT, '>', + "Private key and ECHConfig [default echconfig.pem]"}, + {"public_name", OPT_PUBLICNAME, 's', "public_name value"}, + {"max_name_len", OPT_MAXNAMELENGTH, 'n', + "Maximum host name length value [default: 0]"}, + {"suite", OPT_HPKESUITE, 's', "HPKE ciphersuite: e.g. \"0x20,1,3\""}, + {"ech_version", OPT_ECHVERSION, 'n', + "ECHConfig version [default 0xff0d (13)]"}, + {NULL} +}; + +/** + * @brief map version string like 0xff01 or 65291 to uint16_t + * @param arg is the version string, from command line + * @return is the uint16_t value (with zero for error cases) + */ +static uint16_t verstr2us(char *arg) +{ + long lv = strtol(arg, NULL, 0); + uint16_t rv = 0; + + if (lv < 0xffff && lv > 0) { + rv = (uint16_t)lv; + } + return rv; +} + +int ech_main(int argc, char **argv) +{ + char *prog = NULL; + OPTION_CHOICE o; + int verbose = 0; + char *pemfile = NULL; + char *public_name = NULL; + char *suitestr = NULL; + uint16_t ech_version = OSSL_ECH_CURRENT_VERSION; + uint8_t max_name_length = 0; + OSSL_HPKE_SUITE hpke_suite = OSSL_HPKE_SUITE_DEFAULT; + int mode = OSSL_ECH_KEYGEN_MODE; /* key generation */ + + prog = opt_init(argc, argv, ech_options); + while ((o = opt_next()) != OPT_EOF) { + switch (o) { + case OPT_EOF: + case OPT_ERR: + BIO_printf(bio_err, "%s: Use -help for summary.\n", prog); + goto end; + case OPT_HELP: + opt_help(ech_options); + goto end; + case OPT_VERBOSE: + verbose = 1; + break; + case OPT_PEMOUT: + pemfile = opt_arg(); + break; + case OPT_PUBLICNAME: + public_name = opt_arg(); + break; + case OPT_ECHVERSION: + ech_version = verstr2us(opt_arg()); + break; + case OPT_MAXNAMELENGTH: + { + long tmp = strtol(opt_arg(), NULL, 10); + + if (tmp < 0 || tmp > OSSL_ECH_MAX_MAXNAMELEN) { + BIO_printf(bio_err, + "max name length out of range [0,%d] (%ld)\n", + OSSL_ECH_MAX_MAXNAMELEN, tmp); + goto opthelp; + } else { + max_name_length = (uint8_t)tmp; + } + } + break; + case OPT_HPKESUITE: + suitestr = opt_arg(); + break; + } + } + + argc = opt_num_rest(); + argv = opt_rest(); + if (argc != 0) { + BIO_printf(bio_err, "%s: Unknown parameter %s\n", prog, argv[0]); + goto opthelp; + } + + /* + * Check ECH-specific inputs + */ + switch (ech_version) { + case OSSL_ECH_RFCXXXX_VERSION: /* fall through */ + case 13: + ech_version = OSSL_ECH_RFCXXXX_VERSION; + break; + default: + BIO_printf(bio_err, "Un-supported version (0x%04x)\n", ech_version); + goto end; + } + + if (max_name_length > TLSEXT_MAXLEN_host_name) { + BIO_printf(bio_err, "Weird max name length (0x%04x) - biggest is " + "(0x%04x) - exiting\n", max_name_length, + TLSEXT_MAXLEN_host_name); + ERR_print_errors(bio_err); + goto end; + } + + if (suitestr != NULL) { + if (OSSL_HPKE_str2suite(suitestr, &hpke_suite) != 1) { + BIO_printf(bio_err, "Bad OSSL_HPKE_SUITE (%s)\n", suitestr); + ERR_print_errors(bio_err); + goto end; + } + } + + /* Set default if needed */ + if (pemfile == NULL) + pemfile = "echconfig.pem"; + + if (mode == OSSL_ECH_KEYGEN_MODE) { + OSSL_ECHSTORE *es = NULL; + BIO *ecf = NULL; + + if (verbose) + BIO_printf(bio_err, "Calling OSSL_ECHSTORE_new_config\n"); + if ((ecf = BIO_new_file(pemfile, "w")) == NULL + || (es = OSSL_ECHSTORE_new(NULL, NULL)) == NULL + || OSSL_ECHSTORE_new_config(es, ech_version, max_name_length, + public_name, hpke_suite) != 1 + || OSSL_ECHSTORE_write_pem(es, 0, ecf) != 1) { + BIO_printf(bio_err, "OSSL_ECHSTORE_new_config error\n"); + goto end; + } + if (verbose) + BIO_printf(bio_err, "OSSL_ECHSTORE_new_config success\n"); + OSSL_ECHSTORE_free(es); + BIO_free_all(ecf); + return 1; + } + +opthelp: + BIO_printf(bio_err, "%s: Use -help for summary.\n", prog); + goto end; + +end: + return 0; +} + +#endif diff --git a/apps/lib/s_cb.c b/apps/lib/s_cb.c index b567b179b685f..1741c6ccc15f1 100644 --- a/apps/lib/s_cb.c +++ b/apps/lib/s_cb.c @@ -788,6 +788,10 @@ static const STRINT_PAIR tlsext_types[] = { {"certificate authorities", TLSEXT_TYPE_certificate_authorities}, {"post handshake auth", TLSEXT_TYPE_post_handshake_auth}, {"early_data", TLSEXT_TYPE_early_data}, +#ifndef OPENSSL_NO_ECH + {"encrypted ClientHello (draft-13)", TLSEXT_TYPE_ech}, + {"outer exts", TLSEXT_TYPE_outer_extensions}, +#endif {NULL} }; diff --git a/apps/list.c b/apps/list.c index 2aeedbbda8959..373d9ce916113 100644 --- a/apps/list.c +++ b/apps/list.c @@ -1614,6 +1614,9 @@ static void list_disabled(void) #ifdef OPENSSL_NO_ZSTD BIO_puts(bio_out, "ZSTD\n"); #endif +#ifdef OPENSSL_NO_ECH + BIO_puts(bio_out, "ECH\n"); +#endif } /* Unified enum for help and list commands. */ diff --git a/crypto/err/openssl.txt b/crypto/err/openssl.txt index d8333369722f2..5019362551bed 100644 --- a/crypto/err/openssl.txt +++ b/crypto/err/openssl.txt @@ -1453,6 +1453,7 @@ SSL_R_DTLS_MESSAGE_TOO_BIG:334:dtls message too big SSL_R_DUPLICATE_COMPRESSION_ID:309:duplicate compression id SSL_R_ECC_CERT_NOT_FOR_SIGNING:318:ecc cert not for signing SSL_R_ECDH_REQUIRED_FOR_SUITEB_MODE:374:ecdh required for suiteb mode +SSL_R_ECH_REQUIRED:424:ech required SSL_R_EE_KEY_TOO_SMALL:399:ee key too small SSL_R_EMPTY_RAW_PUBLIC_KEY:349:empty raw public key SSL_R_EMPTY_SRTP_PROTECTION_PROFILE_LIST:354:empty srtp protection profile list diff --git a/crypto/ssl_err.c b/crypto/ssl_err.c index b791daf5489a9..a3703458574d3 100644 --- a/crypto/ssl_err.c +++ b/crypto/ssl_err.c @@ -156,6 +156,7 @@ static const ERR_STRING_DATA SSL_str_reasons[] = { "ecc cert not for signing"}, {ERR_PACK(ERR_LIB_SSL, 0, SSL_R_ECDH_REQUIRED_FOR_SUITEB_MODE), "ecdh required for suiteb mode"}, + {ERR_PACK(ERR_LIB_SSL, 0, SSL_R_ECH_REQUIRED), "ech required"}, {ERR_PACK(ERR_LIB_SSL, 0, SSL_R_EE_KEY_TOO_SMALL), "ee key too small"}, {ERR_PACK(ERR_LIB_SSL, 0, SSL_R_EMPTY_RAW_PUBLIC_KEY), "empty raw public key"}, diff --git a/demos/sslecho/Makefile b/demos/sslecho/Makefile index defb1597e1c76..79b0efe697207 100644 --- a/demos/sslecho/Makefile +++ b/demos/sslecho/Makefile @@ -4,7 +4,7 @@ # # LD_LIBRARY_PATH=../.. ./sslecho -TESTS = sslecho +TESTS = sslecho echecho CFLAGS = -I../../include -g -Wall LDFLAGS = -L../.. @@ -14,6 +14,8 @@ all: $(TESTS) sslecho: main.o +echecho: echecho.o + $(TESTS): $(CC) $(CFLAGS) $(LDFLAGS) -o $@ $< $(LDLIBS) diff --git a/demos/sslecho/README.md b/demos/sslecho/README.md index 58f7ca07245b4..8ef3e93b7484d 100644 --- a/demos/sslecho/README.md +++ b/demos/sslecho/README.md @@ -24,3 +24,48 @@ The cert.pem and key.pem files included are self signed certificates with the "Common Name" of 'localhost'. Best to create the 'pem' files using an actual hostname. + +Encrypted Client Hello (ECH) Variant +==================================== + +``echecho.c`` implements the same functionality but demonstrates minimal code +changes needed to use ECH. The ``echecho`` binary has the same user interface +discussed above but enables ECH for the connection, based on hard-coded ECH +configuration data. A real server would load file(s), and a real client would +acquire an ECHConfigList from the DNS. + +All that's required to use ECH is to load ECH data via `OSSL_ECHSTORE_read_*` +APIs and then enable ECH via ``SSL_CTX_set1_echstore()``. Both client and +server check and print out the status of ECH using ``SSL_ech_get1_status()``, +but that's optional. + +To run the server: + + $ LD_LIBRARY_PATH=../.. ./echecho s + +To run the client: + + $ LD_LIBRARY_PATH=../.. ./echecho c localhost + +All going well both server and client will print the ECH status at the +start of each connection. That looks like: + + ECH worked (status: 1, inner: localhost, outer: example.com) + +If the non-ECH demo client (``sslecho``) is used instead the server will +output: + + ECH failed/not-tried (status: -101, inner: (null), outer: (null)) + +If the non-ECH demo server (i.e., ``sslecho``) is used, the client will exit +with an error as ECH was attempted and failed. In a debug build, that looks +like: + + 80EBEE54227F0000:error:0A000163:SSL routines:tls_process_initial_server_flight:ech required:ssl/statem/statem_clnt.c:3274: + +A real client would likely fall back to not using ECH, but the above +is ok for a demo. + +In that case, the server will also exit based on the ECH alert from the client: + + 403787A8307F0000:error:0A000461:SSL routines:ssl3_read_bytes:reason(1121):../ssl/record/rec_layer_s3.c:1588:SSL alert number 121 diff --git a/doc/build.info b/doc/build.info index 36aa5b08bb5bc..013c483f03343 100644 --- a/doc/build.info +++ b/doc/build.info @@ -88,6 +88,12 @@ DEPEND[man/man1/openssl-ec.1]=man1/openssl-ec.pod GENERATE[man/man1/openssl-ec.1]=man1/openssl-ec.pod DEPEND[man1/openssl-ec.pod]{pod}=man1/openssl-ec.pod.in GENERATE[man1/openssl-ec.pod]=man1/openssl-ec.pod.in +DEPEND[html/man1/openssl-ech.html]=man1/openssl-ech.pod +GENERATE[html/man1/openssl-ech.html]=man1/openssl-ech.pod +DEPEND[man/man1/openssl-ech.1]=man1/openssl-ech.pod +GENERATE[man/man1/openssl-ech.1]=man1/openssl-ech.pod +DEPEND[man1/openssl-ech.pod]{pod}=man1/openssl-ech.pod.in +GENERATE[man1/openssl-ech.pod]=man1/openssl-ech.pod.in DEPEND[html/man1/openssl-ecparam.html]=man1/openssl-ecparam.pod GENERATE[html/man1/openssl-ecparam.html]=man1/openssl-ecparam.pod DEPEND[man/man1/openssl-ecparam.1]=man1/openssl-ecparam.pod @@ -374,6 +380,7 @@ html/man1/openssl-dhparam.html \ html/man1/openssl-dsa.html \ html/man1/openssl-dsaparam.html \ html/man1/openssl-ec.html \ +html/man1/openssl-ech.html \ html/man1/openssl-ecparam.html \ html/man1/openssl-enc.html \ html/man1/openssl-engine.html \ @@ -436,6 +443,7 @@ man/man1/openssl-dhparam.1 \ man/man1/openssl-dsa.1 \ man/man1/openssl-dsaparam.1 \ man/man1/openssl-ec.1 \ +man/man1/openssl-ech.1 \ man/man1/openssl-ecparam.1 \ man/man1/openssl-enc.1 \ man/man1/openssl-engine.1 \ @@ -2791,6 +2799,10 @@ DEPEND[html/man3/SSL_session_reused.html]=man3/SSL_session_reused.pod GENERATE[html/man3/SSL_session_reused.html]=man3/SSL_session_reused.pod DEPEND[man/man3/SSL_session_reused.3]=man3/SSL_session_reused.pod GENERATE[man/man3/SSL_session_reused.3]=man3/SSL_session_reused.pod +DEPEND[html/man3/SSL_set1_echstore.html]=man3/SSL_set1_echstore.pod +GENERATE[html/man3/SSL_set1_echstore.html]=man3/SSL_set1_echstore.pod +DEPEND[man/man3/SSL_set1_echstore.3]=man3/SSL_set1_echstore.pod +GENERATE[man/man3/SSL_set1_echstore.3]=man3/SSL_set1_echstore.pod DEPEND[html/man3/SSL_set1_host.html]=man3/SSL_set1_host.pod GENERATE[html/man3/SSL_set1_host.html]=man3/SSL_set1_host.pod DEPEND[man/man3/SSL_set1_host.3]=man3/SSL_set1_host.pod @@ -3757,6 +3769,7 @@ html/man3/SSL_read.html \ html/man3/SSL_read_early_data.html \ html/man3/SSL_rstate_string.html \ html/man3/SSL_session_reused.html \ +html/man3/SSL_set1_echstore.html \ html/man3/SSL_set1_host.html \ html/man3/SSL_set1_initial_peer_addr.html \ html/man3/SSL_set1_server_cert_type.html \ @@ -4431,6 +4444,7 @@ man/man3/SSL_read.3 \ man/man3/SSL_read_early_data.3 \ man/man3/SSL_rstate_string.3 \ man/man3/SSL_session_reused.3 \ +man/man3/SSL_set1_echstore.3 \ man/man3/SSL_set1_host.3 \ man/man3/SSL_set1_initial_peer_addr.3 \ man/man3/SSL_set1_server_cert_type.3 \ diff --git a/doc/designs/ech-api.md b/doc/designs/ech-api.md index 90fed89172999..eb78bbc25d019 100644 --- a/doc/designs/ech-api.md +++ b/doc/designs/ech-api.md @@ -173,25 +173,25 @@ 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. + 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. + 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. + 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.) + 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 -------------- @@ -206,7 +206,7 @@ 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); +OSSL_ECHSTORE *OSSL_ECHSTORE_new(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, @@ -226,7 +226,7 @@ int OSSL_ECHSTORE_num_keys(OSSL_ECHSTORE *es, int *numkeys); int OSSL_ECHSTORE_flush_keys(OSSL_ECHSTORE *es, time_t age); ``` -`OSSL_ECHSTORE_init()` and `OSSL_ECHSTORE_free()` are relatively obvious. +`OSSL_ECHSTORE_new()` 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. @@ -288,6 +288,7 @@ To access the `OSSL_ECHSTORE` associated with an `SSL_CTX` or 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. @@ -374,20 +375,20 @@ 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. + 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.) + 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. + 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. diff --git a/doc/man1/build.info b/doc/man1/build.info index 5c41a1687dd45..4a998af5348df 100644 --- a/doc/man1/build.info +++ b/doc/man1/build.info @@ -17,6 +17,7 @@ DEPEND[openssl-dsaparam.pod]=../perlvars.pm DEPEND[openssl-dsa.pod]=../perlvars.pm DEPEND[openssl-ecparam.pod]=../perlvars.pm DEPEND[openssl-ec.pod]=../perlvars.pm +DEPEND[openssl-ech.pod]=../perlvars.pm DEPEND[openssl-enc.pod]=../perlvars.pm DEPEND[openssl-engine.pod]=../perlvars.pm DEPEND[openssl-errstr.pod]=../perlvars.pm diff --git a/doc/man1/openssl-ech.pod.in b/doc/man1/openssl-ech.pod.in new file mode 100644 index 0000000000000..b7736d4b96fa2 --- /dev/null +++ b/doc/man1/openssl-ech.pod.in @@ -0,0 +1,94 @@ +=pod +{- OpenSSL::safe::output_do_not_edit_headers(); -} + +=head1 NAME + +openssl-ech - ECH key generation + +=head1 SYNOPSIS + +B B +[B<-help>] +[B<-verbose>] +[B<-pemout> I] +[B<-public_name> I] +[B<-max_name_len> I] +[B<-suite> I] +[B<-ech_version> I] + +=head1 DESCRIPTION + +The L command generates Encrypted Client Hello (ECH) private keys +and public keys in the ECHConfig format. + +The "ECHConfig PEM file" format mentioned below is specified in +L and consists of +one private key in PKCS#8 format and a base64 encoded ECHConfig containing one +matching public value. + +=head1 OPTIONS + +=over 4 + +=item B<-help> + +Print out a usage message. + +=item B<-verbose> + +Print more verbosely. + +=item B<-pemout> I + +Name of output ECHConfig PEM file. + +=item B<-public_name> I + +The DNS name to use in the "public_name" field of the ECHConfig. + +=item B<-max_name_len> I + +Maximum name length field value to use in the ECHConfig. + +=item B<-suite> I + +HPKE suite to use in the ECHConfig. + +=item B<-ech_version> I + +The ECH version to use in the ECHConfig. Only 0xfe0d is supported in this version. + +=back + +=head1 NOTES + +Ciphersuites are specified using a comma-separated list of IANA-registered +codes/numbers e.g. "-c 0x20,1,3" or a comma-separated list of strings from: +- KEMs: p256, p384, p521, x25519, x448 +- KDFs: hkdf-sha256, hkdf-sha384, hkdf-sha512 +- AEADs: aes128gcm, aes256gcm, chachapoly1305 + +For example the default is: x25519, hkdf-sha256, aes128gcm +See L for details. + +=head1 SEE ALSO + +L, +L, +L, +L + +=head1 HISTORY + +This functionality described here was added in OpenSSL 3.5. + +=head1 COPYRIGHT + +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 +L. + +=cut diff --git a/doc/man1/openssl.pod b/doc/man1/openssl.pod index edef2ff598948..9fc416d3cbcee 100644 --- a/doc/man1/openssl.pod +++ b/doc/man1/openssl.pod @@ -123,6 +123,10 @@ L and L. EC (Elliptic curve) key processing. +=item B + +Encrypted Client Hello (ECH) admin. See L. + =item B EC parameter manipulation and generation. diff --git a/doc/man3/SSL_CTX_set_options.pod b/doc/man3/SSL_CTX_set_options.pod index d78bdd5a31038..21c33e22345e2 100644 --- a/doc/man3/SSL_CTX_set_options.pod +++ b/doc/man3/SSL_CTX_set_options.pod @@ -370,6 +370,30 @@ only understands up to SSLv3. In this case the client must still use the same SSLv3.1=TLSv1 announcement. Some clients step down to SSLv3 with respect to the server's answer and violate the version rollback protection.) +=item SSL_OP_ECH_GREASE + +If set, TLS ClientHello messages emitted by the client will include GREASE +Encrypted ClientHello (ECH) extension values, if ECH is not really being +attempted. + +=item SSL_OP_ECH_TRIALDECRYPT + +If set, servers will attempt to decrypt ECH extensions using all loaded +ECH key pairs. By default, servers will only attempt decryption using +an ECH key pair that matches the config_id in the ECH extension value +received. + +=item SSL_OP_ECH_GREASE_RETRY_CONFIG + +If set, servers will add GREASEy ECHConfig values to those sent to the +client after the client GREASEd or the client tried and failed to use +ECH. + +=item SSL_OP_ECH_IGNORED_CID + +If set, TLS ClientHello messages emitted by the client will ignore the +ECHConfig config_id chosen by the server and use a random octet. + =back The following options no longer have any effect but their identifiers are diff --git a/doc/man3/SSL_set1_echstore.pod b/doc/man3/SSL_set1_echstore.pod new file mode 100644 index 0000000000000..0dd889b1ba927 --- /dev/null +++ b/doc/man3/SSL_set1_echstore.pod @@ -0,0 +1,187 @@ +=pod + +=head1 NAME + +SSL_set1_echstore, +OSSL_ECHSTORE_new, OSSL_ECHSTORE_free, +OSSL_ECHSTORE_new_config, OSSL_ECHSTORE_write_pem, +OSSL_ECHSTORE_read_echconfiglist, OSSL_ECHSTORE_get1_info, +OSSL_ECHSTORE_downselect, OSSL_ECHSTORE_set1_key_and_read_pem, +OSSL_ECHSTORE_read_pem, OSSL_ECHSTORE_num_keys, OSSL_ECHSTORE_flush_keys, +OSSL_ECH_INFO_free, OSSL_ECH_INFO_print, SSL_CTX_set1_echstore, +SSL_CTX_get1_echstore, SSL_get1_echstore, SSL_ech_set_server_names, +SSL_ech_set_outer_server_name, SSL_ech_set_outer_alpn_protos, +SSL_ech_get1_status, SSL_ech_set_grease_suite, SSL_ech_set_grease_type, +SSL_ech_set_callback, SSL_ech_get_retry_config, +SSL_CTX_ech_set_outer_alpn_protos, SSL_CTX_ech_raw_decrypt, +SSL_CTX_ech_set_callback +- Encrypted Client Hello (ECH) functions + +=head1 SYNOPSIS + + #include + + OSSL_ECHSTORE *OSSL_ECHSTORE_new(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, uint16_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); + 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_CTX_set1_echstore(SSL_CTX *ctx, OSSL_ECHSTORE *es); + int SSL_set1_echstore(SSL *s, OSSL_ECHSTORE *es); + OSSL_ECHSTORE *SSL_CTX_get1_echstore(const SSL_CTX *ctx); + OSSL_ECHSTORE *SSL_get1_echstore(const SSL *s); + int SSL_ech_set_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, + const size_t protos_len); + int SSL_ech_get1_status(SSL *s, char **inner_sni, char **outer_sni); + int SSL_ech_set_grease_suite(SSL *s, const char *suite); + int SSL_ech_set_grease_type(SSL *s, uint16_t type); + void SSL_ech_set_callback(SSL *s, SSL_ech_cb_func f); + int SSL_ech_get_retry_config(SSL *s, unsigned char **ec, size_t *eclen); + int SSL_CTX_ech_set_outer_alpn_protos(SSL_CTX *s, const unsigned char *protos, + const size_t protos_len); + int SSL_CTX_ech_raw_decrypt(SSL_CTX *ctx, + int *decrypted_ok, + char **inner_sni, char **outer_sni, + unsigned char *outer_ch, size_t outer_len, + unsigned char *inner_ch, size_t *inner_len, + unsigned char **hrrtok, size_t *toklen); + void SSL_CTX_ech_set_callback(SSL_CTX *ctx, SSL_ech_cb_func f); + +=head1 DESCRIPTION + +TODO(ECH): Text is TBD, this is just enough for the build. + +Mention SSL_set1_echstore() is a thing +Mention OSSL_ECHSTORE_new() is a thing +Mention OSSL_ECHSTORE_free() is a thing +Mention OSSL_ECHSTORE_new_config() is a thing +Mention OSSL_ECHSTORE_write_pem() is a thing +Mention OSSL_ECHSTORE_read_echconfiglist() is a thing +Mention OSSL_ECHSTORE_get1_info() is a thing +Mention OSSL_ECHSTORE_downselect() is a thing +Mention OSSL_ECHSTORE_set1_key_and_read_pem() is a thing +Mention OSSL_ECHSTORE_read_pem() is a thing +Mention OSSL_ECHSTORE_num_keys() is a thing +Mention OSSL_ECHSTORE_flush_keys() is a thing +Mention OSSL_ECH_INFO_free() is a thing +Mention OSSL_ECH_INFO_print() is a thing +Mention SSL_CTX_set1_echstore() is a thing +Mention SSL_CTX_get1_echstore() is a thing +Mention SSL_get1_echstore() is a thing +Mention SSL_ech_set_server_names() is a thing +Mention SSL_ech_set_outer_server_name() is a thing +Mention SSL_ech_set_outer_alpn_protos() is a thing +Mention SSL_ech_get1_status() is a thing +Mention SSL_ech_set_grease_suite() is a thing +Mention SSL_ech_set_grease_type() is a thing +Mention SSL_ech_set_callback() is a thing +Mention SSL_ech_get_retry_config() is a thing +Mention SSL_CTX_ech_set_outer_alpn_protos() is a thing +Mention SSL_CTX_ech_raw_decrypt() is a thing +Mention SSL_CTX_ech_set_callback() is a thing + +=head2 Callback Function + +Applications can set a callback function that will be called when the +outcome from an attempt at ECH has been determined. On the server, +that happens early, as part of construction of the ServerHello message. +On the client, the callback will happen after the SeverHello has +been processed. In the event of HelloRetryRequest, the callback will +only be triggered when processing the second ServerHello. The callback +function will be triggered even if the client is only GREASEing. + +The callback function prototype is: + + typedef unsigned int (*SSL_ech_cb_func)(SSL *s, const char *str); + +To set a callback function use SSL_ech_set_callback() or +SSL_CTX_ech_set_callback() - the I input should match the +above prototype. + +When the callback function is called, the I will point at a string +intended for logging describing the state of ECH processing. +Applications should not attempt to parse that string as the value depends +on compile time settings, local configuration and the specific processing +that happened prior to the callback. Applications that need to branch based +on the outcome of ECH processing should instead make a call to +SSL_ech_get1_status() from within their callback function. + +An example string I as seen on a client might be: + + ech_attempted=1 + ech_attempted_type=0xfe0d + ech_atttempted_cid=0x5d + ech_done=1 + ech_grease=0 + ech_returned_len=0 + ech_backend=0 + ech_success=1 + 2 ECHConfig values loaded + cfg(0): [fe0d,5d,cover.defo.ie,0020,[0001,0001],190984309c1a24cb944c005eb79d9c72ca9a4a979194b553dfd0bffc6b5c152d,00,00] + cfg(1): [fe0d,fd,cover.defo.ie,0020,[0001,0001],46dd4e2c81bb15ef9d194c99b86983844e2a1387e4fb7e7d3b8d368c8e1b4d2a,00,00] + +=head1 RETURN VALUES + +SSL_set1_echstore() returns zero on error +OSSL_ECHSTORE_new() returns zero on error +OSSL_ECHSTORE_free() returns zero on error +OSSL_ECHSTORE_new_config() returns zero on error +OSSL_ECHSTORE_write_pem() returns zero on error +OSSL_ECHSTORE_read_echconfiglist() returns zero on error +OSSL_ECHSTORE_get1_info() returns zero on error +OSSL_ECHSTORE_downselect() returns zero on error +OSSL_ECHSTORE_set1_key_and_read_pem() returns zero on error +OSSL_ECHSTORE_read_pem() returns zero on error +OSSL_ECHSTORE_num_keys() returns zero on error +OSSL_ECHSTORE_flush_keys() returns zero on error +OSSL_ECH_INFO_free() returns zero on error +OSSL_ECH_INFO_print() returns zero on error +SSL_CTX_set1_echstore() returns zero on error +SSL_CTX_get1_echstore() returns zero on error +SSL_get1_echstore() returns zero on error +SSL_ech_set_server_names() returns zero on error +SSL_ech_set_outer_server_name() returns zero on error +SSL_ech_set_outer_alpn_protos() returns zero on error +SSL_ech_get1_status() returns zero on error +SSL_ech_set_grease_suite() returns zero on error +SSL_ech_set_grease_type() returns zero on error +SSL_ech_set_callback() returns zero on error +SSL_ech_get_retry_config() returns zero on error +SSL_CTX_ech_set_outer_alpn_protos() returns zero on error +SSL_CTX_ech_raw_decrypt() returns zero on error +SSL_CTX_ech_set_callback() returns zero on error + +=head1 SEE ALSO + +The Encrypted ClientHello specification: L +TODO(ECH) update link to RFC. + +=head1 HISTORY + +This functionality described here was added in OpenSSL 3.4. + +=head1 COPYRIGHT + +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 +L. + +=cut diff --git a/include/openssl/ech.h b/include/openssl/ech.h new file mode 100644 index 0000000000000..f79a1e9ae1783 --- /dev/null +++ b/include/openssl/ech.h @@ -0,0 +1,137 @@ +/* + * Copyright 2024 The OpenSSL Project Authors. All Rights Reserved. + * + * Licensed under the OpenSSL license (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 + */ + +/* + * Externally-visible data structures and prototypes for handling + * Encrypted ClientHello (ECH). + */ +#ifndef OPENSSL_ECH_H +# define OPENSSL_ECH_H +# pragma once + +# include +# include + +# ifndef OPENSSL_NO_ECH + +/* + * Some externally visible limits - most used for sanity checks that could be + * bigger if needed, but that work for now + */ +# define OSSL_ECH_MAX_PAYLOAD_LEN 1500 /* max ECH ciphertext to en/decode */ +# define OSSL_ECH_MIN_ECHCONFIG_LEN 32 /* min for all encodings */ +# define OSSL_ECH_MAX_ECHCONFIG_LEN 1500 /* max for all encodings */ +# define OSSL_ECH_MAX_ECHCONFIGEXT_LEN 512 /* ECHConfig extension max */ +# define OSSL_ECH_MAX_MAXNAMELEN 255 /* ECHConfig max for max name length */ +# define OSSL_ECH_MAX_PUBLICNAME 255 /* max ECHConfig public name length */ +# define OSSL_ECH_MAX_ALPNLEN 255 /* max alpn length */ +# define OSSL_ECH_OUTERS_MAX 20 /* max extensions we compress via outer-exts */ +# define OSSL_ECH_ALLEXTS_MAX 32 /* max total number of extension we allow */ + +/* + * ECH version. We only support RFC XXXX as of now. As/if new ECHConfig + * versions are added, those will be noted here. + * TODO(ECH): Replace XXXX with the actual RFC number once known. + */ +# define OSSL_ECH_RFCXXXX_VERSION 0xfe0d /* official ECHConfig version */ +/* latest version from an RFC */ +# define OSSL_ECH_CURRENT_VERSION OSSL_ECH_RFCXXXX_VERSION + +/* Return codes from SSL_ech_get1_status */ +# define SSL_ECH_STATUS_BACKEND 4 /* ECH backend: 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 */ +# define SSL_ECH_STATUS_SUCCESS 1 /* Success */ +# define SSL_ECH_STATUS_FAILED 0 /* Some internal or protocol error */ +# define SSL_ECH_STATUS_BAD_CALL -100 /* Some in/out arguments were NULL */ +# define SSL_ECH_STATUS_NOT_TRIED -101 /* ECH wasn't attempted */ +# define SSL_ECH_STATUS_BAD_NAME -102 /* ECH ok but server cert bad */ +# define SSL_ECH_STATUS_NOT_CONFIGURED -103 /* ECH wasn't configured */ +# define SSL_ECH_STATUS_FAILED_ECH -105 /* We tried, failed and got an ECH, from a good name */ +# define SSL_ECH_STATUS_FAILED_ECH_BAD_NAME -106 /* We tried, failed and got an ECH, from a bad name */ + +/* if a caller wants to index the last entry in the store */ +# define OSSL_ECHSTORE_LAST -1 + +/* + * 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 */ + time_t seconds_in_memory; /* number of seconds since this was loaded */ + 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; + +/* Values for the for_retry inputs */ +# define SSL_ECH_USE_FOR_RETRY 1 +# define SSL_ECH_NOT_FOR_RETRY 0 + +/* + * API calls built around OSSL_ECHSTORE + */ +OSSL_ECHSTORE *OSSL_ECHSTORE_new(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); + +void OSSL_ECH_INFO_free(OSSL_ECH_INFO *info, int count); +int OSSL_ECH_INFO_print(BIO *out, OSSL_ECH_INFO *info, int count); + +/* + * APIs relating OSSL_ECHSTORE to SSL/SSL_CTX + */ +int SSL_CTX_set1_echstore(SSL_CTX *ctx, OSSL_ECHSTORE *es); +int SSL_set1_echstore(SSL *s, OSSL_ECHSTORE *es); + +OSSL_ECHSTORE *SSL_CTX_get1_echstore(const SSL_CTX *ctx); +OSSL_ECHSTORE *SSL_get1_echstore(const SSL *s); + +int SSL_ech_set_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, + const size_t protos_len); + +int SSL_ech_get1_status(SSL *s, char **inner_sni, char **outer_sni); +int SSL_ech_set_grease_suite(SSL *s, const char *suite); +int SSL_ech_set_grease_type(SSL *s, uint16_t type); +typedef unsigned int (*SSL_ech_cb_func)(SSL *s, const char *str); +void SSL_ech_set_callback(SSL *s, SSL_ech_cb_func f); +int SSL_ech_get_retry_config(SSL *s, unsigned char **ec, size_t *eclen); + +int SSL_CTX_ech_set_outer_alpn_protos(SSL_CTX *s, const unsigned char *protos, + const size_t protos_len); +int SSL_CTX_ech_raw_decrypt(SSL_CTX *ctx, + int *decrypted_ok, + char **inner_sni, char **outer_sni, + unsigned char *outer_ch, size_t outer_len, + unsigned char *inner_ch, size_t *inner_len, + unsigned char **hrrtok, size_t *toklen); +void SSL_CTX_ech_set_callback(SSL_CTX *ctx, SSL_ech_cb_func f); + +# endif +#endif diff --git a/include/openssl/pem.h b/include/openssl/pem.h index de1b6581f28f6..214c6cd5b9da5 100644 --- a/include/openssl/pem.h +++ b/include/openssl/pem.h @@ -60,6 +60,7 @@ extern "C" { # define PEM_STRING_SM2PRIVATEKEY "SM2 PRIVATE KEY" # define PEM_STRING_SM2PARAMETERS "SM2 PARAMETERS" # define PEM_STRING_ACERT "ATTRIBUTE CERTIFICATE" +# define PEM_STRING_ECHCONFIG "ECHCONFIG" # define PEM_TYPE_ENCRYPTED 10 # define PEM_TYPE_MIC_ONLY 20 diff --git a/include/openssl/ssl.h.in b/include/openssl/ssl.h.in index cb30dda92d2c8..f9f825a253570 100644 --- a/include/openssl/ssl.h.in +++ b/include/openssl/ssl.h.in @@ -44,6 +44,9 @@ use OpenSSL::stackhash qw(generate_stack_macros generate_const_stack_macros); # include # include # include +# ifndef OPENSSL_NO_ECH +# include +# endif # ifndef OPENSSL_NO_STDIO # include # endif @@ -429,6 +432,28 @@ typedef int (*SSL_async_callback_fn)(SSL *s, void *arg); # define SSL_OP_PREFER_NO_DHE_KEX SSL_OP_BIT(35) # define SSL_OP_LEGACY_EC_POINT_FORMATS SSL_OP_BIT(36) + +#ifndef OPENSSL_NO_ECH +/* Set this to tell client to emit greased ECH values */ +# define SSL_OP_ECH_GREASE SSL_OP_BIT(37) +/* + * If this is set then the server side will attempt trial decryption + * of ECHs even if there is no matching ECH config_id. That's a bit + * inefficient, but more privacy friendly. + */ +# define SSL_OP_ECH_TRIALDECRYPT SSL_OP_BIT(38) +/* + * If set, clients will ignore the supplied ECH config_id and replace + * that with a random value. + */ +# define SSL_OP_ECH_IGNORE_CID SSL_OP_BIT(39) +/* + * If set, servers will add GREASEy ECHConfig values to those sent + * in retry_configs. + */ +# define SSL_OP_ECH_GREASE_RETRY_CONFIG SSL_OP_BIT(40) +#endif + /* * Option "collections." */ @@ -1194,6 +1219,9 @@ DECLARE_PEM_rw(SSL_SESSION, SSL_SESSION) # define SSL_AD_NO_RENEGOTIATION TLS1_AD_NO_RENEGOTIATION # define SSL_AD_MISSING_EXTENSION TLS13_AD_MISSING_EXTENSION # define SSL_AD_CERTIFICATE_REQUIRED TLS13_AD_CERTIFICATE_REQUIRED +# ifndef OPENSSL_NO_ECH +# define SSL_AD_ECH_REQUIRED TLS1_AD_ECH_REQUIRED +# endif # define SSL_AD_UNSUPPORTED_EXTENSION TLS1_AD_UNSUPPORTED_EXTENSION # define SSL_AD_CERTIFICATE_UNOBTAINABLE TLS1_AD_CERTIFICATE_UNOBTAINABLE # define SSL_AD_UNRECOGNIZED_NAME TLS1_AD_UNRECOGNIZED_NAME diff --git a/include/openssl/sslerr.h b/include/openssl/sslerr.h index 53e0decbe6768..bee7aa7169e01 100644 --- a/include/openssl/sslerr.h +++ b/include/openssl/sslerr.h @@ -113,6 +113,7 @@ # define SSL_R_DUPLICATE_COMPRESSION_ID 309 # define SSL_R_ECC_CERT_NOT_FOR_SIGNING 318 # define SSL_R_ECDH_REQUIRED_FOR_SUITEB_MODE 374 +# define SSL_R_ECH_REQUIRED 424 # define SSL_R_EE_KEY_TOO_SMALL 399 # define SSL_R_EMPTY_RAW_PUBLIC_KEY 349 # define SSL_R_EMPTY_SRTP_PROTECTION_PROFILE_LIST 354 diff --git a/include/openssl/tls1.h b/include/openssl/tls1.h index 50910d0e4c0b5..0935828ddd0ae 100644 --- a/include/openssl/tls1.h +++ b/include/openssl/tls1.h @@ -78,6 +78,9 @@ extern "C" { # define TLS1_AD_BAD_CERTIFICATE_HASH_VALUE 114 # define TLS1_AD_UNKNOWN_PSK_IDENTITY 115/* fatal */ # define TLS1_AD_NO_APPLICATION_PROTOCOL 120 /* fatal */ +# ifndef OPENSSL_NO_ECH +# define TLS1_AD_ECH_REQUIRED 121 /* fatal */ +# endif /* ExtensionType values from RFC3546 / RFC4366 / RFC6066 */ # define TLSEXT_TYPE_server_name 0 @@ -168,6 +171,11 @@ extern "C" { # define TLSEXT_TYPE_next_proto_neg 13172 # endif +# ifndef OPENSSL_NO_ECH +# define TLSEXT_TYPE_ech 0xfe0d +# define TLSEXT_TYPE_outer_extensions 0xfd00 +# endif + /* NameType value from RFC3546 */ # define TLSEXT_NAMETYPE_host_name 0 /* status request value from RFC3546 */ diff --git a/include/openssl/types.h b/include/openssl/types.h index a83811e748510..8cb13af0b0a93 100644 --- a/include/openssl/types.h +++ b/include/openssl/types.h @@ -235,6 +235,12 @@ typedef struct ossl_decoder_ctx_st OSSL_DECODER_CTX; typedef struct ossl_self_test_st OSSL_SELF_TEST; +#ifndef OPENSSL_NO_ECH +/* opaque type for ECH related information */ +typedef struct ossl_echstore_st OSSL_ECHSTORE; +#endif + + #ifdef __cplusplus } #endif diff --git a/ssl/build.info b/ssl/build.info index 7f4ecaa68f50b..6d9834b304125 100644 --- a/ssl/build.info +++ b/ssl/build.info @@ -16,6 +16,7 @@ SOURCE[../libssl]=\ bio_ssl.c ssl_err_legacy.c tls_srp.c t1_trce.c ssl_utst.c \ statem/statem.c \ ssl_cert_comp.c \ + ech.c \ tls_depr.c # For shared builds we need to include the libcrypto packet.c and quic_vlint.c diff --git a/ssl/ech.c b/ssl/ech.c new file mode 100644 index 0000000000000..5b0d5878ef4a8 --- /dev/null +++ b/ssl/ech.c @@ -0,0 +1,467 @@ +/* + * Copyright 2024 The OpenSSL Project Authors. All Rights Reserved. + * + * Licensed under the OpenSSL license (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 "ssl_local.h" +#include "ech_local.h" +#include "statem/statem_local.h" +#include +#include +#include +#include + +#ifndef OPENSSL_NO_ECH + +/* a size for some crypto vars */ +# define OSSL_ECH_CRYPTO_VAR_SIZE 2048 + +/* + * @brief hash a buffer as a pretend file name being ascii-hex of hashed buffer + * @param es is the OSSL_ECHSTORE we're dealing with + * @param buf is the input buffer + * @param blen is the length of buf + * @param ah_hash is a pointer to where to put the result + * @param ah_len is the length of ah_hash + */ +static int ech_hash_pub_as_fname(OSSL_ECHSTORE *es, + const unsigned char *buf, size_t blen, + char *ah_hash, size_t ah_len) +{ + unsigned char hashval[EVP_MAX_MD_SIZE]; + size_t hashlen, actual_ah_len; + + if (es == NULL + || EVP_Q_digest(es->libctx, "SHA2-256", es->propq, + buf, blen, hashval, &hashlen) != 1 + || OPENSSL_buf2hexstr_ex(ah_hash, ah_len, &actual_ah_len, + hashval, hashlen, '\0') != 1) { + ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR); + return 0; + } + return 1; +} + +/* + * API calls built around OSSL_ECHSTORE + */ + +OSSL_ECHSTORE *OSSL_ECHSTORE_new(OSSL_LIB_CTX *libctx, const char *propq) +{ + OSSL_ECHSTORE *es = NULL; + + es = OPENSSL_zalloc(sizeof(*es)); + if (es == NULL) { + ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR); + return 0; + } + es->libctx = libctx; + es->propq = propq; + return es; +} + +static void ossl_echext_free(OSSL_ECHEXT *e) +{ + if (e == NULL) + return; + OPENSSL_free(e->val); + OPENSSL_free(e); + return; +} + +static void ossl_echstore_entry_free(OSSL_ECHSTORE_ENTRY *ee) +{ + if (ee == NULL) + return; + OPENSSL_free(ee->public_name); + OPENSSL_free(ee->pub); + OPENSSL_free(ee->pemfname); + EVP_PKEY_free(ee->keyshare); + OPENSSL_free(ee->encoded); + OPENSSL_free(ee->suites); + sk_OSSL_ECHEXT_pop_free(ee->exts, ossl_echext_free); + OPENSSL_free(ee); + return; +} + +void OSSL_ECHSTORE_free(OSSL_ECHSTORE *es) +{ + if (es == NULL) + return; + sk_OSSL_ECHSTORE_ENTRY_pop_free(es->entries, ossl_echstore_entry_free); + OPENSSL_free(es); + return; +} + +int OSSL_ECHSTORE_new_config(OSSL_ECHSTORE *es, + uint16_t echversion, uint8_t max_name_length, + const char *public_name, OSSL_HPKE_SUITE suite) +{ + size_t pnlen = 0; + size_t publen = OSSL_ECH_CRYPTO_VAR_SIZE; + unsigned char pub[OSSL_ECH_CRYPTO_VAR_SIZE]; + int rv = 0; + unsigned char *bp = NULL; + size_t bblen = 0; + EVP_PKEY *privp = NULL; + uint8_t config_id = 0; + WPACKET epkt; + BUF_MEM *epkt_mem = NULL; + OSSL_ECHSTORE_ENTRY *ee = NULL; + char pembuf[2 * EVP_MAX_MD_SIZE + 1]; + size_t pembuflen = 2 * EVP_MAX_MD_SIZE + 1; + + /* basic checks */ + if (es == NULL) { + ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_NULL_PARAMETER); + return 0; + } + pnlen = (public_name == NULL ? 0 : strlen(public_name)); + if (pnlen == 0 || pnlen > OSSL_ECH_MAX_PUBLICNAME + || max_name_length > OSSL_ECH_MAX_MAXNAMELEN) { + ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_INVALID_ARGUMENT); + return 0; + } + /* this used have more versions and will again in future */ + switch (echversion) { + case OSSL_ECH_RFCXXXX_VERSION: + break; + default: + ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_INVALID_ARGUMENT); + return 0; + } + + /* so WPACKET_cleanup() won't go wrong */ + memset(&epkt, 0, sizeof(epkt)); + /* random config_id */ + if (RAND_bytes_ex(es->libctx, (unsigned char *)&config_id, 1, + RAND_DRBG_STRENGTH) <= 0) { + ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR); + goto err; + } + /* key pair */ + if (OSSL_HPKE_keygen(suite, pub, &publen, &privp, NULL, 0, + es->libctx, es->propq) != 1) { + ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR); + goto err; + } + /* + * Reminder, for draft-13 we want this: + * + * opaque HpkePublicKey<1..2^16-1>; + * uint16 HpkeKemId; // Defined in I-D.irtf-cfrg-hpke + * uint16 HpkeKdfId; // Defined in I-D.irtf-cfrg-hpke + * uint16 HpkeAeadId; // Defined in I-D.irtf-cfrg-hpke + * struct { + * HpkeKdfId kdf_id; + * HpkeAeadId aead_id; + * } HpkeSymmetricCipherSuite; + * struct { + * uint8 config_id; + * HpkeKemId kem_id; + * HpkePublicKey public_key; + * HpkeSymmetricCipherSuite cipher_suites<4..2^16-4>; + * } HpkeKeyConfig; + * struct { + * HpkeKeyConfig key_config; + * uint8 maximum_name_length; + * opaque public_name<1..255>; + * Extension extensions<0..2^16-1>; + * } ECHConfigContents; + * struct { + * uint16 version; + * uint16 length; + * select (ECHConfig.version) { + * case 0xfe0d: ECHConfigContents contents; + * } + * } ECHConfig; + * ECHConfig ECHConfigList<1..2^16-1>; + */ + if ((epkt_mem = BUF_MEM_new()) == NULL + || !BUF_MEM_grow(epkt_mem, OSSL_ECH_MAX_ECHCONFIG_LEN)) { + ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR); + goto err; + } + /* config id, KEM, public, KDF, AEAD, max name len, public_name, exts */ + if (!WPACKET_init(&epkt, epkt_mem) + || (bp = WPACKET_get_curr(&epkt)) == NULL + || !WPACKET_start_sub_packet_u16(&epkt) + || !WPACKET_put_bytes_u16(&epkt, echversion) + || !WPACKET_start_sub_packet_u16(&epkt) + || !WPACKET_put_bytes_u8(&epkt, config_id) + || !WPACKET_put_bytes_u16(&epkt, suite.kem_id) + || !WPACKET_start_sub_packet_u16(&epkt) + || !WPACKET_memcpy(&epkt, pub, publen) + || !WPACKET_close(&epkt) + || !WPACKET_start_sub_packet_u16(&epkt) + || !WPACKET_put_bytes_u16(&epkt, suite.kdf_id) + || !WPACKET_put_bytes_u16(&epkt, suite.aead_id) + || !WPACKET_close(&epkt) + || !WPACKET_put_bytes_u8(&epkt, max_name_length) + || !WPACKET_start_sub_packet_u8(&epkt) + || !WPACKET_memcpy(&epkt, public_name, pnlen) + || !WPACKET_close(&epkt) + || !WPACKET_start_sub_packet_u16(&epkt) + || !WPACKET_memcpy(&epkt, NULL, 0) /* no extensions */ + || !WPACKET_close(&epkt) + || !WPACKET_close(&epkt) + || !WPACKET_close(&epkt)) { + ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR); + goto err; + } + /* bp, bblen has encoding */ + WPACKET_get_total_written(&epkt, &bblen); + if ((ee = OPENSSL_zalloc(sizeof(*ee))) == NULL) { + ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR); + goto err; + } + ee->suites = OPENSSL_malloc(sizeof(OSSL_HPKE_SUITE)); + if (ee->suites == NULL) { + ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR); + goto err; + } + if (ech_hash_pub_as_fname(es, pub, publen, pembuf, pembuflen) != 1) { + ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR); + goto err; + } + ee->version = echversion; + ee->pub_len = publen; + ee->pub = OPENSSL_memdup(pub, publen); + if (ee->pub == NULL) { + ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR); + goto err; + } + ee->nsuites = 1; + ee->suites[0] = suite; + ee->public_name = OPENSSL_strdup(public_name); + if (ee->public_name == NULL) { + ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR); + goto err; + } + ee->max_name_length = max_name_length; + ee->config_id = config_id; + ee->keyshare = privp; + ee->encoded = OPENSSL_memdup(bp, bblen); + if (ee->encoded == NULL) { + ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR); + goto err; + } + ee->encoded_len = bblen; + ee->pemfname = OPENSSL_strdup(pembuf); + if (ee->pemfname == NULL) { + ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR); + goto err; + } + ee->loadtime = time(0); + /* push entry into store */ + if (es->entries == NULL) + es->entries = sk_OSSL_ECHSTORE_ENTRY_new_null(); + if (es->entries == NULL) { + ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR); + goto err; + } + if (!sk_OSSL_ECHSTORE_ENTRY_push(es->entries, ee)) { + ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR); + goto err; + } + WPACKET_finish(&epkt); + BUF_MEM_free(epkt_mem); + return 1; + +err: + EVP_PKEY_free(privp); + WPACKET_cleanup(&epkt); + BUF_MEM_free(epkt_mem); + ossl_echstore_entry_free(ee); + OPENSSL_free(ee); + return rv; +} + +int OSSL_ECHSTORE_write_pem(OSSL_ECHSTORE *es, int index, BIO *out) +{ + OSSL_ECHSTORE_ENTRY *ee = NULL; + int rv = 0, num = 0, chosen = 0; + + if (es == NULL) { + /* + * TODO(ECH): this is a bit of a bogus error, just so as + * to get the `make update` command to add the required + * error number. We don't need it yet, but it's involved + * in some of the build artefacts, so may as well jump + * the gun a bit on it. + */ + ERR_raise(ERR_LIB_SSL, SSL_R_ECH_REQUIRED); + return 0; + } + num = sk_OSSL_ECHSTORE_ENTRY_num(es->entries); + if (num <= 0) { + ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_INVALID_ARGUMENT); + return 0; + } + if (index >= num) { + ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_INVALID_ARGUMENT); + return 0; + } + if (index == OSSL_ECHSTORE_LAST) + chosen = num - 1; + else + chosen = index; + ee = sk_OSSL_ECHSTORE_ENTRY_value(es->entries, chosen); + if (ee == NULL || ee->keyshare == NULL || ee->encoded == NULL) { + ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_INVALID_ARGUMENT); + return 0; + } + /* private key first */ + if (!PEM_write_bio_PrivateKey(out, ee->keyshare, NULL, NULL, 0, + NULL, NULL)) { + ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR); + goto err; + } + if (PEM_write_bio(out, PEM_STRING_ECHCONFIG, NULL, + ee->encoded, ee->encoded_len) <= 0) { + ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR); + goto err; + } + rv = 1; +err: + return rv; +} + +int OSSL_ECHSTORE_read_echconfiglist(OSSL_ECHSTORE *es, BIO *in) +{ + return 0; +} + +int OSSL_ECHSTORE_get1_info(OSSL_ECHSTORE *es, OSSL_ECH_INFO **info, + int *count) +{ + return 0; +} + +int OSSL_ECHSTORE_downselect(OSSL_ECHSTORE *es, int index) +{ + return 0; +} + +int OSSL_ECHSTORE_set1_key_and_read_pem(OSSL_ECHSTORE *es, EVP_PKEY *priv, + BIO *in, int for_retry) +{ + return 0; +} + +int OSSL_ECHSTORE_read_pem(OSSL_ECHSTORE *es, BIO *in, int for_retry) +{ + return 0; +} + +int OSSL_ECHSTORE_num_keys(OSSL_ECHSTORE *es, int *numkeys) +{ + return 0; +} + +int OSSL_ECHSTORE_flush_keys(OSSL_ECHSTORE *es, time_t age) +{ + return 0; +} + +void OSSL_ECH_INFO_free(OSSL_ECH_INFO *info, int count) +{ + return; +} + +int OSSL_ECH_INFO_print(BIO *out, OSSL_ECH_INFO *info, int count) +{ + return 0; +} + +int SSL_CTX_set1_echstore(SSL_CTX *ctx, OSSL_ECHSTORE *es) +{ + return 0; +} + +int SSL_set1_echstore(SSL *s, OSSL_ECHSTORE *es) +{ + return 0; +} + +OSSL_ECHSTORE *SSL_CTX_get1_echstore(const SSL_CTX *ctx) +{ + return NULL; +} + +OSSL_ECHSTORE *SSL_get1_echstore(const SSL *s) +{ + return NULL; +} + +int SSL_ech_set_server_names(SSL *s, const char *inner_name, + const char *outer_name, int no_outer) +{ + return 0; +} + +int SSL_ech_set_outer_server_name(SSL *s, const char *outer_name, int no_outer) +{ + return 0; +} + +int SSL_ech_set_outer_alpn_protos(SSL *s, const unsigned char *protos, + const size_t protos_len) +{ + return 0; +} + +int SSL_ech_get1_status(SSL *s, char **inner_sni, char **outer_sni) +{ + return 0; +} + +int SSL_ech_set_grease_suite(SSL *s, const char *suite) +{ + return 0; +} + +int SSL_ech_set_grease_type(SSL *s, uint16_t type) +{ + return 0; +} + +void SSL_ech_set_callback(SSL *s, SSL_ech_cb_func f) +{ + return; +} + +int SSL_ech_get_retry_config(SSL *s, unsigned char **ec, size_t *eclen) +{ + return 0; +} + +int SSL_CTX_ech_set_outer_alpn_protos(SSL_CTX *s, const unsigned char *protos, + const size_t protos_len) +{ + return 0; +} + +int SSL_CTX_ech_raw_decrypt(SSL_CTX *ctx, + int *decrypted_ok, + char **inner_sni, char **outer_sni, + unsigned char *outer_ch, size_t outer_len, + unsigned char *inner_ch, size_t *inner_len, + unsigned char **hrrtok, size_t *toklen) +{ + return 0; +} + +void SSL_CTX_ech_set_callback(SSL_CTX *ctx, SSL_ech_cb_func f) +{ + return; +} + +#endif diff --git a/ssl/ech_local.h b/ssl/ech_local.h new file mode 100644 index 0000000000000..125795fc2a6fc --- /dev/null +++ b/ssl/ech_local.h @@ -0,0 +1,102 @@ +/* + * Copyright 2024 The OpenSSL Project Authors. All Rights Reserved. + * + * Licensed under the OpenSSL license (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 + */ + +/* + * Internal data structures and prototypes for handling + * Encrypted ClientHello (ECH) + */ +#ifndef OPENSSL_NO_ECH + +# ifndef HEADER_ECH_LOCAL_H +# define HEADER_ECH_LOCAL_H + +# include +# include +# include + +/* + * Define this to get loads more lines of tracing which is + * very useful for interop. + * This needs tracing enabled at build time, e.g.: + * $ ./config enable-ssl-trace enable-trace + * This added tracing will finally (mostly) disappear once the ECH RFC + * has issued, but is very useful for interop testing so some of it might + * be retained. + */ +# define OSSL_ECH_SUPERVERBOSE + +/* + * Reminder of what goes in DNS for ECH RFC XXXX + * + * opaque HpkePublicKey<1..2^16-1>; + * uint16 HpkeKemId; // Defined in I-D.irtf-cfrg-hpke + * uint16 HpkeKdfId; // Defined in I-D.irtf-cfrg-hpke + * uint16 HpkeAeadId; // Defined in I-D.irtf-cfrg-hpke + * struct { + * HpkeKdfId kdf_id; + * HpkeAeadId aead_id; + * } HpkeSymmetricCipherSuite; + * struct { + * uint8 config_id; + * HpkeKemId kem_id; + * HpkePublicKey public_key; + * HpkeSymmetricCipherSuite cipher_suites<4..2^16-4>; + * } HpkeKeyConfig; + * struct { + * HpkeKeyConfig key_config; + * uint8 maximum_name_length; + * opaque public_name<1..255>; + * Extension extensions<0..2^16-1>; + * } ECHConfigContents; + * struct { + * uint16 version; + * uint16 length; + * select (ECHConfig.version) { + * case 0xfe0d: ECHConfigContents contents; + * } + * } ECHConfig; + * ECHConfig ECHConfigList<1..2^16-1>; + */ + +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) + +struct ossl_echstore_st { + STACK_OF(OSSL_ECHSTORE_ENTRY) *entries; + OSSL_LIB_CTX *libctx; + const char *propq; +}; + +# endif +#endif diff --git a/test/build.info b/test/build.info index 57ee94071a822..34c9fd4ab59ee 100644 --- a/test/build.info +++ b/test/build.info @@ -70,7 +70,8 @@ IF[{- !$disabled{tests} -}] ca_internals_test bio_tfo_test membio_test bio_dgram_test list_test \ fips_version_test x509_test hpke_test pairwise_fail_test \ nodefltctxtest evp_xof_test x509_load_cert_file_test bio_meth_test \ - x509_acert_test x509_req_test strtoultest bio_pw_callback_test + x509_acert_test x509_req_test strtoultest bio_pw_callback_test \ + ech_test IF[{- !$disabled{'rpk'} -}] PROGRAMS{noinst}=rpktest @@ -233,6 +234,10 @@ IF[{- !$disabled{tests} -}] DEPEND[lms_test]=../libcrypto.a libtestutil.a ENDIF + SOURCE[ech_test]=ech_test.c helpers/ssltestlib.c + INCLUDE[ech_test]=../include ../apps/include + DEPEND[ech_test]=../libssl.a ../libcrypto.a libtestutil.a + SOURCE[evp_extra_test2]=evp_extra_test2.c $INITSRC tls-provider.c INCLUDE[evp_extra_test2]=../include ../apps/include DEPEND[evp_extra_test2]=../libcrypto libtestutil.a diff --git a/test/ech_test.c b/test/ech_test.c new file mode 100644 index 0000000000000..2e49b6b0c982c --- /dev/null +++ b/test/ech_test.c @@ -0,0 +1,66 @@ +/* + * 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 "testutil.h" +#include "helpers/ssltestlib.h" + +#ifndef OPENSSL_NO_ECH + +static int verbose = 0; + +typedef enum OPTION_choice { + OPT_ERR = -1, + OPT_EOF = 0, + OPT_VERBOSE, + OPT_TEST_ENUM +} OPTION_CHOICE; + +const OPTIONS *test_get_options(void) +{ + static const OPTIONS test_options[] = { + OPT_TEST_OPTIONS_DEFAULT_USAGE, + { "v", OPT_VERBOSE, '-', "Enable verbose mode" }, + { OPT_HELP_STR, 1, '-', "Run ECH tests\n" }, + { NULL } + }; + return test_options; +} + +#endif + +int setup_tests(void) +{ +#ifndef OPENSSL_NO_ECH + OPTION_CHOICE o; + + while ((o = opt_next()) != OPT_EOF) { + switch (o) { + case OPT_VERBOSE: + verbose = 1; + break; + case OPT_TEST_CASES: + break; + default: + return 0; + } + } + /* TODO(ECH): we'll move test code over later */ + return 1; +#endif + return 1; +} + +void cleanup_tests(void) +{ +#ifndef OPENSSL_NO_ECH + ; +#endif +} diff --git a/test/recipes/30-test_ech.t b/test/recipes/30-test_ech.t new file mode 100644 index 0000000000000..73ecd2a9c6a42 --- /dev/null +++ b/test/recipes/30-test_ech.t @@ -0,0 +1,21 @@ +#! /usr/bin/env perl +# Copyright 2022 The OpenSSL Project Authors. All Rights Reserved. +# Copyright (c) 2022, Oracle and/or its affiliates. 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 + +use strict; +use OpenSSL::Test::Utils; +use OpenSSL::Test qw/:DEFAULT srctop_file srctop_dir bldtop_dir bldtop_file/; + +setup("test_ech"); + +plan skip_all => "ECH tests not supported in this build" + if disabled("ech") || disabled("tls1_3") || disabled("ec") || disabled("ecx"); + +plan tests => 1; + +ok(run(test(["ech_test", srctop_dir("test", "certs")]))) diff --git a/util/libssl.num b/util/libssl.num index f64c8ac069316..847e77399daba 100644 --- a/util/libssl.num +++ b/util/libssl.num @@ -605,4 +605,31 @@ SSL_CTX_get0_client_cert_type ? 4_0_0 EXIST::FUNCTION: SSL_CTX_get0_server_cert_type ? 4_0_0 EXIST::FUNCTION: SSL_set_quic_tls_cbs ? 4_0_0 EXIST::FUNCTION: SSL_set_quic_tls_transport_params ? 4_0_0 EXIST::FUNCTION: -SSL_set_quic_tls_early_data_enabled ? 4_0_0 EXIST::FUNCTION: +SSL_set_quic_tls_early_data_enabled ? 4_0_0 EXIST::FUNCTION:OSSL_ECHSTORE_new ? 4_0_0 EXIST::FUNCTION:ECH +OSSL_ECHSTORE_free ? 4_0_0 EXIST::FUNCTION:ECH +OSSL_ECHSTORE_new_config ? 4_0_0 EXIST::FUNCTION:ECH +OSSL_ECHSTORE_write_pem ? 4_0_0 EXIST::FUNCTION:ECH +OSSL_ECHSTORE_read_echconfiglist ? 4_0_0 EXIST::FUNCTION:ECH +OSSL_ECHSTORE_get1_info ? 4_0_0 EXIST::FUNCTION:ECH +OSSL_ECHSTORE_downselect ? 4_0_0 EXIST::FUNCTION:ECH +OSSL_ECHSTORE_set1_key_and_read_pem ? 4_0_0 EXIST::FUNCTION:ECH +OSSL_ECHSTORE_read_pem ? 4_0_0 EXIST::FUNCTION:ECH +OSSL_ECHSTORE_num_keys ? 4_0_0 EXIST::FUNCTION:ECH +OSSL_ECHSTORE_flush_keys ? 4_0_0 EXIST::FUNCTION:ECH +OSSL_ECH_INFO_free ? 4_0_0 EXIST::FUNCTION:ECH +OSSL_ECH_INFO_print ? 4_0_0 EXIST::FUNCTION:ECH +SSL_CTX_set1_echstore ? 4_0_0 EXIST::FUNCTION:ECH +SSL_set1_echstore ? 4_0_0 EXIST::FUNCTION:ECH +SSL_CTX_get1_echstore ? 4_0_0 EXIST::FUNCTION:ECH +SSL_get1_echstore ? 4_0_0 EXIST::FUNCTION:ECH +SSL_ech_set_server_names ? 4_0_0 EXIST::FUNCTION:ECH +SSL_ech_set_outer_server_name ? 4_0_0 EXIST::FUNCTION:ECH +SSL_ech_set_outer_alpn_protos ? 4_0_0 EXIST::FUNCTION:ECH +SSL_ech_get1_status ? 4_0_0 EXIST::FUNCTION:ECH +SSL_ech_set_grease_suite ? 4_0_0 EXIST::FUNCTION:ECH +SSL_ech_set_grease_type ? 4_0_0 EXIST::FUNCTION:ECH +SSL_ech_set_callback ? 4_0_0 EXIST::FUNCTION:ECH +SSL_ech_get_retry_config ? 4_0_0 EXIST::FUNCTION:ECH +SSL_CTX_ech_set_outer_alpn_protos ? 4_0_0 EXIST::FUNCTION:ECH +SSL_CTX_ech_raw_decrypt ? 4_0_0 EXIST::FUNCTION:ECH +SSL_CTX_ech_set_callback ? 4_0_0 EXIST::FUNCTION:ECH diff --git a/util/perl/TLSProxy/Message.pm b/util/perl/TLSProxy/Message.pm index de923f0903f11..f082e7b16a9c7 100644 --- a/util/perl/TLSProxy/Message.pm +++ b/util/perl/TLSProxy/Message.pm @@ -99,6 +99,8 @@ use constant { EXT_RENEGOTIATE => 65281, EXT_NPN => 13172, EXT_CRYPTOPRO_BUG_EXTENSION => 0xfde8, + EXT_ECH => 0xfe0d, + EXT_ECH_OUTER => 0xfd00, EXT_UNKNOWN => 0xfffe, #Unknown extension that should appear last EXT_FORCE_LAST => 0xffff From f59d683e4986e1cc8fa53ce9c2e73067f395108b Mon Sep 17 00:00:00 2001 From: sftcd Date: Wed, 11 Sep 2024 00:28:32 +0100 Subject: [PATCH 04/24] ECH CLI implementation Reviewed-by: Tomas Mraz Reviewed-by: Matt Caswell (Merged from https://github.com/openssl/openssl/pull/25420) --- apps/ech.c | 143 +++- crypto/err/openssl.txt | 2 + crypto/ssl_err.c | 3 + doc/designs/ech-api.md | 6 +- doc/man1/openssl-ech.pod.in | 41 +- include/openssl/ech.h | 7 +- include/openssl/sslerr.h | 2 + ssl/build.info | 5 +- ssl/ech.c | 467 ------------- ssl/ech/build.info | 3 + ssl/ech/ech_helper.c | 15 + ssl/ech/ech_internal.c | 15 + ssl/{ => ech}/ech_local.h | 1 + ssl/ech/ech_ssl_apis.c | 106 +++ ssl/ech/ech_store.c | 1168 ++++++++++++++++++++++++++++++++ test/certs/ech-big.pem | 25 + test/certs/ech-eg.pem | 7 + test/certs/ech-giant.pem | 37 + test/certs/ech-mid.pem | 11 + test/certs/ech-rsa.pem | 14 + test/ech_test.c | 1006 ++++++++++++++++++++++++++- test/recipes/20-test_app_ech.t | 93 +++ 22 files changed, 2665 insertions(+), 512 deletions(-) delete mode 100644 ssl/ech.c create mode 100644 ssl/ech/build.info create mode 100644 ssl/ech/ech_helper.c create mode 100644 ssl/ech/ech_internal.c rename ssl/{ => ech}/ech_local.h (97%) create mode 100644 ssl/ech/ech_ssl_apis.c create mode 100644 ssl/ech/ech_store.c create mode 100644 test/certs/ech-big.pem create mode 100644 test/certs/ech-eg.pem create mode 100644 test/certs/ech-giant.pem create mode 100644 test/certs/ech-mid.pem create mode 100644 test/certs/ech-rsa.pem create mode 100644 test/recipes/20-test_app_ech.t diff --git a/apps/ech.c b/apps/ech.c index 06f123bca682c..fa13ee6cf5cfd 100644 --- a/apps/ech.c +++ b/apps/ech.c @@ -28,31 +28,35 @@ # define OSSL_ECH_KEYGEN_MODE 0 /* default: generate a key pair/ECHConfig */ # define OSSL_ECH_SELPRINT_MODE 1 /* we can print/down-select ECHConfigList */ - -# define PEM_SELECT_ALL -1 /* to indicate we're not downselecting another */ +# define OSSL_ECH_MAXINFILES 5 /* we'll only take this many inputs */ typedef enum OPTION_choice { /* standard openssl options */ - OPT_ERR = -1, OPT_EOF = 0, OPT_HELP, OPT_VERBOSE, - OPT_PEMOUT, + OPT_ERR = -1, OPT_EOF = 0, OPT_HELP, OPT_VERBOSE, OPT_TEXT, + OPT_OUT, OPT_IN, /* ECHConfig specifics */ OPT_PUBLICNAME, OPT_ECHVERSION, - OPT_MAXNAMELENGTH, OPT_HPKESUITE + OPT_MAXNAMELENGTH, OPT_HPKESUITE, + OPT_SELECT } OPTION_CHOICE; const OPTIONS ech_options[] = { OPT_SECTION("General options"), {"help", OPT_HELP, '-', "Display this summary"}, {"verbose", OPT_VERBOSE, '-', "Provide additional output"}, + {"text", OPT_TEXT, '-', "Provide human-readable output"}, OPT_SECTION("Key generation"), - {"pemout", OPT_PEMOUT, '>', - "Private key and ECHConfig [default echconfig.pem]"}, + {"out", OPT_OUT, '>', + "Private key and/or ECHConfig [default: echconfig.pem]"}, {"public_name", OPT_PUBLICNAME, 's', "public_name value"}, {"max_name_len", OPT_MAXNAMELENGTH, 'n', "Maximum host name length value [default: 0]"}, {"suite", OPT_HPKESUITE, 's', "HPKE ciphersuite: e.g. \"0x20,1,3\""}, {"ech_version", OPT_ECHVERSION, 'n', - "ECHConfig version [default 0xff0d (13)]"}, + "ECHConfig version [default: 0xff0d (13)]"}, + OPT_SECTION("ECH PEM file downselect/display"), + {"in", OPT_IN, '<', "An ECH PEM file"}, + {"select", OPT_SELECT, 'n', "Downselect to the numbered ECH config"}, {NULL} }; @@ -66,9 +70,8 @@ static uint16_t verstr2us(char *arg) long lv = strtol(arg, NULL, 0); uint16_t rv = 0; - if (lv < 0xffff && lv > 0) { + if (lv < 0xffff && lv > 0) rv = (uint16_t)lv; - } return rv; } @@ -76,14 +79,19 @@ int ech_main(int argc, char **argv) { char *prog = NULL; OPTION_CHOICE o; - int verbose = 0; - char *pemfile = NULL; + int i, rv = 1, verbose = 0, text = 0, outsupp = 0; + int select = OSSL_ECHSTORE_ALL; + char *outfile = NULL, *infile = NULL; + char *infiles[OSSL_ECH_MAXINFILES] = { NULL }; + int numinfiles = 0; char *public_name = NULL; char *suitestr = NULL; uint16_t ech_version = OSSL_ECH_CURRENT_VERSION; uint8_t max_name_length = 0; OSSL_HPKE_SUITE hpke_suite = OSSL_HPKE_SUITE_DEFAULT; int mode = OSSL_ECH_KEYGEN_MODE; /* key generation */ + OSSL_ECHSTORE *es = NULL; + BIO *ecf = NULL; prog = opt_init(argc, argv, ech_options); while ((o = opt_next()) != OPT_EOF) { @@ -94,12 +102,32 @@ int ech_main(int argc, char **argv) goto end; case OPT_HELP: opt_help(ech_options); + rv = 0; goto end; case OPT_VERBOSE: verbose = 1; break; - case OPT_PEMOUT: - pemfile = opt_arg(); + case OPT_TEXT: + text = 1; + break; + case OPT_SELECT: + mode = OSSL_ECH_SELPRINT_MODE; + select = strtol(opt_arg(), NULL, 10); + break; + case OPT_OUT: + outfile = opt_arg(); + outsupp = 1; + break; + case OPT_IN: + mode = OSSL_ECH_SELPRINT_MODE; + infile = opt_arg(); + if (numinfiles >= OSSL_ECH_MAXINFILES) { + BIO_printf(bio_err, "too many input files, only %d allowed\n", + OSSL_ECH_MAXINFILES); + goto opthelp; + } + infiles[numinfiles] = infile; + numinfiles++; break; case OPT_PUBLICNAME: public_name = opt_arg(); @@ -147,10 +175,10 @@ int ech_main(int argc, char **argv) goto end; } - if (max_name_length > TLSEXT_MAXLEN_host_name) { + if (max_name_length > OSSL_ECH_MAX_MAXNAMELEN) { BIO_printf(bio_err, "Weird max name length (0x%04x) - biggest is " "(0x%04x) - exiting\n", max_name_length, - TLSEXT_MAXLEN_host_name); + OSSL_ECH_MAX_MAXNAMELEN); ERR_print_errors(bio_err); goto end; } @@ -164,17 +192,15 @@ int ech_main(int argc, char **argv) } /* Set default if needed */ - if (pemfile == NULL) - pemfile = "echconfig.pem"; - + if (outfile == NULL) + outfile = "echconfig.pem"; + es = OSSL_ECHSTORE_new(NULL, NULL); + if (es == NULL) + goto end; if (mode == OSSL_ECH_KEYGEN_MODE) { - OSSL_ECHSTORE *es = NULL; - BIO *ecf = NULL; - if (verbose) BIO_printf(bio_err, "Calling OSSL_ECHSTORE_new_config\n"); - if ((ecf = BIO_new_file(pemfile, "w")) == NULL - || (es = OSSL_ECHSTORE_new(NULL, NULL)) == NULL + if ((ecf = BIO_new_file(outfile, "w")) == NULL || OSSL_ECHSTORE_new_config(es, ech_version, max_name_length, public_name, hpke_suite) != 1 || OSSL_ECHSTORE_write_pem(es, 0, ecf) != 1) { @@ -183,17 +209,72 @@ int ech_main(int argc, char **argv) } if (verbose) BIO_printf(bio_err, "OSSL_ECHSTORE_new_config success\n"); - OSSL_ECHSTORE_free(es); - BIO_free_all(ecf); - return 1; + rv = 0; } -opthelp: - BIO_printf(bio_err, "%s: Use -help for summary.\n", prog); - goto end; + if (mode == OSSL_ECH_SELPRINT_MODE) { + if (numinfiles == 0) + goto opthelp; + for (i = 0; i != numinfiles; i++) { + if ((ecf = BIO_new_file(infiles[i], "r")) == NULL + || OSSL_ECHSTORE_read_pem(es, ecf, OSSL_ECH_FOR_RETRY) != 1) { + if (verbose) + BIO_printf(bio_err, "OSSL_ECHSTORE_read_pem error for %s\n", + infiles[i]); + /* try read it as an ECHConfigList */ + goto end; + } + BIO_free(ecf); + ecf = NULL; + } + if (verbose) + BIO_printf(bio_err, "Success reading %d files\n", numinfiles); + if (outsupp == 1) { + /* write result to that, with downselection if required */ + if (verbose) + BIO_printf(bio_err, "Will write to %s\n", outfile); + if (verbose && select != OSSL_ECHSTORE_ALL) + BIO_printf(bio_err, "Selected entry: %d\n", select); + if ((ecf = BIO_new_file(outfile, "w")) == NULL + || OSSL_ECHSTORE_write_pem(es, select, ecf) != 1) { + BIO_printf(bio_err, "OSSL_ECHSTORE_write_pem error\n"); + goto end; + } + if (verbose) + BIO_printf(bio_err, "Success writing to %s\n", outfile); + } + rv = 0; + } + + if (text) { + OSSL_ECH_INFO *oi = NULL; + int oi_ind, oi_cnt = 0; + + if (OSSL_ECHSTORE_get1_info(es, &oi, &oi_cnt) != 1) + goto end; + if (verbose) + BIO_printf(bio_err, "Printing %d ECHConfigList\n", oi_cnt); + for (oi_ind = 0; oi_ind != oi_cnt; oi_ind++) { + if (OSSL_ECH_INFO_print(bio_out, oi, oi_ind) != 1) { + BIO_printf(bio_err, "OSSL_ECH_INFO_print error entry (%d)\n", + oi_ind); + goto end; + } + } + OSSL_ECH_INFO_free(oi, oi_cnt); + if (verbose) + BIO_printf(bio_err, "Success printing %d ECHConfigList\n", oi_cnt); + rv = 0; + } end: - return 0; + OSSL_ECHSTORE_free(es); + BIO_free_all(ecf); + return rv; +opthelp: + BIO_printf(bio_err, "%s: Use -help for summary.\n", prog); + BIO_printf(bio_err, "\tup to %d -in instances allowed\n", OSSL_ECH_MAXINFILES); + return rv; } #endif diff --git a/crypto/err/openssl.txt b/crypto/err/openssl.txt index 5019362551bed..1e80022159029 100644 --- a/crypto/err/openssl.txt +++ b/crypto/err/openssl.txt @@ -1373,6 +1373,7 @@ SSL_R_BAD_DH_VALUE:102:bad dh value SSL_R_BAD_DIGEST_LENGTH:111:bad digest length SSL_R_BAD_EARLY_DATA:233:bad early data SSL_R_BAD_ECC_CERT:304:bad ecc cert +SSL_R_BAD_ECHCONFIG_EXTENSION:425:bad echconfig extension SSL_R_BAD_ECPOINT:306:bad ecpoint SSL_R_BAD_EXTENSION:110:bad extension SSL_R_BAD_HANDSHAKE_LENGTH:332:bad handshake length @@ -1453,6 +1454,7 @@ SSL_R_DTLS_MESSAGE_TOO_BIG:334:dtls message too big SSL_R_DUPLICATE_COMPRESSION_ID:309:duplicate compression id SSL_R_ECC_CERT_NOT_FOR_SIGNING:318:ecc cert not for signing SSL_R_ECDH_REQUIRED_FOR_SUITEB_MODE:374:ecdh required for suiteb mode +SSL_R_ECH_DECODE_ERROR:426:ech decode error SSL_R_ECH_REQUIRED:424:ech required SSL_R_EE_KEY_TOO_SMALL:399:ee key too small SSL_R_EMPTY_RAW_PUBLIC_KEY:349:empty raw public key diff --git a/crypto/ssl_err.c b/crypto/ssl_err.c index a3703458574d3..fd15792aa935e 100644 --- a/crypto/ssl_err.c +++ b/crypto/ssl_err.c @@ -37,6 +37,8 @@ static const ERR_STRING_DATA SSL_str_reasons[] = { {ERR_PACK(ERR_LIB_SSL, 0, SSL_R_BAD_DIGEST_LENGTH), "bad digest length"}, {ERR_PACK(ERR_LIB_SSL, 0, SSL_R_BAD_EARLY_DATA), "bad early data"}, {ERR_PACK(ERR_LIB_SSL, 0, SSL_R_BAD_ECC_CERT), "bad ecc cert"}, + {ERR_PACK(ERR_LIB_SSL, 0, SSL_R_BAD_ECHCONFIG_EXTENSION), + "bad echconfig extension"}, {ERR_PACK(ERR_LIB_SSL, 0, SSL_R_BAD_ECPOINT), "bad ecpoint"}, {ERR_PACK(ERR_LIB_SSL, 0, SSL_R_BAD_EXTENSION), "bad extension"}, {ERR_PACK(ERR_LIB_SSL, 0, SSL_R_BAD_HANDSHAKE_LENGTH), @@ -156,6 +158,7 @@ static const ERR_STRING_DATA SSL_str_reasons[] = { "ecc cert not for signing"}, {ERR_PACK(ERR_LIB_SSL, 0, SSL_R_ECDH_REQUIRED_FOR_SUITEB_MODE), "ecdh required for suiteb mode"}, + {ERR_PACK(ERR_LIB_SSL, 0, SSL_R_ECH_DECODE_ERROR), "ech decode error"}, {ERR_PACK(ERR_LIB_SSL, 0, SSL_R_ECH_REQUIRED), "ech required"}, {ERR_PACK(ERR_LIB_SSL, 0, SSL_R_EE_KEY_TOO_SMALL), "ee key too small"}, {ERR_PACK(ERR_LIB_SSL, 0, SSL_R_EMPTY_RAW_PUBLIC_KEY), diff --git a/doc/designs/ech-api.md b/doc/designs/ech-api.md index eb78bbc25d019..a7f1ffdbd0c13 100644 --- a/doc/designs/ech-api.md +++ b/doc/designs/ech-api.md @@ -205,6 +205,8 @@ typedef struct ossl_echstore_st OSSL_ECHSTORE; /* if a caller wants to index the last entry in the store */ # define OSSL_ECHSTORE_LAST -1 +/* if a caller wants all entries in the store, e.g. to print public values */ +# define OSSL_ECHSTORE_ALL -2 OSSL_ECHSTORE *OSSL_ECHSTORE_new(OSSL_LIB_CTX *libctx, const char *propq); void OSSL_ECHSTORE_free(OSSL_ECHSTORE *es); @@ -234,7 +236,8 @@ value and the related "singleton" ECHConfigList structure. 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.) +`OSSL_ECHSTORE_LAST` will select the last entry. An `index` of +`OSSL_ECHSTORE_ALL` will output all public values, and no private values.) 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 @@ -323,6 +326,7 @@ typedef struct ossl_ech_info_st { unsigned char *inner_alpns; /* inner ALPN string */ size_t inner_alpns_len; char *echconfig; /* a JSON-like version of the associated ECHConfig */ + int has_private_key; /* 0 if we don't have a related private key */ } OSSL_ECH_INFO; void OSSL_ECH_INFO_free(OSSL_ECH_INFO *info, int count); diff --git a/doc/man1/openssl-ech.pod.in b/doc/man1/openssl-ech.pod.in index b7736d4b96fa2..c894feee1ff6c 100644 --- a/doc/man1/openssl-ech.pod.in +++ b/doc/man1/openssl-ech.pod.in @@ -10,24 +10,30 @@ openssl-ech - ECH key generation B B [B<-help>] [B<-verbose>] -[B<-pemout> I] +[B<-in> I] +[B<-out> I] [B<-public_name> I] [B<-max_name_len> I] [B<-suite> I] [B<-ech_version> I] +[B<-select> I] +[B<-text>] =head1 DESCRIPTION -The L command generates Encrypted Client Hello (ECH) private keys -and public keys in the ECHConfig format. +The L command generates Encrypted Client Hello (ECH) key pairs +in the ECHConfig PEM file format as specified in +L. +TODO(ECH): update I-D reference to RFC when possible. -The "ECHConfig PEM file" format mentioned below is specified in -L and consists of -one private key in PKCS#8 format and a base64 encoded ECHConfig containing one -matching public value. +That format consists of an optional private key in PKCS#8 format and a base64 +encoded ECHConfigList containing an entry with a matching public value (and +possibly other entries as well). =head1 OPTIONS +The following options are supported: + =over 4 =item B<-help> @@ -38,9 +44,21 @@ Print out a usage message. Print more verbosely. -=item B<-pemout> I +=item B<-in> + +Provide an input ECH PEM file for printing or merging. Up to five +input files can be provided via use of multiple B arguments. + +=item B<-out> I + +Name of output ECHConfig PEM file. If a new key pair was generated the output +file will contain the private key and encoded ECHConfigList. If one or more +input files was provided the output file will contain a set of ECHConfigList +values with public keys from the inputs, and no private key(s). -Name of output ECHConfig PEM file. +=item B<-text> + +Provide human-readable text ouput. =item B<-public_name> I @@ -58,6 +76,11 @@ HPKE suite to use in the ECHConfig. The ECH version to use in the ECHConfig. Only 0xfe0d is supported in this version. +=item B<-select> I + +Select the N-th ECHConfig/public key from the set of input ECH PEM files and output +that. + =back =head1 NOTES diff --git a/include/openssl/ech.h b/include/openssl/ech.h index f79a1e9ae1783..95705fc921c73 100644 --- a/include/openssl/ech.h +++ b/include/openssl/ech.h @@ -58,6 +58,8 @@ /* if a caller wants to index the last entry in the store */ # define OSSL_ECHSTORE_LAST -1 +/* if a caller wants all entries in the store, e.g. to print public values */ +# define OSSL_ECHSTORE_ALL -2 /* * Application-visible form of ECH information from the DNS, from config @@ -73,11 +75,12 @@ typedef struct ossl_ech_info_st { unsigned char *inner_alpns; /* inner ALPN string */ size_t inner_alpns_len; char *echconfig; /* a JSON-like version of the associated ECHConfig */ + int has_private_key; /* 0 if we don't have a related private key */ } OSSL_ECH_INFO; /* Values for the for_retry inputs */ -# define SSL_ECH_USE_FOR_RETRY 1 -# define SSL_ECH_NOT_FOR_RETRY 0 +# define OSSL_ECH_FOR_RETRY 1 +# define OSSL_ECH_NO_RETRY 0 /* * API calls built around OSSL_ECHSTORE diff --git a/include/openssl/sslerr.h b/include/openssl/sslerr.h index bee7aa7169e01..b1f10f5685087 100644 --- a/include/openssl/sslerr.h +++ b/include/openssl/sslerr.h @@ -36,6 +36,7 @@ # define SSL_R_BAD_DIGEST_LENGTH 111 # define SSL_R_BAD_EARLY_DATA 233 # define SSL_R_BAD_ECC_CERT 304 +# define SSL_R_BAD_ECHCONFIG_EXTENSION 425 # define SSL_R_BAD_ECPOINT 306 # define SSL_R_BAD_EXTENSION 110 # define SSL_R_BAD_HANDSHAKE_LENGTH 332 @@ -113,6 +114,7 @@ # define SSL_R_DUPLICATE_COMPRESSION_ID 309 # define SSL_R_ECC_CERT_NOT_FOR_SIGNING 318 # define SSL_R_ECDH_REQUIRED_FOR_SUITEB_MODE 374 +# define SSL_R_ECH_DECODE_ERROR 426 # define SSL_R_ECH_REQUIRED 424 # define SSL_R_EE_KEY_TOO_SMALL 399 # define SSL_R_EMPTY_RAW_PUBLIC_KEY 349 diff --git a/ssl/build.info b/ssl/build.info index 6d9834b304125..d5166e64220a3 100644 --- a/ssl/build.info +++ b/ssl/build.info @@ -2,6 +2,10 @@ SUBDIRS=record rio quic LIBS=../libssl +IF[{- !$disabled{ech} -}] + SUBDIRS=ech +ENDIF + SOURCE[../libssl]=\ pqueue.c \ statem/statem_srvr.c statem/statem_clnt.c s3_lib.c s3_enc.c \ @@ -16,7 +20,6 @@ SOURCE[../libssl]=\ bio_ssl.c ssl_err_legacy.c tls_srp.c t1_trce.c ssl_utst.c \ statem/statem.c \ ssl_cert_comp.c \ - ech.c \ tls_depr.c # For shared builds we need to include the libcrypto packet.c and quic_vlint.c diff --git a/ssl/ech.c b/ssl/ech.c deleted file mode 100644 index 5b0d5878ef4a8..0000000000000 --- a/ssl/ech.c +++ /dev/null @@ -1,467 +0,0 @@ -/* - * Copyright 2024 The OpenSSL Project Authors. All Rights Reserved. - * - * Licensed under the OpenSSL license (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 "ssl_local.h" -#include "ech_local.h" -#include "statem/statem_local.h" -#include -#include -#include -#include - -#ifndef OPENSSL_NO_ECH - -/* a size for some crypto vars */ -# define OSSL_ECH_CRYPTO_VAR_SIZE 2048 - -/* - * @brief hash a buffer as a pretend file name being ascii-hex of hashed buffer - * @param es is the OSSL_ECHSTORE we're dealing with - * @param buf is the input buffer - * @param blen is the length of buf - * @param ah_hash is a pointer to where to put the result - * @param ah_len is the length of ah_hash - */ -static int ech_hash_pub_as_fname(OSSL_ECHSTORE *es, - const unsigned char *buf, size_t blen, - char *ah_hash, size_t ah_len) -{ - unsigned char hashval[EVP_MAX_MD_SIZE]; - size_t hashlen, actual_ah_len; - - if (es == NULL - || EVP_Q_digest(es->libctx, "SHA2-256", es->propq, - buf, blen, hashval, &hashlen) != 1 - || OPENSSL_buf2hexstr_ex(ah_hash, ah_len, &actual_ah_len, - hashval, hashlen, '\0') != 1) { - ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR); - return 0; - } - return 1; -} - -/* - * API calls built around OSSL_ECHSTORE - */ - -OSSL_ECHSTORE *OSSL_ECHSTORE_new(OSSL_LIB_CTX *libctx, const char *propq) -{ - OSSL_ECHSTORE *es = NULL; - - es = OPENSSL_zalloc(sizeof(*es)); - if (es == NULL) { - ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR); - return 0; - } - es->libctx = libctx; - es->propq = propq; - return es; -} - -static void ossl_echext_free(OSSL_ECHEXT *e) -{ - if (e == NULL) - return; - OPENSSL_free(e->val); - OPENSSL_free(e); - return; -} - -static void ossl_echstore_entry_free(OSSL_ECHSTORE_ENTRY *ee) -{ - if (ee == NULL) - return; - OPENSSL_free(ee->public_name); - OPENSSL_free(ee->pub); - OPENSSL_free(ee->pemfname); - EVP_PKEY_free(ee->keyshare); - OPENSSL_free(ee->encoded); - OPENSSL_free(ee->suites); - sk_OSSL_ECHEXT_pop_free(ee->exts, ossl_echext_free); - OPENSSL_free(ee); - return; -} - -void OSSL_ECHSTORE_free(OSSL_ECHSTORE *es) -{ - if (es == NULL) - return; - sk_OSSL_ECHSTORE_ENTRY_pop_free(es->entries, ossl_echstore_entry_free); - OPENSSL_free(es); - return; -} - -int OSSL_ECHSTORE_new_config(OSSL_ECHSTORE *es, - uint16_t echversion, uint8_t max_name_length, - const char *public_name, OSSL_HPKE_SUITE suite) -{ - size_t pnlen = 0; - size_t publen = OSSL_ECH_CRYPTO_VAR_SIZE; - unsigned char pub[OSSL_ECH_CRYPTO_VAR_SIZE]; - int rv = 0; - unsigned char *bp = NULL; - size_t bblen = 0; - EVP_PKEY *privp = NULL; - uint8_t config_id = 0; - WPACKET epkt; - BUF_MEM *epkt_mem = NULL; - OSSL_ECHSTORE_ENTRY *ee = NULL; - char pembuf[2 * EVP_MAX_MD_SIZE + 1]; - size_t pembuflen = 2 * EVP_MAX_MD_SIZE + 1; - - /* basic checks */ - if (es == NULL) { - ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_NULL_PARAMETER); - return 0; - } - pnlen = (public_name == NULL ? 0 : strlen(public_name)); - if (pnlen == 0 || pnlen > OSSL_ECH_MAX_PUBLICNAME - || max_name_length > OSSL_ECH_MAX_MAXNAMELEN) { - ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_INVALID_ARGUMENT); - return 0; - } - /* this used have more versions and will again in future */ - switch (echversion) { - case OSSL_ECH_RFCXXXX_VERSION: - break; - default: - ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_INVALID_ARGUMENT); - return 0; - } - - /* so WPACKET_cleanup() won't go wrong */ - memset(&epkt, 0, sizeof(epkt)); - /* random config_id */ - if (RAND_bytes_ex(es->libctx, (unsigned char *)&config_id, 1, - RAND_DRBG_STRENGTH) <= 0) { - ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR); - goto err; - } - /* key pair */ - if (OSSL_HPKE_keygen(suite, pub, &publen, &privp, NULL, 0, - es->libctx, es->propq) != 1) { - ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR); - goto err; - } - /* - * Reminder, for draft-13 we want this: - * - * opaque HpkePublicKey<1..2^16-1>; - * uint16 HpkeKemId; // Defined in I-D.irtf-cfrg-hpke - * uint16 HpkeKdfId; // Defined in I-D.irtf-cfrg-hpke - * uint16 HpkeAeadId; // Defined in I-D.irtf-cfrg-hpke - * struct { - * HpkeKdfId kdf_id; - * HpkeAeadId aead_id; - * } HpkeSymmetricCipherSuite; - * struct { - * uint8 config_id; - * HpkeKemId kem_id; - * HpkePublicKey public_key; - * HpkeSymmetricCipherSuite cipher_suites<4..2^16-4>; - * } HpkeKeyConfig; - * struct { - * HpkeKeyConfig key_config; - * uint8 maximum_name_length; - * opaque public_name<1..255>; - * Extension extensions<0..2^16-1>; - * } ECHConfigContents; - * struct { - * uint16 version; - * uint16 length; - * select (ECHConfig.version) { - * case 0xfe0d: ECHConfigContents contents; - * } - * } ECHConfig; - * ECHConfig ECHConfigList<1..2^16-1>; - */ - if ((epkt_mem = BUF_MEM_new()) == NULL - || !BUF_MEM_grow(epkt_mem, OSSL_ECH_MAX_ECHCONFIG_LEN)) { - ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR); - goto err; - } - /* config id, KEM, public, KDF, AEAD, max name len, public_name, exts */ - if (!WPACKET_init(&epkt, epkt_mem) - || (bp = WPACKET_get_curr(&epkt)) == NULL - || !WPACKET_start_sub_packet_u16(&epkt) - || !WPACKET_put_bytes_u16(&epkt, echversion) - || !WPACKET_start_sub_packet_u16(&epkt) - || !WPACKET_put_bytes_u8(&epkt, config_id) - || !WPACKET_put_bytes_u16(&epkt, suite.kem_id) - || !WPACKET_start_sub_packet_u16(&epkt) - || !WPACKET_memcpy(&epkt, pub, publen) - || !WPACKET_close(&epkt) - || !WPACKET_start_sub_packet_u16(&epkt) - || !WPACKET_put_bytes_u16(&epkt, suite.kdf_id) - || !WPACKET_put_bytes_u16(&epkt, suite.aead_id) - || !WPACKET_close(&epkt) - || !WPACKET_put_bytes_u8(&epkt, max_name_length) - || !WPACKET_start_sub_packet_u8(&epkt) - || !WPACKET_memcpy(&epkt, public_name, pnlen) - || !WPACKET_close(&epkt) - || !WPACKET_start_sub_packet_u16(&epkt) - || !WPACKET_memcpy(&epkt, NULL, 0) /* no extensions */ - || !WPACKET_close(&epkt) - || !WPACKET_close(&epkt) - || !WPACKET_close(&epkt)) { - ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR); - goto err; - } - /* bp, bblen has encoding */ - WPACKET_get_total_written(&epkt, &bblen); - if ((ee = OPENSSL_zalloc(sizeof(*ee))) == NULL) { - ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR); - goto err; - } - ee->suites = OPENSSL_malloc(sizeof(OSSL_HPKE_SUITE)); - if (ee->suites == NULL) { - ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR); - goto err; - } - if (ech_hash_pub_as_fname(es, pub, publen, pembuf, pembuflen) != 1) { - ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR); - goto err; - } - ee->version = echversion; - ee->pub_len = publen; - ee->pub = OPENSSL_memdup(pub, publen); - if (ee->pub == NULL) { - ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR); - goto err; - } - ee->nsuites = 1; - ee->suites[0] = suite; - ee->public_name = OPENSSL_strdup(public_name); - if (ee->public_name == NULL) { - ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR); - goto err; - } - ee->max_name_length = max_name_length; - ee->config_id = config_id; - ee->keyshare = privp; - ee->encoded = OPENSSL_memdup(bp, bblen); - if (ee->encoded == NULL) { - ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR); - goto err; - } - ee->encoded_len = bblen; - ee->pemfname = OPENSSL_strdup(pembuf); - if (ee->pemfname == NULL) { - ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR); - goto err; - } - ee->loadtime = time(0); - /* push entry into store */ - if (es->entries == NULL) - es->entries = sk_OSSL_ECHSTORE_ENTRY_new_null(); - if (es->entries == NULL) { - ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR); - goto err; - } - if (!sk_OSSL_ECHSTORE_ENTRY_push(es->entries, ee)) { - ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR); - goto err; - } - WPACKET_finish(&epkt); - BUF_MEM_free(epkt_mem); - return 1; - -err: - EVP_PKEY_free(privp); - WPACKET_cleanup(&epkt); - BUF_MEM_free(epkt_mem); - ossl_echstore_entry_free(ee); - OPENSSL_free(ee); - return rv; -} - -int OSSL_ECHSTORE_write_pem(OSSL_ECHSTORE *es, int index, BIO *out) -{ - OSSL_ECHSTORE_ENTRY *ee = NULL; - int rv = 0, num = 0, chosen = 0; - - if (es == NULL) { - /* - * TODO(ECH): this is a bit of a bogus error, just so as - * to get the `make update` command to add the required - * error number. We don't need it yet, but it's involved - * in some of the build artefacts, so may as well jump - * the gun a bit on it. - */ - ERR_raise(ERR_LIB_SSL, SSL_R_ECH_REQUIRED); - return 0; - } - num = sk_OSSL_ECHSTORE_ENTRY_num(es->entries); - if (num <= 0) { - ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_INVALID_ARGUMENT); - return 0; - } - if (index >= num) { - ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_INVALID_ARGUMENT); - return 0; - } - if (index == OSSL_ECHSTORE_LAST) - chosen = num - 1; - else - chosen = index; - ee = sk_OSSL_ECHSTORE_ENTRY_value(es->entries, chosen); - if (ee == NULL || ee->keyshare == NULL || ee->encoded == NULL) { - ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_INVALID_ARGUMENT); - return 0; - } - /* private key first */ - if (!PEM_write_bio_PrivateKey(out, ee->keyshare, NULL, NULL, 0, - NULL, NULL)) { - ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR); - goto err; - } - if (PEM_write_bio(out, PEM_STRING_ECHCONFIG, NULL, - ee->encoded, ee->encoded_len) <= 0) { - ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR); - goto err; - } - rv = 1; -err: - return rv; -} - -int OSSL_ECHSTORE_read_echconfiglist(OSSL_ECHSTORE *es, BIO *in) -{ - return 0; -} - -int OSSL_ECHSTORE_get1_info(OSSL_ECHSTORE *es, OSSL_ECH_INFO **info, - int *count) -{ - return 0; -} - -int OSSL_ECHSTORE_downselect(OSSL_ECHSTORE *es, int index) -{ - return 0; -} - -int OSSL_ECHSTORE_set1_key_and_read_pem(OSSL_ECHSTORE *es, EVP_PKEY *priv, - BIO *in, int for_retry) -{ - return 0; -} - -int OSSL_ECHSTORE_read_pem(OSSL_ECHSTORE *es, BIO *in, int for_retry) -{ - return 0; -} - -int OSSL_ECHSTORE_num_keys(OSSL_ECHSTORE *es, int *numkeys) -{ - return 0; -} - -int OSSL_ECHSTORE_flush_keys(OSSL_ECHSTORE *es, time_t age) -{ - return 0; -} - -void OSSL_ECH_INFO_free(OSSL_ECH_INFO *info, int count) -{ - return; -} - -int OSSL_ECH_INFO_print(BIO *out, OSSL_ECH_INFO *info, int count) -{ - return 0; -} - -int SSL_CTX_set1_echstore(SSL_CTX *ctx, OSSL_ECHSTORE *es) -{ - return 0; -} - -int SSL_set1_echstore(SSL *s, OSSL_ECHSTORE *es) -{ - return 0; -} - -OSSL_ECHSTORE *SSL_CTX_get1_echstore(const SSL_CTX *ctx) -{ - return NULL; -} - -OSSL_ECHSTORE *SSL_get1_echstore(const SSL *s) -{ - return NULL; -} - -int SSL_ech_set_server_names(SSL *s, const char *inner_name, - const char *outer_name, int no_outer) -{ - return 0; -} - -int SSL_ech_set_outer_server_name(SSL *s, const char *outer_name, int no_outer) -{ - return 0; -} - -int SSL_ech_set_outer_alpn_protos(SSL *s, const unsigned char *protos, - const size_t protos_len) -{ - return 0; -} - -int SSL_ech_get1_status(SSL *s, char **inner_sni, char **outer_sni) -{ - return 0; -} - -int SSL_ech_set_grease_suite(SSL *s, const char *suite) -{ - return 0; -} - -int SSL_ech_set_grease_type(SSL *s, uint16_t type) -{ - return 0; -} - -void SSL_ech_set_callback(SSL *s, SSL_ech_cb_func f) -{ - return; -} - -int SSL_ech_get_retry_config(SSL *s, unsigned char **ec, size_t *eclen) -{ - return 0; -} - -int SSL_CTX_ech_set_outer_alpn_protos(SSL_CTX *s, const unsigned char *protos, - const size_t protos_len) -{ - return 0; -} - -int SSL_CTX_ech_raw_decrypt(SSL_CTX *ctx, - int *decrypted_ok, - char **inner_sni, char **outer_sni, - unsigned char *outer_ch, size_t outer_len, - unsigned char *inner_ch, size_t *inner_len, - unsigned char **hrrtok, size_t *toklen) -{ - return 0; -} - -void SSL_CTX_ech_set_callback(SSL_CTX *ctx, SSL_ech_cb_func f) -{ - return; -} - -#endif diff --git a/ssl/ech/build.info b/ssl/ech/build.info new file mode 100644 index 0000000000000..7f60fb957c531 --- /dev/null +++ b/ssl/ech/build.info @@ -0,0 +1,3 @@ +$LIBSSL=../../libssl + +SOURCE[$LIBSSL]=ech_ssl_apis.c ech_store.c ech_internal.c ech_helper.c diff --git a/ssl/ech/ech_helper.c b/ssl/ech/ech_helper.c new file mode 100644 index 0000000000000..bbe62eee21be5 --- /dev/null +++ b/ssl/ech/ech_helper.c @@ -0,0 +1,15 @@ +/* + * Copyright 2024 The OpenSSL Project Authors. All Rights Reserved. + * + * Licensed under the OpenSSL license (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 "../ssl_local.h" +#include "ech_local.h" + +/* TODO(ECH): move code that's used by internals and test here */ diff --git a/ssl/ech/ech_internal.c b/ssl/ech/ech_internal.c new file mode 100644 index 0000000000000..94842526e5cb2 --- /dev/null +++ b/ssl/ech/ech_internal.c @@ -0,0 +1,15 @@ +/* + * Copyright 2024 The OpenSSL Project Authors. All Rights Reserved. + * + * Licensed under the OpenSSL license (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 "../ssl_local.h" +#include "ech_local.h" + +/* TODO(ECH): move ECH internal code here when we get to it */ diff --git a/ssl/ech_local.h b/ssl/ech/ech_local.h similarity index 97% rename from ssl/ech_local.h rename to ssl/ech/ech_local.h index 125795fc2a6fc..1d89e410818a7 100644 --- a/ssl/ech_local.h +++ b/ssl/ech/ech_local.h @@ -31,6 +31,7 @@ */ # define OSSL_ECH_SUPERVERBOSE +# define OSSL_ECH_CIPHER_LEN 4 /* ECHCipher length (2 for kdf, 2 for aead) */ /* * Reminder of what goes in DNS for ECH RFC XXXX * diff --git a/ssl/ech/ech_ssl_apis.c b/ssl/ech/ech_ssl_apis.c new file mode 100644 index 0000000000000..9bdc4bbdbe43e --- /dev/null +++ b/ssl/ech/ech_ssl_apis.c @@ -0,0 +1,106 @@ +/* + * Copyright 2024 The OpenSSL Project Authors. All Rights Reserved. + * + * Licensed under the OpenSSL license (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 "../ssl_local.h" + +int SSL_CTX_set1_echstore(SSL_CTX *ctx, OSSL_ECHSTORE *es) +{ + return 0; +} + +int SSL_set1_echstore(SSL *s, OSSL_ECHSTORE *es) +{ + return 0; +} + +OSSL_ECHSTORE *SSL_CTX_get1_echstore(const SSL_CTX *ctx) +{ + return NULL; +} + +OSSL_ECHSTORE *SSL_get1_echstore(const SSL *s) +{ + return NULL; +} + +int SSL_ech_set_server_names(SSL *s, const char *inner_name, + const char *outer_name, int no_outer) +{ + return 0; +} + +int SSL_ech_set_outer_server_name(SSL *s, const char *outer_name, int no_outer) +{ + return 0; +} + +int SSL_ech_set_outer_alpn_protos(SSL *s, const unsigned char *protos, + const size_t protos_len) +{ + return 0; +} + +int SSL_ech_get1_status(SSL *s, char **inner_sni, char **outer_sni) +{ + return 0; +} + +int SSL_ech_set_grease_suite(SSL *s, const char *suite) +{ + return 0; +} + +int SSL_ech_set_grease_type(SSL *s, uint16_t type) +{ + return 0; +} + +void SSL_ech_set_callback(SSL *s, SSL_ech_cb_func f) +{ + return; +} + +int SSL_ech_get_retry_config(SSL *s, unsigned char **ec, size_t *eclen) +{ + return 0; +} + +int SSL_CTX_ech_set_outer_alpn_protos(SSL_CTX *s, const unsigned char *protos, + const size_t protos_len) +{ + return 0; +} + +int SSL_CTX_ech_raw_decrypt(SSL_CTX *ctx, + int *decrypted_ok, + char **inner_sni, char **outer_sni, + unsigned char *outer_ch, size_t outer_len, + unsigned char *inner_ch, size_t *inner_len, + unsigned char **hrrtok, size_t *toklen) +{ + if (ctx == NULL) { + /* + * TODO(ECH): this is a bit of a bogus error, just so as + * to get the `make update` command to add the required + * error number. We don't need it yet, but it's involved + * in some of the build artefacts, so may as well jump + * the gun a bit on it. + */ + ERR_raise(ERR_LIB_SSL, SSL_R_ECH_REQUIRED); + return 0; + } + return 0; +} + +void SSL_CTX_ech_set_callback(SSL_CTX *ctx, SSL_ech_cb_func f) +{ + return; +} diff --git a/ssl/ech/ech_store.c b/ssl/ech/ech_store.c new file mode 100644 index 0000000000000..58ba862dc97dd --- /dev/null +++ b/ssl/ech/ech_store.c @@ -0,0 +1,1168 @@ +/* + * Copyright 2024 The OpenSSL Project Authors. All Rights Reserved. + * + * Licensed under the OpenSSL license (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 "../ssl_local.h" +#include "ech_local.h" +#include +#include +#include + +/* a size for some crypto vars */ +#define OSSL_ECH_CRYPTO_VAR_SIZE 2048 + +/* + * Used for ech_bio2buf, when reading from a BIO we allocate in chunks sized + * as per below, with a max number of chunks as indicated, we don't expect to + * go beyond one chunk in almost all cases + */ +#define OSSL_ECH_BUFCHUNK 512 +#define OSSL_ECH_MAXITER 32 + +/* + * ECHConfigList input to OSSL_ECHSTORE_read_echconfiglist() + * can be either binary encoded ECHConfigList or a base64 + * encoded ECHConfigList. + */ +#define OSSL_ECH_FMT_BIN 1 /* binary ECHConfigList */ +#define OSSL_ECH_FMT_B64TXT 2 /* base64 ECHConfigList */ + +/* + * Telltales we use when guessing which form of encoded input we've + * been given for an RR value or ECHConfig. + * We give these the EBCDIC treatment as well - why not? :-) + */ +static const char B64_alphabet[] = + "\x41\x42\x43\x44\x45\x46\x47\x48\x49\x4a\x4b\x4c\x4d\x4e\x4f\x50\x51\x52" + "\x53\x54\x55\x56\x57\x58\x59\x5a\x61\x62\x63\x64\x65\x66\x67\x68\x69\x6a" + "\x6b\x6c\x6d\x6e\x6f\x70\x71\x72\x73\x74\x75\x76\x77\x78\x79\x7a\x30\x31" + "\x32\x33\x34\x35\x36\x37\x38\x39\x2b\x2f\x3d\x3b"; + +#ifndef TLSEXT_MINLEN_host_name +/* + * TODO(ECH): shortest DNS name we allow, e.g. "a.bc" - maybe that should + * be defined elsewhere, or should the check be skipped in case there's + * a local deployment that uses shorter names? + */ +# define TLSEXT_MINLEN_host_name 4 +#endif + +/* + * local functions - public APIs are at the end + */ + +static void ossl_echext_free(OSSL_ECHEXT *e) +{ + if (e == NULL) + return; + OPENSSL_free(e->val); + OPENSSL_free(e); + return; +} + +static void ossl_echstore_entry_free(OSSL_ECHSTORE_ENTRY *ee) +{ + if (ee == NULL) + return; + OPENSSL_free(ee->public_name); + OPENSSL_free(ee->pub); + OPENSSL_free(ee->pemfname); + EVP_PKEY_free(ee->keyshare); + OPENSSL_free(ee->encoded); + OPENSSL_free(ee->suites); + sk_OSSL_ECHEXT_pop_free(ee->exts, ossl_echext_free); + OPENSSL_free(ee); + return; +} + +/* + * @brief hash a buffer as a pretend file name being ascii-hex of hashed buffer + * @param es is the OSSL_ECHSTORE we're dealing with + * @param buf is the input buffer + * @param blen is the length of buf + * @param ah_hash is a pointer to where to put the result + * @param ah_len is the length of ah_hash + */ +static int ech_hash_pub_as_fname(OSSL_ECHSTORE *es, + const unsigned char *buf, size_t blen, + char *ah_hash, size_t ah_len) +{ + unsigned char hashval[EVP_MAX_MD_SIZE]; + size_t hashlen, actual_ah_len; + + if (es == NULL + || EVP_Q_digest(es->libctx, "SHA2-256", es->propq, + buf, blen, hashval, &hashlen) != 1 + || OPENSSL_buf2hexstr_ex(ah_hash, ah_len, &actual_ah_len, + hashval, hashlen, '\0') != 1) { + ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR); + return 0; + } + return 1; +} + +/* + * @brief Read a buffer from an input 'till eof + * @param in is the BIO input + * @param buf is where to put the buffer, allocated inside here + * @param len is the length of that buffer + * + * This is intended for small inputs, either files or buffers and + * not other kinds of BIO. + * TODO(ECH): how to check for oddball input BIOs? + */ +static int ech_bio2buf(BIO *in, unsigned char **buf, size_t *len) +{ + unsigned char *lptr = NULL, *lbuf = NULL, *tmp = NULL; + size_t sofar = 0, readbytes = 0; + int done = 0, brv, iter = 0; + + if (buf == NULL || len == NULL) + return 0; + sofar = OSSL_ECH_BUFCHUNK; + lbuf = OPENSSL_zalloc(sofar); + if (lbuf == NULL) + return 0; + lptr = lbuf; + while (!BIO_eof(in) && !done && iter++ < OSSL_ECH_MAXITER) { + brv = BIO_read_ex(in, lptr, OSSL_ECH_BUFCHUNK, &readbytes); + if (brv != 1) + goto err; + if (readbytes < OSSL_ECH_BUFCHUNK) { + done = 1; + break; + } + sofar += OSSL_ECH_BUFCHUNK; + tmp = OPENSSL_realloc(lbuf, sofar); + if (tmp == NULL) + goto err; + lbuf = tmp; + lptr = lbuf + sofar - OSSL_ECH_BUFCHUNK; + } + if (BIO_eof(in) && done == 1) { + *len = sofar + readbytes - OSSL_ECH_BUFCHUNK; + *buf = lbuf; + return 1; + } +err: + OPENSSL_free(lbuf); + return 0; +} + +/* + * @brief Figure out ECHConfig encoding + * @param encodedval is a buffer with the encoding + * @param encodedlen is the length of that buffer + * @param guessedfmt is the detected format + * @return 1 for success, 0 for error + */ +static int ech_check_format(const unsigned char *val, size_t len, int *fmt) +{ + size_t span = 0; + + if (fmt == NULL || len <= 4 || val == NULL) + return 0; + /* binary encoding starts with two octet length and ECH version */ + if (len == 2 + ((size_t)(val[0]) * 256 + (size_t)(val[1])) + && val[2] == ((OSSL_ECH_RFCXXXX_VERSION / 256) & 0xff) + && val[3] == ((OSSL_ECH_RFCXXXX_VERSION % 256) & 0xff)) { + *fmt = OSSL_ECH_FMT_BIN; + return 1; + } + span = strspn((char *)val, B64_alphabet); + if (len <= span) { + *fmt = OSSL_ECH_FMT_B64TXT; + return 1; + } + return 0; +} + +/* + * @brief helper to decode ECHConfig extensions + * @param ee is the OSSL_ECHSTORE entry for these + * @param exts is the binary form extensions + * @return 1 for good, 0 for error + */ +static int ech_decode_echconfig_exts(OSSL_ECHSTORE_ENTRY *ee, PACKET *exts) +{ + unsigned int exttype = 0; + size_t extlen = 0; + unsigned char *extval = NULL; + OSSL_ECHEXT *oe = NULL; + PACKET ext; + + /* + * reminder: exts is a two-octet length prefixed list of: + * - two octet extension type + * - two octet extension length (can be zero) + * - length octets + * we've consumed the overall length before getting here + */ + while (PACKET_remaining(exts) > 0) { + exttype = 0, extlen = 0; + extval = NULL; + oe = NULL; + if (!PACKET_get_net_2(exts, &exttype) || + !PACKET_get_length_prefixed_2(exts, &ext)) { + ERR_raise(ERR_LIB_SSL, SSL_R_BAD_ECHCONFIG_EXTENSION); + goto err; + } + if (PACKET_remaining(&ext) >= OSSL_ECH_MAX_ECHCONFIGEXT_LEN) { + ERR_raise(ERR_LIB_SSL, SSL_R_BAD_ECHCONFIG_EXTENSION); + goto err; + } + if (!PACKET_memdup(&ext, &extval, &extlen)) { + ERR_raise(ERR_LIB_SSL, SSL_R_BAD_ECHCONFIG_EXTENSION); + goto err; + } + oe = OPENSSL_malloc(sizeof(*oe)); + if (oe == NULL) + goto err; + oe->type = (uint16_t) exttype; + oe->val = extval; + extval = NULL; /* avoid double free */ + oe->len = (uint16_t) extlen; + if (ee->exts == NULL) + ee->exts = sk_OSSL_ECHEXT_new_null(); + if (ee->exts == NULL) { + ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR); + goto err; + } + if (!sk_OSSL_ECHEXT_push(ee->exts, oe)) { + ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR); + goto err; + } + } + return 1; +err: + sk_OSSL_ECHEXT_pop_free(ee->exts, ossl_echext_free); + ee->exts = NULL; + ossl_echext_free(oe); + OPENSSL_free(extval); + return 0; +} + +/* + * @brief Check entry to see if looks good or bad + * @param ee is the ECHConfig to check + * @return 1 for all good, 0 otherwise + */ +static int ech_final_config_checks(OSSL_ECHSTORE_ENTRY *ee) +{ + OSSL_HPKE_SUITE hpke_suite; + size_t ind, num; + int goodsuitefound = 0; + + /* check local support for some suite */ + for (ind = 0; ind != ee->nsuites; ind++) { + /* + * suite_check says yes to the pseudo-aead for export, but we don't + * want to see it here coming from outside in an encoding + */ + hpke_suite = ee->suites[ind]; + if (OSSL_HPKE_suite_check(hpke_suite) == 1 + && hpke_suite.aead_id != OSSL_HPKE_AEAD_ID_EXPORTONLY) { + goodsuitefound = 1; + break; + } + } + if (goodsuitefound == 0) { + ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_INVALID_ARGUMENT); + return 0; + } + /* check no mandatory exts (with high bit set in type) */ + num = (ee->exts == NULL ? 0 : sk_OSSL_ECHEXT_num(ee->exts)); + for (ind = 0; ind != num; ind++) { + OSSL_ECHEXT *oe = sk_OSSL_ECHEXT_value(ee->exts, ind); + + if (oe->type & 0x8000) { + ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_INVALID_ARGUMENT); + return 0; + } + } + /* check public_name rules, as per spec section 4 */ + if (ee->public_name == NULL + || ee->public_name[0] == '\0' + || ee->public_name[0] == '.' + || ee->public_name[strlen(ee->public_name) - 1] == '.') + return 0; + return 1; +} + +/** + * @brief decode one ECHConfig from a packet into an entry + * @param rent ptr to an entry allocated within (on success) + * @param pkt is the encoding + * @param priv is an optional private key (NULL if absent) + * @param for_retry says whether to include in a retry_config (if priv present) + * @return 1 for success, 0 for error + */ +static int ech_decode_one_entry(OSSL_ECHSTORE_ENTRY **rent, PACKET *pkt, + EVP_PKEY *priv, int for_retry) +{ + unsigned int ech_content_length = 0, tmpi; + const unsigned char *tmpecp = NULL; + size_t tmpeclen = 0, test_publen = 0; + PACKET ver_pkt, pub_pkt, cipher_suites, public_name_pkt, exts; + uint16_t thiskemid; + unsigned int suiteoctets = 0, ci = 0; + unsigned char cipher[OSSL_ECH_CIPHER_LEN], max_name_len; + unsigned char test_pub[OSSL_ECH_CRYPTO_VAR_SIZE]; + OSSL_ECHSTORE_ENTRY *ee = NULL; + + if (rent == NULL || pkt == NULL) { + ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR); + goto err; + } + ee = OPENSSL_zalloc(sizeof(*ee)); + if (ee == NULL) + goto err; + /* note start of encoding so we can make a copy later */ + tmpeclen = PACKET_remaining(pkt); + if (PACKET_peek_bytes(pkt, &tmpecp, tmpeclen) != 1 + || !PACKET_get_net_2(pkt, &tmpi)) { + ERR_raise(ERR_LIB_SSL, SSL_R_ECH_DECODE_ERROR); + goto err; + } + ee->version = (uint16_t) tmpi; + + /* grab versioned packet data */ + if (!PACKET_get_length_prefixed_2(pkt, &ver_pkt)) { + ERR_raise(ERR_LIB_SSL, SSL_R_ECH_DECODE_ERROR); + goto err; + } + ech_content_length = PACKET_remaining(&ver_pkt); + switch (ee->version) { + case OSSL_ECH_RFCXXXX_VERSION: + break; + default: + /* skip over in case we get something we can handle later */ + if (!PACKET_forward(&ver_pkt, ech_content_length)) { + ERR_raise(ERR_LIB_SSL, SSL_R_ECH_DECODE_ERROR); + goto err; + } + /* nothing to return but not a fail */ + ossl_echstore_entry_free(ee); + *rent = NULL; + return 1; + } + if (!PACKET_copy_bytes(&ver_pkt, &ee->config_id, 1) + || !PACKET_get_net_2(&ver_pkt, &tmpi) + || !PACKET_get_length_prefixed_2(&ver_pkt, &pub_pkt) + || !PACKET_memdup(&pub_pkt, &ee->pub, &ee->pub_len) + || !PACKET_get_length_prefixed_2(&ver_pkt, &cipher_suites) + || (suiteoctets = PACKET_remaining(&cipher_suites)) <= 0 + || (suiteoctets % 2) == 1) { + ERR_raise(ERR_LIB_SSL, SSL_R_ECH_DECODE_ERROR); + goto err; + } + thiskemid = (uint16_t) tmpi; + ee->nsuites = suiteoctets / OSSL_ECH_CIPHER_LEN; + ee->suites = OPENSSL_malloc(ee->nsuites * sizeof(*ee->suites)); + if (ee->suites == NULL) + goto err; + while (PACKET_copy_bytes(&cipher_suites, cipher, + OSSL_ECH_CIPHER_LEN)) { + ee->suites[ci].kem_id = thiskemid; + ee->suites[ci].kdf_id = cipher[0] << 8 | cipher [1]; + ee->suites[ci].aead_id = cipher[2] << 8 | cipher [3]; + if (ci++ >= ee->nsuites) { + ERR_raise(ERR_LIB_SSL, SSL_R_ECH_DECODE_ERROR); + goto err; + } + } + if (PACKET_remaining(&cipher_suites) > 0 + || !PACKET_copy_bytes(&ver_pkt, &max_name_len, 1)) { + ERR_raise(ERR_LIB_SSL, SSL_R_ECH_DECODE_ERROR); + goto err; + } + ee->max_name_length = max_name_len; + if (!PACKET_get_length_prefixed_1(&ver_pkt, &public_name_pkt)) { + ERR_raise(ERR_LIB_SSL, SSL_R_ECH_DECODE_ERROR); + goto err; + } + if (PACKET_contains_zero_byte(&public_name_pkt) + || PACKET_remaining(&public_name_pkt) < TLSEXT_MINLEN_host_name + || !PACKET_strndup(&public_name_pkt, &ee->public_name)) { + ERR_raise(ERR_LIB_SSL, SSL_R_ECH_DECODE_ERROR); + goto err; + } + if (!PACKET_get_length_prefixed_2(&ver_pkt, &exts)) { + ERR_raise(ERR_LIB_SSL, SSL_R_ECH_DECODE_ERROR); + goto err; + } + if (PACKET_remaining(&exts) > 0 + && ech_decode_echconfig_exts(ee, &exts) != 1) { + ERR_raise(ERR_LIB_SSL, SSL_R_ECH_DECODE_ERROR); + goto err; + } + /* set length of encoding of this ECHConfig */ + ee->encoded_len = PACKET_data(&ver_pkt) - tmpecp; + /* copy encoded as it might get free'd if a reduce happens */ + ee->encoded = OPENSSL_memdup(tmpecp, ee->encoded_len); + if (ee->encoded == NULL) + goto err; + if (priv != NULL) { + if (EVP_PKEY_get_octet_string_param(priv, + OSSL_PKEY_PARAM_ENCODED_PUBLIC_KEY, + test_pub, OSSL_ECH_CRYPTO_VAR_SIZE, + &test_publen) != 1) { + ERR_raise(ERR_LIB_SSL, SSL_R_ECH_DECODE_ERROR); + goto err; + } + if (test_publen == ee->pub_len + && !memcmp(test_pub, ee->pub, ee->pub_len)) { + EVP_PKEY_up_ref(priv); /* associate the private key */ + ee->keyshare = priv; + ee->for_retry = for_retry; + } + } + ee->loadtime = time(0); + *rent = ee; + return 1; +err: + ossl_echstore_entry_free(ee); + *rent = NULL; + return 0; +} + +/* + * @brief decode and flatten a binary encoded ECHConfigList + * @param es an OSSL_ECHSTORE + * @param priv is an optional private key (NULL if absent) + * @param for_retry says whether to include in a retry_config (if priv present) + * @param binbuf binary encoded ECHConfigList (we hope) + * @param binlen length of binbuf + * @return 1 for success, 0 for error + * + * We may only get one ECHConfig per list, but there can be more. We want each + * element of the output to contain exactly one ECHConfig so that a client + * could sensibly down select to the one they prefer later, and so that we have + * the specific encoded value of that ECHConfig for inclusion in the HPKE info + * parameter when finally encrypting or decrypting an inner ClientHello. + * + * If a private value is provided then that'll only be associated with the + * relevant public value, if >1 public value was present in the ECHConfigList. + */ +static int ech_decode_and_flatten(OSSL_ECHSTORE *es, EVP_PKEY *priv, int for_retry, + unsigned char *binbuf, size_t binblen) +{ + int rv = 0; + size_t remaining = 0; + PACKET opkt, pkt; + OSSL_ECHSTORE_ENTRY *ee = NULL; + + if (binbuf == NULL || binblen == 0 || binblen < OSSL_ECH_MIN_ECHCONFIG_LEN + || binblen >= OSSL_ECH_MAX_ECHCONFIG_LEN) { + ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_NULL_PARAMETER); + goto err; + } + if (PACKET_buf_init(&opkt, binbuf, binblen) != 1 + || !PACKET_get_length_prefixed_2(&opkt, &pkt)) { + ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR); + goto err; + } + remaining = PACKET_remaining(&pkt); + while (remaining > 0) { + if (ech_decode_one_entry(&ee, &pkt, priv, for_retry) != 1) { + ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR); + goto err; + } + remaining = PACKET_remaining(&pkt); + /* if unsupported version we can skip over */ + if (ee == NULL) + continue; + /* do final checks on suites, exts, and fail if issues */ + if (ech_final_config_checks(ee) != 1) + goto err; + /* push entry into store */ + if (es->entries == NULL) + es->entries = sk_OSSL_ECHSTORE_ENTRY_new_null(); + if (es->entries == NULL) { + ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR); + goto err; + } + if (!sk_OSSL_ECHSTORE_ENTRY_push(es->entries, ee)) { + ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR); + goto err; + } + ee = NULL; + } + rv = 1; +err: + ossl_echstore_entry_free(ee); + return rv; +} + +/* + * @brief check a private matches some public + * @param es is the ECH store + * @param priv is the private value + * @return 1 if we have a match, zero otherwise + */ +static int check_priv_matches(OSSL_ECHSTORE *es, EVP_PKEY *priv) +{ + int num, ent, gotone = 0; + OSSL_ECHSTORE_ENTRY *ee = NULL; + + num = (es->entries == NULL ? 0 : sk_OSSL_ECHSTORE_ENTRY_num(es->entries)); + for (ent = 0; ent != num; ent++) { + ee = sk_OSSL_ECHSTORE_ENTRY_value(es->entries, ent); + if (ee == NULL) { + ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_INVALID_ARGUMENT); + return 0; + } + if (EVP_PKEY_eq(ee->keyshare, priv)) { + gotone = 1; + break; + } + } + return gotone; +} + +/* + * @brief decode input ECHConfigList and associate optional private info + * @param es is the OSSL_ECHSTORE + * @param in is the BIO from which we'll get the ECHConfigList + * @param priv is an optional private key + * @param for_retry 1 if the public related to priv ought be in retry_config + */ +static int ech_read_priv_echconfiglist(OSSL_ECHSTORE *es, BIO *in, + EVP_PKEY *priv, int for_retry) +{ + int rv = 0, detfmt, tdeclen = 0; + size_t encodedlen = 0, binlen = 0; + unsigned char *encodedval = NULL, *binbuf = NULL; + BIO *btmp = NULL, *btmp1 = NULL; + + if (es == NULL || in == NULL) { + ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_NULL_PARAMETER); + return 0; + } + if (ech_bio2buf(in, &encodedval, &encodedlen) != 1) { + ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR); + return 0; + } + if (encodedlen >= OSSL_ECH_MAX_ECHCONFIG_LEN) { /* sanity check */ + ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR); + goto err; + } + if (ech_check_format(encodedval, encodedlen, &detfmt) != 1) { + ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_INVALID_ARGUMENT); + goto err; + } + if (detfmt == OSSL_ECH_FMT_BIN) { /* copy buffer if binary format */ + binbuf = OPENSSL_memdup(encodedval, encodedlen); + if (binbuf == NULL) + goto err; + binlen = encodedlen; + } + if (detfmt == OSSL_ECH_FMT_B64TXT) { + btmp = BIO_new_mem_buf(encodedval, -1); + if (btmp == NULL) { + ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR); + goto err; + } + btmp1 = BIO_new(BIO_f_base64()); + if (btmp1 == NULL) { + ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR); + goto err; + } + BIO_set_flags(btmp1, BIO_FLAGS_BASE64_NO_NL); + btmp = BIO_push(btmp1, btmp); + /* overestimate but good enough */ + binbuf = OPENSSL_malloc(encodedlen); + if (binbuf == NULL) { + ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR); + goto err; + } + tdeclen = BIO_read(btmp, binbuf, encodedlen); + if (tdeclen <= 0) { /* need int for -1 return in failure case */ + ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR); + goto err; + } + binlen = tdeclen; + } + if (ech_decode_and_flatten(es, priv, for_retry, binbuf, binlen) != 1) { + ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR); + goto err; + } + if (priv != NULL && check_priv_matches(es, priv) == 0) + goto err; + rv = 1; +err: + BIO_free_all(btmp); + OPENSSL_free(binbuf); + OPENSSL_free(encodedval); + return rv; +} + +/* + * API calls built around OSSL_ECHSSTORE + */ + +OSSL_ECHSTORE *OSSL_ECHSTORE_new(OSSL_LIB_CTX *libctx, const char *propq) +{ + OSSL_ECHSTORE *es = NULL; + + es = OPENSSL_zalloc(sizeof(*es)); + if (es == NULL) { + ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR); + return 0; + } + es->libctx = libctx; + es->propq = propq; + return es; +} + +void OSSL_ECHSTORE_free(OSSL_ECHSTORE *es) +{ + if (es == NULL) + return; + sk_OSSL_ECHSTORE_ENTRY_pop_free(es->entries, ossl_echstore_entry_free); + OPENSSL_free(es); + return; +} + +int OSSL_ECHSTORE_new_config(OSSL_ECHSTORE *es, + uint16_t echversion, uint8_t max_name_length, + const char *public_name, OSSL_HPKE_SUITE suite) +{ + size_t pnlen = 0, publen = OSSL_ECH_CRYPTO_VAR_SIZE; + unsigned char pub[OSSL_ECH_CRYPTO_VAR_SIZE]; + int rv = 0; + unsigned char *bp = NULL; + size_t bblen = 0; + EVP_PKEY *privp = NULL; + uint8_t config_id = 0; + WPACKET epkt; + BUF_MEM *epkt_mem = NULL; + OSSL_ECHSTORE_ENTRY *ee = NULL; + char pembuf[2 * EVP_MAX_MD_SIZE + 1]; + size_t pembuflen = 2 * EVP_MAX_MD_SIZE + 1; + + /* basic checks */ + if (es == NULL) { + ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_NULL_PARAMETER); + return 0; + } + pnlen = (public_name == NULL ? 0 : strlen(public_name)); + if (pnlen == 0 || pnlen > OSSL_ECH_MAX_PUBLICNAME + || max_name_length > OSSL_ECH_MAX_MAXNAMELEN) { + ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_INVALID_ARGUMENT); + return 0; + } + /* this used have more versions and will again in future */ + switch (echversion) { + case OSSL_ECH_RFCXXXX_VERSION: + break; + default: + ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_INVALID_ARGUMENT); + return 0; + } + /* + * Reminder, for draft-13 we want this: + * + * opaque HpkePublicKey<1..2^16-1>; + * uint16 HpkeKemId; // Defined in I-D.irtf-cfrg-hpke + * uint16 HpkeKdfId; // Defined in I-D.irtf-cfrg-hpke + * uint16 HpkeAeadId; // Defined in I-D.irtf-cfrg-hpke + * struct { + * HpkeKdfId kdf_id; + * HpkeAeadId aead_id; + * } HpkeSymmetricCipherSuite; + * struct { + * uint8 config_id; + * HpkeKemId kem_id; + * HpkePublicKey public_key; + * HpkeSymmetricCipherSuite cipher_suites<4..2^16-4>; + * } HpkeKeyConfig; + * struct { + * HpkeKeyConfig key_config; + * uint8 maximum_name_length; + * opaque public_name<1..255>; + * Extension extensions<0..2^16-1>; + * } ECHConfigContents; + * struct { + * uint16 version; + * uint16 length; + * select (ECHConfig.version) { + * case 0xfe0d: ECHConfigContents contents; + * } + * } ECHConfig; + * ECHConfig ECHConfigList<1..2^16-1>; + */ + if ((epkt_mem = BUF_MEM_new()) == NULL + || !BUF_MEM_grow(epkt_mem, OSSL_ECH_MAX_ECHCONFIG_LEN) + || !WPACKET_init(&epkt, epkt_mem)) { + ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR); + goto err; + } + /* random config_id */ + if (RAND_bytes_ex(es->libctx, (unsigned char *)&config_id, 1, + RAND_DRBG_STRENGTH) <= 0) { + ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR); + goto err; + } + /* key pair */ + if (OSSL_HPKE_keygen(suite, pub, &publen, &privp, NULL, 0, + es->libctx, es->propq) != 1) { + ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR); + goto err; + } + /* config id, KEM, public, KDF, AEAD, max name len, public_name, exts */ + if ((bp = WPACKET_get_curr(&epkt)) == NULL + || !WPACKET_start_sub_packet_u16(&epkt) + || !WPACKET_put_bytes_u16(&epkt, echversion) + || !WPACKET_start_sub_packet_u16(&epkt) + || !WPACKET_put_bytes_u8(&epkt, config_id) + || !WPACKET_put_bytes_u16(&epkt, suite.kem_id) + || !WPACKET_start_sub_packet_u16(&epkt) + || !WPACKET_memcpy(&epkt, pub, publen) + || !WPACKET_close(&epkt) + || !WPACKET_start_sub_packet_u16(&epkt) + || !WPACKET_put_bytes_u16(&epkt, suite.kdf_id) + || !WPACKET_put_bytes_u16(&epkt, suite.aead_id) + || !WPACKET_close(&epkt) + || !WPACKET_put_bytes_u8(&epkt, max_name_length) + || !WPACKET_start_sub_packet_u8(&epkt) + || !WPACKET_memcpy(&epkt, public_name, pnlen) + || !WPACKET_close(&epkt) + || !WPACKET_start_sub_packet_u16(&epkt) + || !WPACKET_memcpy(&epkt, NULL, 0) /* no extensions */ + || !WPACKET_close(&epkt) + || !WPACKET_close(&epkt) + || !WPACKET_close(&epkt)) { + ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR); + goto err; + } + /* bp, bblen has encoding */ + WPACKET_get_total_written(&epkt, &bblen); + if ((ee = OPENSSL_zalloc(sizeof(*ee))) == NULL) { + ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR); + goto err; + } + ee->suites = OPENSSL_malloc(sizeof(*ee->suites)); + if (ee->suites == NULL) { + ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR); + goto err; + } + if (ech_hash_pub_as_fname(es, pub, publen, pembuf, pembuflen) != 1) { + ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR); + goto err; + } + ee->version = echversion; + ee->pub_len = publen; + ee->pub = OPENSSL_memdup(pub, publen); + if (ee->pub == NULL) { + ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR); + goto err; + } + ee->nsuites = 1; + ee->suites[0] = suite; + ee->public_name = OPENSSL_strdup(public_name); + if (ee->public_name == NULL) { + ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR); + goto err; + } + ee->max_name_length = max_name_length; + ee->config_id = config_id; + ee->keyshare = privp; + /* "steal" the encoding from the memory */ + ee->encoded = (unsigned char *)epkt_mem->data; + ee->encoded_len = bblen; + epkt_mem->data = NULL; + epkt_mem->length = 0; + ee->pemfname = OPENSSL_strdup(pembuf); + if (ee->pemfname == NULL) { + ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR); + goto err; + } + ee->loadtime = time(0); + /* push entry into store */ + if (es->entries == NULL) + es->entries = sk_OSSL_ECHSTORE_ENTRY_new_null(); + if (es->entries == NULL) { + ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR); + goto err; + } + if (!sk_OSSL_ECHSTORE_ENTRY_push(es->entries, ee)) { + ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR); + goto err; + } + WPACKET_finish(&epkt); + BUF_MEM_free(epkt_mem); + return 1; + +err: + EVP_PKEY_free(privp); + WPACKET_cleanup(&epkt); + BUF_MEM_free(epkt_mem); + ossl_echstore_entry_free(ee); + return rv; +} + +int OSSL_ECHSTORE_write_pem(OSSL_ECHSTORE *es, int index, BIO *out) +{ + OSSL_ECHSTORE_ENTRY *ee = NULL; + int rv = 0, num = 0, chosen = 0, doall = 0; + WPACKET epkt; /* used if we want to merge ECHConfigs for output */ + BUF_MEM *epkt_mem = NULL; + size_t allencoded_len; + + if (es == NULL) { + ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_INVALID_ARGUMENT); + return 0; + } + num = (es->entries == NULL ? 0 : sk_OSSL_ECHSTORE_ENTRY_num(es->entries)); + if (num <= 0) { + ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_INVALID_ARGUMENT); + return 0; + } + if (index >= num) { + ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_INVALID_ARGUMENT); + return 0; + } + if (index == OSSL_ECHSTORE_ALL) + doall = 1; + else if (index == OSSL_ECHSTORE_LAST) + chosen = num - 1; + else + chosen = index; + memset(&epkt, 0, sizeof(epkt)); + if (doall == 0) { + ee = sk_OSSL_ECHSTORE_ENTRY_value(es->entries, chosen); + if (ee == NULL || ee->encoded == NULL) { + ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_INVALID_ARGUMENT); + return 0; + } + /* private key first */ + if (ee->keyshare != NULL + && !PEM_write_bio_PrivateKey(out, ee->keyshare, NULL, NULL, 0, + NULL, NULL)) { + ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR); + goto err; + } + if (PEM_write_bio(out, PEM_STRING_ECHCONFIG, NULL, + ee->encoded, ee->encoded_len) <= 0) { + ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR); + goto err; + } + } else { + /* catenate the encodings into one */ + if ((epkt_mem = BUF_MEM_new()) == NULL + || !BUF_MEM_grow(epkt_mem, OSSL_ECH_MAX_ECHCONFIG_LEN) + || !WPACKET_init(&epkt, epkt_mem) + || !WPACKET_start_sub_packet_u16(&epkt)) { + ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR); + goto err; + } + for (chosen = 0; chosen != num; chosen++) { + ee = sk_OSSL_ECHSTORE_ENTRY_value(es->entries, chosen); + if (ee == NULL || ee->encoded == NULL) { + ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_INVALID_ARGUMENT); + return 0; + } + if (!WPACKET_memcpy(&epkt, ee->encoded, ee->encoded_len)) { + ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR); + goto err; + } + } + if (!WPACKET_close(&epkt)) { + ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR); + goto err; + } + WPACKET_get_total_written(&epkt, &allencoded_len); + if (PEM_write_bio(out, PEM_STRING_ECHCONFIG, NULL, + (unsigned char *)epkt_mem->data, + allencoded_len) <= 0) { + ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR); + goto err; + } + } + rv = 1; +err: + WPACKET_cleanup(&epkt); + BUF_MEM_free(epkt_mem); + return rv; +} + +int OSSL_ECHSTORE_read_echconfiglist(OSSL_ECHSTORE *es, BIO *in) +{ + return ech_read_priv_echconfiglist(es, in, NULL, 0); +} + +int OSSL_ECHSTORE_get1_info(OSSL_ECHSTORE *es, OSSL_ECH_INFO **info, + int *count) +{ + OSSL_ECH_INFO *linfo = NULL, *inst = NULL; + OSSL_ECHSTORE_ENTRY *ee = NULL; + unsigned int i = 0, j = 0, num = 0; + BIO *out = NULL; + time_t now = time(0); + size_t ehlen; + unsigned char *ignore = NULL; + + if (es == NULL || info == NULL || count == NULL) { + ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_NULL_PARAMETER); + return 0; + } + num = (es->entries == NULL ? 0 : sk_OSSL_ECHSTORE_ENTRY_num(es->entries)); + if (num == 0) { + *info = NULL; + *count = 0; + return 1; + } + linfo = OPENSSL_zalloc(num * sizeof(*linfo)); + if (linfo == NULL) + goto err; + for (i = 0; i != num; i++) { + inst = &linfo[i]; + ee = sk_OSSL_ECHSTORE_ENTRY_value(es->entries, i); + + inst->index = i; + inst->seconds_in_memory = now - ee->loadtime; + inst->public_name = OPENSSL_strdup(ee->public_name); + inst->has_private_key = (ee->keyshare == NULL ? 0 : 1); + /* Now "print" the ECHConfigList */ + out = BIO_new(BIO_s_mem()); + if (out == NULL) { + ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_NULL_PARAMETER); + goto err; + } + if (ee->version != OSSL_ECH_RFCXXXX_VERSION) { + /* just note we don't support that one today */ + BIO_printf(out, "[Unsupported version (%04x)]", ee->version); + continue; + } + /* version, config_id, public_name, and kem */ + BIO_printf(out, "[%04x,%02x,%s,[", ee->version, + ee->config_id, + ee->public_name != NULL ? (char *)ee->public_name : "NULL"); + /* ciphersuites */ + for (j = 0; j != ee->nsuites; j++) { + BIO_printf(out, "%04x,%04x,%04x", ee->suites[j].kem_id, + ee->suites[j].kdf_id, ee->suites[j].aead_id); + if (j < (ee->nsuites - 1)) + BIO_printf(out, ","); + } + BIO_printf(out, "],"); + /* public key */ + for (j = 0; j != ee->pub_len; j++) + BIO_printf(out, "%02x", ee->pub[j]); + /* max name length and (only) number of extensions */ + BIO_printf(out, ",%02x,%02x]", ee->max_name_length, + ee->exts == NULL ? 0 : sk_OSSL_ECHEXT_num(ee->exts)); + ehlen = BIO_get_mem_data(out, &ignore); + inst->echconfig = OPENSSL_malloc(ehlen + 1); + if (inst->echconfig == NULL) + goto err; + if (BIO_read(out, inst->echconfig, ehlen) <= 0) { + ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_NULL_PARAMETER); + goto err; + } + inst->echconfig[ehlen] = '\0'; + BIO_free(out); + out = NULL; + } + *count = num; + *info = linfo; + return 1; +err: + BIO_free(out); + OSSL_ECH_INFO_free(linfo, num); + return 0; +} + +int OSSL_ECHSTORE_downselect(OSSL_ECHSTORE *es, int index) +{ + OSSL_ECHSTORE_ENTRY *ee = NULL; + int i, num = 0, chosen = OSSL_ECHSTORE_ALL; + + if (es == NULL) { + ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_NULL_PARAMETER); + return 0; + } + num = (es->entries == NULL ? 0 : sk_OSSL_ECHSTORE_ENTRY_num(es->entries)); + if (num == 0) { + ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_INVALID_ARGUMENT); + return 0; + } + if (index <= OSSL_ECHSTORE_ALL) { + ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_INVALID_ARGUMENT); + return 0; + } + if (index == OSSL_ECHSTORE_LAST) { + chosen = num - 1; + } else if (index >= num) { + ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_INVALID_ARGUMENT); + return 0; + } else { + chosen = index; + } + for (i = num - 1; i >= 0; i--) { + if (i == chosen) + continue; + ee = sk_OSSL_ECHSTORE_ENTRY_value(es->entries, i); + ossl_echstore_entry_free(ee); + sk_OSSL_ECHSTORE_ENTRY_delete(es->entries, i); + } + return 1; +} + +int OSSL_ECHSTORE_set1_key_and_read_pem(OSSL_ECHSTORE *es, EVP_PKEY *priv, + BIO *in, int for_retry) +{ + unsigned char *b64 = NULL; + long b64len = 0; + BIO *b64bio = NULL; + int rv = 0; + char *pname = NULL, *pheader = NULL; + + /* we allow for a NULL private key */ + if (es == NULL || in == NULL) { + ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_NULL_PARAMETER); + return 0; + } + if (PEM_read_bio(in, &pname, &pheader, &b64, &b64len) != 1) { + ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR); + return 0; + } + if (pname == NULL || strcmp(pname, PEM_STRING_ECHCONFIG) != 0) { + ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR); + goto err; + } + b64bio = BIO_new(BIO_s_mem()); + if (b64bio == NULL + || BIO_write(b64bio, b64, b64len) <= 0 + || ech_read_priv_echconfiglist(es, b64bio, priv, for_retry) != 1) { + ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR); + goto err; + } + rv = 1; +err: + OPENSSL_free(pname); + OPENSSL_free(pheader); + BIO_free_all(b64bio); + OPENSSL_free(b64); + return rv; +} + +int OSSL_ECHSTORE_read_pem(OSSL_ECHSTORE *es, BIO *in, int for_retry) +{ + EVP_PKEY *priv = NULL; + int rv = 0; + BIO *fbio = BIO_new(BIO_f_buffer()); + + if (fbio == NULL || es == NULL || in == NULL) { + BIO_free_all(fbio); + ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_NULL_PARAMETER); + return 0; + } + /* + * Read private key then handoff to set1_key_and_read_pem. + * We allow for no private key as an option, to handle that + * the BIO_f_buffer allows us to seek back to the start. + */ + BIO_push(fbio, in); + if (!PEM_read_bio_PrivateKey(fbio, &priv, NULL, NULL) + && BIO_seek(fbio, 0) < 0) { + ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR); + goto err; + } + rv = OSSL_ECHSTORE_set1_key_and_read_pem(es, priv, fbio, for_retry); +err: + EVP_PKEY_free(priv); + BIO_pop(fbio); + BIO_free_all(fbio); + return rv; +} + +int OSSL_ECHSTORE_num_keys(OSSL_ECHSTORE *es, int *numkeys) +{ + int i, num = 0, count = 0; + OSSL_ECHSTORE_ENTRY *ee = NULL; + + if (es == NULL || numkeys == NULL) { + ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_NULL_PARAMETER); + return 0; + } + num = (es->entries == NULL ? 0 : sk_OSSL_ECHSTORE_ENTRY_num(es->entries)); + for (i = 0; i != num; i++) { + ee = sk_OSSL_ECHSTORE_ENTRY_value(es->entries, i); + if (ee == NULL) { + ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR); + return 0; + } + count += (ee->keyshare != NULL); + } + *numkeys = count; + return 1; +} + +int OSSL_ECHSTORE_flush_keys(OSSL_ECHSTORE *es, time_t age) +{ + OSSL_ECHSTORE_ENTRY *ee = NULL; + int i, num = 0; + time_t now = time(0); + + if (es == NULL) { + ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_NULL_PARAMETER); + return 0; + } + num = (es->entries == NULL ? 0 : sk_OSSL_ECHSTORE_ENTRY_num(es->entries)); + if (num == 0) { + ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_INVALID_ARGUMENT); + return 0; + } + for (i = num - 1; i >= 0; i--) { + ee = sk_OSSL_ECHSTORE_ENTRY_value(es->entries, i); + if (ee == NULL) { + ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_INVALID_ARGUMENT); + return 0; + } + if (ee->keyshare != NULL && ((ee->loadtime + age) > now)) { + ossl_echstore_entry_free(ee); + sk_OSSL_ECHSTORE_ENTRY_delete(es->entries, i); + } + } + return 1; +} + +void OSSL_ECH_INFO_free(OSSL_ECH_INFO *info, int count) +{ + int i; + + if (info == NULL) + return; + for (i = 0; i != count; i++) { + OPENSSL_free(info[i].public_name); + OPENSSL_free(info[i].inner_name); + OPENSSL_free(info[i].outer_alpns); + OPENSSL_free(info[i].inner_alpns); + OPENSSL_free(info[i].echconfig); + } + OPENSSL_free(info); + return; +} + +int OSSL_ECH_INFO_print(BIO *out, OSSL_ECH_INFO *info, int index) +{ + if (out == NULL || info == NULL) { + ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_NULL_PARAMETER); + return 0; + } + BIO_printf(out, "ECH entry: %d public_name: %s age: %d%s\n", + index, info[index].public_name, + (int)info[index].seconds_in_memory, + info[index].has_private_key ? " (has private key)" : ""); + BIO_printf(out, "\t%s\n", info[index].echconfig); + return 1; +} diff --git a/test/certs/ech-big.pem b/test/certs/ech-big.pem new file mode 100644 index 0000000000000..99c9c67bdee4d --- /dev/null +++ b/test/certs/ech-big.pem @@ -0,0 +1,25 @@ +-----BEGIN ECHCONFIG----- +BNj+DQA6uwAgACBix2B78sX+EQhEbxMspDOc8Z3xVS5aQpYP0Cxpc2AWPAAEAAEAAQALZXhhbXBs +ZS5jb20AAP4NADq7ACAAIGLHYHvyxf4RCERvEyykM5zxnfFVLlpClg/QLGlzYBY8AAQAAQABAAtl +eGFtcGxlLmNvbQAA/g0AOrsAIAAgYsdge/LF/hEIRG8TLKQznPGd8VUuWkKWD9AsaXNgFjwABAAB +AAEAC2V4YW1wbGUuY29tAAD+DQA6uwAgACBix2B78sX+EQhEbxMspDOc8Z3xVS5aQpYP0Cxpc2AW +PAAEAAEAAQALZXhhbXBsZS5jb20AAP4NADq7ACAAIGLHYHvyxf4RCERvEyykM5zxnfFVLlpClg/Q +LGlzYBY8AAQAAQABAAtleGFtcGxlLmNvbQAA/g0AOrsAIAAgYsdge/LF/hEIRG8TLKQznPGd8VUu +WkKWD9AsaXNgFjwABAABAAEAC2V4YW1wbGUuY29tAAD+DQA6uwAgACBix2B78sX+EQhEbxMspDOc +8Z3xVS5aQpYP0Cxpc2AWPAAEAAEAAQALZXhhbXBsZS5jb20AAP4NADq7ACAAIGLHYHvyxf4RCERv +EyykM5zxnfFVLlpClg/QLGlzYBY8AAQAAQABAAtleGFtcGxlLmNvbQAA/g0AOrsAIAAgYsdge/LF +/hEIRG8TLKQznPGd8VUuWkKWD9AsaXNgFjwABAABAAEAC2V4YW1wbGUuY29tAAD+DQA6uwAgACBi +x2B78sX+EQhEbxMspDOc8Z3xVS5aQpYP0Cxpc2AWPAAEAAEAAQALZXhhbXBsZS5jb20AAP4NADq7 +ACAAIGLHYHvyxf4RCERvEyykM5zxnfFVLlpClg/QLGlzYBY8AAQAAQABAAtleGFtcGxlLmNvbQAA +/g0AOrsAIAAgYsdge/LF/hEIRG8TLKQznPGd8VUuWkKWD9AsaXNgFjwABAABAAEAC2V4YW1wbGUu +Y29tAAD+DQA6uwAgACBix2B78sX+EQhEbxMspDOc8Z3xVS5aQpYP0Cxpc2AWPAAEAAEAAQALZXhh +bXBsZS5jb20AAP4NADq7ACAAIGLHYHvyxf4RCERvEyykM5zxnfFVLlpClg/QLGlzYBY8AAQAAQAB +AAtleGFtcGxlLmNvbQAA/g0AOrsAIAAgYsdge/LF/hEIRG8TLKQznPGd8VUuWkKWD9AsaXNgFjwA +BAABAAEAC2V4YW1wbGUuY29tAAD+DQA6uwAgACBix2B78sX+EQhEbxMspDOc8Z3xVS5aQpYP0Cxp +c2AWPAAEAAEAAQALZXhhbXBsZS5jb20AAP4NADq7ACAAIGLHYHvyxf4RCERvEyykM5zxnfFVLlpC +lg/QLGlzYBY8AAQAAQABAAtleGFtcGxlLmNvbQAA/g0AOrsAIAAgYsdge/LF/hEIRG8TLKQznPGd +8VUuWkKWD9AsaXNgFjwABAABAAEAC2V4YW1wbGUuY29tAAD+DQA6uwAgACBix2B78sX+EQhEbxMs +pDOc8Z3xVS5aQpYP0Cxpc2AWPAAEAAEAAQALZXhhbXBsZS5jb20AAP4NADq7ACAAIGLHYHvyxf4R +CERvEyykM5zxnfFVLlpClg/QLGlzYBY8AAQAAQABAAtleGFtcGxlLmNvbQAA +-----END ECHCONFIG----- + diff --git a/test/certs/ech-eg.pem b/test/certs/ech-eg.pem new file mode 100644 index 0000000000000..4d37f5b17d54a --- /dev/null +++ b/test/certs/ech-eg.pem @@ -0,0 +1,7 @@ +-----BEGIN PRIVATE KEY----- +MC4CAQAwBQYDK2VuBCIEIKBC3rocwIF5tGY+/TaYQrCxY+ULsch94ja9DojkcvlT +-----END PRIVATE KEY----- +-----BEGIN ECHCONFIG----- +ADn+DQA1agAgACBtuySC1pphjFlGYKTaSm2KWNg7GQVRS8uAYvLTm5QlGwAEAAEA +AQAGZWcuY29tAAA= +-----END ECHCONFIG----- diff --git a/test/certs/ech-giant.pem b/test/certs/ech-giant.pem new file mode 100644 index 0000000000000..d0e5a46c4134d --- /dev/null +++ b/test/certs/ech-giant.pem @@ -0,0 +1,37 @@ +-----BEGIN ECHCONFIG----- +B8D+DQA6uwAgACBix2B78sX+EQhEbxMspDOc8Z3xVS5aQpYP0Cxpc2AWPAAEAAEAAQALZXhhbXBs +ZS5jb20AAP4NADq7ACAAIGLHYHvyxf4RCERvEyykM5zxnfFVLlpClg/QLGlzYBY8AAQAAQABAAtl +eGFtcGxlLmNvbQAA/g0AOrsAIAAgYsdge/LF/hEIRG8TLKQznPGd8VUuWkKWD9AsaXNgFjwABAAB +AAEAC2V4YW1wbGUuY29tAAD+DQA6uwAgACBix2B78sX+EQhEbxMspDOc8Z3xVS5aQpYP0Cxpc2AW +PAAEAAEAAQALZXhhbXBsZS5jb20AAP4NADq7ACAAIGLHYHvyxf4RCERvEyykM5zxnfFVLlpClg/Q +LGlzYBY8AAQAAQABAAtleGFtcGxlLmNvbQAA/g0AOrsAIAAgYsdge/LF/hEIRG8TLKQznPGd8VUu +WkKWD9AsaXNgFjwABAABAAEAC2V4YW1wbGUuY29tAAD+DQA6uwAgACBix2B78sX+EQhEbxMspDOc +8Z3xVS5aQpYP0Cxpc2AWPAAEAAEAAQALZXhhbXBsZS5jb20AAP4NADq7ACAAIGLHYHvyxf4RCERv +EyykM5zxnfFVLlpClg/QLGlzYBY8AAQAAQABAAtleGFtcGxlLmNvbQAA/g0AOrsAIAAgYsdge/LF +/hEIRG8TLKQznPGd8VUuWkKWD9AsaXNgFjwABAABAAEAC2V4YW1wbGUuY29tAAD+DQA6uwAgACBi +x2B78sX+EQhEbxMspDOc8Z3xVS5aQpYP0Cxpc2AWPAAEAAEAAQALZXhhbXBsZS5jb20AAP4NADq7 +ACAAIGLHYHvyxf4RCERvEyykM5zxnfFVLlpClg/QLGlzYBY8AAQAAQABAAtleGFtcGxlLmNvbQAA +/g0AOrsAIAAgYsdge/LF/hEIRG8TLKQznPGd8VUuWkKWD9AsaXNgFjwABAABAAEAC2V4YW1wbGUu +Y29tAAD+DQA6uwAgACBix2B78sX+EQhEbxMspDOc8Z3xVS5aQpYP0Cxpc2AWPAAEAAEAAQALZXhh +bXBsZS5jb20AAP4NADq7ACAAIGLHYHvyxf4RCERvEyykM5zxnfFVLlpClg/QLGlzYBY8AAQAAQAB +AAtleGFtcGxlLmNvbQAA/g0AOrsAIAAgYsdge/LF/hEIRG8TLKQznPGd8VUuWkKWD9AsaXNgFjwA +BAABAAEAC2V4YW1wbGUuY29tAAD+DQA6uwAgACBix2B78sX+EQhEbxMspDOc8Z3xVS5aQpYP0Cxp +c2AWPAAEAAEAAQALZXhhbXBsZS5jb20AAP4NADq7ACAAIGLHYHvyxf4RCERvEyykM5zxnfFVLlpC +lg/QLGlzYBY8AAQAAQABAAtleGFtcGxlLmNvbQAA/g0AOrsAIAAgYsdge/LF/hEIRG8TLKQznPGd +8VUuWkKWD9AsaXNgFjwABAABAAEAC2V4YW1wbGUuY29tAAD+DQA6uwAgACBix2B78sX+EQhEbxMs +pDOc8Z3xVS5aQpYP0Cxpc2AWPAAEAAEAAQALZXhhbXBsZS5jb20AAP4NADq7ACAAIGLHYHvyxf4R +CERvEyykM5zxnfFVLlpClg/QLGlzYBY8AAQAAQABAAtleGFtcGxlLmNvbQAA/g0AOrsAIAAgYsdg +e/LF/hEIRG8TLKQznPGd8VUuWkKWD9AsaXNgFjwABAABAAEAC2V4YW1wbGUuY29tAAD+DQA6uwAg +ACBix2B78sX+EQhEbxMspDOc8Z3xVS5aQpYP0Cxpc2AWPAAEAAEAAQALZXhhbXBsZS5jb20AAP4N +ADq7ACAAIGLHYHvyxf4RCERvEyykM5zxnfFVLlpClg/QLGlzYBY8AAQAAQABAAtleGFtcGxlLmNv +bQAA/g0AOrsAIAAgYsdge/LF/hEIRG8TLKQznPGd8VUuWkKWD9AsaXNgFjwABAABAAEAC2V4YW1w +bGUuY29tAAD+DQA6uwAgACBix2B78sX+EQhEbxMspDOc8Z3xVS5aQpYP0Cxpc2AWPAAEAAEAAQAL +ZXhhbXBsZS5jb20AAP4NADq7ACAAIGLHYHvyxf4RCERvEyykM5zxnfFVLlpClg/QLGlzYBY8AAQA +AQABAAtleGFtcGxlLmNvbQAA/g0AOrsAIAAgYsdge/LF/hEIRG8TLKQznPGd8VUuWkKWD9AsaXNg +FjwABAABAAEAC2V4YW1wbGUuY29tAAD+DQA6uwAgACBix2B78sX+EQhEbxMspDOc8Z3xVS5aQpYP +0Cxpc2AWPAAEAAEAAQALZXhhbXBsZS5jb20AAP4NADq7ACAAIGLHYHvyxf4RCERvEyykM5zxnfFV +LlpClg/QLGlzYBY8AAQAAQABAAtleGFtcGxlLmNvbQAA/g0AOrsAIAAgYsdge/LF/hEIRG8TLKQz +nPGd8VUuWkKWD9AsaXNgFjwABAABAAEAC2V4YW1wbGUuY29tAAD+DQA6uwAgACBix2B78sX+EQhE +bxMspDOc8Z3xVS5aQpYP0Cxpc2AWPAAEAAEAAQALZXhhbXBsZS5jb20AAP4NADq7ACAAIGLHYHvy +xf4RCERvEyykM5zxnfFVLlpClg/QLGlzYBY8AAQAAQABAAtleGFtcGxlLmNvbQAA +-----END ECHCONFIG-----` diff --git a/test/certs/ech-mid.pem b/test/certs/ech-mid.pem new file mode 100644 index 0000000000000..7c5aa86e144eb --- /dev/null +++ b/test/certs/ech-mid.pem @@ -0,0 +1,11 @@ +-----BEGIN ECHCONFIG----- +AfD+DQA6uwAgACBix2B78sX+EQhEbxMspDOc8Z3xVS5aQpYP0Cxpc2AWPAAEAAEAAQALZXhhbXBs +ZS5jb20AAP4NADq7ACAAIGLHYHvyxf4RCERvEyykM5zxnfFVLlpClg/QLGlzYBY8AAQAAQABAAtl +eGFtcGxlLmNvbQAA/g0AOrsAIAAgYsdge/LF/hEIRG8TLKQznPGd8VUuWkKWD9AsaXNgFjwABAAB +AAEAC2V4YW1wbGUuY29tAAD+DQA6uwAgACBix2B78sX+EQhEbxMspDOc8Z3xVS5aQpYP0Cxpc2AW +PAAEAAEAAQALZXhhbXBsZS5jb20AAP4NADq7ACAAIGLHYHvyxf4RCERvEyykM5zxnfFVLlpClg/Q +LGlzYBY8AAQAAQABAAtleGFtcGxlLmNvbQAA/g0AOrsAIAAgYsdge/LF/hEIRG8TLKQznPGd8VUu +WkKWD9AsaXNgFjwABAABAAEAC2V4YW1wbGUuY29tAAD+DQA6uwAgACBix2B78sX+EQhEbxMspDOc +8Z3xVS5aQpYP0Cxpc2AWPAAEAAEAAQALZXhhbXBsZS5jb20AAP4NADq7ACAAIGLHYHvyxf4RCERv +EyykM5zxnfFVLlpClg/QLGlzYBY8AAQAAQABAAtleGFtcGxlLmNvbQAA +-----END ECHCONFIG----- diff --git a/test/certs/ech-rsa.pem b/test/certs/ech-rsa.pem new file mode 100644 index 0000000000000..17b23cf04f5e0 --- /dev/null +++ b/test/certs/ech-rsa.pem @@ -0,0 +1,14 @@ +-----BEGIN PRIVATE KEY----- +MIIBVQIBADANBgkqhkiG9w0BAQEFAASCAT8wggE7AgEAAkEApeb9fP5SDxyOQZQT +qGg2QeE0ypxY6Th33aDkRCRVB69rDMSA1Thfeyk65IfaPaA3bC4hsqAIBgslcFfk +1/i8KQIDAQABAkAsH3EPizwb1MZo3o8T3ROBFfpKYKas8F3Azgenr9oFfs5kPgya +VDdtZu+UweG5nTo+fZG5ZFmcwWXJTLtiUfABAiEAz2gvTuc0lPTQi3t6RFB5nGCt +h75Ofx/ceusHa2a36QECIQDMxXJQnuWY+bH/wSfPY/ySltQ6U2cy0LHQ37FIfSFr +KQIgUo++hUI0BDeP7HYyrY77WeyCJ07yIFimg6ebRH2XKAECIQCSavhTd1q6qIhD +VMzveRInixvTXMGkzx7mOJzeNUMJCQIhAJjjVdRjUpWPMquRDCddmwegh88ptsFX +T/Ygm1OubAyM +-----END PRIVATE KEY----- +-----BEGIN ECHCONFIG----- +AD7+DQA6bAAgACCY7B0f/3KvHIFdoqFaObdU8YYU+MdBf4vzbLhAAL2QCwAEAAEA +AQALZXhhbXBsZS5jb20AAA== +-----END ECHCONFIG----- diff --git a/test/ech_test.c b/test/ech_test.c index 2e49b6b0c982c..e491740ecdbbf 100644 --- a/test/ech_test.c +++ b/test/ech_test.c @@ -14,7 +14,679 @@ #ifndef OPENSSL_NO_ECH +# define DEF_CERTS_DIR "test/certs" + static int verbose = 0; +static char *certsdir = NULL; + +/* general test vector values */ + +/* standard x25519 ech key pair with public key example.com */ +static const char pem_kp1[] = + "-----BEGIN PRIVATE KEY-----\n" + "MC4CAQAwBQYDK2VuBCIEILDIeo9Eqc4K9/uQ0PNAyMaP60qrxiSHT2tNZL3ksIZS\n" + "-----END PRIVATE KEY-----\n" + "-----BEGIN ECHCONFIG-----\n" + "AD7+DQA6bAAgACCY7B0f/3KvHIFdoqFaObdU8YYU+MdBf4vzbLhAAL2QCwAEAAEA\n" + "AQALZXhhbXBsZS5jb20AAA==\n" + "-----END ECHCONFIG-----\n"; + +/* standard x25519 ECHConfigList with public key example.com */ +static const char pem_pk1[] = + "-----BEGIN ECHCONFIG-----\n" + "AD7+DQA6bAAgACCY7B0f/3KvHIFdoqFaObdU8YYU+MdBf4vzbLhAAL2QCwAEAAEA\n" + "AQALZXhhbXBsZS5jb20AAA==\n" + "-----END ECHCONFIG-----\n"; + +/* an ECDSA private with an x25519 ech public key example.com */ +static const char pem_mismatch_priv[] = + "-----BEGIN EC PRIVATE KEY-----\n" + "MHcCAQEEIGKONznbHOMEKT4AKMufc37O9lUEBHO+Nb6ztkXhGXLcoAoGCCqGSM49\n" + "AwEHoUQDQgAEYDznfezvj5ufhQsZOQvSdiNpYKCd8tRI1aI3gc4y7gmdDUKpwzHa\n" + "VS4Qq0xyeG6fDMJv668UCotQANFsifGirQ==\n" + "-----END EC PRIVATE KEY-----\n" + "-----BEGIN ECHCONFIG-----\n" + "AD7+DQA6bAAgACCY7B0f/3KvHIFdoqFaObdU8YYU+MdBf4vzbLhAAL2QCwAEAAEA\n" + "AQALZXhhbXBsZS5jb20AAA==\n" + "-----END ECHCONFIG-----\n"; + +/* + * This ECHConfigList has 4 entries with different versions, + * from drafts: 13,10,13,9 - since our runtime no longer supports + * version 9 or 10, we should see 2 configs loaded. + */ +static const char pem_4_to_2[] = + "-----BEGIN ECHCONFIG-----\n" + "APv+DQA6xQAgACBm54KSIPXu+pQq2oY183wt3ybx7CKbBYX0ogPq5u6FegAEAAEA\n" + "AQALZXhhbXBsZS5jb20AAP4KADzSACAAIIP+0Qt0WGBF3H5fz8HuhVRTCEMuHS4K\n" + "hu6ibR/6qER4AAQAAQABAAAAC2V4YW1wbGUuY29tAAD+DQA6QwAgACB3xsNUtSgi\n" + "piYpUkW6OSrrg03I4zIENMFa0JR2+Mm1WwAEAAEAAQALZXhhbXBsZS5jb20AAP4J\n" + "ADsAC2V4YW1wbGUuY29tACCjJCv5w/yaHjbOc6nVuM/GksIGLgDR+222vww9dEk8\n" + "FwAgAAQAAQABAAAAAA==\n" + "-----END ECHCONFIG-----\n"; + +/* mis-spelled PEM string */ +static const char pem_typo[] = + "-----BEGIN PRIVATE KEY-----\n" + "MC4CAQAwBQYDK2VuBCIEILDIeo9Eqc4K9/uQ0PNAyMaP60qrxiSHT2tNZL3ksIZS\n" + "-----END PRIVATE KEY-----\n" + "-----BEGIN ExHCOxFIG-----\n" + "AD7+DQA6bAAgACCY7B0f/3KvHIFdoqFaObdU8YYU+MdBf4vzbLhAAL2QCwAEAAEA\n" + "AQALZXhhbXBsZS5jb20AAA==\n" + "-----END ExHCOxFIG-----\n"; + +/* single-line base64(ECHConfigList) form of pem_pk1 */ +static const char b64_pk1[] = + "AD7+DQA6bAAgACCY7B0f/3KvHIFdoqFaObdU8YYU+MdBf4vzbLhAAL2QCwAEAAEA" + "AQALZXhhbXBsZS5jb20AAA=="; + +/* single-line base64(ECHConfigList) form of pem_6_to3 */ +static const char b64_6_to_3[] = + "AXn+DQA6xQAgACBm54KSIPXu+pQq2oY183wt3ybx7CKbBYX0ogPq5u6FegAEAAE" + "AAQALZXhhbXBsZS5jb20AAP4KADzSACAAIIP+0Qt0WGBF3H5fz8HuhVRTCEMuHS" + "4Khu6ibR/6qER4AAQAAQABAAAAC2V4YW1wbGUuY29tAAD+CQA7AAtleGFtcGxlL" + "mNvbQAgoyQr+cP8mh42znOp1bjPxpLCBi4A0ftttr8MPXRJPBcAIAAEAAEAAQAA" + "AAD+DQA6QwAgACB3xsNUtSgipiYpUkW6OSrrg03I4zIENMFa0JR2+Mm1WwAEAAE" + "AAQALZXhhbXBsZS5jb20AAP4KADwDACAAIH0BoAdiJCX88gv8nYpGVX5BpGBa9y" + "T0Pac3Kwx6i8URAAQAAQABAAAAC2V4YW1wbGUuY29tAAD+DQA6QwAgACDcZIAx7" + "OcOiQuk90VV7/DO4lFQr5I3Zw9tVbK8MGw1dgAEAAEAAQALZXhhbXBsZS5jb20A" + "AA=="; + +/* same as above but binary encoded */ +static const unsigned char bin_6_to_3[] = { + 0x01, 0x79, 0xfe, 0x0d, 0x00, 0x3a, 0xc5, 0x00, + 0x20, 0x00, 0x20, 0x66, 0xe7, 0x82, 0x92, 0x20, + 0xf5, 0xee, 0xfa, 0x94, 0x2a, 0xda, 0x86, 0x35, + 0xf3, 0x7c, 0x2d, 0xdf, 0x26, 0xf1, 0xec, 0x22, + 0x9b, 0x05, 0x85, 0xf4, 0xa2, 0x03, 0xea, 0xe6, + 0xee, 0x85, 0x7a, 0x00, 0x04, 0x00, 0x01, 0x00, + 0x01, 0x00, 0x0b, 0x65, 0x78, 0x61, 0x6d, 0x70, + 0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x00, 0x00, + 0xfe, 0x0a, 0x00, 0x3c, 0xd2, 0x00, 0x20, 0x00, + 0x20, 0x83, 0xfe, 0xd1, 0x0b, 0x74, 0x58, 0x60, + 0x45, 0xdc, 0x7e, 0x5f, 0xcf, 0xc1, 0xee, 0x85, + 0x54, 0x53, 0x08, 0x43, 0x2e, 0x1d, 0x2e, 0x0a, + 0x86, 0xee, 0xa2, 0x6d, 0x1f, 0xfa, 0xa8, 0x44, + 0x78, 0x00, 0x04, 0x00, 0x01, 0x00, 0x01, 0x00, + 0x00, 0x00, 0x0b, 0x65, 0x78, 0x61, 0x6d, 0x70, + 0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x00, 0x00, + 0xfe, 0x09, 0x00, 0x3b, 0x00, 0x0b, 0x65, 0x78, + 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x63, 0x6f, + 0x6d, 0x00, 0x20, 0xa3, 0x24, 0x2b, 0xf9, 0xc3, + 0xfc, 0x9a, 0x1e, 0x36, 0xce, 0x73, 0xa9, 0xd5, + 0xb8, 0xcf, 0xc6, 0x92, 0xc2, 0x06, 0x2e, 0x00, + 0xd1, 0xfb, 0x6d, 0xb6, 0xbf, 0x0c, 0x3d, 0x74, + 0x49, 0x3c, 0x17, 0x00, 0x20, 0x00, 0x04, 0x00, + 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, 0xfe, + 0x0d, 0x00, 0x3a, 0x43, 0x00, 0x20, 0x00, 0x20, + 0x77, 0xc6, 0xc3, 0x54, 0xb5, 0x28, 0x22, 0xa6, + 0x26, 0x29, 0x52, 0x45, 0xba, 0x39, 0x2a, 0xeb, + 0x83, 0x4d, 0xc8, 0xe3, 0x32, 0x04, 0x34, 0xc1, + 0x5a, 0xd0, 0x94, 0x76, 0xf8, 0xc9, 0xb5, 0x5b, + 0x00, 0x04, 0x00, 0x01, 0x00, 0x01, 0x00, 0x0b, + 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, + 0x63, 0x6f, 0x6d, 0x00, 0x00, 0xfe, 0x0a, 0x00, + 0x3c, 0x03, 0x00, 0x20, 0x00, 0x20, 0x7d, 0x01, + 0xa0, 0x07, 0x62, 0x24, 0x25, 0xfc, 0xf2, 0x0b, + 0xfc, 0x9d, 0x8a, 0x46, 0x55, 0x7e, 0x41, 0xa4, + 0x60, 0x5a, 0xf7, 0x24, 0xf4, 0x3d, 0xa7, 0x37, + 0x2b, 0x0c, 0x7a, 0x8b, 0xc5, 0x11, 0x00, 0x04, + 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x00, 0x0b, + 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, + 0x63, 0x6f, 0x6d, 0x00, 0x00, 0xfe, 0x0d, 0x00, + 0x3a, 0x43, 0x00, 0x20, 0x00, 0x20, 0xdc, 0x64, + 0x80, 0x31, 0xec, 0xe7, 0x0e, 0x89, 0x0b, 0xa4, + 0xf7, 0x45, 0x55, 0xef, 0xf0, 0xce, 0xe2, 0x51, + 0x50, 0xaf, 0x92, 0x37, 0x67, 0x0f, 0x6d, 0x55, + 0xb2, 0xbc, 0x30, 0x6c, 0x35, 0x76, 0x00, 0x04, + 0x00, 0x01, 0x00, 0x01, 0x00, 0x0b, 0x65, 0x78, + 0x61, 0x6d, 0x70, 0x6c, 0x65, 0x2e, 0x63, 0x6f, + 0x6d, 0x00, 0x00 +}; + +/* base64(ECHConfigList) with corrupt ciphersuite length and public_name */ +static const char b64_bad_cs[] = + "AD7+DQA6uAAgACAogff+HZbirYdQCfXI01GBPP8AEKYyK/D/0DoeXD84fgAQAAE" + "AAQgLZXhhbUNwbGUuYwYAAAAAQwA="; + +/* An ECHConfigList with one ECHConfig but of the wrong version */ +static const unsigned char bin_bad_ver[] = { + 0x00, 0x3e, 0xfe, 0xff, 0x00, 0x3a, 0xbb, 0x00, + 0x20, 0x00, 0x20, 0x62, 0xc7, 0x60, 0x7b, 0xf2, + 0xc5, 0xfe, 0x11, 0x08, 0x44, 0x6f, 0x13, 0x2c, + 0xa4, 0x33, 0x9c, 0xf1, 0x9d, 0xf1, 0x55, 0x2e, + 0x5a, 0x42, 0x96, 0x0f, 0xd0, 0x2c, 0x69, 0x73, + 0x60, 0x16, 0x3c, 0x00, 0x04, 0x00, 0x01, 0x00, + 0x01, 0x00, 0x0b, 0x65, 0x78, 0x61, 0x6d, 0x70, + 0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x00, 0x00 +}; + +/* + * An ECHConflgList with 2 ECHConfig values that are both + * of the wrong version. The versions here are 0xfe03 (we + * currently support only 0xfe0d) + */ +static const unsigned char bin_bad_ver2[] = { + 0x00, 0x80, 0xfe, 0x03, 0x00, 0x3c, 0x00, 0x00, + 0x20, 0x00, 0x20, 0x71, 0xa5, 0xe0, 0xb4, 0x6d, + 0xdf, 0xa4, 0xda, 0xed, 0x69, 0xa5, 0xc7, 0x8b, + 0x9d, 0xa5, 0x13, 0x0c, 0x36, 0x83, 0x7a, 0x03, + 0x72, 0x1d, 0xf6, 0x1e, 0xc5, 0x83, 0x1a, 0x11, + 0x73, 0xce, 0x2d, 0x00, 0x04, 0x00, 0x01, 0x00, + 0x01, 0x00, 0x0d, 0x70, 0x61, 0x72, 0x74, 0x31, + 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, + 0x00, 0x00, 0xfe, 0x03, 0x00, 0x3c, 0x00, 0x00, + 0x20, 0x00, 0x20, 0x69, 0x88, 0xfd, 0x8f, 0xc9, + 0x0b, 0xb7, 0x2d, 0x96, 0x6d, 0xe0, 0x22, 0xf0, + 0xc8, 0x1b, 0x62, 0x2b, 0x1c, 0x94, 0x96, 0xad, + 0xef, 0x55, 0xdb, 0x9f, 0xeb, 0x0d, 0xa1, 0x4b, + 0x0c, 0xd7, 0x36, 0x00, 0x04, 0x00, 0x01, 0x00, + 0x01, 0x00, 0x0d, 0x70, 0x61, 0x72, 0x74, 0x32, + 0x2e, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, + 0x00, 0x00 +}; + +/* + * An ECHConfigList with one ECHConfig with an all-zero public value. + * That should be ok, for 25519, but hey, just in case:-) + */ +static const unsigned char bin_zero[] = { + 0x00, 0x3e, 0xfe, 0x0d, 0x00, 0x3a, 0xbb, 0x00, + 0x20, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x01, 0x00, + 0x01, 0x00, 0x0b, 0x65, 0x78, 0x61, 0x6d, 0x70, + 0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x00, 0x00 +}; + +/* + * The next set of samples are syntactically invalid + * Proper fuzzing is still needed but no harm having + * these too. Generally these are bad version of + * our nominal encoding with some octet(s) replaced + * by 0xFF values. Other hex letters are lowercase + * so you can find the altered octet(s). + */ + +/* wrong overall length (replacing 0x3e with 0xFF) */ +static const unsigned char bin_bad_olen[] = { + 0x00, 0xFF, 0xfe, 0x0d, 0x00, 0x3a, 0xbb, 0x00, + 0x20, 0x00, 0xFF, 0x62, 0xc7, 0x60, 0x7b, 0xf2, + 0xc5, 0xfe, 0x11, 0x08, 0x44, 0x6f, 0x13, 0x2c, + 0xa4, 0x33, 0x9c, 0xf1, 0x9d, 0xf1, 0x55, 0x2e, + 0x5a, 0x42, 0x96, 0x0f, 0xd0, 0x2c, 0x69, 0x73, + 0x60, 0x16, 0x3c, 0x00, 0x04, 0x00, 0x01, 0x00, + 0x01, 0x00, 0x0b, 0x65, 0x78, 0x61, 0x6d, 0x70, + 0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x00, 0x00 +}; + +/* wrong ECHConfig inner length (replacing 0x3a with 0xFF) */ +static const unsigned char bin_bad_ilen[] = { + 0x00, 0x3e, 0xfe, 0x0d, 0x00, 0xFF, 0xbb, 0x00, + 0x20, 0x00, 0x20, 0x62, 0xc7, 0x60, 0x7b, 0xf2, + 0xc5, 0xfe, 0x11, 0x08, 0x44, 0x6f, 0x13, 0x2c, + 0xa4, 0x33, 0x9c, 0xf1, 0x9d, 0xf1, 0x55, 0x2e, + 0x5a, 0x42, 0x96, 0x0f, 0xd0, 0x2c, 0x69, 0x73, + 0x60, 0x16, 0x3c, 0x00, 0x04, 0x00, 0x01, 0x00, + 0x01, 0x00, 0x0b, 0x65, 0x78, 0x61, 0x6d, 0x70, + 0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x00, 0x00 +}; + +/* wrong length for public key (replaced 0x20 with 0xFF) */ +static const unsigned char bin_bad_pklen[] = { + 0x00, 0x3e, 0xfe, 0x0d, 0x00, 0x3a, 0xbb, 0x00, + 0x20, 0x00, 0xFF, 0x62, 0xc7, 0x60, 0x7b, 0xf2, + 0xc5, 0xfe, 0x11, 0x08, 0x44, 0x6f, 0x13, 0x2c, + 0xa4, 0x33, 0x9c, 0xf1, 0x9d, 0xf1, 0x55, 0x2e, + 0x5a, 0x42, 0x96, 0x0f, 0xd0, 0x2c, 0x69, 0x73, + 0x60, 0x16, 0x3c, 0x00, 0x04, 0x00, 0x01, 0x00, + 0x01, 0x00, 0x0b, 0x65, 0x78, 0x61, 0x6d, 0x70, + 0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x00, 0x00 +}; + +/* wrong length for ciphersuites (replaced 0x04 with 0xFF) */ +static const unsigned char bin_bad_cslen[] = { + 0x00, 0x3e, 0xfe, 0x0d, 0x00, 0x3a, 0xbb, 0x00, + 0x20, 0x00, 0x20, 0x62, 0xc7, 0x60, 0x7b, 0xf2, + 0xc5, 0xfe, 0x11, 0x08, 0x44, 0x6f, 0x13, 0x2c, + 0xa4, 0x33, 0x9c, 0xf1, 0x9d, 0xf1, 0x55, 0x2e, + 0x5a, 0x42, 0x96, 0x0f, 0xd0, 0x2c, 0x69, 0x73, + 0x60, 0x16, 0x3c, 0x00, 0xFF, 0x00, 0x01, 0x00, + 0x01, 0x00, 0x0b, 0x65, 0x78, 0x61, 0x6d, 0x70, + 0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x00, 0x00 +}; + +/* wrong length for public name (replaced 0x0b with 0xFF) */ +static const unsigned char bin_bad_pnlen[] = { + 0x00, 0x3e, 0xfe, 0x0d, 0x00, 0x3a, 0xbb, 0x00, + 0x20, 0x00, 0x20, 0x62, 0xc7, 0x60, 0x7b, 0xf2, + 0xc5, 0xfe, 0x11, 0x08, 0x44, 0x6f, 0x13, 0x2c, + 0xa4, 0x33, 0x9c, 0xf1, 0x9d, 0xf1, 0x55, 0x2e, + 0x5a, 0x42, 0x96, 0x0f, 0xd0, 0x2c, 0x69, 0x73, + 0x60, 0x16, 0x3c, 0x00, 0x04, 0x00, 0x01, 0x00, + 0x01, 0x00, 0xFF, 0x65, 0x78, 0x61, 0x6d, 0x70, + 0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x00, 0x00 +}; + +/* non-zero extension length (0xFF at end) but no extension value */ +static const unsigned char bin_bad_extlen[] = { + 0x00, 0x3e, 0xfe, 0x0d, 0x00, 0x3a, 0xbb, 0x00, + 0x20, 0x00, 0x20, 0x62, 0xc7, 0x60, 0x7b, 0xf2, + 0xc5, 0xfe, 0x11, 0x08, 0x44, 0x6f, 0x13, 0x2c, + 0xa4, 0x33, 0x9c, 0xf1, 0x9d, 0xf1, 0x55, 0x2e, + 0x5a, 0x42, 0x96, 0x0f, 0xd0, 0x2c, 0x69, 0x73, + 0x60, 0x16, 0x3c, 0x00, 0x04, 0x00, 0x01, 0x00, + 0x01, 0x00, 0x0b, 0x65, 0x78, 0x61, 0x6d, 0x70, + 0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x00, 0xFF +}; + +/* + * The next set have bad kem, kdf or aead values - this time with + * 0xAA as the replacement value + */ + +/* wrong KEM ID (replaced 0x20 with 0xAA) */ +static const unsigned char bin_bad_kemid[] = { + 0x00, 0x3e, 0xfe, 0x0d, 0x00, 0x3a, 0xbb, 0x00, + 0xAA, 0x00, 0x20, 0x62, 0xc7, 0x60, 0x7b, 0xf2, + 0xc5, 0xfe, 0x11, 0x08, 0x44, 0x6f, 0x13, 0x2c, + 0xa4, 0x33, 0x9c, 0xf1, 0x9d, 0xf1, 0x55, 0x2e, + 0x5a, 0x42, 0x96, 0x0f, 0xd0, 0x2c, 0x69, 0x73, + 0x60, 0x16, 0x3c, 0x00, 0x04, 0x00, 0x01, 0x00, + 0x01, 0x00, 0x0b, 0x65, 0x78, 0x61, 0x6d, 0x70, + 0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x00, 0x00 +}; + +/* wrong KDF ID (replaced 0x01 with 0xAA) */ +static const unsigned char bin_bad_kdfid[] = { + 0x00, 0x3e, 0xfe, 0x0d, 0x00, 0x3a, 0xbb, 0x00, + 0x20, 0x00, 0x20, 0x62, 0xc7, 0x60, 0x7b, 0xf2, + 0xc5, 0xfe, 0x11, 0x08, 0x44, 0x6f, 0x13, 0x2c, + 0xa4, 0x33, 0x9c, 0xf1, 0x9d, 0xf1, 0x55, 0x2e, + 0x5a, 0x42, 0x96, 0x0f, 0xd0, 0x2c, 0x69, 0x73, + 0x60, 0x16, 0x3c, 0x00, 0x04, 0x00, 0xAA, 0x00, + 0x01, 0x00, 0x0b, 0x65, 0x78, 0x61, 0x6d, 0x70, + 0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x00, 0x00 +}; + +/* wrong AEAD ID (replaced 0x01 with 0xAA) */ +static const unsigned char bin_bad_aeadid[] = { + 0x00, 0x3e, 0xfe, 0x0d, 0x00, 0x3a, 0xbb, 0x00, + 0x20, 0x00, 0x20, 0x62, 0xc7, 0x60, 0x7b, 0xf2, + 0xc5, 0xfe, 0x11, 0x08, 0x44, 0x6f, 0x13, 0x2c, + 0xa4, 0x33, 0x9c, 0xf1, 0x9d, 0xf1, 0x55, 0x2e, + 0x5a, 0x42, 0x96, 0x0f, 0xd0, 0x2c, 0x69, 0x73, + 0x60, 0x16, 0x3c, 0x00, 0x04, 0x00, 0x01, 0x00, + 0xAA, 0x00, 0x0b, 0x65, 0x78, 0x61, 0x6d, 0x70, + 0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x00, 0x00 +}; + +/* ECHConfig supports two symmetric suites */ +static const unsigned char bin_multi_suite[] = { + 0x00, 0x42, 0xfe, 0x0d, 0x00, 0x3e, 0xbb, 0x00, + 0x20, 0x00, 0x20, 0x62, 0xc7, 0x60, 0x7b, 0xf2, + 0xc5, 0xfe, 0x11, 0x08, 0x44, 0x6f, 0x13, 0x2c, + 0xa4, 0x33, 0x9c, 0xf1, 0x9d, 0xf1, 0x55, 0x2e, + 0x5a, 0x42, 0x96, 0x0f, 0xd0, 0x2c, 0x69, 0x73, + 0x60, 0x16, 0x3c, 0x00, 0x08, 0x00, 0x01, 0x00, + 0x01, + 0x00, 0x02, 0x00, 0x02, + 0x00, 0x0b, 0x65, 0x78, 0x61, 0x6d, 0x70, + 0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x00, 0x00 +}; + +/* + * sorta wrong AEAD ID; replaced 0x0001 with 0xFFFF + * which is the export only pseudo-aead-id - that + * should not work in our test, same as the others, + * but worth a specific test, as it'll fail in a + * different manner + */ +static const unsigned char bin_bad_aeadid_ff[] = { + 0x00, 0x3e, 0xfe, 0x0d, 0x00, 0x3a, 0xbb, 0x00, + 0x20, 0x00, 0x20, 0x62, 0xc7, 0x60, 0x7b, 0xf2, + 0xc5, 0xfe, 0x11, 0x08, 0x44, 0x6f, 0x13, 0x2c, + 0xa4, 0x33, 0x9c, 0xf1, 0x9d, 0xf1, 0x55, 0x2e, + 0x5a, 0x42, 0x96, 0x0f, 0xd0, 0x2c, 0x69, 0x73, + 0x60, 0x16, 0x3c, 0x00, 0x04, 0x00, 0x01, 0xFF, + 0xFF, 0x00, 0x0b, 0x65, 0x78, 0x61, 0x6d, 0x70, + 0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x00, 0x00 +}; + +/* + * An ECHConfigList with a bad ECHConfig + * (aead is 0xFFFF), followed by a good + * one. + */ +static const unsigned char bin_bad_then_good[] = { + 0x00, 0x7c, 0xfe, 0x0d, 0x00, 0x3a, 0xbb, 0x00, + 0x20, 0x00, 0x20, 0x62, 0xc7, 0x60, 0x7b, 0xf2, + 0xc5, 0xfe, 0x11, 0x08, 0x44, 0x6f, 0x13, 0x2c, + 0xa4, 0x33, 0x9c, 0xf1, 0x9d, 0xf1, 0x55, 0x2e, + 0x5a, 0x42, 0x96, 0x0f, 0xd0, 0x2c, 0x69, 0x73, + 0x60, 0x16, 0x3c, 0x00, 0x04, 0x00, 0x01, 0xFF, + 0xFF, 0x00, 0x0b, 0x65, 0x78, 0x61, 0x6d, 0x70, + 0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x00, 0x00, + 0xfe, 0x0d, 0x00, 0x3a, 0xbb, 0x00, 0x20, 0x00, + 0x20, 0x62, 0xc7, 0x60, 0x7b, 0xf2, 0xc5, 0xfe, + 0x11, 0x08, 0x44, 0x6f, 0x13, 0x2c, 0xa4, 0x33, + 0x9c, 0xf1, 0x9d, 0xf1, 0x55, 0x2e, 0x5a, 0x42, + 0x96, 0x0f, 0xd0, 0x2c, 0x69, 0x73, 0x60, 0x16, + 0x3c, 0x00, 0x04, 0x00, 0x01, 0x00, 0x01, 0x00, + 0x0b, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, + 0x2e, 0x63, 0x6f, 0x6d, 0x00, 0x00 +}; + +/* couple of harmless extensions */ +static const unsigned char bin_ok_exts[] = { + 0x00, 0x47, 0xfe, 0x0d, 0x00, 0x43, 0xbb, 0x00, + 0x20, 0x00, 0x20, 0x62, 0xc7, 0x60, 0x7b, 0xf2, + 0xc5, 0xfe, 0x11, 0x08, 0x44, 0x6f, 0x13, 0x2c, + 0xa4, 0x33, 0x9c, 0xf1, 0x9d, 0xf1, 0x55, 0x2e, + 0x5a, 0x42, 0x96, 0x0f, 0xd0, 0x2c, 0x69, 0x73, + 0x60, 0x16, 0x3c, 0x00, 0x04, 0x00, 0x01, 0x00, + 0x01, 0x00, 0x0b, 0x65, 0x78, 0x61, 0x6d, 0x70, + 0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x00, 0x09, + 0x0a, 0x0b, 0x00, 0x00, 0x0c, 0x0d, 0x00, 0x01, + 0x02 +}; + +/* one "mandatory" extension (high bit of type set) */ +static const unsigned char bin_mand_ext[] = { + 0x00, 0x47, 0xfe, 0x0d, 0x00, 0x43, 0xbb, 0x00, + 0x20, 0x00, 0x20, 0x62, 0xc7, 0x60, 0x7b, 0xf2, + 0xc5, 0xfe, 0x11, 0x08, 0x44, 0x6f, 0x13, 0x2c, + 0xa4, 0x33, 0x9c, 0xf1, 0x9d, 0xf1, 0x55, 0x2e, + 0x5a, 0x42, 0x96, 0x0f, 0xd0, 0x2c, 0x69, 0x73, + 0x60, 0x16, 0x3c, 0x00, 0x04, 0x00, 0x01, 0x00, + 0x01, 0x00, 0x0b, 0x65, 0x78, 0x61, 0x6d, 0x70, + 0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x00, 0x09, + 0x0a, 0x0b, 0x00, 0x00, 0xFc, 0x0d, 0x00, 0x01, + 0x02 +}; + +/* extension with bad length (0xFFFF) */ +static const unsigned char bin_bad_inner_extlen[] = { + 0x00, 0x47, 0xfe, 0x0d, 0x00, 0x43, 0xbb, 0x00, + 0x20, 0x00, 0x20, 0x62, 0xc7, 0x60, 0x7b, 0xf2, + 0xc5, 0xfe, 0x11, 0x08, 0x44, 0x6f, 0x13, 0x2c, + 0xa4, 0x33, 0x9c, 0xf1, 0x9d, 0xf1, 0x55, 0x2e, + 0x5a, 0x42, 0x96, 0x0f, 0xd0, 0x2c, 0x69, 0x73, + 0x60, 0x16, 0x3c, 0x00, 0x04, 0x00, 0x01, 0x00, + 0x01, 0x00, 0x0b, 0x65, 0x78, 0x61, 0x6d, 0x70, + 0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x00, 0x09, + 0x0a, 0x0b, 0x00, 0x00, 0x0c, 0x0d, 0x00, 0xFF, + 0x02 +}; + +/* good, other than a NUL inside the public_name */ +static const unsigned char bin_nul_in_pn[] = { + 0x00, 0x3e, 0xfe, 0x0d, 0x00, 0x3a, 0xbb, 0x00, + 0x20, 0x00, 0x20, 0x62, 0xc7, 0x60, 0x7b, 0xf2, + 0xc5, 0xfe, 0x11, 0x08, 0x44, 0x6f, 0x13, 0x2c, + 0xa4, 0x33, 0x9c, 0xf1, 0x9d, 0xf1, 0x55, 0x2e, + 0x5a, 0x42, 0x96, 0x0f, 0xd0, 0x2c, 0x69, 0x73, + 0x60, 0x16, 0x3c, 0x00, 0x04, 0x00, 0x01, 0x00, + 0x01, 0x00, 0x0b, 0x65, 0x78, 0x61, 0x6d, 0x70, + 0x6c, 0x00, 0x2e, 0x63, 0x6f, 0x6d, 0x00, 0x00 +}; + +/* good, other than a dot at the end of the public_name */ +static const unsigned char bin_pn_dot_at_end[] = { + 0x00, 0x3e, 0xfe, 0x0d, 0x00, 0x3a, 0xbb, 0x00, + 0x20, 0x00, 0x20, 0x62, 0xc7, 0x60, 0x7b, 0xf2, + 0xc5, 0xfe, 0x11, 0x08, 0x44, 0x6f, 0x13, 0x2c, + 0xa4, 0x33, 0x9c, 0xf1, 0x9d, 0xf1, 0x55, 0x2e, + 0x5a, 0x42, 0x96, 0x0f, 0xd0, 0x2c, 0x69, 0x73, + 0x60, 0x16, 0x3c, 0x00, 0x04, 0x00, 0x01, 0x00, + 0x01, 0x00, 0x0b, 0x65, 0x78, 0x61, 0x6d, 0x70, + 0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x2e, 0x00, 0x00 +}; + +/* + * An ECHConfigList with a good ECHConfig followed by a bad + * one with the 1st internal length (0xFFFF) too big + */ +static const unsigned char bin_good_then_bad[] = { + 0x00, 0x7c, 0xfe, 0x0d, 0x00, 0x3a, 0xbb, 0x00, + 0x20, 0x00, 0x20, 0x62, 0xc7, 0x60, 0x7b, 0xf2, + 0xc5, 0xfe, 0x11, 0x08, 0x44, 0x6f, 0x13, 0x2c, + 0xa4, 0x33, 0x9c, 0xf1, 0x9d, 0xf1, 0x55, 0x2e, + 0x5a, 0x42, 0x96, 0x0f, 0xd0, 0x2c, 0x69, 0x73, + 0x60, 0x16, 0x3c, 0x00, 0x04, 0x00, 0x01, 0x00, + 0x01, 0x00, 0x0b, 0x65, 0x78, 0x61, 0x6d, 0x70, + 0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x00, 0x00, + 0xfe, 0x0d, 0xFF, 0xFF, 0xbb, 0x00, 0x20, 0x00, + 0x20, 0x62, 0xc7, 0x60, 0x7b, 0xf2, 0xc5, 0xfe, + 0x11, 0x08, 0x44, 0x6f, 0x13, 0x2c, 0xa4, 0x33, + 0x9c, 0xf1, 0x9d, 0xf1, 0x55, 0x2e, 0x5a, 0x42, + 0x96, 0x0f, 0xd0, 0x2c, 0x69, 0x73, 0x60, 0x16, + 0x3c, 0x00, 0x04, 0x00, 0x01, 0x00, 0x01, 0x00, + 0x0b, 0x65, 0x78, 0x61, 0x6d, 0x70, 0x6c, 0x65, + 0x2e, 0x63, 0x6f, 0x6d, 0x00, 0x00 +}; + +/* generally very short:-) */ +static const unsigned char bin_short[] = { + 0x00, 0x05, 0xfe, 0x0d, 0x00, 0x01, 0x01 +}; + +/* kind of an empty value */ +static const unsigned char bin_empty[] = { + 0x00, 0x00 +}; + +/* + * An ECHConfigList with an unsupported ECHConfig and + * that's too short. + */ +static const unsigned char bin_ver_short[] = { + 0x00, 0x3e, 0xfe, 0xFF, 0x00, 0x3a, 0xbb, 0x00, + 0x20, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x20, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x20, 0x00, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, +}; + +/* + * too-long extension - OSSL_ECH_MAX_ECHCONFIGEXT_LEN is + * 512, this is 513 (0x0201), end of the 8-th line + * */ +static const unsigned char bin_long_ext[] = { + 0x02, 0x43, 0xfe, 0x0d, 0x02, 0x3f, 0xbb, 0x00, + 0x20, 0x00, 0x20, 0x62, 0xc7, 0x60, 0x7b, 0xf2, + 0xc5, 0xfe, 0x11, 0x08, 0x44, 0x6f, 0x13, 0x2c, + 0xa4, 0x33, 0x9c, 0xf1, 0x9d, 0xf1, 0x55, 0x2e, + 0x5a, 0x42, 0x96, 0x0f, 0xd0, 0x2c, 0x69, 0x73, + 0x60, 0x16, 0x3c, 0x00, 0x04, 0x00, 0x01, 0x00, + 0x01, 0x00, 0x0b, 0x65, 0x78, 0x61, 0x6d, 0x70, + 0x6c, 0x65, 0x2e, 0x63, 0x6f, 0x6d, 0x02, 0x05, + 0xFF, 0xFF, 0x02, 0x01, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00 +}; + +/* struct for ingest test vector and results */ +typedef struct INGEST_TV_T { + char *name; /* name for verbose output */ + const unsigned char *tv; /* test vector */ + size_t len; /* len(tv) - sizeof(tv) if binary, subtract 1 for strings */ + int pemenc; /* whether PEM encoded (1) or not (0) */ + int read; /* result expected from read function on tv */ + int keysb4; /* the number of private keys expected before downselect */ + int entsb4; /* the number of public keys b4 */ + int index; /* the index to use for downselect */ + int expected; /* the result expected from a downselect */ + int keysaftr; /* the number of keys expected after downselect */ + int entsaftr; /* the number of public keys after */ +} ingest_tv_t; + +static ingest_tv_t ingest_tvs[] = { + /* PEM test vectors */ + { "PEM basic/last", (unsigned char *)pem_kp1, sizeof(pem_kp1) - 1, + 1, 1, 1, 1, OSSL_ECHSTORE_LAST, 1, 1, 1 }, + { "PEM basic/0", (unsigned char *)pem_pk1, sizeof(pem_pk1) - 1, + 1, 1, 0, 1, 0, 1, 0, 1 }, + { "PEM basic/2nd", (unsigned char *)pem_pk1, sizeof(pem_pk1) - 1, + 1, 1, 0, 1, 2, 0, 0, 1 }, + { "ECDSA priv + 25519 pub", (unsigned char *)pem_mismatch_priv, + sizeof(pem_mismatch_priv) - 1, + 1, 0, 0, 0, 0, 0, 0, 0 }, + { "PEM string typo", (unsigned char *)pem_typo, sizeof(pem_typo) - 1, + 1, 0, 0, 0, 0, 0, 0, 0 }, + /* downselect from the 2, at each position */ + { "PEM 4->2/0", (unsigned char *)pem_4_to_2, sizeof(pem_4_to_2) - 1, + 1, 1, 0, 2, 0, 1, 0, 1 }, + { "PEM 4->2/1", (unsigned char *)pem_4_to_2, sizeof(pem_4_to_2) - 1, + 1, 1, 0, 2, 1, 1, 0, 1 }, + /* in the next one below, downselect fails, so we still have 2 entries */ + { "PEM 4->2/2", (unsigned char *)pem_4_to_2, sizeof(pem_4_to_2) - 1, + 1, 1, 0, 2, 3, 0, 0, 2 }, + /* b64 test vectors */ + { "B64 basic/last", (unsigned char *)b64_pk1, sizeof(b64_pk1) - 1, + 0, 1, 0, 1, OSSL_ECHSTORE_LAST, 1, 0, 1 }, + { "B64 6->3/2", (unsigned char *)b64_6_to_3, sizeof(b64_6_to_3) - 1, + 0, 1, 0, 3, 2, 1, 0, 1 }, + { "B64 bad suitelen", (unsigned char *)b64_bad_cs, sizeof(b64_bad_cs) - 1, + 0, 0, 0, 0, 0, 0, 0, 0 }, + /* binary test vectors */ + { "bin 6->3/2", (unsigned char *)bin_6_to_3, sizeof(bin_6_to_3), + 0, 1, 0, 3, 2, 1, 0, 1 }, + { "bin 2 symm suites", (unsigned char *)bin_multi_suite, + sizeof(bin_multi_suite), + 0, 1, 0, 1, OSSL_ECHSTORE_LAST, 1, 0, 1 }, + { "bin all-zero pub", (unsigned char *)bin_zero, sizeof(bin_zero), + 0, 1, 0, 1, OSSL_ECHSTORE_LAST, 1, 0, 1 }, + { "bin ok exts", (unsigned char *)bin_ok_exts, sizeof(bin_ok_exts), + 0, 1, 0, 1, OSSL_ECHSTORE_LAST, 1, 0, 1 }, + { "bin bad ver", (unsigned char *)bin_bad_ver, sizeof(bin_bad_ver), + 0, 0, 0, 0, 0, 0, 0, 0 }, + { "bin 2 bad ver", (unsigned char *)bin_bad_ver2, sizeof(bin_bad_ver2), + 0, 0, 0, 0, 0, 0, 0, 0 }, + { "bin bad len", (unsigned char *)bin_bad_olen, sizeof(bin_bad_olen), + 0, 0, 0, 0, 0, 0, 0, 0 }, + { "bin bad inner len", (unsigned char *)bin_bad_ilen, sizeof(bin_bad_ilen), + 0, 0, 0, 0, 0, 0, 0, 0 }, + { "bin bad pk len", (unsigned char *)bin_bad_pklen, sizeof(bin_bad_pklen), + 0, 0, 0, 0, 0, 0, 0, 0 }, + { "bin bad suitelen", (unsigned char *)bin_bad_cslen, sizeof(bin_bad_cslen), + 0, 0, 0, 0, 0, 0, 0, 0 }, + { "bin bad pn len", (unsigned char *)bin_bad_pnlen, sizeof(bin_bad_pnlen), + 0, 0, 0, 0, 0, 0, 0, 0 }, + { "bin bad extlen", (unsigned char *)bin_bad_extlen, sizeof(bin_bad_extlen), + 0, 0, 0, 0, 0, 0, 0, 0 }, + { "bin bad kemid", (unsigned char *)bin_bad_kemid, sizeof(bin_bad_kemid), + 0, 0, 0, 0, 0, 0, 0, 0 }, + { "bin bad kdfid", (unsigned char *)bin_bad_kdfid, sizeof(bin_bad_kdfid), + 0, 0, 0, 0, 0, 0, 0, 0 }, + { "bin bad aeadid", (unsigned char *)bin_bad_aeadid, sizeof(bin_bad_aeadid), + 0, 0, 0, 0, 0, 0, 0, 0 }, + { "bin exp aeadid", (unsigned char *)bin_bad_aeadid_ff, + sizeof(bin_bad_aeadid_ff), + 0, 0, 0, 0, 0, 0, 0, 0 }, + { "bin bad,good", (unsigned char *)bin_bad_then_good, + sizeof(bin_bad_then_good), + 0, 0, 0, 0, 0, 0, 0, 0 }, + { "bin mand ext", (unsigned char *)bin_mand_ext, sizeof(bin_mand_ext), + 0, 0, 0, 0, 0, 0, 0, 0 }, + { "bin bad inner extlen", (unsigned char *)bin_bad_inner_extlen, + sizeof(bin_bad_inner_extlen), + 0, 0, 0, 0, 0, 0, 0, 0 }, + { "bin NUL in PN", (unsigned char *)bin_nul_in_pn, sizeof(bin_nul_in_pn), + 0, 0, 0, 0, 0, 0, 0, 0 }, + { "bin PN ends in dot", (unsigned char *)bin_pn_dot_at_end, + sizeof(bin_pn_dot_at_end), + 0, 0, 0, 0, 0, 0, 0, 0 }, + { "bin short", (unsigned char *)bin_short, sizeof(bin_short), + 0, 0, 0, 0, 0, 0, 0, 0 }, + { "bin empty", (unsigned char *)bin_empty, sizeof(bin_empty), + 0, 0, 0, 0, 0, 0, 0, 0 }, + { "bin ver short", (unsigned char *)bin_ver_short, sizeof(bin_ver_short), + 0, 0, 0, 0, 0, 0, 0, 0 }, + { "bin long ext", (unsigned char *)bin_long_ext, sizeof(bin_long_ext), + 0, 0, 0, 0, 0, 0, 0, 0 }, + { "bin good then bad", (unsigned char *)bin_good_then_bad, + sizeof(bin_good_then_bad), + 0, 0, 0, 0, 0, 0, 0, 0 }, +}; + +/* similar, but slightly simpler setup for file reading tests */ +typedef struct FNT_T { + char *fname; /* relative file name */ + int read; /* expected result from a pem_read of that */ +} fnt_t; + +static fnt_t fnames[] = { + { "ech-eg.pem", 1 }, + { "ech-mid.pem", 1 }, + { "ech-big.pem", 1 }, + { "ech-giant.pem", 0 }, + { "ech-rsa.pem", 0 }, +}; typedef enum OPTION_choice { OPT_ERR = -1, @@ -34,6 +706,332 @@ const OPTIONS *test_get_options(void) return test_options; } +/* + * For the relevant test vector in our array above: + * - try decode + * - if not expected to decode, we're done + * - check we got the right number of keys/ECHConfig values + * - do some calls with getting info, downselecting etc. and + * check results as expected + * - do a write_pem call on the results + * - flush keys 'till now and check they're all gone + */ +static int ech_ingest_test(int run) +{ + OSSL_ECHSTORE *es = NULL; + OSSL_ECH_INFO *ei = NULL; + BIO *in = NULL, *out = NULL; + int i, rv = 0, keysb4, keysaftr, actual_ents = 0; + ingest_tv_t *tv = &ingest_tvs[run]; + time_t now = 0; + + if ((in = BIO_new(BIO_s_mem())) == NULL + || BIO_write(in, tv->tv, tv->len) <= 0 + || (out = BIO_new(BIO_s_mem())) == NULL + || (es = OSSL_ECHSTORE_new(NULL, NULL)) == NULL) + goto end; + if (verbose) + TEST_info("Iteration: %d %s", run + 1, tv->name); + /* just in case of bad edits to table */ + if (tv->pemenc != 1 && tv->pemenc != 0) { + TEST_info("Bad test vector entry"); + goto end; + } + if (tv->pemenc == 1 + && !TEST_int_eq(OSSL_ECHSTORE_read_pem(es, in, OSSL_ECH_NO_RETRY), + tv->read)) { + TEST_info("OSSL_ECHSTORE_read_pem unexpected result"); + goto end; + } + if (tv->pemenc != 1 + && !TEST_int_eq(OSSL_ECHSTORE_read_echconfiglist(es, in), + tv->read)) { + TEST_info("OSSL_ECHSTORE_read_echconfiglist unexpected result"); + goto end; + } + /* if we provided a deliberately bad tv then we're done */ + if (tv->read != 1) { + rv = 1; + goto end; + } + if (!TEST_int_eq(OSSL_ECHSTORE_num_keys(es, &keysb4), 1)) { + TEST_info("OSSL_ECHSTORE_num_keys unexpected fail"); + goto end; + } + if (!TEST_int_eq(keysb4, tv->keysb4)) { + TEST_info("OSSL_ECHSTORE_num_keys unexpected number of keys (b4)"); + goto end; + } + if (!TEST_int_eq(OSSL_ECHSTORE_get1_info(es, &ei, &actual_ents), 1)) { + TEST_info("OSSL_ECHSTORE_get1_info unexpected fail"); + goto end; + } + for (i = 0; i != actual_ents; i++) { + if (!TEST_int_eq(OSSL_ECH_INFO_print(bio_err, ei, i), 1)) { + TEST_info("OSSL_ECH_INFO_print unexpected fail"); + OSSL_ECH_INFO_free(ei, actual_ents); + goto end; + } + } + if (!TEST_int_eq(actual_ents, tv->entsb4)) { + TEST_info("OSSL_ECHSTORE_get1_info unexpected number of entries (b4)"); + goto end; + } + OSSL_ECH_INFO_free(ei, actual_ents); + ei = NULL; + /* ensure silly index fails ok */ + if (!TEST_int_eq(OSSL_ECHSTORE_downselect(es, -20), 0)) { + TEST_info("OSSL_ECHSTORE_downselect unexpected non-zero"); + goto end; + } + if (!TEST_int_eq(OSSL_ECHSTORE_downselect(es, tv->index), tv->expected)) { + TEST_info("OSSL_ECHSTORE_downselect unexpected result"); + goto end; + } + if (!TEST_int_eq(OSSL_ECHSTORE_num_keys(es, &keysaftr), 1)) { + TEST_info("OSSL_ECHSTORE_num_keys unexpected fail"); + goto end; + } + if (!TEST_int_eq(keysaftr, tv->keysaftr)) { + TEST_info("OSSL_ECHSTORE_num_keys unexpected number of keys (aftr)"); + goto end; + } + if (!TEST_int_eq(OSSL_ECHSTORE_get1_info(es, &ei, &actual_ents), 1)) { + TEST_info("OSSL_ECHSTORE_get1_info unexpected fail"); + goto end; + } + OSSL_ECH_INFO_free(ei, actual_ents); + ei = NULL; + if (!TEST_int_eq(actual_ents, tv->entsaftr)) { + TEST_info("OSSL_ECHSTORE_get1_info unexpected number of entries (aftr)"); + goto end; + } + if (!TEST_int_eq(OSSL_ECHSTORE_write_pem(es, OSSL_ECHSTORE_ALL, out), 1)) { + TEST_info("OSSL_ECHSTORE_write_pem unexpected fail"); + goto end; + } + if (!TEST_int_eq(OSSL_ECHSTORE_write_pem(es, 100, out), 0)) { + TEST_info("OSSL_ECHSTORE_write_pem unexpected result"); + goto end; + } + now = time(0); + if (!TEST_int_eq(OSSL_ECHSTORE_flush_keys(es, now), 1)) { + TEST_info("OSSL_ECHSTORE_flush_keys unexpected fail"); + goto end; + } + if (!TEST_int_eq(OSSL_ECHSTORE_num_keys(es, &keysaftr), 1)) { + TEST_info("OSSL_ECHSTORE_num_keys unexpected fail"); + goto end; + } + if (!TEST_int_eq(keysaftr, 0)) { + TEST_info("OSSL_ECHSTORE_flush_keys unexpected non-zero"); + goto end; + } + rv = 1; +end: + OSSL_ECH_INFO_free(ei, actual_ents); + OSSL_ECHSTORE_free(es); + BIO_free_all(in); + BIO_free_all(out); + return rv; +} + +/* make a bunch of calls with bad, mostly NULL, arguments */ +static int ech_store_null_calls(void) +{ + int rv = 0, count = 0; + OSSL_ECHSTORE *es = OSSL_ECHSTORE_new(NULL, NULL); + OSSL_HPKE_SUITE hpke_suite = OSSL_HPKE_SUITE_DEFAULT; + BIO *inout = BIO_new(BIO_s_mem()); + OSSL_ECH_INFO *info = NULL; + EVP_PKEY *priv = EVP_PKEY_new(); + + OSSL_ECHSTORE_free(NULL); + if (!TEST_int_eq(OSSL_ECHSTORE_new_config(NULL, OSSL_ECH_CURRENT_VERSION, + 0, "example.com", hpke_suite), + 0)) { + TEST_info("OSSL_ECHSTORE_new_config unexpected non-zero"); + goto end; + } + if (!TEST_int_eq(OSSL_ECHSTORE_new_config(es, OSSL_ECH_CURRENT_VERSION, + 0, NULL, hpke_suite), + 0)) { + TEST_info("OSSL_ECHSTORE_new_config unexpected non-zero"); + goto end; + } + if (!TEST_int_eq(OSSL_ECHSTORE_new_config(es, 0xffff, + 0, "example.com", hpke_suite), + 0)) { + TEST_info("OSSL_ECHSTORE_new_config unexpected non-zero"); + goto end; + } + hpke_suite.kdf_id = 0xAAAA; /* a bad value */ + if (!TEST_int_eq(OSSL_ECHSTORE_new_config(es, OSSL_ECH_CURRENT_VERSION, + 0, "example.com", hpke_suite), + 0)) { + TEST_info("OSSL_ECHSTORE_new_config unexpected non-zero"); + goto end; + } + if (!TEST_int_eq(OSSL_ECHSTORE_write_pem(NULL, 0, inout), 0)) { + TEST_info("OSSL_ECHSTORE_write_pem unexpected non-zero"); + goto end; + } + if (!TEST_int_eq(OSSL_ECHSTORE_write_pem(es, 0, NULL), 0)) { + TEST_info("OSSL_ECHSTORE_write_pem unexpected non-zero"); + goto end; + } + if (!TEST_int_eq(OSSL_ECHSTORE_write_pem(es, 100, inout), 0)) { + TEST_info("OSSL_ECHSTORE_write_pem unexpected non-zero"); + goto end; + } + if (!TEST_int_eq(OSSL_ECHSTORE_read_echconfiglist(NULL, inout), 0)) { + TEST_info("OSSL_ECHSTORE_read_echconfiglist unexpected non-zero"); + goto end; + } + if (!TEST_int_eq(OSSL_ECHSTORE_read_echconfiglist(es, NULL), 0)) { + TEST_info("OSSL_ECHSTORE_read_echconfiglist unexpected non-zero"); + goto end; + } + if (!TEST_int_eq(OSSL_ECHSTORE_get1_info(NULL, &info, &count), 0)) { + TEST_info("OSSL_ECHSTORE_get1_info unexpected non-zero"); + goto end; + } + if (!TEST_int_eq(OSSL_ECHSTORE_get1_info(es, NULL, &count), 0)) { + TEST_info("OSSL_ECHSTORE_get1_info unexpected non-zero"); + goto end; + } + if (!TEST_int_eq(OSSL_ECHSTORE_get1_info(es, &info, NULL), 0)) { + TEST_info("OSSL_ECHSTORE_get1_info unexpected non-zero"); + goto end; + } + if (!TEST_int_eq(OSSL_ECHSTORE_get1_info(es, &info, &count), 1)) { + TEST_info("OSSL_ECHSTORE_get1_info unexpected zero"); + goto end; + } + if (!TEST_int_eq(OSSL_ECHSTORE_downselect(NULL, 0), 0)) { + TEST_info("OSSL_ECHSTORE_downselect unexpected non-zero"); + goto end; + } + if (!TEST_int_eq(OSSL_ECHSTORE_downselect(es, 100), 0)) { + TEST_info("OSSL_ECHSTORE_downselect unexpected non-zero"); + goto end; + } + if (!TEST_int_eq(OSSL_ECHSTORE_set1_key_and_read_pem(NULL, priv, + inout, 0), 0)) { + TEST_info("OSSL_ECHSTORE_set1_key_and_readp_pem unexpected non-zero"); + goto end; + } + if (!TEST_int_eq(OSSL_ECHSTORE_set1_key_and_read_pem(es, NULL, + inout, 0), 0)) { + TEST_info("OSSL_ECHSTORE_set1_key_and_readp_pem unexpected non-zero"); + goto end; + } + if (!TEST_int_eq(OSSL_ECHSTORE_set1_key_and_read_pem(es, priv, + NULL, 0), 0)) { + TEST_info("OSSL_ECHSTORE_set1_key_and_readp_pem unexpected non-zero"); + goto end; + } + if (!TEST_int_eq(OSSL_ECHSTORE_set1_key_and_read_pem(es, priv, + inout, 100), 0)) { + TEST_info("OSSL_ECHSTORE_set1_key_and_readp_pem unexpected non-zero"); + goto end; + } + /* this one fails 'cause priv has no real value, even if non NULL */ + if (!TEST_int_eq(OSSL_ECHSTORE_set1_key_and_read_pem(es, priv, inout, + OSSL_ECH_NO_RETRY), + 0)) { + TEST_info("OSSL_ECHSTORE_set1_key_and_readp_pem unexpected non-zero"); + goto end; + } + if (!TEST_int_eq(OSSL_ECHSTORE_read_pem(NULL, inout, OSSL_ECH_NO_RETRY), 0)) { + TEST_info("OSSL_ECHSTORE_read_pem unexpected non-zero"); + goto end; + } + if (!TEST_int_eq(OSSL_ECHSTORE_read_pem(es, NULL, OSSL_ECH_NO_RETRY), 0)) { + TEST_info("OSSL_ECHSTORE_read_pem unexpected non-zero"); + goto end; + } + if (!TEST_int_eq(OSSL_ECHSTORE_read_pem(es, inout, 100), 0)) { + TEST_info("OSSL_ECHSTORE_read_pem unexpected non-zero"); + goto end; + } + if (!TEST_int_eq(OSSL_ECHSTORE_num_keys(NULL, &count), 0)) { + TEST_info("OSSL_ECHSTORE_num_keys unexpected non-zero"); + goto end; + } + if (!TEST_int_eq(OSSL_ECHSTORE_num_keys(es, NULL), 0)) { + TEST_info("OSSL_ECHSTORE_num_keys unexpected non-zero"); + goto end; + } + if (!TEST_int_eq(OSSL_ECHSTORE_flush_keys(NULL, 0), 0)) { + TEST_info("OSSL_ECHSTORE_flush_keys unexpected non-zero"); + goto end; + } + if (!TEST_int_eq(OSSL_ECHSTORE_flush_keys(es, -1), 0)) { + TEST_info("OSSL_ECHSTORE_flush_keys unexpected non-zero"); + goto end; + } + /* check free NULL is ok */ + OSSL_ECH_INFO_free(NULL, 100); + if (!TEST_int_eq(OSSL_ECH_INFO_print(inout, NULL, -1), 0)) { + TEST_info("OSSL_ECHSTORE_flush_keys unexpected non-zero"); + goto end; + } + if (!TEST_int_eq(OSSL_ECH_INFO_print(NULL, info, -1), 0)) { + TEST_info("OSSL_ECHSTORE_flush_keys unexpected non-zero"); + goto end; + } + if (!TEST_int_eq(OSSL_ECH_INFO_print(inout, info, 0), 0)) { + TEST_info("OSSL_ECHSTORE_flush_keys unexpected non-zero"); + goto end; + } + rv = 1; +end: + OSSL_ECH_INFO_free(info, count); + OSSL_ECHSTORE_free(es); + BIO_free_all(inout); + EVP_PKEY_free(priv); + return rv; +} + +/* read some files, some that work, some that fail */ +static int ech_test_file_read(int run) +{ + int rv = 0; + OSSL_ECHSTORE *es = NULL; + BIO *in = NULL; + fnt_t *ft = &fnames[run]; + char *fullname = NULL; + size_t fnlen = 0; + + es = OSSL_ECHSTORE_new(NULL, NULL); + if (es == NULL) + goto end; + fnlen = strlen(certsdir) + 1 + strlen(ft->fname) + 1; + fullname = OPENSSL_malloc(fnlen); + if (fullname == NULL) + goto end; + snprintf(fullname, fnlen, "%s/%s", certsdir, ft->fname); + if (verbose) + TEST_info("testing read of %s", fullname); + in = BIO_new_file(fullname, "r"); + if (in == NULL) { + TEST_info("BIO_new_file failed for %s", ft->fname); + goto end; + } + if (!TEST_int_eq(OSSL_ECHSTORE_read_pem(es, in, OSSL_ECH_NO_RETRY), + ft->read)) { + TEST_info("OSSL_ECHSTORE_read_pem unexpected fail"); + goto end; + } + rv = 1; +end: + OPENSSL_free(fullname); + OSSL_ECHSTORE_free(es); + BIO_free_all(in); + return rv; +} + #endif int setup_tests(void) @@ -52,7 +1050,13 @@ int setup_tests(void) return 0; } } - /* TODO(ECH): we'll move test code over later */ + certsdir = test_get_argument(0); + if (certsdir == NULL) + certsdir = DEF_CERTS_DIR; + ADD_ALL_TESTS(ech_ingest_test, OSSL_NELEM(ingest_tvs)); + ADD_TEST(ech_store_null_calls); + ADD_ALL_TESTS(ech_test_file_read, OSSL_NELEM(fnames)); + /* TODO(ECH): we'll add more test code once other TODO's settle */ return 1; #endif return 1; diff --git a/test/recipes/20-test_app_ech.t b/test/recipes/20-test_app_ech.t new file mode 100644 index 0000000000000..60a3b963094f8 --- /dev/null +++ b/test/recipes/20-test_app_ech.t @@ -0,0 +1,93 @@ +#! /usr/bin/env perl +# Copyright 2020-2023 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 +# + +use strict; +use warnings; + +use OpenSSL::Test::Utils; +use OpenSSL::Test qw/:DEFAULT srctop_file srctop_dir bldtop_dir bldtop_file with/; + +setup("test_app_ech"); + +plan skip_all => "ECH tests not supported in this build" + if disabled("ech") || disabled("tls1_3") + || disabled("ec") || disabled("ecx"); + +plan tests => 13; + +ok(run(app(["openssl", "ech", "-help"])), + "Run openssl ech with help"); +ok(run(app(["openssl", "ech", + "-ech_version", "13", + "-public_name", "example.com", + "-out", "eg1.pem", + "-verbose", + "-text"])), + "Generate an ECH key pair for example.com"); +ok(run(app(["openssl", "ech", + "-suite", "0x10,2,2", + "-public_name", "example.com", + "-out", "eg2.pem", + "-text"])), + "Generate an ECDSA ECH key pair for example.com"); +ok(run(app(["openssl", "ech", + "-max_name_len", "13", + "-public_name", "example.com", + "-out", "eg2.pem", + "-text"])), + "Generate an ECH key pair for example.com with max name len 13"); +ok(run(app(["openssl", "ech", + "-in", "eg1.pem", + "-in", "eg2.pem", + "-out", "eg3.pem", + "-verbose"])), + "Catenate the ECH for example.com twice"); +ok(run(app(["openssl", "ech", + "-in", "eg3.pem", + "-select", "1", + "-verbose", + "-out", "eg4.pem"])), + "Select one ECH Config"); + +with({ exit_checker => sub { return shift == 1; } }, + sub { + ok(run(app(["openssl", "ech" ])), + "Run openssl ech with no arg"); + ok(run(app(["openssl", "ech", "-nohelpatall"])), + "Run openssl ech with unknown arg"); + ok(run(app(["openssl", "ech", "nohelpatall"])), + "Run openssl ech with unknown non arg"); + ok(run(app(["openssl", "ech", + "-ech_version", "0xfe09", + "-public_name", "example.com", + "-out", "eg1.pem", + "-text"])), + "Fail to generate an ECH key pair for old draft version"); + ok(run(app(["openssl", "ech", + "-suite", "not,a,good,one", + "-public_name", "example.com", + "-out", "eg2.pem", + "-text"])), + "Fail to generate an ECH key pair with bad suite"); + ok(run(app(["openssl", "ech", + "-max_name_len", "1300", + "-public_name", "example.com", + "-text"])), + "(Fail to) Generate an ECH key pair for example.com with max name len 1300"); + ok(run(app(["openssl", "ech", + "-in", "eg1.pem", + "-in", "eg2.pem", + "-in", "eg3.pem", + "-in", "eg4.pem", + "-in", "eg1.pem", + "-in", "eg2.pem", + "-in", "eg3.pem", + "-in", "eg4.pem"])), + "Too many input files"); +}); From bb90a1c5579af3ea331c356378d7b6ab56f7d5db Mon Sep 17 00:00:00 2001 From: sftcd Date: Thu, 10 Oct 2024 17:46:11 +0100 Subject: [PATCH 05/24] ECH external APIs Reviewed-by: Tomas Mraz Reviewed-by: Matt Caswell (Merged from https://github.com/openssl/openssl/pull/25663) --- apps/ech.c | 39 +- demos/sslecho/echecho.c | 2 +- doc/designs/ech-api.md | 146 ++----- doc/man3/SSL_set1_echstore.pod | 82 ++-- include/openssl/ech.h | 59 ++- ssl/ech/ech_internal.c | 150 ++++++- ssl/ech/ech_local.h | 123 +++++- ssl/ech/ech_ssl_apis.c | 373 ++++++++++++++-- ssl/ech/ech_store.c | 201 ++++----- ssl/ssl_lib.c | 12 + ssl/ssl_local.h | 10 + test/certs/echserver.key | 28 ++ test/certs/echserver.pem | 80 ++++ test/ech_test.c | 776 +++++++++++++++++++++++---------- util/libssl.num | 19 +- 15 files changed, 1524 insertions(+), 576 deletions(-) create mode 100644 test/certs/echserver.key create mode 100644 test/certs/echserver.pem diff --git a/apps/ech.c b/apps/ech.c index fa13ee6cf5cfd..bb52b9aa32760 100644 --- a/apps/ech.c +++ b/apps/ech.c @@ -80,12 +80,10 @@ int ech_main(int argc, char **argv) char *prog = NULL; OPTION_CHOICE o; int i, rv = 1, verbose = 0, text = 0, outsupp = 0; - int select = OSSL_ECHSTORE_ALL; + int select = OSSL_ECHSTORE_ALL, numinfiles = 0; char *outfile = NULL, *infile = NULL; char *infiles[OSSL_ECH_MAXINFILES] = { NULL }; - int numinfiles = 0; - char *public_name = NULL; - char *suitestr = NULL; + char *public_name = NULL, *suitestr = NULL; uint16_t ech_version = OSSL_ECH_CURRENT_VERSION; uint8_t max_name_length = 0; OSSL_HPKE_SUITE hpke_suite = OSSL_HPKE_SUITE_DEFAULT; @@ -154,17 +152,13 @@ int ech_main(int argc, char **argv) break; } } - argc = opt_num_rest(); argv = opt_rest(); if (argc != 0) { BIO_printf(bio_err, "%s: Unknown parameter %s\n", prog, argv[0]); goto opthelp; } - - /* - * Check ECH-specific inputs - */ + /* Check ECH-specific inputs */ switch (ech_version) { case OSSL_ECH_RFCXXXX_VERSION: /* fall through */ case 13: @@ -174,7 +168,6 @@ int ech_main(int argc, char **argv) BIO_printf(bio_err, "Un-supported version (0x%04x)\n", ech_version); goto end; } - if (max_name_length > OSSL_ECH_MAX_MAXNAMELEN) { BIO_printf(bio_err, "Weird max name length (0x%04x) - biggest is " "(0x%04x) - exiting\n", max_name_length, @@ -182,7 +175,6 @@ int ech_main(int argc, char **argv) ERR_print_errors(bio_err); goto end; } - if (suitestr != NULL) { if (OSSL_HPKE_str2suite(suitestr, &hpke_suite) != 1) { BIO_printf(bio_err, "Bad OSSL_HPKE_SUITE (%s)\n", suitestr); @@ -190,7 +182,6 @@ int ech_main(int argc, char **argv) goto end; } } - /* Set default if needed */ if (outfile == NULL) outfile = "echconfig.pem"; @@ -211,7 +202,6 @@ int ech_main(int argc, char **argv) BIO_printf(bio_err, "OSSL_ECHSTORE_new_config success\n"); rv = 0; } - if (mode == OSSL_ECH_SELPRINT_MODE) { if (numinfiles == 0) goto opthelp; @@ -245,28 +235,35 @@ int ech_main(int argc, char **argv) } rv = 0; } - if (text) { - OSSL_ECH_INFO *oi = NULL; int oi_ind, oi_cnt = 0; - if (OSSL_ECHSTORE_get1_info(es, &oi, &oi_cnt) != 1) + if (OSSL_ECHSTORE_num_entries(es, &oi_cnt) != 1) goto end; if (verbose) BIO_printf(bio_err, "Printing %d ECHConfigList\n", oi_cnt); for (oi_ind = 0; oi_ind != oi_cnt; oi_ind++) { - if (OSSL_ECH_INFO_print(bio_out, oi, oi_ind) != 1) { - BIO_printf(bio_err, "OSSL_ECH_INFO_print error entry (%d)\n", - oi_ind); + time_t secs = 0; + char *pn = NULL, *ec = NULL; + int has_priv, for_retry; + + if (OSSL_ECHSTORE_get1_info(es, oi_ind, &secs, &pn, &ec, + &has_priv, &for_retry) != 1) { + OPENSSL_free(pn); /* just in case */ + OPENSSL_free(ec); goto end; } + BIO_printf(bio_err, "ECH entry: %d public_name: %s age: %lld%s\n", + oi_ind, pn, (long long)secs, + has_priv ? " (has private key)" : ""); + BIO_printf(bio_err, "\t%s\n", ec); + OPENSSL_free(pn); + OPENSSL_free(ec); } - OSSL_ECH_INFO_free(oi, oi_cnt); if (verbose) BIO_printf(bio_err, "Success printing %d ECHConfigList\n", oi_cnt); rv = 0; } - end: OSSL_ECHSTORE_free(es); BIO_free_all(ecf); diff --git a/demos/sslecho/echecho.c b/demos/sslecho/echecho.c index c273f16a243da..36f6845de4b50 100644 --- a/demos/sslecho/echecho.c +++ b/demos/sslecho/echecho.c @@ -100,7 +100,7 @@ static int configure_ech(SSL_CTX *ctx, int server, OSSL_ECHSTORE *es = NULL; BIO *es_in = BIO_new_mem_buf(buf, len); - if (es_in == NULL || (es = OSSL_ECHSTORE_init(NULL, NULL)) == NULL) + if (es_in == NULL || (es = OSSL_ECHSTORE_new(NULL, NULL)) == NULL) goto err; if (server && OSSL_ECHSTORE_read_pem(es, es_in, 1) != 1) goto err; diff --git a/doc/designs/ech-api.md b/doc/designs/ech-api.md index a7f1ffdbd0c13..e9808035141e5 100644 --- a/doc/designs/ech-api.md +++ b/doc/designs/ech-api.md @@ -4,9 +4,10 @@ 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. +migrated into (PRs for) the feature branch. + +The `OSSL_ECHSTORE` related text here matches the ECH +[feature branch](https://github.com/openssl/openssl/tree/feature/ech). 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 @@ -217,13 +218,15 @@ 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_get1_info(OSSL_ECHSTORE *es, int index, time_t *loaded_secs, + char **public_name, char **echconfig, + int *has_private, int *for_retry); 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_entries(OSSL_ECHSTORE *es, int *numentries); int OSSL_ECHSTORE_num_keys(OSSL_ECHSTORE *es, int *numkeys); int OSSL_ECHSTORE_flush_keys(OSSL_ECHSTORE *es, time_t age); ``` @@ -245,15 +248,8 @@ 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_get1_info()` presents the caller with information about the +content of the store for logging or for display, e.g. in a command line tool. `OSSL_ECHSTORE_downselect()` API gives the client a way to select one particular ECHConfig value from the set stored (discarding the rest). @@ -264,13 +260,14 @@ 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. +`OSSL_ECHSTORE_num_entries()` and `OSSL_ECHSTORE_num_keys()` allow an +application to see how many usable ECH configs and 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: @@ -284,6 +281,15 @@ 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. +There is also an API that allows setting an ECHConfigList for an SSL +connection, that is compatible with BoringSSL. Note that the input +`ecl` here can be either base64 or binary encoded, but for +BoringSSL it must be binary encoded. + +```c +int SSL_set1_ech_config_list(SSL *ssl, const uint8_t *ecl, size_t ecl_len); +``` + To access the `OSSL_ECHSTORE` associated with an `SSL_CTX` or `SSL` connection: @@ -295,44 +301,6 @@ 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 */ - int has_private_key; /* 0 if we don't have a related private key */ -} 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); -``` - ECH Store Internals ------------------- @@ -357,21 +325,20 @@ typedef struct ossl_echstore_entry_st { 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; +} OSSL_ECHSTORE_ENTRY; -DEFINE_STACK_OF(OSSL_ECHSTORE_entry) +DEFINE_STACK_OF(OSSL_ECHSTORE_ENTRY) -typedef struct ossl_echstore_st { - STACK_OF(OSSL_ECHSTORE_entry) *entries; +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: @@ -471,17 +438,7 @@ Different encodings 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. 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_ECHSTORE_find_echconfigs()` attempts to find and return the (possibly empty) -set of ECHConfigList values as an `OSSL_ECHSTORE` from the input `BIO`. - -```c -OSSL_ECHSTORE *OSSL_ECHSTORE_find_echconfigs(BIO *in); -``` +(base64 or binary) each of which may suit different applications. If the input contains more than one (syntactically correct) ECHConfigList, then only those that contain locally supported options (e.g. AEAD ciphers) will be @@ -500,7 +457,7 @@ to send the `public_name` provided from the ECHConfigList. ```c int SSL_ech_set1_server_names(SSL *s, const char *inner_name, - const char *outer_name, int no_outer); + const char *outer_name, int no_outer); 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); @@ -599,28 +556,6 @@ The following options are defined for ECH and may be set via #define SSL_OP_ECH_GREASE_RETRY_CONFIG SSL_OP_BIT(39) ``` -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 ------------- @@ -650,16 +585,6 @@ 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 - -```c -OPENSSL_EXPORT int SSL_set1_ech_config_list(SSL *ssl, - const uint8_t *ech_config_list, - size_t ech_config_list_len); -``` - -This provides a subset of the equivalent client capabilities from our fork. - ### Verifying the outer CH rather than inner BoringSSL seems to use this API to change the DNS name being verified in order @@ -696,7 +621,6 @@ OPENSSL_EXPORT int SSL_ECH_KEYS_add(SSL_ECH_KEYS *keys, int is_retry_config, OPENSSL_EXPORT int SSL_ECH_KEYS_marshal_retry_configs(const SSL_ECH_KEYS *keys, uint8_t **out, size_t *out_len); - ``` Collectively these are similar to `OSSL_ECH_make_echconfig()`. @@ -710,8 +634,6 @@ ECH keys using: 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. - ### Getting status BoringSSL has: diff --git a/doc/man3/SSL_set1_echstore.pod b/doc/man3/SSL_set1_echstore.pod index 0dd889b1ba927..05034d3102b94 100644 --- a/doc/man3/SSL_set1_echstore.pod +++ b/doc/man3/SSL_set1_echstore.pod @@ -7,14 +7,15 @@ OSSL_ECHSTORE_new, OSSL_ECHSTORE_free, OSSL_ECHSTORE_new_config, OSSL_ECHSTORE_write_pem, OSSL_ECHSTORE_read_echconfiglist, OSSL_ECHSTORE_get1_info, OSSL_ECHSTORE_downselect, OSSL_ECHSTORE_set1_key_and_read_pem, -OSSL_ECHSTORE_read_pem, OSSL_ECHSTORE_num_keys, OSSL_ECHSTORE_flush_keys, -OSSL_ECH_INFO_free, OSSL_ECH_INFO_print, SSL_CTX_set1_echstore, -SSL_CTX_get1_echstore, SSL_get1_echstore, SSL_ech_set_server_names, -SSL_ech_set_outer_server_name, SSL_ech_set_outer_alpn_protos, -SSL_ech_get1_status, SSL_ech_set_grease_suite, SSL_ech_set_grease_type, -SSL_ech_set_callback, SSL_ech_get_retry_config, -SSL_CTX_ech_set_outer_alpn_protos, SSL_CTX_ech_raw_decrypt, -SSL_CTX_ech_set_callback +OSSL_ECHSTORE_read_pem, OSSL_ECHSTORE_num_entries, +OSSL_ECHSTORE_num_keys, OSSL_ECHSTORE_flush_keys, +SSL_CTX_set1_echstore, +SSL_CTX_get1_echstore, SSL_get1_echstore, SSL_ech_set1_server_names, +SSL_ech_set1_outer_server_name, SSL_ech_set1_outer_alpn_protos, +SSL_ech_get1_status, SSL_ech_set1_grease_suite, SSL_ech_set_grease_type, +SSL_ech_set_callback, SSL_ech_get1_retry_config, +SSL_CTX_ech_set1_outer_alpn_protos, SSL_CTX_ech_raw_decrypt, +SSL_CTX_ech_set_callback,SSL_set1_ech_config_list - Encrypted Client Hello (ECH) functions =head1 SYNOPSIS @@ -28,32 +29,30 @@ SSL_CTX_ech_set_callback 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_get1_info(OSSL_ECHSTORE *es, int index, time_t *loaded_secs, + char **public_name, char **echconfig, + int *has_private, int *for_retry); 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_entries(OSSL_ECHSTORE *es, int *numentries); int OSSL_ECHSTORE_num_keys(OSSL_ECHSTORE *es, int *numkeys); int OSSL_ECHSTORE_flush_keys(OSSL_ECHSTORE *es, time_t age); - 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_CTX_set1_echstore(SSL_CTX *ctx, OSSL_ECHSTORE *es); int SSL_set1_echstore(SSL *s, OSSL_ECHSTORE *es); OSSL_ECHSTORE *SSL_CTX_get1_echstore(const SSL_CTX *ctx); OSSL_ECHSTORE *SSL_get1_echstore(const SSL *s); - int SSL_ech_set_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, - const size_t protos_len); + int SSL_ech_set1_server_names(SSL *s, const char *inner_name, + const char *outer_name, int no_outer); + 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, + const size_t protos_len); int SSL_ech_get1_status(SSL *s, char **inner_sni, char **outer_sni); - 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); void SSL_ech_set_callback(SSL *s, SSL_ech_cb_func f); - int SSL_ech_get_retry_config(SSL *s, unsigned char **ec, size_t *eclen); - int SSL_CTX_ech_set_outer_alpn_protos(SSL_CTX *s, const unsigned char *protos, - const size_t protos_len); + int SSL_ech_get1_retry_config(SSL *s, unsigned char **ec, size_t *eclen); int SSL_CTX_ech_raw_decrypt(SSL_CTX *ctx, int *decrypted_ok, char **inner_sni, char **outer_sni, @@ -61,10 +60,18 @@ SSL_CTX_ech_set_callback unsigned char *inner_ch, size_t *inner_len, unsigned char **hrrtok, size_t *toklen); void SSL_CTX_ech_set_callback(SSL_CTX *ctx, SSL_ech_cb_func f); + int SSL_CTX_ech_set1_outer_alpn_protos(SSL_CTX *ctx, + const unsigned char *protos, + const size_t protos_len); + int SSL_set1_ech_config_list(SSL *ssl, const uint8_t *ecl, size_t ecl_len); =head1 DESCRIPTION -TODO(ECH): Text is TBD, this is just enough for the build. +TODO(ECH): Complete this text... + +The Encrypted Client Hello (ECH) APIs described here are built around +the concept of an `OSSL_ECHSTORE` which contains ECH configuration +information relevant for the current 'SSL_CTX' or 'SSL' connection. Mention SSL_set1_echstore() is a thing Mention OSSL_ECHSTORE_new() is a thing @@ -77,23 +84,23 @@ Mention OSSL_ECHSTORE_downselect() is a thing Mention OSSL_ECHSTORE_set1_key_and_read_pem() is a thing Mention OSSL_ECHSTORE_read_pem() is a thing Mention OSSL_ECHSTORE_num_keys() is a thing +Mention OSSL_ECHSTORE_num_entries() is a thing Mention OSSL_ECHSTORE_flush_keys() is a thing -Mention OSSL_ECH_INFO_free() is a thing -Mention OSSL_ECH_INFO_print() is a thing Mention SSL_CTX_set1_echstore() is a thing Mention SSL_CTX_get1_echstore() is a thing Mention SSL_get1_echstore() is a thing -Mention SSL_ech_set_server_names() is a thing -Mention SSL_ech_set_outer_server_name() is a thing -Mention SSL_ech_set_outer_alpn_protos() is a thing +Mention SSL_ech_set1_server_names() is a thing +Mention SSL_ech_set1_outer_server_name() is a thing +Mention SSL_ech_set1_outer_alpn_protos() is a thing Mention SSL_ech_get1_status() is a thing -Mention SSL_ech_set_grease_suite() is a thing +Mention SSL_ech_set1_grease_suite() is a thing Mention SSL_ech_set_grease_type() is a thing Mention SSL_ech_set_callback() is a thing -Mention SSL_ech_get_retry_config() is a thing -Mention SSL_CTX_ech_set_outer_alpn_protos() is a thing +Mention SSL_ech_get1_retry_config() is a thing +Mention SSL_CTX_ech1_set_outer_alpn_protos() is a thing Mention SSL_CTX_ech_raw_decrypt() is a thing Mention SSL_CTX_ech_set_callback() is a thing +Mention SSL_set1_ech_config_list() is a thing =head2 Callback Function @@ -137,7 +144,10 @@ An example string I as seen on a client might be: =head1 RETURN VALUES +All functions named here return one on success and zero on error. + SSL_set1_echstore() returns zero on error +SSL_set1_ech_config_list() returns zero on error OSSL_ECHSTORE_new() returns zero on error OSSL_ECHSTORE_free() returns zero on error OSSL_ECHSTORE_new_config() returns zero on error @@ -148,9 +158,8 @@ OSSL_ECHSTORE_downselect() returns zero on error OSSL_ECHSTORE_set1_key_and_read_pem() returns zero on error OSSL_ECHSTORE_read_pem() returns zero on error OSSL_ECHSTORE_num_keys() returns zero on error +OSSL_ECHSTORE_num_entries() returns zero on error OSSL_ECHSTORE_flush_keys() returns zero on error -OSSL_ECH_INFO_free() returns zero on error -OSSL_ECH_INFO_print() returns zero on error SSL_CTX_set1_echstore() returns zero on error SSL_CTX_get1_echstore() returns zero on error SSL_get1_echstore() returns zero on error @@ -162,10 +171,15 @@ SSL_ech_set_grease_suite() returns zero on error SSL_ech_set_grease_type() returns zero on error SSL_ech_set_callback() returns zero on error SSL_ech_get_retry_config() returns zero on error -SSL_CTX_ech_set_outer_alpn_protos() returns zero on error +SSL_CTX_ech_set1_outer_alpn_protos() returns zero on error SSL_CTX_ech_raw_decrypt() returns zero on error SSL_CTX_ech_set_callback() returns zero on error +Note that SSL_CTX_ech_set1_outer_alpn_protos() and +SSL_ech_set1_outer_alpn_protos() return zero on error and 1 on success. +This is in contrast to SSL_CTX_set1_alpn_protos() and SSL_set1_alpn_protos() +which (unusually for OpenSSL) return 0 on success and 1 on error. + =head1 SEE ALSO The Encrypted ClientHello specification: L @@ -173,7 +187,7 @@ TODO(ECH) update link to RFC. =head1 HISTORY -This functionality described here was added in OpenSSL 3.4. +The functionality described here was added in OpenSSL 3.5. =head1 COPYRIGHT diff --git a/include/openssl/ech.h b/include/openssl/ech.h index 95705fc921c73..74f6deed9eee8 100644 --- a/include/openssl/ech.h +++ b/include/openssl/ech.h @@ -53,31 +53,14 @@ # define SSL_ECH_STATUS_NOT_TRIED -101 /* ECH wasn't attempted */ # define SSL_ECH_STATUS_BAD_NAME -102 /* ECH ok but server cert bad */ # define SSL_ECH_STATUS_NOT_CONFIGURED -103 /* ECH wasn't configured */ -# define SSL_ECH_STATUS_FAILED_ECH -105 /* We tried, failed and got an ECH, from a good name */ -# define SSL_ECH_STATUS_FAILED_ECH_BAD_NAME -106 /* We tried, failed and got an ECH, from a bad name */ +# define SSL_ECH_STATUS_FAILED_ECH -105 /* Tried, failed, got an ECH, from a good name */ +# define SSL_ECH_STATUS_FAILED_ECH_BAD_NAME -106 /* Tried, failed, got an ECH, from a bad name */ /* if a caller wants to index the last entry in the store */ # define OSSL_ECHSTORE_LAST -1 /* if a caller wants all entries in the store, e.g. to print public values */ # define OSSL_ECHSTORE_ALL -2 -/* - * 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 */ - time_t seconds_in_memory; /* number of seconds since this was loaded */ - 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 */ - int has_private_key; /* 0 if we don't have a related private key */ -} OSSL_ECH_INFO; - /* Values for the for_retry inputs */ # define OSSL_ECH_FOR_RETRY 1 # define OSSL_ECH_NO_RETRY 0 @@ -92,18 +75,17 @@ int OSSL_ECHSTORE_new_config(OSSL_ECHSTORE *es, 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_get1_info(OSSL_ECHSTORE *es, int index, time_t *loaded_secs, + char **public_name, char **echconfig, + int *has_private, int *for_retry); 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_entries(const OSSL_ECHSTORE *es, int *numentries); int OSSL_ECHSTORE_num_keys(OSSL_ECHSTORE *es, int *numkeys); int OSSL_ECHSTORE_flush_keys(OSSL_ECHSTORE *es, time_t age); -void OSSL_ECH_INFO_free(OSSL_ECH_INFO *info, int count); -int OSSL_ECH_INFO_print(BIO *out, OSSL_ECH_INFO *info, int count); - /* * APIs relating OSSL_ECHSTORE to SSL/SSL_CTX */ @@ -113,21 +95,31 @@ int SSL_set1_echstore(SSL *s, OSSL_ECHSTORE *es); OSSL_ECHSTORE *SSL_CTX_get1_echstore(const SSL_CTX *ctx); OSSL_ECHSTORE *SSL_get1_echstore(const SSL *s); -int SSL_ech_set_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, - const size_t protos_len); +int SSL_ech_set1_server_names(SSL *s, const char *inner_name, + const char *outer_name, int no_outer); +int SSL_ech_set1_outer_server_name(SSL *s, const char *outer_name, int no_outer); +/* + * Note that this function returns 1 for success and 0 for error. This + * contrasts with SSL_set1_alpn_protos() which (unusually for OpenSSL) + * returns 0 for success and 1 on error. + */ +int SSL_ech_set1_outer_alpn_protos(SSL *s, const unsigned char *protos, + const size_t protos_len); int SSL_ech_get1_status(SSL *s, char **inner_sni, char **outer_sni); -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); typedef unsigned int (*SSL_ech_cb_func)(SSL *s, const char *str); void SSL_ech_set_callback(SSL *s, SSL_ech_cb_func f); -int SSL_ech_get_retry_config(SSL *s, unsigned char **ec, size_t *eclen); +int SSL_ech_get1_retry_config(SSL *s, unsigned char **ec, size_t *eclen); -int SSL_CTX_ech_set_outer_alpn_protos(SSL_CTX *s, const unsigned char *protos, - const size_t protos_len); +/* + * Note that this function returns 1 for success and 0 for error. This + * contrasts with SSL_set1_alpn_protos() which (unusually for OpenSSL) + * returns 0 for success and 1 on error. + */ +int SSL_CTX_ech_set1_outer_alpn_protos(SSL_CTX *s, const unsigned char *protos, + const size_t protos_len); int SSL_CTX_ech_raw_decrypt(SSL_CTX *ctx, int *decrypted_ok, char **inner_sni, char **outer_sni, @@ -135,6 +127,7 @@ int SSL_CTX_ech_raw_decrypt(SSL_CTX *ctx, unsigned char *inner_ch, size_t *inner_len, unsigned char **hrrtok, size_t *toklen); void SSL_CTX_ech_set_callback(SSL_CTX *ctx, SSL_ech_cb_func f); +int SSL_set1_ech_config_list(SSL *ssl, const uint8_t *ecl, size_t ecl_len); # endif #endif diff --git a/ssl/ech/ech_internal.c b/ssl/ech/ech_internal.c index 94842526e5cb2..403beb66de590 100644 --- a/ssl/ech/ech_internal.c +++ b/ssl/ech/ech_internal.c @@ -12,4 +12,152 @@ #include "../ssl_local.h" #include "ech_local.h" -/* TODO(ECH): move ECH internal code here when we get to it */ +#ifndef OPENSSL_NO_ECH + +/* ECH internal API functions */ + +static OSSL_ECHSTORE_ENTRY *ossl_echstore_entry_dup(const OSSL_ECHSTORE_ENTRY *orig) +{ + OSSL_ECHSTORE_ENTRY *ret = NULL; + + if (orig == NULL) + return NULL; + ret = OPENSSL_zalloc(sizeof(*ret)); + if (ret == NULL) + return NULL; + ret->version = orig->version; + if (orig->public_name != NULL) { + ret->public_name = OPENSSL_strdup(orig->public_name); + if (ret->public_name == NULL) + goto err; + } + ret->pub_len = orig->pub_len; + if (orig->pub != NULL) { + ret->pub = OPENSSL_memdup(orig->pub, orig->pub_len); + if (ret->pub == NULL) + goto err; + } + ret->nsuites = orig->nsuites; + ret->suites = OPENSSL_memdup(orig->suites, sizeof(OSSL_HPKE_SUITE) * ret->nsuites); + if (ret->suites == NULL) + goto err; + ret->max_name_length = orig->max_name_length; + ret->config_id = orig->config_id; + if (orig->exts != NULL) { + ret->exts = sk_OSSL_ECHEXT_deep_copy(orig->exts, ossl_echext_dup, + ossl_echext_free); + if (ret->exts == NULL) + goto err; + } + ret->loadtime = orig->loadtime; + if (orig->keyshare != NULL) { + if (!EVP_PKEY_up_ref(orig->keyshare)) + goto err; + ret->keyshare = orig->keyshare; + } + ret->for_retry = orig->for_retry; + if (orig->encoded != NULL) { + ret->encoded_len = orig->encoded_len; + ret->encoded = OPENSSL_memdup(orig->encoded, ret->encoded_len); + if (ret->encoded == NULL) + goto err; + } + return ret; +err: + ossl_echstore_entry_free(ret); + return NULL; +} + +/* duplicate an OSSL_ECHSTORE as needed */ +OSSL_ECHSTORE *ossl_echstore_dup(const OSSL_ECHSTORE *old) +{ + OSSL_ECHSTORE *cp = NULL; + + if (old == NULL) + return NULL; + cp = OPENSSL_zalloc(sizeof(*cp)); + if (cp == NULL) + return NULL; + cp->libctx = old->libctx; + if (old->propq != NULL) { + cp->propq = OPENSSL_strdup(old->propq); + if (cp->propq == NULL) + goto err; + } + if (old->entries != NULL) { + cp->entries = sk_OSSL_ECHSTORE_ENTRY_deep_copy(old->entries, + ossl_echstore_entry_dup, + ossl_echstore_entry_free); + if (cp->entries == NULL) + goto err; + } + return cp; +err: + OSSL_ECHSTORE_free(cp); + return NULL; +} + +void ossl_ech_ctx_clear(OSSL_ECH_CTX *ce) +{ + if (ce == NULL) + return; + OSSL_ECHSTORE_free(ce->es); + OPENSSL_free(ce->alpn_outer); + return; +} + +void ossl_ech_conn_clear(OSSL_ECH_CONN *ec) +{ + if (ec == NULL) + return; + OSSL_ECHSTORE_free(ec->es); + OPENSSL_free(ec->outer_hostname); + OPENSSL_free(ec->alpn_outer); + OPENSSL_free(ec->former_inner); + OPENSSL_free(ec->innerch); + OPENSSL_free(ec->encoded_innerch); + OPENSSL_free(ec->innerch1); + OPENSSL_free(ec->kepthrr); + OPENSSL_free(ec->grease_suite); + OPENSSL_free(ec->sent); + OPENSSL_free(ec->returned); + OPENSSL_free(ec->pub); + OSSL_HPKE_CTX_free(ec->hpke_ctx); + EVP_PKEY_free(ec->tmp_pkey); + return; +} + +/* called from ssl/ssl_lib.c: ossl_ssl_connection_new_int */ +int ossl_ech_conn_init(SSL_CONNECTION *s, SSL_CTX *ctx, + const SSL_METHOD *method) +{ + memset(&s->ext.ech, 0, sizeof(s->ext.ech)); + if (ctx->ext.ech.es != NULL + && (s->ext.ech.es = ossl_echstore_dup(ctx->ext.ech.es)) == NULL) + goto err; + s->ext.ech.cb = ctx->ext.ech.cb; + if (ctx->ext.ech.alpn_outer != NULL) { + s->ext.ech.alpn_outer = OPENSSL_memdup(ctx->ext.ech.alpn_outer, + ctx->ext.ech.alpn_outer_len); + if (s->ext.ech.alpn_outer == NULL) + goto err; + s->ext.ech.alpn_outer_len = ctx->ext.ech.alpn_outer_len; + } + /* initialise type/cid to unknown */ + s->ext.ech.attempted_type = OSSL_ECH_type_unknown; + s->ext.ech.attempted_cid = OSSL_ECH_config_id_unset; + if (s->ext.ech.es != NULL) + s->ext.ech.attempted = 1; + if (ctx->options & SSL_OP_ECH_GREASE) + s->options |= SSL_OP_ECH_GREASE; + return 1; +err: + OSSL_ECHSTORE_free(s->ext.ech.es); + s->ext.ech.es = NULL; + OPENSSL_free(s->ext.ech.alpn_outer); + s->ext.ech.alpn_outer = NULL; + s->ext.ech.alpn_outer_len = 0; + return 0; +} + +#endif diff --git a/ssl/ech/ech_local.h b/ssl/ech/ech_local.h index 1d89e410818a7..387fe713b60c9 100644 --- a/ssl/ech/ech_local.h +++ b/ssl/ech/ech_local.h @@ -31,6 +31,16 @@ */ # define OSSL_ECH_SUPERVERBOSE +/* values for s->ext.ech.grease */ +# define OSSL_ECH_GREASE_UNKNOWN -1 /* when we're not yet sure */ +# define OSSL_ECH_NOT_GREASE 0 /* when decryption worked */ +# define OSSL_ECH_IS_GREASE 1 /* when decryption failed or GREASE wanted */ + +/* value for uninitialised ECH version */ +# define OSSL_ECH_type_unknown 0xffff +/* value for not yet set ECH config_id */ +# define OSSL_ECH_config_id_unset -1 + # define OSSL_ECH_CIPHER_LEN 4 /* ECHCipher length (2 for kdf, 2 for aead) */ /* * Reminder of what goes in DNS for ECH RFC XXXX @@ -83,7 +93,6 @@ typedef struct ossl_echstore_entry_st { 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 */ @@ -96,8 +105,118 @@ DEFINE_STACK_OF(OSSL_ECHSTORE_ENTRY) struct ossl_echstore_st { STACK_OF(OSSL_ECHSTORE_ENTRY) *entries; OSSL_LIB_CTX *libctx; - const char *propq; + char *propq; }; +/* ECH details associated with an SSL_CTX */ +typedef struct ossl_ech_ctx_st { + /* TODO(ECH): consider making es ref-counted */ + OSSL_ECHSTORE *es; + unsigned char *alpn_outer; + size_t alpn_outer_len; + SSL_ech_cb_func cb; /* callback function for when ECH "done" */ +} OSSL_ECH_CTX; + +/* ECH details associated with an SSL_CONNECTION */ +typedef struct ossl_ech_conn_st { + /* TODO(ECH): consider making es ref-counted */ + OSSL_ECHSTORE *es; /* ECHConfigList details */ + int no_outer; /* set to 1 if we should send no outer SNI at all */ + char *outer_hostname; + unsigned char *alpn_outer; + size_t alpn_outer_len; + SSL_ech_cb_func cb; /* callback function for when ECH "done" */ + /* + * If ECH fails, then we switch to verifying the cert for the + * outer_hostname, meanwhile we still want to be able to trace + * the value we tried as the inner SNI for debug purposes + */ + char *former_inner; + /* + * TODO(ECH): The next 4 buffers (and lengths) may change later + * if a better way to handle the mutiple transcripts needed is + * suggested/invented. I'd suggest we review these when that code + * is part of a PR (which won't be for a few PR's yet.) + */ + /* + * encoded inner ClientHello before/after ECH compression, which` + * is nitty/complex (to avoid repeating the same extenstion value + * in outer and inner, thus saving bandwidth) but (re-)calculating + * the compression is a pain, so we'll store those as we make them + */ + unsigned char *innerch; /* before compression */ + size_t innerch_len; + unsigned char *encoded_innerch; /* after compression */ + size_t encoded_innerch_len; + /* + * in case of HRR, we need to record the 1st inner client hello, and + * the first server hello (aka the HRR) so we can independently + * generate the transcript and accept confirmation when making the + * 2nd server hello + */ + unsigned char *innerch1; + size_t innerch1_len; + unsigned char *kepthrr; + size_t kepthrr_len; + /* + * Extensions are "outer-only" if the value is only sent in the + * outer CH and only the type is sent in the inner CH. + * We use this array to keep track of the extension types that + * have values only in the outer CH + * Currently, this is basically controlled at compile time, but + * in a way that could be varied, or, in future, put under + * run-time control, so having this isn't so much an overhead. + */ + uint16_t outer_only[OSSL_ECH_OUTERS_MAX]; + size_t n_outer_only; /* the number of outer_only extensions so far */ + /* + * Index of the current extension's entry in ext_defs - this is + * to avoid the need to change a couple of extension APIs. + * TODO(ECH): check if there's another way to get that value + */ + size_t ext_ind; + /* ECH status vars */ + int ch_depth; /* set during CH creation, 0: doing outer, 1: doing inner */ + int attempted; /* 1 if ECH was or is being attempted, 0 otherwise */ + int done; /* 1 if we've finished ECH calculations, 0 otherwise */ + uint16_t attempted_type; /* ECH version used */ + int attempted_cid; /* ECH config id sent/rx'd */ + int backend; /* 1 if we're a server backend in split-mode, 0 otherwise */ + /* + * success is 1 if ECH succeeded, 0 otherwise, on the server this + * is known early, on the client we need to wait for the ECH confirm + * calculation based on the SH (or 2nd SH in case of HRR) + */ + int success; + int grease; /* 1 if we're GREASEing, 0 otherwise */ + char *grease_suite; /* HPKE suite string for GREASEing */ + unsigned char *sent; /* GREASEy ECH value sent, in case needed for re-tx */ + size_t sent_len; + unsigned char *returned; /* binary ECHConfigList retry-configs value */ + size_t returned_len; + unsigned char *pub; /* client ephemeral public kept by server in case HRR */ + size_t pub_len; + OSSL_HPKE_CTX *hpke_ctx; /* HPKE context, needed for HRR */ + /* + * Fields that differ on client between inner and outer that we need to + * keep and swap over IFF ECH has succeeded. Same names chosen as are + * used in SSL_CONNECTION + */ + EVP_PKEY *tmp_pkey; /* client's key share for inner */ + int group_id; /* key share group */ + unsigned char client_random[SSL3_RANDOM_SIZE]; /* CH random */ +} OSSL_ECH_CONN; + +/* Internal ECH APIs */ + +OSSL_ECHSTORE *ossl_echstore_dup(const OSSL_ECHSTORE *old); +void ossl_echstore_entry_free(OSSL_ECHSTORE_ENTRY *ee); +void ossl_ech_ctx_clear(OSSL_ECH_CTX *ce); +int ossl_ech_conn_init(SSL_CONNECTION *s, SSL_CTX *ctx, + const SSL_METHOD *method); +void ossl_ech_conn_clear(OSSL_ECH_CONN *ec); +void ossl_echext_free(OSSL_ECHEXT *e); +OSSL_ECHEXT *ossl_echext_dup(const OSSL_ECHEXT *src); + # endif #endif diff --git a/ssl/ech/ech_ssl_apis.c b/ssl/ech/ech_ssl_apis.c index 9bdc4bbdbe43e..7e38ac0e036bc 100644 --- a/ssl/ech/ech_ssl_apis.c +++ b/ssl/ech/ech_ssl_apis.c @@ -9,74 +9,355 @@ #include #include +#include "internal/ssl_unwrap.h" #include "../ssl_local.h" int SSL_CTX_set1_echstore(SSL_CTX *ctx, OSSL_ECHSTORE *es) { - return 0; + if (ctx == NULL) { + ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_NULL_PARAMETER); + return 0; + } + OSSL_ECHSTORE_free(ctx->ext.ech.es); + ctx->ext.ech.es = NULL; + if (es == NULL) + return 1; + if ((ctx->ext.ech.es = ossl_echstore_dup(es)) == NULL) { + ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR); + return 0; + } + return 1; } -int SSL_set1_echstore(SSL *s, OSSL_ECHSTORE *es) +int SSL_set1_echstore(SSL *ssl, OSSL_ECHSTORE *es) { - return 0; + SSL_CONNECTION *s; + + s = SSL_CONNECTION_FROM_SSL(ssl); + if (s == NULL) + return 0; + OSSL_ECHSTORE_free(s->ext.ech.es); + s->ext.ech.es = NULL; + if (es == NULL) + return 1; + if ((s->ext.ech.es = ossl_echstore_dup(es)) == NULL) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + return 0; + } + /* + * Here, and below, if the application calls an API that implies it + * wants to try ECH, then we set attempted to 1 + */ + s->ext.ech.attempted = 1; + return 1; } OSSL_ECHSTORE *SSL_CTX_get1_echstore(const SSL_CTX *ctx) { - return NULL; + OSSL_ECHSTORE *dup = NULL; + + if (ctx == NULL) { + ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_NULL_PARAMETER); + return NULL; + } + if (ctx->ext.ech.es == NULL) + return NULL; + if ((dup = ossl_echstore_dup(ctx->ext.ech.es)) == NULL) { + ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR); + return NULL; + } + return dup; } -OSSL_ECHSTORE *SSL_get1_echstore(const SSL *s) +OSSL_ECHSTORE *SSL_get1_echstore(const SSL *ssl) { - return NULL; + SSL_CONNECTION *s; + OSSL_ECHSTORE *dup = NULL; + + s = SSL_CONNECTION_FROM_SSL(ssl); + if (s == NULL) { + ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_NULL_PARAMETER); + return NULL; + } + if (s->ext.ech.es == NULL) + return NULL; + if ((dup = ossl_echstore_dup(s->ext.ech.es)) == NULL) { + ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR); + return NULL; + } + return dup; } -int SSL_ech_set_server_names(SSL *s, const char *inner_name, - const char *outer_name, int no_outer) +int SSL_ech_set1_server_names(SSL *ssl, const char *inner_name, + const char *outer_name, int no_outer) { - return 0; + SSL_CONNECTION *s; + + s = SSL_CONNECTION_FROM_SSL(ssl); + if (s == NULL) + return 0; + OPENSSL_free(s->ext.hostname); + s->ext.hostname = NULL; + if (inner_name != NULL) { + s->ext.hostname = OPENSSL_strdup(inner_name); + if (s->ext.hostname == NULL) + return 0; + } + OPENSSL_free(s->ext.ech.outer_hostname); + s->ext.ech.outer_hostname = NULL; + if (no_outer == 0 && outer_name != NULL && strlen(outer_name) > 0) { + s->ext.ech.outer_hostname = OPENSSL_strdup(outer_name); + if (s->ext.ech.outer_hostname == NULL) + return 0; + } + s->ext.ech.no_outer = no_outer; + s->ext.ech.attempted = 1; + return 1; } -int SSL_ech_set_outer_server_name(SSL *s, const char *outer_name, int no_outer) +int SSL_ech_set1_outer_server_name(SSL *ssl, const char *outer_name, + int no_outer) { - return 0; + SSL_CONNECTION *s; + + s = SSL_CONNECTION_FROM_SSL(ssl); + if (s == NULL) + return 0; + OPENSSL_free(s->ext.ech.outer_hostname); + s->ext.ech.outer_hostname = NULL; + if (no_outer == 0 && outer_name != NULL && strlen(outer_name) > 0) { + s->ext.ech.outer_hostname = OPENSSL_strdup(outer_name); + if (s->ext.ech.outer_hostname == NULL) + return 0; + } + s->ext.ech.no_outer = no_outer; + s->ext.ech.attempted = 1; + return 1; } -int SSL_ech_set_outer_alpn_protos(SSL *s, const unsigned char *protos, - const size_t protos_len) +/* + * Note that this function returns 1 for success and 0 for error. This + * contrasts with SSL_set1_alpn_protos() which (unusually for OpenSSL) + * returns 0 for success and 1 on error. + */ +int SSL_ech_set1_outer_alpn_protos(SSL *ssl, const unsigned char *protos, + const size_t protos_len) { - return 0; + SSL_CONNECTION *s; + + s = SSL_CONNECTION_FROM_SSL(ssl); + if (s == NULL) + return 0; + OPENSSL_free(s->ext.ech.alpn_outer); + s->ext.ech.alpn_outer = NULL; + if (protos == NULL) + return 1; + if (protos_len == 0) { + ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_NULL_PARAMETER); + return 0; + } + s->ext.ech.alpn_outer = OPENSSL_memdup(protos, protos_len); + if (s->ext.ech.alpn_outer == NULL) + return 0; + s->ext.ech.alpn_outer_len = protos_len; + s->ext.ech.attempted = 1; + return 1; } -int SSL_ech_get1_status(SSL *s, char **inner_sni, char **outer_sni) +int SSL_ech_get1_status(SSL *ssl, char **inner_sni, char **outer_sni) { - return 0; + char *sinner = NULL; + char *souter = NULL; + SSL_CONNECTION *s = SSL_CONNECTION_FROM_SSL(ssl); + + if (s == NULL) { + ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_NULL_PARAMETER); + return SSL_ECH_STATUS_FAILED; + } + if (outer_sni == NULL || inner_sni == NULL) { + ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR); + return SSL_ECH_STATUS_FAILED; + } + *outer_sni = NULL; + *inner_sni = NULL; + if (s->ext.ech.grease == OSSL_ECH_IS_GREASE) { + if (s->ext.ech.returned != NULL) + return SSL_ECH_STATUS_GREASE_ECH; + return SSL_ECH_STATUS_GREASE; + } + if (s->options & SSL_OP_ECH_GREASE) + return SSL_ECH_STATUS_GREASE; + if (s->ext.ech.backend == 1) { + if (s->ext.hostname != NULL + && (*inner_sni = OPENSSL_strdup(s->ext.hostname)) == NULL) { + ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR); + return SSL_ECH_STATUS_FAILED; + } + return SSL_ECH_STATUS_BACKEND; + } + if (s->ext.ech.es == NULL) + return SSL_ECH_STATUS_NOT_CONFIGURED; + /* Set output vars - note we may be pointing to NULL which is fine */ + if (s->server == 0) { + sinner = s->ext.hostname; + if (s->ext.ech.attempted == 1 && s->ext.ech.success == 0) + sinner = s->ext.ech.former_inner; + if (s->ext.ech.no_outer == 0) + souter = s->ext.ech.outer_hostname; + else + souter = NULL; + } else { + if (s->ext.ech.es != NULL && s->ext.ech.success == 1) { + sinner = s->ext.hostname; + souter = s->ext.ech.outer_hostname; + } + } + if (s->ext.ech.es != NULL && s->ext.ech.attempted == 1 + && s->ext.ech.attempted_type == TLSEXT_TYPE_ech + && s->ext.ech.grease != OSSL_ECH_IS_GREASE) { + long vr = X509_V_OK; + + vr = SSL_get_verify_result(ssl); + if (sinner != NULL + && (*inner_sni = OPENSSL_strdup(sinner)) == NULL) { + ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR); + return SSL_ECH_STATUS_FAILED; + } + if (souter != NULL + && (*outer_sni = OPENSSL_strdup(souter)) == NULL) { + ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR); + return SSL_ECH_STATUS_FAILED; + } + if (s->ext.ech.success == 1) { + if (vr == X509_V_OK) + return SSL_ECH_STATUS_SUCCESS; + else + return SSL_ECH_STATUS_BAD_NAME; + } else { + if (vr == X509_V_OK && s->ext.ech.returned != NULL) + return SSL_ECH_STATUS_FAILED_ECH; + else if (vr != X509_V_OK && s->ext.ech.returned != NULL) + return SSL_ECH_STATUS_FAILED_ECH_BAD_NAME; + ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR); + return SSL_ECH_STATUS_FAILED; + } + } + return SSL_ECH_STATUS_NOT_TRIED; } -int SSL_ech_set_grease_suite(SSL *s, const char *suite) +int SSL_ech_set1_grease_suite(SSL *ssl, const char *suite) { - return 0; + SSL_CONNECTION *s; + + s = SSL_CONNECTION_FROM_SSL(ssl); + if (s == NULL) + return 0; + OPENSSL_free(s->ext.ech.grease_suite); + s->ext.ech.grease_suite = NULL; + if (suite == NULL) + return 1; + s->ext.ech.grease_suite = OPENSSL_strdup(suite); + if (s->ext.ech.grease_suite == NULL) + return 0; + s->ext.ech.attempted = 1; + return 1; } -int SSL_ech_set_grease_type(SSL *s, uint16_t type) +int SSL_ech_set_grease_type(SSL *ssl, uint16_t type) { - return 0; + SSL_CONNECTION *s; + + s = SSL_CONNECTION_FROM_SSL(ssl); + if (s == NULL) + return 0; + s->ext.ech.attempted_type = type; + s->ext.ech.attempted = 1; + return 1; } -void SSL_ech_set_callback(SSL *s, SSL_ech_cb_func f) +void SSL_ech_set_callback(SSL *ssl, SSL_ech_cb_func f) { + SSL_CONNECTION *s; + + s = SSL_CONNECTION_FROM_SSL(ssl); + if (s == NULL) + return; + s->ext.ech.cb = f; return; } -int SSL_ech_get_retry_config(SSL *s, unsigned char **ec, size_t *eclen) +int SSL_ech_get1_retry_config(SSL *ssl, unsigned char **ec, size_t *eclen) { - return 0; + SSL_CONNECTION *s; + OSSL_ECHSTORE *ve = NULL; + BIO *in = NULL; + int rv = 0; + + s = SSL_CONNECTION_FROM_SSL(ssl); + if (s == NULL || ec == NULL || eclen == NULL) + goto err; + if (s->ext.ech.returned == NULL) { + *ec = NULL; + *eclen = 0; + return 1; + } + /* + * To not hand rubbish to application, we'll decode the value we have + * so only syntactically good things are passed up. We won't insist + * though that every entry in the retry_config list seems good - it + * could be that e.g. one is a newer version than we support now, + * and letting the application see that might cause someone to do an + * upgrade. + */ + if ((in = BIO_new(BIO_s_mem())) == NULL + || BIO_write(in, s->ext.ech.returned, s->ext.ech.returned_len) <= 0 + || (ve = OSSL_ECHSTORE_new(s->ext.ech.es->libctx, + s->ext.ech.es->propq)) == NULL) { + ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR); + goto err; + } + if (OSSL_ECHSTORE_read_echconfiglist(ve, in) != 1) { + ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR); + goto err; + } + /* all good, copy and return */ + *ec = OPENSSL_memdup(s->ext.ech.returned, s->ext.ech.returned_len); + if (*ec == NULL) + goto err; + *eclen = s->ext.ech.returned_len; + rv = 1; +err: + OSSL_ECHSTORE_free(ve); + BIO_free_all(in); + return rv; } -int SSL_CTX_ech_set_outer_alpn_protos(SSL_CTX *s, const unsigned char *protos, - const size_t protos_len) +/* + * Note that this function returns 1 for success and 0 for error. This + * contrasts with SSL_CTX_set1_alpn_protos() which (unusually for OpenSSL) + * returns 0 for success and 1 on error. + */ +int SSL_CTX_ech_set1_outer_alpn_protos(SSL_CTX *ctx, + const unsigned char *protos, + const size_t protos_len) { - return 0; + if (ctx == NULL) { + ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_NULL_PARAMETER); + return 0; + } + OPENSSL_free(ctx->ext.ech.alpn_outer); + ctx->ext.ech.alpn_outer = NULL; + if (protos == NULL) + return 1; + if (protos_len == 0) { + ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_NULL_PARAMETER); + return 0; + } + ctx->ext.ech.alpn_outer = OPENSSL_memdup(protos, protos_len); + if (ctx->ext.ech.alpn_outer == NULL) + return 0; + ctx->ext.ech.alpn_outer_len = protos_len; + return 1; } int SSL_CTX_ech_raw_decrypt(SSL_CTX *ctx, @@ -102,5 +383,45 @@ int SSL_CTX_ech_raw_decrypt(SSL_CTX *ctx, void SSL_CTX_ech_set_callback(SSL_CTX *ctx, SSL_ech_cb_func f) { + if (ctx == NULL) { + ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_NULL_PARAMETER); + return; + } + ctx->ext.ech.cb = f; return; } + +int SSL_set1_ech_config_list(SSL *ssl, const uint8_t *ecl, size_t ecl_len) +{ + int rv = 0; + SSL_CONNECTION *s; + OSSL_ECHSTORE *es = NULL; + BIO *es_in = NULL; + + s = SSL_CONNECTION_FROM_SSL(ssl); + if (s == NULL) { + ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_NULL_PARAMETER); + goto err; + } + if (ecl == NULL) { + OSSL_ECHSTORE_free(s->ext.ech.es); + s->ext.ech.es = NULL; + return 1; + } + if (ecl_len == 0) { + ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR); + goto err; + } + if ((es_in = BIO_new_mem_buf(ecl, ecl_len)) == NULL + || (es = OSSL_ECHSTORE_new(NULL, NULL)) == NULL + || OSSL_ECHSTORE_read_echconfiglist(es, es_in) != 1 + || SSL_set1_echstore(ssl, es) != 1) { + ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR); + goto err; + } + rv = 1; +err: + OSSL_ECHSTORE_free(es); + BIO_free_all(es_in); + return rv; +} diff --git a/ssl/ech/ech_store.c b/ssl/ech/ech_store.c index 58ba862dc97dd..653b99f3494da 100644 --- a/ssl/ech/ech_store.c +++ b/ssl/ech/ech_store.c @@ -58,7 +58,7 @@ static const char B64_alphabet[] = * local functions - public APIs are at the end */ -static void ossl_echext_free(OSSL_ECHEXT *e) +void ossl_echext_free(OSSL_ECHEXT *e) { if (e == NULL) return; @@ -67,13 +67,30 @@ static void ossl_echext_free(OSSL_ECHEXT *e) return; } -static void ossl_echstore_entry_free(OSSL_ECHSTORE_ENTRY *ee) +OSSL_ECHEXT *ossl_echext_dup(const OSSL_ECHEXT *src) +{ + OSSL_ECHEXT *ext = OPENSSL_zalloc(sizeof(*src)); + + if (ext == NULL) + return NULL; + *ext = *src; + ext->val = NULL; + if (ext->len != 0) { + ext->val = OPENSSL_memdup(src->val, src->len); + if (ext->val == NULL) { + ossl_echext_free(ext); + return NULL; + } + } + return ext; +} + +void ossl_echstore_entry_free(OSSL_ECHSTORE_ENTRY *ee) { if (ee == NULL) return; OPENSSL_free(ee->public_name); OPENSSL_free(ee->pub); - OPENSSL_free(ee->pemfname); EVP_PKEY_free(ee->keyshare); OPENSSL_free(ee->encoded); OPENSSL_free(ee->suites); @@ -82,32 +99,6 @@ static void ossl_echstore_entry_free(OSSL_ECHSTORE_ENTRY *ee) return; } -/* - * @brief hash a buffer as a pretend file name being ascii-hex of hashed buffer - * @param es is the OSSL_ECHSTORE we're dealing with - * @param buf is the input buffer - * @param blen is the length of buf - * @param ah_hash is a pointer to where to put the result - * @param ah_len is the length of ah_hash - */ -static int ech_hash_pub_as_fname(OSSL_ECHSTORE *es, - const unsigned char *buf, size_t blen, - char *ah_hash, size_t ah_len) -{ - unsigned char hashval[EVP_MAX_MD_SIZE]; - size_t hashlen, actual_ah_len; - - if (es == NULL - || EVP_Q_digest(es->libctx, "SHA2-256", es->propq, - buf, blen, hashval, &hashlen) != 1 - || OPENSSL_buf2hexstr_ex(ah_hash, ah_len, &actual_ah_len, - hashval, hashlen, '\0') != 1) { - ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR); - return 0; - } - return 1; -} - /* * @brief Read a buffer from an input 'till eof * @param in is the BIO input @@ -618,7 +609,14 @@ OSSL_ECHSTORE *OSSL_ECHSTORE_new(OSSL_LIB_CTX *libctx, const char *propq) return 0; } es->libctx = libctx; - es->propq = propq; + if (propq != NULL) { + es->propq = OPENSSL_strdup(propq); + if (es->propq == NULL) { + ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR); + return 0; + } + } + return es; } @@ -627,6 +625,7 @@ void OSSL_ECHSTORE_free(OSSL_ECHSTORE *es) if (es == NULL) return; sk_OSSL_ECHSTORE_ENTRY_pop_free(es->entries, ossl_echstore_entry_free); + OPENSSL_free(es->propq); OPENSSL_free(es); return; } @@ -645,8 +644,6 @@ int OSSL_ECHSTORE_new_config(OSSL_ECHSTORE *es, WPACKET epkt; BUF_MEM *epkt_mem = NULL; OSSL_ECHSTORE_ENTRY *ee = NULL; - char pembuf[2 * EVP_MAX_MD_SIZE + 1]; - size_t pembuflen = 2 * EVP_MAX_MD_SIZE + 1; /* basic checks */ if (es == NULL) { @@ -754,10 +751,6 @@ int OSSL_ECHSTORE_new_config(OSSL_ECHSTORE *es, ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR); goto err; } - if (ech_hash_pub_as_fname(es, pub, publen, pembuf, pembuflen) != 1) { - ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR); - goto err; - } ee->version = echversion; ee->pub_len = publen; ee->pub = OPENSSL_memdup(pub, publen); @@ -780,11 +773,6 @@ int OSSL_ECHSTORE_new_config(OSSL_ECHSTORE *es, ee->encoded_len = bblen; epkt_mem->data = NULL; epkt_mem->length = 0; - ee->pemfname = OPENSSL_strdup(pembuf); - if (ee->pemfname == NULL) { - ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR); - goto err; - } ee->loadtime = time(0); /* push entry into store */ if (es->entries == NULL) @@ -899,52 +887,54 @@ int OSSL_ECHSTORE_read_echconfiglist(OSSL_ECHSTORE *es, BIO *in) return ech_read_priv_echconfiglist(es, in, NULL, 0); } -int OSSL_ECHSTORE_get1_info(OSSL_ECHSTORE *es, OSSL_ECH_INFO **info, - int *count) +int OSSL_ECHSTORE_get1_info(OSSL_ECHSTORE *es, int index, time_t *loaded_secs, + char **public_name, char **echconfig, + int *has_private, int *for_retry) { - OSSL_ECH_INFO *linfo = NULL, *inst = NULL; OSSL_ECHSTORE_ENTRY *ee = NULL; - unsigned int i = 0, j = 0, num = 0; + unsigned int j = 0; + int num = 0; BIO *out = NULL; time_t now = time(0); size_t ehlen; unsigned char *ignore = NULL; - if (es == NULL || info == NULL || count == NULL) { + if (es == NULL || loaded_secs == NULL || public_name == NULL + || echconfig == NULL || has_private == NULL || for_retry == NULL) { ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_NULL_PARAMETER); return 0; } num = (es->entries == NULL ? 0 : sk_OSSL_ECHSTORE_ENTRY_num(es->entries)); - if (num == 0) { - *info = NULL; - *count = 0; - return 1; + if (num == 0 || index < 0 || index >= num) { + ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_INVALID_ARGUMENT); + return 0; } - linfo = OPENSSL_zalloc(num * sizeof(*linfo)); - if (linfo == NULL) - goto err; - for (i = 0; i != num; i++) { - inst = &linfo[i]; - ee = sk_OSSL_ECHSTORE_ENTRY_value(es->entries, i); - - inst->index = i; - inst->seconds_in_memory = now - ee->loadtime; - inst->public_name = OPENSSL_strdup(ee->public_name); - inst->has_private_key = (ee->keyshare == NULL ? 0 : 1); - /* Now "print" the ECHConfigList */ - out = BIO_new(BIO_s_mem()); - if (out == NULL) { - ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_NULL_PARAMETER); + ee = sk_OSSL_ECHSTORE_ENTRY_value(es->entries, index); + if (ee == NULL) { + ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_INVALID_ARGUMENT); + return 0; + } + *loaded_secs = now - ee->loadtime; + if (ee->public_name != NULL) { + *public_name = OPENSSL_strdup(ee->public_name); + if (*public_name == NULL) goto err; - } - if (ee->version != OSSL_ECH_RFCXXXX_VERSION) { - /* just note we don't support that one today */ - BIO_printf(out, "[Unsupported version (%04x)]", ee->version); - continue; - } + } else { + *public_name = NULL; + } + *has_private = (ee->keyshare == NULL ? 0 : 1); + /* Now "print" the ECHConfigList */ + out = BIO_new(BIO_s_mem()); + if (out == NULL) { + ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR); + goto err; + } + if (ee->version != OSSL_ECH_RFCXXXX_VERSION) { + /* just note we don't support that one today */ + BIO_printf(out, "[Unsupported version (%04x)]", ee->version); + } else { /* version, config_id, public_name, and kem */ - BIO_printf(out, "[%04x,%02x,%s,[", ee->version, - ee->config_id, + BIO_printf(out, "[%04x,%02x,%s,[", ee->version, ee->config_id, ee->public_name != NULL ? (char *)ee->public_name : "NULL"); /* ciphersuites */ for (j = 0; j != ee->nsuites; j++) { @@ -960,24 +950,20 @@ int OSSL_ECHSTORE_get1_info(OSSL_ECHSTORE *es, OSSL_ECH_INFO **info, /* max name length and (only) number of extensions */ BIO_printf(out, ",%02x,%02x]", ee->max_name_length, ee->exts == NULL ? 0 : sk_OSSL_ECHEXT_num(ee->exts)); - ehlen = BIO_get_mem_data(out, &ignore); - inst->echconfig = OPENSSL_malloc(ehlen + 1); - if (inst->echconfig == NULL) - goto err; - if (BIO_read(out, inst->echconfig, ehlen) <= 0) { - ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_NULL_PARAMETER); - goto err; - } - inst->echconfig[ehlen] = '\0'; - BIO_free(out); - out = NULL; } - *count = num; - *info = linfo; + ehlen = BIO_get_mem_data(out, &ignore); + *echconfig = OPENSSL_malloc(ehlen + 1); + if (*echconfig == NULL) + goto err; + if (BIO_read(out, *echconfig, ehlen) <= 0) { + ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR); + goto err; + } + (*echconfig)[ehlen] = '\0'; + BIO_free(out); return 1; err: BIO_free(out); - OSSL_ECH_INFO_free(linfo, num); return 0; } @@ -1085,6 +1071,16 @@ int OSSL_ECHSTORE_read_pem(OSSL_ECHSTORE *es, BIO *in, int for_retry) return rv; } +int OSSL_ECHSTORE_num_entries(const OSSL_ECHSTORE *es, int *numentries) +{ + if (es == NULL || numentries == NULL) { + ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_NULL_PARAMETER); + return 0; + } + *numentries = (es->entries == NULL ? 0 : sk_OSSL_ECHSTORE_ENTRY_num(es->entries)); + return 1; +} + int OSSL_ECHSTORE_num_keys(OSSL_ECHSTORE *es, int *numkeys) { int i, num = 0, count = 0; @@ -1135,34 +1131,3 @@ int OSSL_ECHSTORE_flush_keys(OSSL_ECHSTORE *es, time_t age) } return 1; } - -void OSSL_ECH_INFO_free(OSSL_ECH_INFO *info, int count) -{ - int i; - - if (info == NULL) - return; - for (i = 0; i != count; i++) { - OPENSSL_free(info[i].public_name); - OPENSSL_free(info[i].inner_name); - OPENSSL_free(info[i].outer_alpns); - OPENSSL_free(info[i].inner_alpns); - OPENSSL_free(info[i].echconfig); - } - OPENSSL_free(info); - return; -} - -int OSSL_ECH_INFO_print(BIO *out, OSSL_ECH_INFO *info, int index) -{ - if (out == NULL || info == NULL) { - ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_NULL_PARAMETER); - return 0; - } - BIO_printf(out, "ECH entry: %d public_name: %s age: %d%s\n", - index, info[index].public_name, - (int)info[index].seconds_in_memory, - info[index].has_private_key ? " (has private key)" : ""); - BIO_printf(out, "\t%s\n", info[index].echconfig); - return 1; -} diff --git a/ssl/ssl_lib.c b/ssl/ssl_lib.c index 951723748ffd3..63d3726931b4a 100644 --- a/ssl/ssl_lib.c +++ b/ssl/ssl_lib.c @@ -968,6 +968,11 @@ SSL *ossl_ssl_connection_new_int(SSL_CTX *ctx, SSL *user_ssl, goto sslerr; #endif +#ifndef OPENSSL_NO_ECH + if (!ossl_ech_conn_init(s, ctx, method)) + goto sslerr; +#endif + s->ssl_pkey_num = SSL_PKEY_NUM + ctx->sigalg_list_len; return ssl; cerr: @@ -1552,6 +1557,9 @@ void ossl_ssl_connection_free(SSL *ssl) BIO_free_all(s->rbio); s->rbio = NULL; OPENSSL_free(s->s3.tmp.valid_flags); +#ifndef OPENSSL_NO_ECH + ossl_ech_conn_clear(&s->ext.ech); +#endif } void SSL_set0_rbio(SSL *s, BIO *rbio) @@ -4480,6 +4488,10 @@ void SSL_CTX_free(SSL_CTX *a) ossl_quic_free_token_store(a->tokencache); #endif +#ifndef OPENSSL_NO_ECH + ossl_ech_ctx_clear(&a->ext.ech); +#endif + OPENSSL_free(a); } diff --git a/ssl/ssl_local.h b/ssl/ssl_local.h index 5cf7368a82ccb..66a5cad13ac9d 100644 --- a/ssl/ssl_local.h +++ b/ssl/ssl_local.h @@ -41,6 +41,9 @@ # include "record/record.h" # include "internal/quic_predef.h" # include "internal/quic_tls.h" +# ifndef OPENSSL_NO_ECH +# include "ech/ech_local.h" +# endif # ifdef OPENSSL_BUILD_SHLIBSSL # undef OPENSSL_EXTERN @@ -1078,6 +1081,9 @@ struct ssl_ctx_st { # endif unsigned char cookie_hmac_key[SHA256_DIGEST_LENGTH]; +# ifndef OPENSSL_NO_ECH + OSSL_ECH_CTX ech; +# endif } ext; # ifndef OPENSSL_NO_PSK @@ -1736,6 +1742,10 @@ struct ssl_connection_st { uint8_t client_cert_type_ctos; uint8_t server_cert_type; uint8_t server_cert_type_ctos; + +# ifndef OPENSSL_NO_ECH + OSSL_ECH_CONN ech; +# endif } ext; /* diff --git a/test/certs/echserver.key b/test/certs/echserver.key new file mode 100644 index 0000000000000..372878ce57385 --- /dev/null +++ b/test/certs/echserver.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQCJHNs4e27KjdYU +8IgiT539WbEl16Eve6tu1UFpGdkqsHH8+yceoFkMWSdpr+Sh3PYDRk/Ek0qB33uK +y3FKlIejtolxVhBybtja5zYMmVXnRHsB/qe7FgyM/lv0xtO1nfSGFkVZVz1+xWPr +aslJN3U4HPaaL4SGghw5KIRD8FPx174v8FDOBeVhn6pzTK/xpTeqXLvAAxgPhF+Q +HOQ0pTXrOHbaiK4l+8JfVm+0fCJjMnT20mmGuTNjvdWZ4XIgPPYkEQrf1CpdONcU +kiiFcpcYtbVS0YyC91qqJLKFv51eki9STwUQISn5jLWIRQlXkBXhK6aGlkLWnov/ +kqqUWEQTAgMBAAECggEAAah0LDAt7EwfyRwJgWS2E+C4SC1d2R2lOo9gnZ0+54m/ +rx/4XqHwwbn4RIpoeN6bqPl6MHXZgk2KCGkiYxT9uOiVq+WvCDs36xm9qRRXmhbV +Z/ZE3/nJyBCxWvnmiH0y/kYZq5Vm/Hf1l9ywN27wv292OfIWJ6w+HCDVzJ6E3VlK +fuzBFhZmnBjul6Nlo76blNXwn5loWYomkg6nVWzrTjYWosGd0aKpZJR948nWpuEj +fkevLqMMfSuB+cXQ+zB4lttqB5dphFxbNv5gHOd1rllzHFdhK+/7e7ktGYmpQtuH +WRKPD1y173ek5FtvTxtcrL2rST+hoSDWcCQCws/70QKBgQC5QGlldraTs0JXhoH9 +6X+V1mvsAWCItq7JhUvFHFtAxHuYacrlnsJRxv8aRS8AhuNYTtThJQcWzFL2UU7W +CdiB0VZr7phPNOVYsa95V8a8A1CHllfdTzxw1TyiOJ0sdeU7irWo3vnTDxftfySP +lkdPNbItO1RXqeIR6mJf+rVGowKBgQC9egaDDdM2YsMtl61fIRoPMPdJB3fGcL0A +FwUAtGQ1twETykzcUCeAqx0yx7zCAyPeA9WmpHzuz/LR8uA3TP/nDcLlCaQowfeR +VPdS3Q1iAnSyaCsF1THQPvhFsYMXbIn/svSSpddLOrP6ltSr+PORLOXXUmQJnffk +hxKaxK6T0QKBgFzyVm9UGtMMk/K6SCqPpzYUuV1Wa4rsrdHqkVO6oIZkjuav3d9L +wo+pWoFhyO1owFSkaOb13xKvPcjcjsOReRHZaJUKx1ymW5Qewr4NLmdS+mqtIjSl +9tteAegao7GVDYjMVcz+4zXkUssUidGJQwoZFObg57Z8RDNc+DLT5XQlAoGAYaO8 +L1S0ftYuFhSPdvIr56AoDi4W/t+hxaYXIeHTsgp4N6aMLQvxD1EeXsim8KOFnCcF +tjYVW0s1qhMqj9TSGlLxF+379jTeSroqKT1YZCU31afwY7UVUmbgsalkEHISOv4R +InDrnQzHKl8HgQdtHGayml8OxhXtZIpmf/LSs8ECgYBrFbKl8ylhlzw5rC8DuP8n +hzKLOKzipKmHLn4eDBEFyLTyoyYrqx/nxLi3kSIyNP4fJ9vHOXgdjdrp9xRMcFEx +IA2sdywI5VuymxktP8OlORa0NK4eFZXkDNsQlkathYiKqCwGjUWdGk5+Ry5qO/UC +9ua9adjNa108aBzWLYZFCw== +-----END PRIVATE KEY----- diff --git a/test/certs/echserver.pem b/test/certs/echserver.pem new file mode 100644 index 0000000000000..11e0617e887fe --- /dev/null +++ b/test/certs/echserver.pem @@ -0,0 +1,80 @@ +Certificate: + Data: + Version: 3 (0x2) + Serial Number: + 18:45:8f:30:1d:fe:dc:22:9d:95:40:8c:e5:36:f9:38:0d:d5:58:a0 + Signature Algorithm: sha256WithRSAEncryption + Issuer: CN=Root CA + Validity + Not Before: Oct 6 18:36:12 2023 GMT + Not After : Sep 12 18:36:12 2123 GMT + Subject: CN=server.example + Subject Public Key Info: + Public Key Algorithm: rsaEncryption + Public-Key: (2048 bit) + Modulus: + 00:89:1c:db:38:7b:6e:ca:8d:d6:14:f0:88:22:4f: + 9d:fd:59:b1:25:d7:a1:2f:7b:ab:6e:d5:41:69:19: + d9:2a:b0:71:fc:fb:27:1e:a0:59:0c:59:27:69:af: + e4:a1:dc:f6:03:46:4f:c4:93:4a:81:df:7b:8a:cb: + 71:4a:94:87:a3:b6:89:71:56:10:72:6e:d8:da:e7: + 36:0c:99:55:e7:44:7b:01:fe:a7:bb:16:0c:8c:fe: + 5b:f4:c6:d3:b5:9d:f4:86:16:45:59:57:3d:7e:c5: + 63:eb:6a:c9:49:37:75:38:1c:f6:9a:2f:84:86:82: + 1c:39:28:84:43:f0:53:f1:d7:be:2f:f0:50:ce:05: + e5:61:9f:aa:73:4c:af:f1:a5:37:aa:5c:bb:c0:03: + 18:0f:84:5f:90:1c:e4:34:a5:35:eb:38:76:da:88: + ae:25:fb:c2:5f:56:6f:b4:7c:22:63:32:74:f6:d2: + 69:86:b9:33:63:bd:d5:99:e1:72:20:3c:f6:24:11: + 0a:df:d4:2a:5d:38:d7:14:92:28:85:72:97:18:b5: + b5:52:d1:8c:82:f7:5a:aa:24:b2:85:bf:9d:5e:92: + 2f:52:4f:05:10:21:29:f9:8c:b5:88:45:09:57:90: + 15:e1:2b:a6:86:96:42:d6:9e:8b:ff:92:aa:94:58: + 44:13 + Exponent: 65537 (0x10001) + X509v3 extensions: + X509v3 Basic Constraints: + CA:FALSE + X509v3 Subject Key Identifier: + 8C:E0:38:04:70:7E:B4:CB:1F:BF:AA:E6:67:42:74:63:46:88:58:74 + X509v3 Authority Key Identifier: + 70:7F:2E:AE:83:68:59:98:04:23:2A:CD:EB:3E:17:CD:24:DD:01:49 + X509v3 Subject Alternative Name: + DNS:*.server.example, DNS:server.example + Signature Algorithm: sha256WithRSAEncryption + Signature Value: + 9b:fe:bc:b1:40:d4:08:91:f6:1f:b4:0f:8c:50:ac:49:36:6f: + 27:93:e8:94:13:bc:fe:1a:2a:cf:93:98:13:b3:b4:85:a5:62: + 4d:58:8f:da:cd:f7:1b:c3:1f:42:ba:2a:89:45:11:33:49:86: + 2c:3a:0a:99:17:4f:0c:f1:1e:35:31:2c:69:f9:15:d5:37:54: + cc:9e:e3:67:9f:d5:6e:ad:b1:26:60:df:aa:84:63:da:a7:31: + c9:69:a0:d8:c2:96:d3:82:b4:99:70:8c:3c:92:a4:c0:f0:7c: + 3f:04:d3:29:4f:6c:c5:fd:39:12:95:65:7f:37:fb:52:5b:12: + 99:d6:d7:b5:ba:44:6e:36:ec:5d:f2:5d:d4:aa:2d:8a:46:ce: + 29:66:c1:ed:36:13:f2:f3:ae:92:4a:97:db:99:ed:8f:4e:4e: + ed:73:1b:fa:3e:64:63:40:5c:c2:03:76:2c:dc:58:01:3f:17: + d0:ae:a6:b2:64:85:47:ba:7d:5a:36:53:e4:90:00:8e:f5:17: + a5:ff:a3:81:ee:ed:25:ca:10:76:75:2d:65:ff:f8:b1:8c:3c: + a3:ff:81:12:72:c7:bc:b5:17:06:d8:c6:13:97:cb:8e:58:51: + 2a:a4:be:91:59:40:4b:07:8d:69:2f:92:ee:ea:9c:bf:eb:42: + b7:62:b8:e3 +-----BEGIN CERTIFICATE----- +MIIDNTCCAh2gAwIBAgIUGEWPMB3+3CKdlUCM5Tb5OA3VWKAwDQYJKoZIhvcNAQEL +BQAwEjEQMA4GA1UEAwwHUm9vdCBDQTAgFw0yMzEwMDYxODM2MTJaGA8yMTIzMDkx +MjE4MzYxMlowGTEXMBUGA1UEAwwOc2VydmVyLmV4YW1wbGUwggEiMA0GCSqGSIb3 +DQEBAQUAA4IBDwAwggEKAoIBAQCJHNs4e27KjdYU8IgiT539WbEl16Eve6tu1UFp +GdkqsHH8+yceoFkMWSdpr+Sh3PYDRk/Ek0qB33uKy3FKlIejtolxVhBybtja5zYM +mVXnRHsB/qe7FgyM/lv0xtO1nfSGFkVZVz1+xWPraslJN3U4HPaaL4SGghw5KIRD +8FPx174v8FDOBeVhn6pzTK/xpTeqXLvAAxgPhF+QHOQ0pTXrOHbaiK4l+8JfVm+0 +fCJjMnT20mmGuTNjvdWZ4XIgPPYkEQrf1CpdONcUkiiFcpcYtbVS0YyC91qqJLKF +v51eki9STwUQISn5jLWIRQlXkBXhK6aGlkLWnov/kqqUWEQTAgMBAAGjejB4MAkG +A1UdEwQCMAAwHQYDVR0OBBYEFIzgOARwfrTLH7+q5mdCdGNGiFh0MB8GA1UdIwQY +MBaAFHB/Lq6DaFmYBCMqzes+F80k3QFJMCsGA1UdEQQkMCKCECouc2VydmVyLmV4 +YW1wbGWCDnNlcnZlci5leGFtcGxlMA0GCSqGSIb3DQEBCwUAA4IBAQCb/ryxQNQI +kfYftA+MUKxJNm8nk+iUE7z+GirPk5gTs7SFpWJNWI/azfcbwx9CuiqJRREzSYYs +OgqZF08M8R41MSxp+RXVN1TMnuNnn9VurbEmYN+qhGPapzHJaaDYwpbTgrSZcIw8 +kqTA8Hw/BNMpT2zF/TkSlWV/N/tSWxKZ1te1ukRuNuxd8l3Uqi2KRs4pZsHtNhPy +866SSpfbme2PTk7tcxv6PmRjQFzCA3Ys3FgBPxfQrqayZIVHun1aNlPkkACO9Rel +/6OB7u0lyhB2dS1l//ixjDyj/4EScse8tRcG2MYTl8uOWFEqpL6RWUBLB41pL5Lu +6py/60K3Yrjj +-----END CERTIFICATE----- diff --git a/test/ech_test.c b/test/ech_test.c index e491740ecdbbf..ef71d158a571a 100644 --- a/test/ech_test.c +++ b/test/ech_test.c @@ -16,8 +16,102 @@ # define DEF_CERTS_DIR "test/certs" +static OSSL_LIB_CTX *libctx = NULL; +static char *propq = NULL; static int verbose = 0; static char *certsdir = NULL; +static char *cert = NULL; +static char *privkey = NULL; +static char *rootcert = NULL; + +/* callback */ +static unsigned int test_cb(SSL *s, const char *str) +{ + return 1; +} + +/* + * The define/vars below and the 3 callback functions are modified + * from test/sslapitest.c + */ +# define TEST_EXT_TYPE1 0xffab /* custom ext type 1: has 1 octet payload */ +# define TEST_EXT_TYPE2 0xffcd /* custom ext type 2: no payload */ + +/* A well-encoded ECH extension value */ +static const unsigned char encoded_ech_val[] = { + 0x00, 0x00, 0x01, 0x00, 0x01, 0xf7, 0x00, 0x20, + 0xc9, 0x2c, 0x12, 0xc9, 0xc0, 0x4d, 0x11, 0x5d, + 0x09, 0xe1, 0xeb, 0x7a, 0x18, 0xb2, 0x83, 0x28, + 0x35, 0x00, 0x3c, 0x8d, 0x78, 0x09, 0xfd, 0x09, + 0x84, 0xca, 0x94, 0x77, 0xcf, 0x78, 0xd0, 0x04, + 0x00, 0x90, 0x5e, 0xc7, 0xc0, 0x62, 0x84, 0x8d, + 0x4b, 0x85, 0xd5, 0x6a, 0x9a, 0xc1, 0xc6, 0xc2, + 0x28, 0xac, 0x87, 0xb9, 0x2f, 0x36, 0xa0, 0xf7, + 0x5f, 0xd0, 0x23, 0x7b, 0xf4, 0xc1, 0x62, 0x1c, + 0xf1, 0x91, 0xfd, 0x46, 0x35, 0x41, 0xc9, 0x06, + 0xd3, 0x19, 0xd6, 0x34, 0x01, 0xc3, 0xb3, 0x66, + 0x4e, 0x7a, 0x28, 0xac, 0xd4, 0xd2, 0x35, 0x2b, + 0xd0, 0xc6, 0x94, 0x34, 0xc1, 0x94, 0x62, 0x77, + 0x1b, 0x5a, 0x02, 0x3c, 0xdd, 0xa2, 0x4d, 0x33, + 0xa5, 0xd0, 0x59, 0x12, 0xf5, 0x17, 0x03, 0xe5, + 0xab, 0xbd, 0x83, 0x52, 0x40, 0x6c, 0x99, 0xac, + 0x25, 0x07, 0x63, 0x8c, 0x16, 0x5d, 0x93, 0x34, + 0x56, 0x34, 0x60, 0x86, 0x25, 0xa7, 0x0d, 0xac, + 0xb8, 0x5e, 0x87, 0xc6, 0xf7, 0x23, 0xaf, 0xf8, + 0x3e, 0x2a, 0x46, 0x75, 0xa9, 0x5f, 0xaf, 0xd2, + 0x91, 0xe6, 0x44, 0xcb, 0xe7, 0xe0, 0x85, 0x36, + 0x9d, 0xd2, 0xaf, 0xae, 0xb3, 0x0f, 0x70, 0x6a, + 0xaf, 0x42, 0xc0, 0xb3, 0xe4, 0x65, 0x53, 0x01, + 0x75, 0xbf +}; + +static int new_add_cb(SSL *s, unsigned int ext_type, unsigned int context, + const unsigned char **out, size_t *outlen, X509 *x, + size_t chainidx, int *al, void *add_arg) +{ + int *server = (int *)add_arg; + unsigned char *data; + + if (*server != SSL_is_server(s)) + return -1; + if (ext_type == TEST_EXT_TYPE1) { + if ((data = OPENSSL_malloc(sizeof(*data))) == NULL) + return -1; + *data = 1; + *out = data; + *outlen = sizeof(*data); + } else if (ext_type == OSSL_ECH_CURRENT_VERSION) { + /* inject a sample ECH extension value into the CH */ + if ((data = OPENSSL_memdup(encoded_ech_val, + sizeof(encoded_ech_val))) == NULL) + return -1; + *out = data; + *outlen = sizeof(encoded_ech_val); + } else { + /* inject a TEST_EXT_TYPE2, with a zero-length payload */ + *out = NULL; + *outlen = 0; + } + return 1; +} + +static void new_free_cb(SSL *s, unsigned int ext_type, unsigned int context, + const unsigned char *out, void *add_arg) +{ + OPENSSL_free((unsigned char *)out); +} + +static int new_parse_cb(SSL *s, unsigned int ext_type, unsigned int context, + const unsigned char *in, size_t inlen, X509 *x, + size_t chainidx, int *al, void *parse_arg) +{ + int *server = (int *)parse_arg; + + if (*server != SSL_is_server(s) + || inlen != sizeof(char) || *in != 1) + return -1; + return 1; +} /* general test vector values */ @@ -688,6 +782,17 @@ static fnt_t fnames[] = { { "ech-rsa.pem", 0 }, }; +/* string from which we construct varieties of HPKE suite */ +static const char *kem_str_list[] = { + "P-256", "P-384", "P-521", "x25519", "x448", +}; +static const char *kdf_str_list[] = { + "hkdf-sha256", "hkdf-sha384", "hkdf-sha512", +}; +static const char *aead_str_list[] = { + "aes-128-gcm", "aes-256-gcm", "chacha20-poly1305", +}; + typedef enum OPTION_choice { OPT_ERR = -1, OPT_EOF = 0, @@ -719,11 +824,11 @@ const OPTIONS *test_get_options(void) static int ech_ingest_test(int run) { OSSL_ECHSTORE *es = NULL; - OSSL_ECH_INFO *ei = NULL; BIO *in = NULL, *out = NULL; - int i, rv = 0, keysb4, keysaftr, actual_ents = 0; + int i, rv = 0, keysb4, keysaftr, actual_ents = 0, has_priv, for_retry; ingest_tv_t *tv = &ingest_tvs[run]; - time_t now = 0; + time_t now = 0, secs = 0; + char *pn = NULL, *ec = NULL; if ((in = BIO_new(BIO_s_mem())) == NULL || BIO_write(in, tv->tv, tv->len) <= 0 @@ -739,97 +844,56 @@ static int ech_ingest_test(int run) } if (tv->pemenc == 1 && !TEST_int_eq(OSSL_ECHSTORE_read_pem(es, in, OSSL_ECH_NO_RETRY), - tv->read)) { - TEST_info("OSSL_ECHSTORE_read_pem unexpected result"); + tv->read)) goto end; - } if (tv->pemenc != 1 - && !TEST_int_eq(OSSL_ECHSTORE_read_echconfiglist(es, in), - tv->read)) { - TEST_info("OSSL_ECHSTORE_read_echconfiglist unexpected result"); + && !TEST_int_eq(OSSL_ECHSTORE_read_echconfiglist(es, in), tv->read)) goto end; - } /* if we provided a deliberately bad tv then we're done */ if (tv->read != 1) { rv = 1; goto end; } - if (!TEST_int_eq(OSSL_ECHSTORE_num_keys(es, &keysb4), 1)) { - TEST_info("OSSL_ECHSTORE_num_keys unexpected fail"); - goto end; - } - if (!TEST_int_eq(keysb4, tv->keysb4)) { - TEST_info("OSSL_ECHSTORE_num_keys unexpected number of keys (b4)"); - goto end; - } - if (!TEST_int_eq(OSSL_ECHSTORE_get1_info(es, &ei, &actual_ents), 1)) { - TEST_info("OSSL_ECHSTORE_get1_info unexpected fail"); + if (!TEST_true(OSSL_ECHSTORE_num_keys(es, &keysb4)) + || !TEST_true(OSSL_ECHSTORE_num_entries(es, &actual_ents)) + || !TEST_int_eq(keysb4, tv->keysb4) + || !TEST_int_eq(actual_ents, tv->entsb4) + || !TEST_int_eq(OSSL_ECHSTORE_get1_info(es, -1, &secs, &pn, &ec, + &has_priv, &for_retry), 0)) goto end; - } + OPENSSL_free(pn); + pn = NULL; + OPENSSL_free(ec); + ec = NULL; for (i = 0; i != actual_ents; i++) { - if (!TEST_int_eq(OSSL_ECH_INFO_print(bio_err, ei, i), 1)) { - TEST_info("OSSL_ECH_INFO_print unexpected fail"); - OSSL_ECH_INFO_free(ei, actual_ents); + if (!TEST_true(OSSL_ECHSTORE_get1_info(es, i, &secs, &pn, &ec, + &has_priv, &for_retry))) goto end; - } - } - if (!TEST_int_eq(actual_ents, tv->entsb4)) { - TEST_info("OSSL_ECHSTORE_get1_info unexpected number of entries (b4)"); - goto end; + OPENSSL_free(pn); + pn = NULL; + OPENSSL_free(ec); + ec = NULL; } - OSSL_ECH_INFO_free(ei, actual_ents); - ei = NULL; /* ensure silly index fails ok */ - if (!TEST_int_eq(OSSL_ECHSTORE_downselect(es, -20), 0)) { - TEST_info("OSSL_ECHSTORE_downselect unexpected non-zero"); - goto end; - } - if (!TEST_int_eq(OSSL_ECHSTORE_downselect(es, tv->index), tv->expected)) { - TEST_info("OSSL_ECHSTORE_downselect unexpected result"); + if (!TEST_false(OSSL_ECHSTORE_downselect(es, -20)) + || !TEST_int_eq(OSSL_ECHSTORE_downselect(es, tv->index), tv->expected) + || !TEST_true(OSSL_ECHSTORE_num_keys(es, &keysaftr)) + || !TEST_int_eq(keysaftr, tv->keysaftr) + || !TEST_true(OSSL_ECHSTORE_num_entries(es, &actual_ents)) + || !TEST_int_eq(actual_ents, tv->entsaftr) + || !TEST_true(OSSL_ECHSTORE_write_pem(es, OSSL_ECHSTORE_LAST, out)) + || !TEST_true(OSSL_ECHSTORE_write_pem(es, OSSL_ECHSTORE_ALL, out)) + || !TEST_false(OSSL_ECHSTORE_write_pem(es, 100, out))) goto end; - } - if (!TEST_int_eq(OSSL_ECHSTORE_num_keys(es, &keysaftr), 1)) { - TEST_info("OSSL_ECHSTORE_num_keys unexpected fail"); - goto end; - } - if (!TEST_int_eq(keysaftr, tv->keysaftr)) { - TEST_info("OSSL_ECHSTORE_num_keys unexpected number of keys (aftr)"); - goto end; - } - if (!TEST_int_eq(OSSL_ECHSTORE_get1_info(es, &ei, &actual_ents), 1)) { - TEST_info("OSSL_ECHSTORE_get1_info unexpected fail"); - goto end; - } - OSSL_ECH_INFO_free(ei, actual_ents); - ei = NULL; - if (!TEST_int_eq(actual_ents, tv->entsaftr)) { - TEST_info("OSSL_ECHSTORE_get1_info unexpected number of entries (aftr)"); - goto end; - } - if (!TEST_int_eq(OSSL_ECHSTORE_write_pem(es, OSSL_ECHSTORE_ALL, out), 1)) { - TEST_info("OSSL_ECHSTORE_write_pem unexpected fail"); - goto end; - } - if (!TEST_int_eq(OSSL_ECHSTORE_write_pem(es, 100, out), 0)) { - TEST_info("OSSL_ECHSTORE_write_pem unexpected result"); - goto end; - } now = time(0); - if (!TEST_int_eq(OSSL_ECHSTORE_flush_keys(es, now), 1)) { - TEST_info("OSSL_ECHSTORE_flush_keys unexpected fail"); + if (!TEST_true(OSSL_ECHSTORE_flush_keys(es, now)) + || !TEST_true(OSSL_ECHSTORE_num_keys(es, &keysaftr)) + || !TEST_false(keysaftr)) goto end; - } - if (!TEST_int_eq(OSSL_ECHSTORE_num_keys(es, &keysaftr), 1)) { - TEST_info("OSSL_ECHSTORE_num_keys unexpected fail"); - goto end; - } - if (!TEST_int_eq(keysaftr, 0)) { - TEST_info("OSSL_ECHSTORE_flush_keys unexpected non-zero"); - goto end; - } rv = 1; end: - OSSL_ECH_INFO_free(ei, actual_ents); + OPENSSL_free(pn); + OPENSSL_free(ec); OSSL_ECHSTORE_free(es); BIO_free_all(in); BIO_free_all(out); @@ -839,155 +903,54 @@ static int ech_ingest_test(int run) /* make a bunch of calls with bad, mostly NULL, arguments */ static int ech_store_null_calls(void) { - int rv = 0, count = 0; + int rv = 0, count = 0, has_priv, for_retry; OSSL_ECHSTORE *es = OSSL_ECHSTORE_new(NULL, NULL); OSSL_HPKE_SUITE hpke_suite = OSSL_HPKE_SUITE_DEFAULT; BIO *inout = BIO_new(BIO_s_mem()); - OSSL_ECH_INFO *info = NULL; EVP_PKEY *priv = EVP_PKEY_new(); + time_t secs; + char *pn = NULL, *ec = NULL; OSSL_ECHSTORE_free(NULL); - if (!TEST_int_eq(OSSL_ECHSTORE_new_config(NULL, OSSL_ECH_CURRENT_VERSION, - 0, "example.com", hpke_suite), - 0)) { - TEST_info("OSSL_ECHSTORE_new_config unexpected non-zero"); - goto end; - } - if (!TEST_int_eq(OSSL_ECHSTORE_new_config(es, OSSL_ECH_CURRENT_VERSION, - 0, NULL, hpke_suite), - 0)) { - TEST_info("OSSL_ECHSTORE_new_config unexpected non-zero"); + if (!TEST_false(OSSL_ECHSTORE_new_config(NULL, OSSL_ECH_CURRENT_VERSION, + 0, "example.com", hpke_suite)) + || !TEST_false(OSSL_ECHSTORE_new_config(es, OSSL_ECH_CURRENT_VERSION, + 0, NULL, hpke_suite)) + || !TEST_false(OSSL_ECHSTORE_new_config(es, 0xffff, 0, + "example.com", hpke_suite))) goto end; - } - if (!TEST_int_eq(OSSL_ECHSTORE_new_config(es, 0xffff, - 0, "example.com", hpke_suite), - 0)) { - TEST_info("OSSL_ECHSTORE_new_config unexpected non-zero"); - goto end; - } hpke_suite.kdf_id = 0xAAAA; /* a bad value */ - if (!TEST_int_eq(OSSL_ECHSTORE_new_config(es, OSSL_ECH_CURRENT_VERSION, - 0, "example.com", hpke_suite), - 0)) { - TEST_info("OSSL_ECHSTORE_new_config unexpected non-zero"); - goto end; - } - if (!TEST_int_eq(OSSL_ECHSTORE_write_pem(NULL, 0, inout), 0)) { - TEST_info("OSSL_ECHSTORE_write_pem unexpected non-zero"); - goto end; - } - if (!TEST_int_eq(OSSL_ECHSTORE_write_pem(es, 0, NULL), 0)) { - TEST_info("OSSL_ECHSTORE_write_pem unexpected non-zero"); - goto end; - } - if (!TEST_int_eq(OSSL_ECHSTORE_write_pem(es, 100, inout), 0)) { - TEST_info("OSSL_ECHSTORE_write_pem unexpected non-zero"); - goto end; - } - if (!TEST_int_eq(OSSL_ECHSTORE_read_echconfiglist(NULL, inout), 0)) { - TEST_info("OSSL_ECHSTORE_read_echconfiglist unexpected non-zero"); - goto end; - } - if (!TEST_int_eq(OSSL_ECHSTORE_read_echconfiglist(es, NULL), 0)) { - TEST_info("OSSL_ECHSTORE_read_echconfiglist unexpected non-zero"); - goto end; - } - if (!TEST_int_eq(OSSL_ECHSTORE_get1_info(NULL, &info, &count), 0)) { - TEST_info("OSSL_ECHSTORE_get1_info unexpected non-zero"); - goto end; - } - if (!TEST_int_eq(OSSL_ECHSTORE_get1_info(es, NULL, &count), 0)) { - TEST_info("OSSL_ECHSTORE_get1_info unexpected non-zero"); - goto end; - } - if (!TEST_int_eq(OSSL_ECHSTORE_get1_info(es, &info, NULL), 0)) { - TEST_info("OSSL_ECHSTORE_get1_info unexpected non-zero"); - goto end; - } - if (!TEST_int_eq(OSSL_ECHSTORE_get1_info(es, &info, &count), 1)) { - TEST_info("OSSL_ECHSTORE_get1_info unexpected zero"); - goto end; - } - if (!TEST_int_eq(OSSL_ECHSTORE_downselect(NULL, 0), 0)) { - TEST_info("OSSL_ECHSTORE_downselect unexpected non-zero"); - goto end; - } - if (!TEST_int_eq(OSSL_ECHSTORE_downselect(es, 100), 0)) { - TEST_info("OSSL_ECHSTORE_downselect unexpected non-zero"); - goto end; - } - if (!TEST_int_eq(OSSL_ECHSTORE_set1_key_and_read_pem(NULL, priv, - inout, 0), 0)) { - TEST_info("OSSL_ECHSTORE_set1_key_and_readp_pem unexpected non-zero"); - goto end; - } - if (!TEST_int_eq(OSSL_ECHSTORE_set1_key_and_read_pem(es, NULL, - inout, 0), 0)) { - TEST_info("OSSL_ECHSTORE_set1_key_and_readp_pem unexpected non-zero"); - goto end; - } - if (!TEST_int_eq(OSSL_ECHSTORE_set1_key_and_read_pem(es, priv, - NULL, 0), 0)) { - TEST_info("OSSL_ECHSTORE_set1_key_and_readp_pem unexpected non-zero"); - goto end; - } - if (!TEST_int_eq(OSSL_ECHSTORE_set1_key_and_read_pem(es, priv, - inout, 100), 0)) { - TEST_info("OSSL_ECHSTORE_set1_key_and_readp_pem unexpected non-zero"); - goto end; - } - /* this one fails 'cause priv has no real value, even if non NULL */ - if (!TEST_int_eq(OSSL_ECHSTORE_set1_key_and_read_pem(es, priv, inout, - OSSL_ECH_NO_RETRY), - 0)) { - TEST_info("OSSL_ECHSTORE_set1_key_and_readp_pem unexpected non-zero"); - goto end; - } - if (!TEST_int_eq(OSSL_ECHSTORE_read_pem(NULL, inout, OSSL_ECH_NO_RETRY), 0)) { - TEST_info("OSSL_ECHSTORE_read_pem unexpected non-zero"); - goto end; - } - if (!TEST_int_eq(OSSL_ECHSTORE_read_pem(es, NULL, OSSL_ECH_NO_RETRY), 0)) { - TEST_info("OSSL_ECHSTORE_read_pem unexpected non-zero"); - goto end; - } - if (!TEST_int_eq(OSSL_ECHSTORE_read_pem(es, inout, 100), 0)) { - TEST_info("OSSL_ECHSTORE_read_pem unexpected non-zero"); - goto end; - } - if (!TEST_int_eq(OSSL_ECHSTORE_num_keys(NULL, &count), 0)) { - TEST_info("OSSL_ECHSTORE_num_keys unexpected non-zero"); - goto end; - } - if (!TEST_int_eq(OSSL_ECHSTORE_num_keys(es, NULL), 0)) { - TEST_info("OSSL_ECHSTORE_num_keys unexpected non-zero"); - goto end; - } - if (!TEST_int_eq(OSSL_ECHSTORE_flush_keys(NULL, 0), 0)) { - TEST_info("OSSL_ECHSTORE_flush_keys unexpected non-zero"); - goto end; - } - if (!TEST_int_eq(OSSL_ECHSTORE_flush_keys(es, -1), 0)) { - TEST_info("OSSL_ECHSTORE_flush_keys unexpected non-zero"); - goto end; - } - /* check free NULL is ok */ - OSSL_ECH_INFO_free(NULL, 100); - if (!TEST_int_eq(OSSL_ECH_INFO_print(inout, NULL, -1), 0)) { - TEST_info("OSSL_ECHSTORE_flush_keys unexpected non-zero"); + if (!TEST_false(OSSL_ECHSTORE_new_config(es, OSSL_ECH_CURRENT_VERSION, + 0, "example.com", hpke_suite)) + || !TEST_false(OSSL_ECHSTORE_write_pem(NULL, 0, inout)) + || !TEST_false(OSSL_ECHSTORE_write_pem(es, 0, NULL)) + || !TEST_false(OSSL_ECHSTORE_write_pem(es, 100, inout)) + || !TEST_false(OSSL_ECHSTORE_read_echconfiglist(NULL, inout)) + || !TEST_false(OSSL_ECHSTORE_read_echconfiglist(es, NULL)) + || !TEST_false(OSSL_ECHSTORE_get1_info(NULL, 0, &secs, &pn, &ec, + &has_priv, &for_retry)) + || !TEST_false(OSSL_ECHSTORE_downselect(NULL, 0)) + || !TEST_false(OSSL_ECHSTORE_downselect(es, 100)) + || !TEST_false(OSSL_ECHSTORE_set1_key_and_read_pem(NULL, priv, + inout, 0)) + || !TEST_false(OSSL_ECHSTORE_set1_key_and_read_pem(es, NULL, inout, 0)) + || !TEST_false(OSSL_ECHSTORE_set1_key_and_read_pem(es, priv, NULL, 0)) + || !TEST_false(OSSL_ECHSTORE_set1_key_and_read_pem(es, priv, + inout, 100)) + /* this one fails 'cause priv has no real value, even if non NULL */ + || !TEST_false(OSSL_ECHSTORE_set1_key_and_read_pem(es, priv, inout, + OSSL_ECH_NO_RETRY)) + || !TEST_false(OSSL_ECHSTORE_read_pem(NULL, inout, OSSL_ECH_NO_RETRY)) + || !TEST_false(OSSL_ECHSTORE_read_pem(es, NULL, OSSL_ECH_NO_RETRY)) + || !TEST_false(OSSL_ECHSTORE_read_pem(es, inout, 100)) + || !TEST_false(OSSL_ECHSTORE_num_keys(NULL, &count)) + || !TEST_false(OSSL_ECHSTORE_num_keys(es, NULL)) + || !TEST_false(OSSL_ECHSTORE_flush_keys(NULL, 0)) + || !TEST_false(OSSL_ECHSTORE_flush_keys(es, -1)) + || !TEST_false(OSSL_ECHSTORE_num_entries(es, NULL))) goto end; - } - if (!TEST_int_eq(OSSL_ECH_INFO_print(NULL, info, -1), 0)) { - TEST_info("OSSL_ECHSTORE_flush_keys unexpected non-zero"); - goto end; - } - if (!TEST_int_eq(OSSL_ECH_INFO_print(inout, info, 0), 0)) { - TEST_info("OSSL_ECHSTORE_flush_keys unexpected non-zero"); - goto end; - } rv = 1; end: - OSSL_ECH_INFO_free(info, count); OSSL_ECHSTORE_free(es); BIO_free_all(inout); EVP_PKEY_free(priv); @@ -1020,10 +983,8 @@ static int ech_test_file_read(int run) goto end; } if (!TEST_int_eq(OSSL_ECHSTORE_read_pem(es, in, OSSL_ECH_NO_RETRY), - ft->read)) { - TEST_info("OSSL_ECHSTORE_read_pem unexpected fail"); + ft->read)) goto end; - } rv = 1; end: OPENSSL_free(fullname); @@ -1032,12 +993,368 @@ static int ech_test_file_read(int run) return rv; } +/* calls with bad, NULL, and simple, arguments, for generic code coverage */ +static int ech_api_basic_calls(void) +{ + int rv = 0; + SSL_CTX *ctx = NULL; + SSL *s = NULL; + OSSL_ECHSTORE *es = NULL, *es1 = NULL; + char *rinner, *inner = "inner.example.com"; + char *router, *outer = "example.com"; + unsigned char alpns[] = { 'h', '2' }; + size_t alpns_len = sizeof(alpns); + char *gsuite = "X25519,hkdf-sha256,aes-256-gcm"; + uint16_t gtype = 0xfe09; + unsigned char *rc = NULL; + size_t rclen = 0; + BIO *in = NULL; + + /* NULL args */ + if (!TEST_false(SSL_CTX_set1_echstore(NULL, NULL)) + || !TEST_false(SSL_set1_echstore(NULL, NULL)) + || !TEST_ptr_eq(SSL_CTX_get1_echstore(NULL), NULL) + || !TEST_ptr_eq(SSL_get1_echstore(NULL), NULL) + || !TEST_false(SSL_ech_set1_server_names(NULL, NULL, NULL, -1)) + || !TEST_false(SSL_ech_set1_outer_server_name(NULL, NULL, -1)) + || !TEST_false(SSL_CTX_ech_set1_outer_alpn_protos(NULL, NULL, -1)) + || !TEST_false(SSL_ech_set1_outer_alpn_protos(NULL, NULL, -1)) + || !TEST_false(SSL_ech_set1_grease_suite(NULL, NULL)) + || !TEST_false(SSL_ech_set_grease_type(NULL, 0))) + goto end; + SSL_CTX_ech_set_callback(NULL, NULL); + SSL_ech_set_callback(NULL, NULL); + if (!TEST_false(SSL_ech_get1_retry_config(NULL, NULL, NULL)) + || !TEST_false(SSL_CTX_ech_raw_decrypt(NULL, NULL, NULL, NULL, + NULL, 0, NULL, NULL, + NULL, NULL)) + || !TEST_int_eq(SSL_ech_get1_status(NULL, &rinner, &router), + SSL_ECH_STATUS_FAILED)) + goto end; + + /* add an ECHConfigList with extensions to exercise init code */ + if (!TEST_ptr(es = OSSL_ECHSTORE_new(NULL, NULL)) + || !TEST_ptr(in = BIO_new(BIO_s_mem())) + || !TEST_int_gt(BIO_write(in, bin_ok_exts, sizeof(bin_ok_exts)), 0) + || !TEST_true(OSSL_ECHSTORE_read_echconfiglist(es, in)) + || !TEST_ptr(ctx = SSL_CTX_new_ex(NULL, NULL, TLS_server_method()))) + goto end; + /* check status of SSL connection before OSSL_ECHSTORE set */ + if (!TEST_ptr(s = SSL_new(ctx)) + || !TEST_int_eq(SSL_ech_get1_status(s, NULL, NULL), + SSL_ECH_STATUS_FAILED) + || !TEST_int_eq(SSL_ech_get1_status(s, &rinner, &router), + SSL_ECH_STATUS_NOT_CONFIGURED)) + goto end; + SSL_set_options(s, SSL_OP_ECH_GREASE); + if (!TEST_int_eq(SSL_ech_get1_status(s, &rinner, &router), + SSL_ECH_STATUS_GREASE)) + goto end; + SSL_free(s); + s = NULL; /* for some other tests */ + if (!TEST_true(SSL_CTX_set1_echstore(ctx, es))) + goto end; + if (!TEST_ptr((es1 = SSL_CTX_get1_echstore(ctx)))) + goto end; + OSSL_ECHSTORE_free(es1); + es1 = NULL; + if (!TEST_false(SSL_set1_echstore(s, es))) + goto end; + /* do this one before SSL_new to exercise a bit of init code */ + if (!TEST_true(SSL_CTX_ech_set1_outer_alpn_protos(ctx, alpns, alpns_len))) + goto end; + s = SSL_new(ctx); + if (!TEST_true(SSL_set1_echstore(s, es))) + goto end; + if (!TEST_ptr(es1 = SSL_get1_echstore(s))) + goto end; + OSSL_ECHSTORE_free(es1); + es1 = NULL; + if (!TEST_true(SSL_ech_set1_server_names(s, inner, outer, 0)) + || !TEST_true(SSL_ech_set1_outer_server_name(s, outer, 0)) + || !TEST_true(SSL_ech_set1_outer_alpn_protos(s, alpns, alpns_len)) + || !TEST_true(SSL_ech_set1_grease_suite(s, gsuite)) + || !TEST_true(SSL_ech_set_grease_type(s, gtype)) + || !TEST_true(SSL_ech_get1_retry_config(s, &rc, &rclen)) + || !TEST_false(rclen) + || !TEST_ptr_eq(rc, NULL)) + goto end; + SSL_CTX_ech_set_callback(ctx, test_cb); + SSL_ech_set_callback(s, test_cb); + + /* all good */ + rv = 1; +end: + BIO_free_all(in); + OSSL_ECHSTORE_free(es1); + OSSL_ECHSTORE_free(es); + SSL_CTX_free(ctx); + SSL_free(s); + return rv; +} + +/* + * Test boringssl compatibility API. We don't need exhaustive + * tests here as this is a simple enough wrapper on things + * tested elsewhere. + */ +static int ech_boring_compat(void) +{ + int rv = 0; + SSL_CTX *ctx = NULL; + SSL *s = NULL; + + if (!TEST_false(SSL_set1_ech_config_list(NULL, NULL, 0)) + || !TEST_ptr(ctx = SSL_CTX_new_ex(NULL, NULL, TLS_server_method())) + || !TEST_ptr(s = SSL_new(ctx)) + || !TEST_true(SSL_set1_ech_config_list(s, NULL, 0)) + || !TEST_true(SSL_set1_ech_config_list(s, (uint8_t *)b64_pk1, + sizeof(b64_pk1) - 1)) + || !TEST_true(SSL_set1_ech_config_list(s, (uint8_t *)bin_6_to_3, + sizeof(bin_6_to_3))) + /* test a fail */ + || !TEST_false(SSL_set1_ech_config_list(s, (uint8_t *)b64_pk1, + sizeof(b64_pk1) - 2))) + goto end; + rv = 1; +end: + SSL_CTX_free(ctx); + SSL_free(s); + return rv; +} + +/* values that can be used in helper below */ +# define OSSL_ECH_TEST_BASIC 0 +# define OSSL_ECH_TEST_HRR 1 +# define OSSL_ECH_TEST_EARLY 2 +# define OSSL_ECH_TEST_CUSTOM 3 + +/* + * @brief ECH roundtrip test helper + * @param idx specifies which ciphersuite + * @araam combo specifies which particular test we want to roundtrip + * @return 1 for good, 0 for bad + * + * The idx input here is from 0..44 and is broken down into a + * kem, kdf and aead. If you run in verbose more ("-v") then + * there'll be a "Doing: ..." trace line that says which suite + * is being tested in string form. + * + * The combo input is one of the #define'd OSSL_ECH_TEST_* + * values above. + * + * TODO(ECH): we're not yet really attempting ECH, but we currently + * set the inputs as if we were doing ECH, yet don't expect to see + * real ECH status outcomes, so while we do make calls to get that + * status outcome, we don't compare vs. real expected results. + * That's done via the "if (0 &&" clauses below which will be + * removed once ECH is really being attempted. + */ +static int test_ech_roundtrip_helper(int idx, int combo) +{ + int res = 0, kemind, kdfind, aeadind, kemsz, kdfsz, aeadsz; + char suitestr[100]; + OSSL_ECHSTORE *es = NULL; + OSSL_HPKE_SUITE hpke_suite = OSSL_HPKE_SUITE_DEFAULT; + uint16_t ech_version = OSSL_ECH_CURRENT_VERSION; + uint8_t max_name_length = 0; + char *public_name = "example.com"; + SSL_CTX *cctx = NULL, *sctx = NULL; + SSL *clientssl = NULL, *serverssl = NULL; + int clientstatus, serverstatus; + char *cinner = NULL, *couter = NULL, *sinner = NULL, *souter = NULL; + SSL_SESSION *sess = NULL; + unsigned char ed[21]; + size_t written = 0, readbytes = 0; + unsigned char buf[1024]; + unsigned int context; + int server = 1, client = 0; + + /* split idx into kemind, kdfind, aeadind */ + kemsz = OSSL_NELEM(kem_str_list); + kdfsz = OSSL_NELEM(kdf_str_list); + aeadsz = OSSL_NELEM(aead_str_list); + kemind = (idx / (kdfsz * aeadsz)) % kemsz; + kdfind = (idx / aeadsz) % kdfsz; + aeadind = idx % aeadsz; + /* initialise early data stuff, just in case */ + memset(ed, 'A', sizeof(ed)); + snprintf(suitestr, 100, "%s,%s,%s", kem_str_list[kemind], + kdf_str_list[kdfind], aead_str_list[aeadind]); + if (verbose) + TEST_info("Doing: iter: %d, suite: %s", idx, suitestr); + if (!TEST_true(OSSL_HPKE_str2suite(suitestr, &hpke_suite)) + || !TEST_ptr(es = OSSL_ECHSTORE_new(libctx, propq)) + || !TEST_true(OSSL_ECHSTORE_new_config(es, ech_version, max_name_length, + public_name, hpke_suite)) + || !TEST_true(create_ssl_ctx_pair(libctx, TLS_server_method(), + TLS_client_method(), + TLS1_3_VERSION, TLS1_3_VERSION, + &sctx, &cctx, cert, privkey))) + goto end; + if (combo == OSSL_ECH_TEST_EARLY) { + /* just to keep the format checker happy :-) */ + int lrv = 0; + + if (!TEST_true(SSL_CTX_set_options(sctx, SSL_OP_NO_ANTI_REPLAY)) + || !TEST_true(SSL_CTX_set_max_early_data(sctx, + SSL3_RT_MAX_PLAIN_LENGTH))) + goto end; + lrv = SSL_CTX_set_recv_max_early_data(sctx, SSL3_RT_MAX_PLAIN_LENGTH); + if (!TEST_true(lrv)) + goto end; + } + if (combo == OSSL_ECH_TEST_CUSTOM) { + /* add custom CH ext to client and server */ + context = SSL_EXT_CLIENT_HELLO; + if (!TEST_true(SSL_CTX_add_custom_ext(cctx, TEST_EXT_TYPE1, context, + new_add_cb, new_free_cb, + &client, new_parse_cb, &client)) + || !TEST_true(SSL_CTX_add_custom_ext(sctx, TEST_EXT_TYPE1, context, + new_add_cb, new_free_cb, + &server, new_parse_cb, &server)) + || !TEST_true(SSL_CTX_add_custom_ext(cctx, TEST_EXT_TYPE2, context, + new_add_cb, NULL, + &client, NULL, &client)) + || !TEST_true(SSL_CTX_add_custom_ext(sctx, TEST_EXT_TYPE2, context, + new_add_cb, NULL, + &server, NULL, &server))) + goto end; + } + if (!TEST_true(SSL_CTX_set1_echstore(cctx, es)) + || !TEST_true(SSL_CTX_set1_echstore(sctx, es)) + || !TEST_true(create_ssl_objects(sctx, cctx, &serverssl, + &clientssl, NULL, NULL))) + goto end; + if (combo == OSSL_ECH_TEST_HRR + && !TEST_true(SSL_set1_groups_list(serverssl, "P-384"))) + goto end; + if (!TEST_true(SSL_set_tlsext_host_name(clientssl, "server.example")) + || !TEST_true(create_ssl_connection(serverssl, clientssl, + SSL_ERROR_NONE))) + goto end; + serverstatus = SSL_ech_get1_status(serverssl, &sinner, &souter); + if (verbose) + TEST_info("server status %d, %s, %s", serverstatus, sinner, souter); + if (0 && !TEST_int_eq(serverstatus, SSL_ECH_STATUS_SUCCESS)) + goto end; + /* override cert verification */ + SSL_set_verify_result(clientssl, X509_V_OK); + clientstatus = SSL_ech_get1_status(clientssl, &cinner, &couter); + if (verbose) + TEST_info("client status %d, %s, %s", clientstatus, cinner, couter); + if (0 && !TEST_int_eq(clientstatus, SSL_ECH_STATUS_SUCCESS)) + goto end; + /* all good */ + if (combo == OSSL_ECH_TEST_BASIC + || combo == OSSL_ECH_TEST_HRR + || combo == OSSL_ECH_TEST_CUSTOM) { + res = 1; + goto end; + } + /* continue for EARLY test */ + if (combo != OSSL_ECH_TEST_EARLY) + goto end; + /* shutdown for start over */ + sess = SSL_get1_session(clientssl); + OPENSSL_free(sinner); + OPENSSL_free(souter); + OPENSSL_free(cinner); + OPENSSL_free(couter); + sinner = souter = cinner = couter = NULL; + SSL_shutdown(clientssl); + SSL_shutdown(serverssl); + SSL_free(serverssl); + SSL_free(clientssl); + serverssl = clientssl = NULL; + /* second connection */ + if (!TEST_true(create_ssl_objects(sctx, cctx, &serverssl, + &clientssl, NULL, NULL)) + || !TEST_true(SSL_set_tlsext_host_name(clientssl, "server.example")) + || !TEST_true(SSL_set_session(clientssl, sess)) + || !TEST_true(SSL_write_early_data(clientssl, ed, sizeof(ed), &written)) + || !TEST_size_t_eq(written, sizeof(ed)) + || !TEST_int_eq(SSL_read_early_data(serverssl, buf, + sizeof(buf), &readbytes), + SSL_READ_EARLY_DATA_SUCCESS) + || !TEST_size_t_eq(written, readbytes)) + goto end; + /* + * Server should be able to write data, and client should be able to + * read it. + */ + if (!TEST_true(SSL_write_early_data(serverssl, ed, sizeof(ed), &written)) + || !TEST_size_t_eq(written, sizeof(ed)) + || !TEST_true(SSL_read_ex(clientssl, buf, sizeof(buf), &readbytes)) + || !TEST_mem_eq(buf, readbytes, ed, sizeof(ed))) + goto end; + serverstatus = SSL_ech_get1_status(serverssl, &sinner, &souter); + if (verbose) + TEST_info("server status %d, %s, %s", serverstatus, sinner, souter); + if (0 && !TEST_int_eq(serverstatus, SSL_ECH_STATUS_SUCCESS)) + goto end; + /* override cert verification */ + SSL_set_verify_result(clientssl, X509_V_OK); + clientstatus = SSL_ech_get1_status(clientssl, &cinner, &couter); + if (verbose) + TEST_info("client status %d, %s, %s", clientstatus, cinner, couter); + if (0 && !TEST_int_eq(clientstatus, SSL_ECH_STATUS_SUCCESS)) + goto end; + /* all good */ + res = 1; +end: + OSSL_ECHSTORE_free(es); + OPENSSL_free(sinner); + OPENSSL_free(souter); + OPENSSL_free(cinner); + OPENSSL_free(couter); + SSL_SESSION_free(sess); + SSL_free(clientssl); + SSL_free(serverssl); + SSL_CTX_free(cctx); + SSL_CTX_free(sctx); + return res; +} + +/* Test roundtrip with ECH for any suite */ +static int test_ech_suites(int idx) +{ + if (verbose) + TEST_info("Doing: test_ech_suites"); + return test_ech_roundtrip_helper(idx, OSSL_ECH_TEST_BASIC); +} + +/* ECH with HRR for the given suite */ +static int test_ech_hrr(int idx) +{ + if (verbose) + TEST_info("Doing: test_ech_hrr"); + return test_ech_roundtrip_helper(idx, OSSL_ECH_TEST_HRR); +} + +/* ECH with early data for the given suite */ +static int test_ech_early(int idx) +{ + if (verbose) + TEST_info("Doing: test_ech_early"); + return test_ech_roundtrip_helper(idx, OSSL_ECH_TEST_EARLY); +} + +/* Test a roundtrip with ECH, and a custom CH extension */ +static int ech_custom_test(int idx) +{ + if (verbose) + TEST_info("Doing: ech_custom_test"); + return test_ech_roundtrip_helper(idx, OSSL_ECH_TEST_CUSTOM); +} + #endif int setup_tests(void) { #ifndef OPENSSL_NO_ECH OPTION_CHOICE o; + int suite_combos; while ((o = opt_next()) != OPT_EOF) { switch (o) { @@ -1053,11 +1370,30 @@ int setup_tests(void) certsdir = test_get_argument(0); if (certsdir == NULL) certsdir = DEF_CERTS_DIR; + cert = test_mk_file_path(certsdir, "echserver.pem"); + if (cert == NULL) + goto err; + privkey = test_mk_file_path(certsdir, "echserver.key"); + if (privkey == NULL) + goto err; + rootcert = test_mk_file_path(certsdir, "rootcert.pem"); + if (rootcert == NULL) + goto err; ADD_ALL_TESTS(ech_ingest_test, OSSL_NELEM(ingest_tvs)); ADD_TEST(ech_store_null_calls); ADD_ALL_TESTS(ech_test_file_read, OSSL_NELEM(fnames)); - /* TODO(ECH): we'll add more test code once other TODO's settle */ + ADD_TEST(ech_api_basic_calls); + ADD_TEST(ech_boring_compat); + suite_combos = OSSL_NELEM(kem_str_list) * OSSL_NELEM(kdf_str_list) + * OSSL_NELEM(aead_str_list); + ADD_ALL_TESTS(test_ech_suites, suite_combos); + ADD_ALL_TESTS(test_ech_hrr, suite_combos); + ADD_ALL_TESTS(test_ech_early, suite_combos); + ADD_ALL_TESTS(ech_custom_test, suite_combos); + /* TODO(ECH): add more test code as other PRs done */ return 1; +err: + return 0; #endif return 1; } @@ -1065,6 +1401,8 @@ int setup_tests(void) void cleanup_tests(void) { #ifndef OPENSSL_NO_ECH - ; + OPENSSL_free(cert); + OPENSSL_free(privkey); + OPENSSL_free(rootcert); #endif } diff --git a/util/libssl.num b/util/libssl.num index 847e77399daba..83e25aeee65c6 100644 --- a/util/libssl.num +++ b/util/libssl.num @@ -605,7 +605,8 @@ SSL_CTX_get0_client_cert_type ? 4_0_0 EXIST::FUNCTION: SSL_CTX_get0_server_cert_type ? 4_0_0 EXIST::FUNCTION: SSL_set_quic_tls_cbs ? 4_0_0 EXIST::FUNCTION: SSL_set_quic_tls_transport_params ? 4_0_0 EXIST::FUNCTION: -SSL_set_quic_tls_early_data_enabled ? 4_0_0 EXIST::FUNCTION:OSSL_ECHSTORE_new ? 4_0_0 EXIST::FUNCTION:ECH +SSL_set_quic_tls_early_data_enabled ? 4_0_0 EXIST::FUNCTION: +OSSL_ECHSTORE_new ? 4_0_0 EXIST::FUNCTION:ECH OSSL_ECHSTORE_free ? 4_0_0 EXIST::FUNCTION:ECH OSSL_ECHSTORE_new_config ? 4_0_0 EXIST::FUNCTION:ECH OSSL_ECHSTORE_write_pem ? 4_0_0 EXIST::FUNCTION:ECH @@ -616,20 +617,20 @@ OSSL_ECHSTORE_set1_key_and_read_pem ? 4_0_0 EXIST::FUNCTION:ECH OSSL_ECHSTORE_read_pem ? 4_0_0 EXIST::FUNCTION:ECH OSSL_ECHSTORE_num_keys ? 4_0_0 EXIST::FUNCTION:ECH OSSL_ECHSTORE_flush_keys ? 4_0_0 EXIST::FUNCTION:ECH -OSSL_ECH_INFO_free ? 4_0_0 EXIST::FUNCTION:ECH -OSSL_ECH_INFO_print ? 4_0_0 EXIST::FUNCTION:ECH SSL_CTX_set1_echstore ? 4_0_0 EXIST::FUNCTION:ECH SSL_set1_echstore ? 4_0_0 EXIST::FUNCTION:ECH SSL_CTX_get1_echstore ? 4_0_0 EXIST::FUNCTION:ECH SSL_get1_echstore ? 4_0_0 EXIST::FUNCTION:ECH -SSL_ech_set_server_names ? 4_0_0 EXIST::FUNCTION:ECH -SSL_ech_set_outer_server_name ? 4_0_0 EXIST::FUNCTION:ECH -SSL_ech_set_outer_alpn_protos ? 4_0_0 EXIST::FUNCTION:ECH SSL_ech_get1_status ? 4_0_0 EXIST::FUNCTION:ECH -SSL_ech_set_grease_suite ? 4_0_0 EXIST::FUNCTION:ECH SSL_ech_set_grease_type ? 4_0_0 EXIST::FUNCTION:ECH SSL_ech_set_callback ? 4_0_0 EXIST::FUNCTION:ECH -SSL_ech_get_retry_config ? 4_0_0 EXIST::FUNCTION:ECH -SSL_CTX_ech_set_outer_alpn_protos ? 4_0_0 EXIST::FUNCTION:ECH SSL_CTX_ech_raw_decrypt ? 4_0_0 EXIST::FUNCTION:ECH SSL_CTX_ech_set_callback ? 4_0_0 EXIST::FUNCTION:ECH +OSSL_ECHSTORE_num_entries ? 4_0_0 EXIST::FUNCTION:ECH +SSL_ech_set1_server_names ? 4_0_0 EXIST::FUNCTION:ECH +SSL_ech_set1_outer_server_name ? 4_0_0 EXIST::FUNCTION:ECH +SSL_ech_set1_outer_alpn_protos ? 4_0_0 EXIST::FUNCTION:ECH +SSL_ech_set1_grease_suite ? 4_0_0 EXIST::FUNCTION:ECH +SSL_ech_get1_retry_config ? 4_0_0 EXIST::FUNCTION:ECH +SSL_CTX_ech_set1_outer_alpn_protos ? 4_0_0 EXIST::FUNCTION:ECH +SSL_set1_ech_config_list ? 4_0_0 EXIST::FUNCTION:ECH From 7afca60dbad0d2c70a0abd1a3676bc4d75c6e6f6 Mon Sep 17 00:00:00 2001 From: sftcd Date: Wed, 20 Nov 2024 14:10:30 +0000 Subject: [PATCH 06/24] ECH client side Reviewed-by: Tomas Mraz Reviewed-by: Matt Caswell (Merged from https://github.com/openssl/openssl/pull/26011) --- apps/s_client.c | 137 ++ doc/man1/openssl-s_client.pod.in | 10 + include/internal/ech_helpers.h | 27 + ssl/ech/ech_helper.c | 138 +- ssl/ech/ech_internal.c | 1246 ++++++++++++++++- ssl/ech/ech_local.h | 97 +- ssl/ech/ech_store.c | 2 +- ssl/ssl_ciph.c | 104 ++ ssl/ssl_local.h | 4 + ssl/ssl_stat.c | 8 + ssl/statem/extensions.c | 401 +++++- ssl/statem/extensions_clnt.c | 389 ++++- ssl/statem/statem_clnt.c | 530 +++++-- ssl/statem/statem_local.h | 10 + ssl/t1_enc.c | 4 + ssl/t1_trce.c | 4 + test/ech_test.c | 84 +- test/ext_internal_test.c | 7 + .../75-test_quicapi_data/ssltraceref-zlib.txt | 4 +- .../75-test_quicapi_data/ssltraceref.txt | 4 +- 20 files changed, 3007 insertions(+), 203 deletions(-) create mode 100644 include/internal/ech_helpers.h diff --git a/apps/s_client.c b/apps/s_client.c index 7c3e1ef7bc8a1..89b473b7e69fb 100644 --- a/apps/s_client.c +++ b/apps/s_client.c @@ -107,6 +107,9 @@ static int keymatexportlen = 20; static BIO *bio_c_out = NULL; static int c_quiet = 0; static char *sess_out = NULL; +# ifndef OPENSSL_NO_ECH +static char *ech_config_list = NULL; +# endif static SSL_SESSION *psksess = NULL; static void print_stuff(BIO *berr, SSL *con, int full); @@ -522,6 +525,9 @@ typedef enum OPTION_choice { OPT_ENABLE_CLIENT_RPK, OPT_SCTP_LABEL_BUG, OPT_KTLS, +# ifndef OPENSSL_NO_ECH + OPT_ECHCONFIGLIST, +# endif OPT_R_ENUM, OPT_PROV_ENUM } OPTION_CHOICE; @@ -722,6 +728,10 @@ const OPTIONS s_client_options[] = { {"enable_pha", OPT_ENABLE_PHA, '-', "Enable post-handshake-authentication"}, {"enable_server_rpk", OPT_ENABLE_SERVER_RPK, '-', "Enable raw public keys (RFC7250) from the server"}, {"enable_client_rpk", OPT_ENABLE_CLIENT_RPK, '-', "Enable raw public keys (RFC7250) from the client"}, +# ifndef OPENSSL_NO_ECH + {"ech_config_list", OPT_ECHCONFIGLIST, 's', + "Set ECHConfigList, value is base 64 encoded ECHConfigList"}, +# endif #ifndef OPENSSL_NO_SRTP {"use_srtp", OPT_USE_SRTP, 's', "Offer SRTP key management with a colon-separated profile list"}, @@ -1527,6 +1537,11 @@ int s_client_main(int argc, char **argv) case OPT_SERVERNAME: servername = opt_arg(); break; +# ifndef OPENSSL_NO_ECH + case OPT_ECHCONFIGLIST: + ech_config_list = opt_arg(); + break; +# endif case OPT_NOSERVERNAME: noservername = 1; break; @@ -2129,6 +2144,13 @@ int s_client_main(int argc, char **argv) } } +# ifndef OPENSSL_NO_ECH + if (ech_config_list != NULL + && SSL_set1_ech_config_list(con, (unsigned char *)ech_config_list, + strlen(ech_config_list)) != 1) + goto end; +# endif + if (dane_tlsa_domain != NULL) { if (SSL_dane_enable(con, dane_tlsa_domain) <= 0) { BIO_printf(bio_err, "%s: Error enabling DANE TLSA " @@ -3391,6 +3413,101 @@ static void print_cert_key_info(BIO *bio, X509 *cert) OPENSSL_free(curve); } +# ifndef OPENSSL_NO_ECH +static void print_ech_retry_configs(BIO *bio, SSL *s) +{ + int ind, cnt = 0, has_priv, for_retry; + OSSL_ECHSTORE *es = NULL; + time_t secs = 0; + char *pn = NULL, *ec = NULL; + size_t rtlen = 0; + unsigned char *rtval = NULL; + BIO *biom = NULL; + + if (SSL_ech_get1_retry_config(s, &rtval, &rtlen) != 1) { + BIO_printf(bio, "ECH: Error getting retry-configs\n"); + return; + } + /* + * print nicely, note that any non-supported versions + * sent by server will have been filtered out by now + */ + if ((biom = BIO_new(BIO_s_mem())) == NULL + || BIO_write(biom, rtval, rtlen) <= 0 + || (es = OSSL_ECHSTORE_new(NULL, NULL)) == NULL + || OSSL_ECHSTORE_read_echconfiglist(es, biom) != 1) { + BIO_printf(bio, "ECH: Error loading retry-configs\n"); + goto end; + } + if (OSSL_ECHSTORE_num_entries(es, &cnt) != 1) + goto end; + BIO_printf(bio, "ECH: Got %d retry-configs\n", cnt); + for (ind = 0; ind != cnt; ind++) { + if (OSSL_ECHSTORE_get1_info(es, ind, &secs, &pn, &ec, + &has_priv, &for_retry) != 1) { + BIO_printf(bio, "ECH: Error getting retry-config %d\n", ind); + goto end; + } + BIO_printf(bio, "ECH: entry: %d public_name: %s age: %d%s\n", + ind, pn, (int)secs, has_priv ? " (has private key)" : ""); + BIO_printf(bio, "ECH: \t%s\n", ec); + OPENSSL_free(pn); + pn = NULL; + OPENSSL_free(ec); + ec = NULL; + } +end: + BIO_free_all(biom); + OPENSSL_free(rtval); + OPENSSL_free(pn); + OPENSSL_free(ec); + OSSL_ECHSTORE_free(es); + return; +} + +static void print_ech_status(BIO *bio, SSL *s, int estat) +{ + switch (estat) { + case SSL_ECH_STATUS_NOT_TRIED: + BIO_printf(bio, "ECH: not tried: %d\n", estat); + break; + case SSL_ECH_STATUS_FAILED: + BIO_printf(bio, "ECH: tried but failed: %d\n", estat); + break; + case SSL_ECH_STATUS_FAILED_ECH: + BIO_printf(bio, "ECH: failed+retry-configs: %d\n", estat); + break; + case SSL_ECH_STATUS_SUCCESS: + BIO_printf(bio, "ECH: success: %d\n", estat); + break; + case SSL_ECH_STATUS_GREASE_ECH: + BIO_printf(bio, "ECH: GREASE+retry-configs%d\n", estat); + break; + case SSL_ECH_STATUS_BACKEND: + BIO_printf(bio, "ECH: BACKEND: %d\n", estat); + break; + case SSL_ECH_STATUS_GREASE: + BIO_printf(bio, "ECH: GREASE: %d\n", estat); + break; + case SSL_ECH_STATUS_BAD_CALL: + BIO_printf(bio, "ECH: BAD CALL: %d\n", estat); + break; + case SSL_ECH_STATUS_BAD_NAME: + BIO_printf(bio, "ECH: BAD NAME: %d\n", estat); + break; + case SSL_ECH_STATUS_NOT_CONFIGURED: + BIO_printf(bio, "ECH: NOT CONFIGURED: %d\n", estat); + break; + case SSL_ECH_STATUS_FAILED_ECH_BAD_NAME: + BIO_printf(bio, "ECH: failed+retry-configs: %d\n", estat); + break; + default: + BIO_printf(bio, "ECH: unexpected status: %d\n", estat); + } + return; +} +# endif + static void print_stuff(BIO *bio, SSL *s, int full) { X509 *peer = NULL; @@ -3634,6 +3751,26 @@ static void print_stuff(BIO *bio, SSL *s, int full) OPENSSL_free(exportedkeymat); } BIO_printf(bio, "---\n"); +# ifndef OPENSSL_NO_ECH + { + char *inner = NULL, *outer = NULL; + int estat = 0; + + estat = SSL_ech_get1_status(s, &inner, &outer); + print_ech_status(bio, s, estat); + if (estat == SSL_ECH_STATUS_SUCCESS) { + BIO_printf(bio, "ECH: inner: %s\n", inner); + BIO_printf(bio, "ECH: outer: %s\n", outer); + } + if (estat == SSL_ECH_STATUS_FAILED_ECH + || estat == SSL_ECH_STATUS_FAILED_ECH_BAD_NAME) + print_ech_retry_configs(bio, s); + OPENSSL_free(inner); + OPENSSL_free(outer); + } + BIO_printf(bio, "---\n"); +# endif + /* flush, or debugging output gets mixed with http response */ (void)BIO_flush(bio); } diff --git a/doc/man1/openssl-s_client.pod.in b/doc/man1/openssl-s_client.pod.in index 4c0759ab0f582..5579a9c85f558 100644 --- a/doc/man1/openssl-s_client.pod.in +++ b/doc/man1/openssl-s_client.pod.in @@ -124,6 +124,7 @@ B B [B<-enable_server_rpk>] [B<-enable_client_rpk>] [I:I] +[B<-ech_config_list>] =head1 DESCRIPTION @@ -178,6 +179,15 @@ specified with this flag and issues an HTTP CONNECT command to connect to the desired server. If the host string is an IPv6 address, it must be enclosed in C<[> and C<]>. +=item B<-ech_config_list> I + +Specifies the ECHConfigList value to use for Encrypted Client Hello (ECH) for +the TLS session. The value must be a base64 encoded ECHConfigList. + +The ECHConfigList structure is defined in RFC XXXX. (That's currently in +L) +=for comment TODO(ECH): replace XXXX when RFC published. + =item B<-proxy_user> I When used with the B<-proxy> flag, the program will attempt to authenticate diff --git a/include/internal/ech_helpers.h b/include/internal/ech_helpers.h new file mode 100644 index 0000000000000..b71029857b156 --- /dev/null +++ b/include/internal/ech_helpers.h @@ -0,0 +1,27 @@ +/* + * Copyright 2024 The OpenSSL Project Authors. All Rights Reserved. + * Licensed under the OpenSSL license (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 + */ + +/* + * These functions are ECH helpers that are used within the library but + * also by ECH test code. + */ + +#ifndef OPENSSL_ECH_HELPERS_H +# define OPENSSL_ECH_HELPERS_H +# pragma once + +# ifndef OPENSSL_NO_ECH + +int ossl_ech_get_sh_offsets(const unsigned char *sh, size_t sh_len, + size_t *exts, size_t *echoffset, + uint16_t *echtype); +int ossl_ech_make_enc_info(unsigned char *encoding, size_t encoding_length, + unsigned char *info, size_t *info_len); + +# endif +#endif diff --git a/ssl/ech/ech_helper.c b/ssl/ech/ech_helper.c index bbe62eee21be5..6f900408ef553 100644 --- a/ssl/ech/ech_helper.c +++ b/ssl/ech/ech_helper.c @@ -11,5 +11,141 @@ #include #include "../ssl_local.h" #include "ech_local.h" +#include "internal/ech_helpers.h" -/* TODO(ECH): move code that's used by internals and test here */ +/* TODO(ECH): move more code that's used by internals and test here */ + +/* used in ECH crypto derivations (odd format for EBCDIC goodness) */ +/* "tls ech" */ +static const char OSSL_ECH_CONTEXT_STRING[] = "\x74\x6c\x73\x20\x65\x63\x68"; + +/* + * Given a SH (or HRR) find the offsets of the ECH (if any) + * sh is the SH buffer + * sh_len is the length of the SH + * exts points to offset of extensions + * echoffset points to offset of ECH + * echtype points to the ext type of the ECH + * return 1 for success, zero otherwise + * + * Offsets are returned to the type or length field in question. + * Offsets are set to zero if relevant thing not found. + * + * Note: input here is untrusted! + */ +int ossl_ech_get_sh_offsets(const unsigned char *sh, size_t sh_len, + size_t *exts, size_t *echoffset, + uint16_t *echtype) +{ + unsigned int etype = 0, pi_tmp = 0; + const unsigned char *pp_tmp = NULL, *shstart = NULL; + PACKET pkt, session_id, extpkt, oneext; + size_t extlens = 0; + int done = 0; +#ifdef OSSL_ECH_SUPERVERBOSE + size_t echlen = 0; /* length of ECH, including type & ECH-internal length */ + size_t sessid_offset = 0, sessid_len = 0; +#endif + + if (sh == NULL || sh_len == 0 || exts == NULL || echoffset == NULL + || echtype == NULL) + return 0; + *exts = *echoffset = *echtype = 0; + if (!PACKET_buf_init(&pkt, sh, sh_len)) + return 0; + shstart = PACKET_data(&pkt); + if (!PACKET_get_net_2(&pkt, &pi_tmp)) + return 0; + /* + * TODO(ECH): we've had a TLSv1.2 test in the past where we add an + * ECH to a TLSv1.2 CH to ensure server code ignores that properly. + * We might or might not keep that, if we don't then the test below + * should allow TLSv1.3 only. + */ + /* if we're not TLSv1.2+ then we can bail, but it's not an error */ + if (pi_tmp != TLS1_2_VERSION && pi_tmp != TLS1_3_VERSION) + return 1; + if (!PACKET_get_bytes(&pkt, &pp_tmp, SSL3_RANDOM_SIZE) +#ifdef OSSL_ECH_SUPERVERBOSE + || (sessid_offset = PACKET_data(&pkt) - shstart) == 0 +#endif + || !PACKET_get_length_prefixed_1(&pkt, &session_id) +#ifdef OSSL_ECH_SUPERVERBOSE + || (sessid_len = PACKET_remaining(&session_id)) == 0 +#endif + || !PACKET_get_net_2(&pkt, &pi_tmp) /* ciphersuite */ + || !PACKET_get_1(&pkt, &pi_tmp) /* compression */ + || (*exts = PACKET_data(&pkt) - shstart) == 0 + || !PACKET_as_length_prefixed_2(&pkt, &extpkt) + || PACKET_remaining(&pkt) != 0) + return 0; + extlens = PACKET_remaining(&extpkt); + if (extlens == 0) /* not an error, in theory */ + return 1; + while (PACKET_remaining(&extpkt) > 0 && done < 1) { + if (!PACKET_get_net_2(&extpkt, &etype) + || !PACKET_get_length_prefixed_2(&extpkt, &oneext)) + return 0; + if (etype == TLSEXT_TYPE_ech) { + if (PACKET_remaining(&oneext) != 8) + return 0; + *echoffset = PACKET_data(&oneext) - shstart - 4; + *echtype = etype; +#ifdef OSSL_ECH_SUPERVERBOSE + echlen = PACKET_remaining(&oneext) + 4; /* type/length included */ +#endif + done++; + } + } +#ifdef OSSL_ECH_SUPERVERBOSE + OSSL_TRACE_BEGIN(TLS) { + BIO_printf(trc_out, "orig SH/ECH type: %4x\n", *echtype); + } OSSL_TRACE_END(TLS); + ossl_ech_pbuf("orig SH", (unsigned char *)sh, sh_len); + ossl_ech_pbuf("orig SH session_id", (unsigned char *)sh + sessid_offset, + sessid_len); + ossl_ech_pbuf("orig SH exts", (unsigned char *)sh + *exts, extlens); + ossl_ech_pbuf("orig SH/ECH ", (unsigned char *)sh + *echoffset, echlen); +#endif + return 1; +} + +/* + * make up HPKE "info" input as per spec + * encoding is the ECHconfig being used + * encodinglen is the length of ECHconfig being used + * info is a caller-allocated buffer for results + * info_len is the buffer size on input, used-length on output + * return 1 for success, zero otherwise + */ +int ossl_ech_make_enc_info(unsigned char *encoding, size_t encoding_length, + unsigned char *info, size_t *info_len) +{ + WPACKET ipkt = { 0 }; + BUF_MEM *ipkt_mem = NULL; + + if (encoding == NULL || info == NULL || info_len == NULL) + return 0; + if (*info_len < (sizeof(OSSL_ECH_CONTEXT_STRING) + encoding_length)) + return 0; + if ((ipkt_mem = BUF_MEM_new()) == NULL + || !WPACKET_init(&ipkt, ipkt_mem) + || !WPACKET_memcpy(&ipkt, OSSL_ECH_CONTEXT_STRING, + sizeof(OSSL_ECH_CONTEXT_STRING) - 1) + /* + * the zero valued octet is required by the spec, section 7.1 so + * a tiny bit better to add it explicitly rather than depend on + * the context string being NUL terminated + */ + || !WPACKET_put_bytes_u8(&ipkt, 0) + || !WPACKET_memcpy(&ipkt, encoding, encoding_length)) { + WPACKET_cleanup(&ipkt); + BUF_MEM_free(ipkt_mem); + return 0; + } + *info_len = sizeof(OSSL_ECH_CONTEXT_STRING) + encoding_length; + memcpy(info, WPACKET_get_curr(&ipkt) - *info_len, *info_len); + WPACKET_cleanup(&ipkt); + BUF_MEM_free(ipkt_mem); + return 1; +} diff --git a/ssl/ech/ech_internal.c b/ssl/ech/ech_internal.c index 403beb66de590..a8d60102046c3 100644 --- a/ssl/ech/ech_internal.c +++ b/ssl/ech/ech_internal.c @@ -11,11 +11,65 @@ #include #include "../ssl_local.h" #include "ech_local.h" +#include +#include +#include #ifndef OPENSSL_NO_ECH +/* + * Strings used in ECH crypto derivations (odd format for EBCDIC goodness) + */ +/* "ech accept confirmation" */ +static const char OSSL_ECH_ACCEPT_CONFIRM_STRING[] = "\x65\x63\x68\x20\x61\x63\x63\x65\x70\x74\x20\x63\x6f\x6e\x66\x69\x72\x6d\x61\x74\x69\x6f\x6e"; +/* "hrr ech accept confirmation" */ +static const char OSSL_ECH_HRR_CONFIRM_STRING[] = "\x68\x72\x72\x20\x65\x63\x68\x20\x61\x63\x63\x65\x70\x74\x20\x63\x6f\x6e\x66\x69\x72\x6d\x61\x74\x69\x6f\x6e"; + /* ECH internal API functions */ +# ifdef OSSL_ECH_SUPERVERBOSE +/* ascii-hex print a buffer nicely for debug/interop purposes */ +void ossl_ech_pbuf(const char *msg, const unsigned char *buf, const size_t blen) +{ + OSSL_TRACE_BEGIN(TLS) { + if (msg == NULL) { + BIO_printf(trc_out, "msg is NULL\n"); + } else if (buf == NULL || blen == 0) { + BIO_printf(trc_out, "%s: buf is %p\n", msg, (void *)buf); + BIO_printf(trc_out, "%s: blen is %lu\n", msg, (unsigned long)blen); + } else { + BIO_printf(trc_out, "%s (%lu)\n", msg, (unsigned long)blen); + BIO_dump_indent(trc_out, buf, blen, 4); + } + } OSSL_TRACE_END(TLS); + return; +} + +/* trace out transcript */ +void ossl_ech_ptranscript(SSL_CONNECTION *s, const char *msg) +{ + OSSL_TRACE_BEGIN(TLS) { + size_t hdatalen = 0; + unsigned char *hdata = NULL; + unsigned char ddata[EVP_MAX_MD_SIZE]; + size_t ddatalen; + + if (s == NULL) + return; + hdatalen = BIO_get_mem_data(s->s3.handshake_buffer, &hdata); + ossl_ech_pbuf(msg, hdata, hdatalen); + if (s->s3.handshake_dgst != NULL) { + if (ssl_handshake_hash(s, ddata, sizeof(ddata), &ddatalen) == 0) + BIO_printf(trc_out, "ssl_handshake_hash failed\n"); + ossl_ech_pbuf(msg, ddata, ddatalen); + } else { + BIO_printf(trc_out, "handshake_dgst is NULL\n"); + } + } OSSL_TRACE_END(TLS); + return; +} +# endif + static OSSL_ECHSTORE_ENTRY *ossl_echstore_entry_dup(const OSSL_ECHSTORE_ENTRY *orig) { OSSL_ECHSTORE_ENTRY *ret = NULL; @@ -148,7 +202,7 @@ int ossl_ech_conn_init(SSL_CONNECTION *s, SSL_CTX *ctx, s->ext.ech.attempted_cid = OSSL_ECH_config_id_unset; if (s->ext.ech.es != NULL) s->ext.ech.attempted = 1; - if (ctx->options & SSL_OP_ECH_GREASE) + if ((ctx->options & SSL_OP_ECH_GREASE) != 0) s->options |= SSL_OP_ECH_GREASE; return 1; err: @@ -160,4 +214,1194 @@ int ossl_ech_conn_init(SSL_CONNECTION *s, SSL_CTX *ctx, return 0; } +/* + * Assemble the set of ECHConfig values to return as retry-configs. + * The caller (stoc ECH extension handler) needs to OPENSSL_free the rcfgs + * The rcfgs itself is missing the outer length to make it an ECHConfigList + * so the caller adds that using WPACKET functions + */ +int ossl_ech_get_retry_configs(SSL_CONNECTION *s, unsigned char **rcfgs, + size_t *rcfgslen) +{ + OSSL_ECHSTORE *es = NULL; + OSSL_ECHSTORE_ENTRY *ee = NULL; + int i, num = 0; + size_t retslen = 0, encilen = 0; + unsigned char *tmp = NULL, *enci = NULL, *rets = NULL; + + if (s == NULL || rcfgs == NULL || rcfgslen == NULL) + return 0; + es = s->ext.ech.es; + if (es != NULL && es->entries != NULL) + num = sk_OSSL_ECHSTORE_ENTRY_num(es->entries); + for (i = 0; i != num; i++) { + ee = sk_OSSL_ECHSTORE_ENTRY_value(es->entries, i); + if (ee != NULL && ee->for_retry == OSSL_ECH_FOR_RETRY) { + encilen = ee->encoded_len; + if (encilen < 2) + goto err; + encilen -= 2; + enci = ee->encoded + 2; + tmp = (unsigned char *)OPENSSL_realloc(rets, retslen + encilen); + if (tmp == NULL) + goto err; + rets = tmp; + memcpy(rets + retslen, enci, encilen); + retslen += encilen; + } + } + *rcfgs = rets; + *rcfgslen = retslen; + return 1; +err: + OPENSSL_free(rets); + *rcfgs = NULL; + *rcfgslen = 0; + return 0; +} + +/* GREASEy constants */ +# define OSSL_ECH_MAX_GREASE_PUB 0x100 /* buffer size for 'enc' values */ +# define OSSL_ECH_MAX_GREASE_CT 0x200 /* max GREASEy ciphertext we'll emit */ + +/* + * Send a random value that looks like a real ECH. + * + * TODO(ECH): the "best" thing to do here is not yet known. For now, we do + * GREASEing as currently (20241102) done by chrome: + * - always HKDF-SHA256 + * - always AES-128-GCM + * - random config ID, even for requests to same server in same session + * - random enc + * - random looking payload, randomly 144, 176, 208, 240 bytes, no correlation with server + */ +int ossl_ech_send_grease(SSL_CONNECTION *s, WPACKET *pkt) +{ + OSSL_HPKE_SUITE hpke_suite_in = OSSL_HPKE_SUITE_DEFAULT; + OSSL_HPKE_SUITE *hpke_suite_in_p = NULL; + OSSL_HPKE_SUITE hpke_suite = OSSL_HPKE_SUITE_DEFAULT; + size_t pp_at_start = 0, pp_at_end = 0; + size_t senderpub_len = OSSL_ECH_MAX_GREASE_PUB; + size_t cipher_len = 0, cipher_len_jitter = 0; + unsigned char cid, senderpub[OSSL_ECH_MAX_GREASE_PUB]; + unsigned char cipher[OSSL_ECH_MAX_GREASE_CT]; + unsigned char *pp = WPACKET_get_curr(pkt); + + if (s == NULL) + return 0; + if (s->ssl.ctx == NULL) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + return 0; + } + WPACKET_get_total_written(pkt, &pp_at_start); + /* randomly select cipher_len to be one of 144, 176, 208, 244 */ + if (RAND_bytes_ex(s->ssl.ctx->libctx, &cid, 1, + RAND_DRBG_STRENGTH) <= 0) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + return 0; + } + cipher_len_jitter = cid % 4; + cipher_len = 144; + cipher_len += 32 * cipher_len_jitter; + /* generate a random (1 octet) client id */ + if (RAND_bytes_ex(s->ssl.ctx->libctx, &cid, 1, + RAND_DRBG_STRENGTH) <= 0) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + return 0; + } + s->ext.ech.attempted_cid = cid; + hpke_suite_in_p = &hpke_suite; + if (s->ext.ech.grease_suite != NULL) { + if (OSSL_HPKE_str2suite(s->ext.ech.grease_suite, &hpke_suite_in) != 1) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + return 0; + } + hpke_suite_in_p = &hpke_suite_in; + } + if (OSSL_HPKE_get_grease_value(hpke_suite_in_p, &hpke_suite, + senderpub, &senderpub_len, + cipher, cipher_len, + s->ssl.ctx->libctx, NULL) != 1) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + return 0; + } + if (!WPACKET_put_bytes_u16(pkt, s->ext.ech.attempted_type) + || !WPACKET_start_sub_packet_u16(pkt) + || !WPACKET_put_bytes_u8(pkt, OSSL_ECH_OUTER_CH_TYPE) + || !WPACKET_put_bytes_u16(pkt, hpke_suite.kdf_id) + || !WPACKET_put_bytes_u16(pkt, hpke_suite.aead_id) + || !WPACKET_put_bytes_u8(pkt, cid) + || !WPACKET_sub_memcpy_u16(pkt, senderpub, senderpub_len) + || !WPACKET_sub_memcpy_u16(pkt, cipher, cipher_len) + || !WPACKET_close(pkt) + ) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + return 0; + } + /* record the ECH sent so we can re-tx same if we hit an HRR */ + OPENSSL_free(s->ext.ech.sent); + WPACKET_get_total_written(pkt, &pp_at_end); + s->ext.ech.sent_len = pp_at_end - pp_at_start; + s->ext.ech.sent = OPENSSL_malloc(s->ext.ech.sent_len); + if (s->ext.ech.sent == NULL) { + s->ext.ech.sent_len = 0; + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + return 0; + } + memcpy(s->ext.ech.sent, pp, s->ext.ech.sent_len); + s->ext.ech.grease = OSSL_ECH_IS_GREASE; + OSSL_TRACE_BEGIN(TLS) { + BIO_printf(trc_out, "ECH - sending GREASE\n"); + } OSSL_TRACE_END(TLS); + return 1; +} + +/* + * Search the ECH store for one that's a match. If no outer_name was set via + * API then we just take the 1st match where we locally support the HPKE suite. + * If OTOH, an outer_name was provided via API then we prefer the first that + * matches that. Name comparison is via case-insensitive exact matches. + */ +int ossl_ech_pick_matching_cfg(SSL_CONNECTION *s, OSSL_ECHSTORE_ENTRY **ee, + OSSL_HPKE_SUITE *suite) +{ + int namematch = 0, nameoverride = 0, suitematch = 0, num, cind = 0; + unsigned int csuite = 0, tsuite = 0, hnlen = 0; + OSSL_ECHSTORE_ENTRY *lee = NULL, *tee = NULL; + OSSL_ECHSTORE *es = NULL; + char *hn = NULL; + + if (s == NULL || s->ext.ech.es == NULL || ee == NULL || suite == NULL) + return 0; + es = s->ext.ech.es; + if (es->entries == NULL) + return 0; + num = sk_OSSL_ECHSTORE_ENTRY_num(es->entries); + /* allow API-set pref to override */ + hn = s->ext.ech.outer_hostname; + hnlen = (hn == NULL ? 0 : strlen(hn)); + if (hnlen != 0) + nameoverride = 1; + if (s->ext.ech.no_outer == 1) { + hn = NULL; + hnlen = 0; + nameoverride = 1; + } + for (cind = 0; cind != num && (suitematch == 0 || namematch == 0); cind++) { + lee = sk_OSSL_ECHSTORE_ENTRY_value(es->entries, cind); + if (lee == NULL || lee->version != OSSL_ECH_RFCXXXX_VERSION) + continue; + if (nameoverride == 1 && hnlen == 0) { + namematch = 1; + } else { + namematch = 0; + if (hnlen == 0 + || (lee->public_name != NULL + && strlen(lee->public_name) == hnlen + && !OPENSSL_strncasecmp(hn, (char *)lee->public_name, + hnlen))) + namematch = 1; + } + suitematch = 0; + for (csuite = 0; csuite != lee->nsuites && suitematch == 0; csuite++) { + if (OSSL_HPKE_suite_check(lee->suites[csuite]) == 1) { + if (tee == NULL) { /* remember 1st suite match for override */ + tee = lee; + tsuite = csuite; + } + suitematch = 1; + if (namematch == 1) { /* pick this one if both "fit" */ + *suite = lee->suites[csuite]; + *ee = lee; + break; + } + } + } + } + if (nameoverride == 1 && (namematch == 0 || suitematch == 0)) { + *suite = tee->suites[tsuite]; + *ee = tee; + } else if (namematch == 0 || suitematch == 0) { + /* no joy */ + return 0; + } + if (*ee == NULL || (*ee)->pub_len == 0 || (*ee)->pub == NULL) + return 0; + return 1; +} + +/* Make up the ClientHelloInner and EncodedClientHelloInner buffers */ +int ossl_ech_encode_inner(SSL_CONNECTION *s) +{ + int rv = 0; + size_t nraws = 0, ind = 0, innerlen = 0; + unsigned char *innerch_full = NULL; + WPACKET inner = { 0 }; /* "fake" pkt for inner */ + BUF_MEM *inner_mem = NULL; + RAW_EXTENSION *raws = NULL; + + /* basic checks */ + if (s == NULL) + return 0; + if (s->ext.ech.es == NULL || s->clienthello == NULL) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + return 0; + } + if ((inner_mem = BUF_MEM_new()) == NULL + || !WPACKET_init(&inner, inner_mem) + /* We don't add the type and 3-octet header as usually done */ + /* Add ver/rnd/sess-id/suites to buffer */ + || !WPACKET_put_bytes_u16(&inner, s->client_version) + || !WPACKET_memcpy(&inner, s->ext.ech.client_random, SSL3_RANDOM_SIZE) + /* Session ID is forced to zero in the encoded inner */ + || !WPACKET_sub_memcpy_u8(&inner, NULL, 0) + /* Ciphers supported */ + || !WPACKET_start_sub_packet_u16(&inner) + || !ssl_cipher_list_to_bytes(s, SSL_get_ciphers(&s->ssl), &inner) + || !WPACKET_close(&inner) + /* COMPRESSION */ + || !WPACKET_start_sub_packet_u8(&inner) + /* Add the NULL compression method */ + || !WPACKET_put_bytes_u8(&inner, 0) + || !WPACKET_close(&inner)) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + goto err; + } + /* Now handle extensions */ + if (!WPACKET_start_sub_packet_u16(&inner)) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + goto err; + } + /* Grab a pointer to the already constructed extensions */ + raws = s->clienthello->pre_proc_exts; + nraws = s->clienthello->pre_proc_exts_len; + if (raws == NULL || nraws < TLSEXT_IDX_num_builtins) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + goto err; + } + /* We put ECH-compressed stuff first (if any), because we can */ + if (s->ext.ech.n_outer_only > 0) { + if (!WPACKET_put_bytes_u16(&inner, TLSEXT_TYPE_outer_extensions) + || !WPACKET_start_sub_packet_u16(&inner) + /* redundant encoding of more-or-less the same thing */ + || !WPACKET_start_sub_packet_u8(&inner)) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + goto err; + } + /* add the types for each of the compressed extensions now */ + for (ind = 0; ind != s->ext.ech.n_outer_only; ind++) { + if (!WPACKET_put_bytes_u16(&inner, s->ext.ech.outer_only[ind])) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + goto err; + } + } + /* close the 2 sub-packets with the compressed types */ + if (!WPACKET_close(&inner) || !WPACKET_close(&inner)) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + goto err; + } + } + /* now copy the rest, as "proper" exts, into encoded inner */ + for (ind = 0; ind < TLSEXT_IDX_num_builtins; ind++) { + if (raws[ind].present == 0 || ossl_ech_2bcompressed(ind) == 1) + continue; + if (!WPACKET_put_bytes_u16(&inner, raws[ind].type) + || !WPACKET_sub_memcpy_u16(&inner, PACKET_data(&raws[ind].data), + PACKET_remaining(&raws[ind].data))) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + goto err; + } + } + if (!WPACKET_close(&inner) /* close the encoded inner packet */ + || !WPACKET_get_length(&inner, &innerlen)) { /* len for inner CH */ + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + goto err; + } + innerch_full = OPENSSL_malloc(innerlen); + if (innerch_full == NULL) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + goto err; + } + memcpy(innerch_full, inner_mem->data, innerlen); + OPENSSL_free(s->ext.ech.encoded_innerch); + s->ext.ech.encoded_innerch = innerch_full; + s->ext.ech.encoded_innerch_len = innerlen; + /* and clean up */ + rv = 1; +err: + WPACKET_cleanup(&inner); + BUF_MEM_free(inner_mem); + return rv; +} + +/* + * Find ECH acceptance signal in a SH + * hrr is 1 if this is for an HRR, otherwise for SH + * acbuf is (a preallocated) 8 octet buffer + * shbuf is a pointer to the SH buffer + * shlen is the length of the SH buf + * return: 1 for success, 0 otherwise + */ +int ossl_ech_find_confirm(SSL_CONNECTION *s, int hrr, + unsigned char acbuf[OSSL_ECH_SIGNAL_LEN], + const unsigned char *shbuf, const size_t shlen) +{ + PACKET pkt; + const unsigned char *acp = NULL, *pp_tmp; + unsigned int pi_tmp, etype, elen; + int done = 0; + + if (hrr == 0) { + acp = s->s3.server_random + SSL3_RANDOM_SIZE - OSSL_ECH_SIGNAL_LEN; + memcpy(acbuf, acp, OSSL_ECH_SIGNAL_LEN); + return 1; + } else { + if (!PACKET_buf_init(&pkt, shbuf, shlen) + || !PACKET_get_net_2(&pkt, &pi_tmp) + || !PACKET_get_bytes(&pkt, &pp_tmp, SSL3_RANDOM_SIZE) + || !PACKET_get_1(&pkt, &pi_tmp) /* sessid len */ + || !PACKET_get_bytes(&pkt, &pp_tmp, pi_tmp) /* sessid */ + || !PACKET_get_net_2(&pkt, &pi_tmp) /* ciphersuite */ + || !PACKET_get_1(&pkt, &pi_tmp) /* compression */ + || !PACKET_get_net_2(&pkt, &pi_tmp)) /* len(extensions) */ + return 0; + while (PACKET_remaining(&pkt) > 0 && done < 1) { + if (!PACKET_get_net_2(&pkt, &etype) + || !PACKET_get_net_2(&pkt, &elen)) + return 0; + if (etype == TLSEXT_TYPE_ech) { + if (elen != OSSL_ECH_SIGNAL_LEN + || !PACKET_get_bytes(&pkt, &acp, elen)) + return 0; + memcpy(acbuf, acp, elen); + done++; + } else { + if (!PACKET_get_bytes(&pkt, &pp_tmp, elen)) + return 0; + } + } + return done; + } + return 0; +} + +/* + * make up a buffer to use to reset transcript + * for_hrr is 1 if we've just seen HRR, 0 otherwise + * shbuf is the output buffer + * shlen is the length of that buffer + * tbuf is the output buffer + * tlen is the length of that buffer + * chend returns the offset of the end of the last CH in the buffer + * fixedshbuf_len returns the fixed up length of the SH + * return 1 for good, 0 otherwise + */ +int ossl_ech_make_transcript_buffer(SSL_CONNECTION *s, int for_hrr, + const unsigned char *shbuf, size_t shlen, + unsigned char **tbuf, size_t *tlen, + size_t *chend, size_t *fixedshbuf_len) +{ + unsigned char *fixedshbuf = NULL, *hashin = NULL, hashval[EVP_MAX_MD_SIZE]; + unsigned int hashlen = 0, hashin_len = 0; + EVP_MD_CTX *ctx = NULL; + EVP_MD *md = NULL; + WPACKET tpkt = { 0 }, shpkt = { 0 }; + BUF_MEM *tpkt_mem = NULL, *shpkt_mem = NULL; + + /* + * SH preamble has bad length at this point on server + * and is missing on client so we'll fix + */ + if ((shpkt_mem = BUF_MEM_new()) == NULL + || !WPACKET_init(&shpkt, shpkt_mem)) { + BUF_MEM_free(shpkt_mem); + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + goto err; + } + if (!WPACKET_put_bytes_u8(&shpkt, SSL3_MT_SERVER_HELLO) + || (s->server == 1 && !WPACKET_put_bytes_u24(&shpkt, shlen - 4)) + || (s->server == 1 && !WPACKET_memcpy(&shpkt, shbuf + 4, shlen -4)) + || (s->server == 0 && !WPACKET_put_bytes_u24(&shpkt, shlen)) + || (s->server == 0 && !WPACKET_memcpy(&shpkt, shbuf, shlen)) + || !WPACKET_get_length(&shpkt, fixedshbuf_len)) { + BUF_MEM_free(shpkt_mem); + WPACKET_cleanup(&shpkt); + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + goto err; + } + fixedshbuf = (unsigned char *)shpkt_mem->data; + WPACKET_cleanup(&shpkt); +# ifdef OSSL_ECH_SUPERVERBOSE + ossl_ech_pbuf("cx: fixed sh buf", fixedshbuf, *fixedshbuf_len); +# endif + if ((tpkt_mem = BUF_MEM_new()) == NULL + || !WPACKET_init(&tpkt, tpkt_mem)) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + goto err; + } + if (s->hello_retry_request == SSL_HRR_NONE) { + if (!WPACKET_memcpy(&tpkt, s->ext.ech.innerch, + s->ext.ech.innerch_len) + || !WPACKET_get_length(&tpkt, chend) + || !WPACKET_memcpy(&tpkt, fixedshbuf, *fixedshbuf_len) + || !WPACKET_get_length(&tpkt, tlen)) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + goto err; + } + *tbuf = OPENSSL_malloc(*tlen); + if (*tbuf == NULL) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + goto err; + } + memcpy(*tbuf, WPACKET_get_curr(&tpkt) - *tlen, *tlen); + BUF_MEM_free(shpkt_mem); + WPACKET_cleanup(&tpkt); + BUF_MEM_free(tpkt_mem); + return 1; + } + /* everything below only applies if we're at some stage in doing HRR */ + if ((md = (EVP_MD *)ssl_handshake_md(s)) == NULL) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + goto err; + } + if (for_hrr == 0) { /* after 2nd SH rx'd */ + hashin = s->ext.ech.innerch1; + hashin_len = s->ext.ech.innerch1_len; + } else { /* after HRR rx'd */ + hashin = s->ext.ech.innerch; + hashin_len = s->ext.ech.innerch_len; + OPENSSL_free(s->ext.ech.kepthrr); + /* stash this SH/HRR for later */ + s->ext.ech.kepthrr = OPENSSL_malloc(*fixedshbuf_len); + if (s->ext.ech.kepthrr == NULL) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + goto err; + } + memcpy(s->ext.ech.kepthrr, fixedshbuf, *fixedshbuf_len); + s->ext.ech.kepthrr_len = *fixedshbuf_len; + } +# ifdef OSSL_ECH_SUPERVERBOSE + ossl_ech_pbuf("cx: ch2hash", hashin, hashin_len); +# endif + if ((ctx = EVP_MD_CTX_new()) == NULL + || EVP_DigestInit_ex(ctx, md, NULL) <= 0 + || EVP_DigestUpdate(ctx, hashin, hashin_len) <= 0 + || EVP_DigestFinal_ex(ctx, hashval, &hashlen) <= 0) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + goto err; + } + EVP_MD_CTX_free(ctx); + ctx = NULL; + if (!WPACKET_put_bytes_u8(&tpkt, SSL3_MT_MESSAGE_HASH) + || !WPACKET_put_bytes_u24(&tpkt, hashlen) + || !WPACKET_memcpy(&tpkt, hashval, hashlen)) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + goto err; + } + if (for_hrr == 0) { /* after 2nd SH */ + if (!WPACKET_memcpy(&tpkt, s->ext.ech.kepthrr, + s->ext.ech.kepthrr_len) + || !WPACKET_memcpy(&tpkt, s->ext.ech.innerch, + s->ext.ech.innerch_len)) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + goto err; + } + } + if (!WPACKET_get_length(&tpkt, chend) + || !WPACKET_memcpy(&tpkt, fixedshbuf, *fixedshbuf_len) + || !WPACKET_get_length(&tpkt, tlen)) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + goto err; + } + *tbuf = OPENSSL_malloc(*tlen); + if (*tbuf == NULL) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + goto err; + } + memcpy(*tbuf, WPACKET_get_curr(&tpkt) - *tlen, *tlen); + BUF_MEM_free(shpkt_mem); + WPACKET_cleanup(&tpkt); + BUF_MEM_free(tpkt_mem); + return 1; +err: + BUF_MEM_free(shpkt_mem); + BUF_MEM_free(tpkt_mem); + WPACKET_cleanup(&tpkt); + EVP_MD_CTX_free(ctx); + return 0; +} + +/* + * reset the handshake buffer for transcript after ECH is good + * buf is the data to put into the transcript (inner CH if no HRR) + * blen is the length of buf + * return 1 for success + */ +int ossl_ech_reset_hs_buffer(SSL_CONNECTION *s, const unsigned char *buf, + size_t blen) +{ +# ifdef OSSL_ECH_SUPERVERBOSE + ossl_ech_pbuf("RESET transcript to", buf, blen); +# endif + if (s->s3.handshake_buffer != NULL) { + (void)BIO_set_close(s->s3.handshake_buffer, BIO_CLOSE); + BIO_free(s->s3.handshake_buffer); + s->s3.handshake_buffer = NULL; + } + EVP_MD_CTX_free(s->s3.handshake_dgst); + s->s3.handshake_dgst = NULL; + s->s3.handshake_buffer = BIO_new(BIO_s_mem()); + if (s->s3.handshake_buffer == NULL) + return 0; + /* providing nothing at all is a real use (mid-HRR) */ + if (buf != NULL && blen > 0) + BIO_write(s->s3.handshake_buffer, (void *)buf, (int)blen); + return 1; +} + +/* + * To control the number of zeros added after an EncodedClientHello - we pad + * to a target number of octets or, if there are naturally more, to a number + * divisible by the defined increment (we also do the spec-recommended SNI + * padding thing first) + */ +# define OSSL_ECH_PADDING_TARGET 128 /* ECH cleartext padded to at least this */ +# define OSSL_ECH_PADDING_INCREMENT 32 /* ECH padded to a multiple of this */ + +/* + * figure out how much padding for cleartext (on client) + * ee is the chosen ECHConfig + * return overall length to use including padding or zero on error + * + * "Recommended" inner SNI padding scheme as per spec (section 6.1.3) + * Might remove the mnl stuff later - overall message padding seems + * better really, BUT... we might want to keep this if others (e.g. + * browsers) do it so as to not stand out compared to them. + * + * The "+ 9" constant below is from the specifiation and is the + * expansion comparing a string length to an encoded SNI extension. + * Same is true of the 31/32 formula below. + * + * Note that the AEAD tag will be added later, so if we e.g. have + * a padded cleartext of 128 octets, the ciphertext will be 144 + * octets. + */ +static size_t ech_calc_padding(SSL_CONNECTION *s, OSSL_ECHSTORE_ENTRY *ee) +{ + int length_of_padding = 0, length_with_snipadding = 0; + int innersnipadding = 0, length_with_padding = 0; + size_t mnl = 0, clear_len = 0, isnilen = 0; + + if (s == NULL || ee == NULL) + return 0; + mnl = ee->max_name_length; + if (mnl != 0) { + /* do weirder padding if SNI present in inner */ + if (s->ext.hostname != NULL) { + isnilen = strlen(s->ext.hostname) + 9; + innersnipadding = (mnl > isnilen) ? mnl - isnilen : 0; + } else { + innersnipadding = mnl + 9; + } + } + /* padding is after the inner client hello has been encoded */ + length_with_snipadding = innersnipadding + s->ext.ech.encoded_innerch_len; + length_of_padding = 31 - ((length_with_snipadding - 1) % 32); + length_with_padding = s->ext.ech.encoded_innerch_len + + length_of_padding + innersnipadding; + /* + * Finally - make sure final result is longer than padding target + * and a multiple of our padding increment. + * TODO(ECH): This is a local addition - we might take it out if + * it makes us stick out; or if we take out the above more (uselessly:-) + * complicated scheme, we may only need this in the end. + */ + if (length_with_padding % OSSL_ECH_PADDING_INCREMENT) + length_with_padding += OSSL_ECH_PADDING_INCREMENT + - (length_with_padding % OSSL_ECH_PADDING_INCREMENT); + while (length_with_padding < OSSL_ECH_PADDING_TARGET) + length_with_padding += OSSL_ECH_PADDING_INCREMENT; + clear_len = length_with_padding; + OSSL_TRACE_BEGIN(TLS) { + BIO_printf(trc_out, "EAAE: padding: mnl: %zu, lws: %d " + "lop: %d, lwp: %d, clear_len: %zu, orig: %zu\n", + mnl, length_with_snipadding, length_of_padding, + length_with_padding, clear_len, + s->ext.ech.encoded_innerch_len); + } OSSL_TRACE_END(TLS); + return clear_len; +} + +/* + * Calculate AAD and do ECH encryption + * pkt is the packet to send + * return 1 for success, other otherwise + * + * 1. Make up the AAD: the encoded outer, with ECH ciphertext octets zero'd + * 2. Do the encryption + * 3. Put the ECH back into the encoding + * 4. Encode the outer (again!) + */ +int ossl_ech_aad_and_encrypt(SSL_CONNECTION *s, WPACKET *pkt) +{ + int rv = 0, hpke_mode = OSSL_HPKE_MODE_BASE; + OSSL_ECHSTORE_ENTRY *ee = NULL; + OSSL_HPKE_SUITE hpke_suite = OSSL_HPKE_SUITE_DEFAULT; + unsigned char config_id_to_use = 0x00, info[SSL3_RT_MAX_PLAIN_LENGTH]; + unsigned char *clear = NULL, *cipher = NULL, *aad = NULL, *mypub = NULL; + size_t cipherlen = 0, aad_len = 0, lenclen = 0, mypub_len = 0; + size_t info_len = SSL3_RT_MAX_PLAIN_LENGTH, clear_len = 0; + + if (s == NULL) + return 0; + if (s->ext.ech.es == NULL || s->ext.ech.es->entries == NULL + || pkt == NULL || s->ssl.ctx == NULL) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + goto err; + } + rv = ossl_ech_pick_matching_cfg(s, &ee, &hpke_suite); + if (rv != 1 || ee == NULL) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + goto err; + } + s->ext.ech.attempted_type = ee->version; + OSSL_TRACE_BEGIN(TLS) { + BIO_printf(trc_out, "EAAE: selected: version: %4x, config %2x\n", + ee->version, ee->config_id); + } OSSL_TRACE_END(TLS); + config_id_to_use = ee->config_id; /* if requested, use a random config_id instead */ + if ((s->ssl.ctx->options & SSL_OP_ECH_IGNORE_CID) != 0 + || (s->options & SSL_OP_ECH_IGNORE_CID) != 0) { + if (RAND_bytes_ex(s->ssl.ctx->libctx, &config_id_to_use, 1, + RAND_DRBG_STRENGTH) <= 0) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + return 0; + } +# ifdef OSSL_ECH_SUPERVERBOSE + ossl_ech_pbuf("EAAE: random config_id", &config_id_to_use, 1); +# endif + } + s->ext.ech.attempted_cid = config_id_to_use; +# ifdef OSSL_ECH_SUPERVERBOSE + ossl_ech_pbuf("EAAE: peer pub", ee->pub, ee->pub_len); + ossl_ech_pbuf("EAAE: clear", s->ext.ech.encoded_innerch, + s->ext.ech.encoded_innerch_len); + ossl_ech_pbuf("EAAE: ECHConfig", ee->encoded, ee->encoded_len); +# endif + /* + * The AAD is the full outer client hello but with the correct number of + * zeros for where the ECH ciphertext octets will later be placed. So we + * add the ECH extension to the |pkt| but with zeros for ciphertext, that + * forms up the AAD, then after we've encrypted, we'll splice in the actual + * ciphertext. + * Watch out for the "4" offsets that remove the type and 3-octet length + * from the encoded CH as per the spec. + */ + clear_len = ech_calc_padding(s, ee); + if (clear_len == 0) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + goto err; + } + lenclen = OSSL_HPKE_get_public_encap_size(hpke_suite); + if (s->ext.ech.hpke_ctx == NULL) { /* 1st CH */ + if (ossl_ech_make_enc_info(ee->encoded, ee->encoded_len, + info, &info_len) != 1) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + goto err; + } +# ifdef OSSL_ECH_SUPERVERBOSE + ossl_ech_pbuf("EAAE info", info, info_len); +# endif + s->ext.ech.hpke_ctx = OSSL_HPKE_CTX_new(hpke_mode, hpke_suite, + OSSL_HPKE_ROLE_SENDER, + NULL, NULL); + if (s->ext.ech.hpke_ctx == NULL) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + goto err; + } + mypub = OPENSSL_malloc(lenclen); + if (mypub == NULL) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + goto err; + } + mypub_len = lenclen; + rv = OSSL_HPKE_encap(s->ext.ech.hpke_ctx, mypub, &mypub_len, + ee->pub, ee->pub_len, info, info_len); + if (rv != 1) { + OPENSSL_free(mypub); + mypub = NULL; + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + goto err; + } + s->ext.ech.pub = mypub; + s->ext.ech.pub_len = mypub_len; + } else { /* HRR - retrieve public */ + mypub = s->ext.ech.pub; + mypub_len = s->ext.ech.pub_len; + if (mypub == NULL || mypub_len == 0) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + goto err; + } + } +# ifdef OSSL_ECH_SUPERVERBOSE + ossl_ech_pbuf("EAAE: mypub", mypub, mypub_len); + WPACKET_get_total_written(pkt, &aad_len); /* use aad_len for tracing */ + ossl_ech_pbuf("EAAE pkt b4", WPACKET_get_curr(pkt) - aad_len, aad_len); +# endif + cipherlen = OSSL_HPKE_get_ciphertext_size(hpke_suite, clear_len); + if (cipherlen <= clear_len || cipherlen > OSSL_ECH_MAX_PAYLOAD_LEN) { + SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_BAD_EXTENSION); + goto err; + } + cipher = OPENSSL_zalloc(cipherlen); + if (cipher == NULL) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + goto err; + } + if (!WPACKET_put_bytes_u16(pkt, TLSEXT_TYPE_ech) + || !WPACKET_start_sub_packet_u16(pkt) + || !WPACKET_put_bytes_u8(pkt, OSSL_ECH_OUTER_CH_TYPE) + || !WPACKET_put_bytes_u16(pkt, hpke_suite.kdf_id) + || !WPACKET_put_bytes_u16(pkt, hpke_suite.aead_id) + || !WPACKET_put_bytes_u8(pkt, config_id_to_use) + || (s->hello_retry_request == SSL_HRR_PENDING + && !WPACKET_put_bytes_u16(pkt, 0x00)) /* no pub */ + || (s->hello_retry_request != SSL_HRR_PENDING + && !WPACKET_sub_memcpy_u16(pkt, mypub, mypub_len)) + || !WPACKET_sub_memcpy_u16(pkt, cipher, cipherlen) + || !WPACKET_close(pkt) + || !WPACKET_get_total_written(pkt, &aad_len) + || aad_len < 4) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + goto err; + } + aad_len -= 4; /* aad starts after type + 3-octet len */ + aad = WPACKET_get_curr(pkt) - aad_len; + /* + * close the extensions of the CH - we skipped doing this + * earlier when encoding extensions, to allow for adding the + * ECH here (when doing ECH) - see tls_construct_extensions() + * towards the end + */ + if (!WPACKET_close(pkt)) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + goto err; + } +# ifdef OSSL_ECH_SUPERVERBOSE + ossl_ech_pbuf("EAAE: aad", aad, aad_len); +# endif + clear = OPENSSL_zalloc(clear_len); /* zeros incl. padding */ + if (clear == NULL) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + goto err; + } + memcpy(clear, s->ext.ech.encoded_innerch, s->ext.ech.encoded_innerch_len); +# ifdef OSSL_ECH_SUPERVERBOSE + ossl_ech_pbuf("EAAE: padded clear", clear, clear_len); +# endif + rv = OSSL_HPKE_seal(s->ext.ech.hpke_ctx, cipher, &cipherlen, + aad, aad_len, clear, clear_len); + OPENSSL_free(clear); + if (rv != 1) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + goto err; + } +# ifdef OSSL_ECH_SUPERVERBOSE + ossl_ech_pbuf("EAAE: cipher", cipher, cipherlen); + ossl_ech_pbuf("EAAE: hpke mypub", mypub, mypub_len); +# endif + /* splice real ciphertext back in now */ + memcpy(aad + aad_len - cipherlen, cipher, cipherlen); +# ifdef OSSL_ECH_SUPERVERBOSE + /* re-use aad_len for tracing */ + WPACKET_get_total_written(pkt, &aad_len); + ossl_ech_pbuf("EAAE pkt aftr", WPACKET_get_curr(pkt) - aad_len, aad_len); +# endif + OPENSSL_free(cipher); + return 1; +err: + OPENSSL_free(cipher); + return 0; +} + +/* + * print info about the ECH-status of an SSL connection + * out is the BIO to use (e.g. stdout/whatever) + * selector OSSL_ECH_SELECT_ALL or just one of the SSL_ECH values + */ +static void ech_status_print(BIO *out, SSL_CONNECTION *s, int selector) +{ + int num = 0, i, has_priv, for_retry; + size_t j; + time_t secs = 0; + char *pn = NULL, *ec = NULL; + OSSL_ECHSTORE *es = NULL; + +# ifdef OSSL_ECH_SUPERVERBOSE + BIO_printf(out, "ech_status_print\n"); + BIO_printf(out, "s=%p\n", (void *)s); +# endif + BIO_printf(out, "ech_attempted=%d\n", s->ext.ech.attempted); + BIO_printf(out, "ech_attempted_type=0x%4x\n", + s->ext.ech.attempted_type); + if (s->ext.ech.attempted_cid == OSSL_ECH_config_id_unset) + BIO_printf(out, "ech_atttempted_cid is unset\n"); + else + BIO_printf(out, "ech_atttempted_cid=0x%02x\n", + s->ext.ech.attempted_cid); + BIO_printf(out, "ech_done=%d\n", s->ext.ech.done); + BIO_printf(out, "ech_grease=%d\n", s->ext.ech.grease); +# ifdef OSSL_ECH_SUPERVERBOSE + BIO_printf(out, "HRR=%d\n", s->hello_retry_request); +# endif + BIO_printf(out, "ech_backend=%d\n", s->ext.ech.backend); + BIO_printf(out, "ech_success=%d\n", s->ext.ech.success); + es = s->ext.ech.es; + if (es == NULL || es->entries == NULL) { + BIO_printf(out, "ECH cfg=NONE\n"); + } else { + num = sk_OSSL_ECHSTORE_ENTRY_num(es->entries); + BIO_printf(out, "%d ECHConfig values loaded\n", num); + for (i = 0; i != num; i++) { + if (selector != OSSL_ECHSTORE_ALL && selector != i) + continue; + BIO_printf(out, "cfg(%d): ", i); + if (OSSL_ECHSTORE_get1_info(es, i, &secs, &pn, &ec, + &has_priv, &for_retry) != 1) { + OPENSSL_free(pn); /* just in case */ + OPENSSL_free(ec); + continue; + } + BIO_printf(out, "ECH entry: %d public_name: %s age: %d%s\n", + i, pn, (int)secs, has_priv ? " (has private key)" : ""); + BIO_printf(out, "\t%s\n", ec); + OPENSSL_free(pn); + OPENSSL_free(ec); + } + } + if (s->ext.ech.returned) { + BIO_printf(out, "ret="); + for (j = 0; j != s->ext.ech.returned_len; j++) { + if (j != 0 && j % 16 == 0) + BIO_printf(out, "\n "); + BIO_printf(out, "%02x:", (unsigned)(s->ext.ech.returned[j])); + } + BIO_printf(out, "\n"); + } + return; +} + +/* size of string buffer returned via ECH callback */ +# define OSSL_ECH_PBUF_SIZE 8 * 1024 + +/* + * Swap the inner and outer after ECH success on the client + * return 0 for error, 1 for success + */ +int ossl_ech_swaperoo(SSL_CONNECTION *s) +{ + unsigned char *curr_buf = NULL, *new_buf = NULL; + size_t curr_buflen = 0, new_buflen = 0, outer_chlen = 0, other_octets = 0; + + if (s == NULL) + return 0; +# ifdef OSSL_ECH_SUPERVERBOSE + ossl_ech_ptranscript(s, "ech_swaperoo, b4"); +# endif + /* un-stash inner key share */ + if (s->ext.ech.tmp_pkey == NULL) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + return 0; + } + EVP_PKEY_free(s->s3.tmp.pkey); + s->s3.tmp.pkey = s->ext.ech.tmp_pkey; + s->s3.group_id = s->ext.ech.group_id; + s->ext.ech.tmp_pkey = NULL; + /* + * TODO(ECH): I suggest re-factoring transcript handling (which + * is probably needed) after/with the PR that includes the server- + * side ECH code. That should be much easier as at that point the + * full set of tests can be run, whereas for now, we're limited + * to testing the client side really works via bodged s_client + * scripts, so there'd be a bigger risk of breaking something + * subtly if we try re-factor now. + * + * When not doing HRR... fix up the transcript to reflect the inner CH. + * If there's a client hello at the start of the buffer, then that's + * the outer CH and we want to replace that with the inner. We need to + * be careful that there could be a server hello following and can't + * lose that. + * + * For HRR... HRR processing code has already done the necessary. + */ + if (s->hello_retry_request == SSL_HRR_NONE) { + curr_buflen = BIO_get_mem_data(s->s3.handshake_buffer, + &curr_buf); + if (curr_buflen > 4 && curr_buf[0] == SSL3_MT_CLIENT_HELLO) { + outer_chlen = 1 + curr_buf[1] * 256 * 256 + + curr_buf[2] * 256 + curr_buf[3]; + if (outer_chlen > curr_buflen) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + return 0; + } + other_octets = curr_buflen - outer_chlen; + if (other_octets > 0) { + new_buflen = s->ext.ech.innerch_len + other_octets; + new_buf = OPENSSL_malloc(new_buflen); + if (new_buf == NULL) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + return 0; + } + if (s->ext.ech.innerch != NULL) /* asan check added */ + memcpy(new_buf, s->ext.ech.innerch, + s->ext.ech.innerch_len); + memcpy(new_buf + s->ext.ech.innerch_len, + &curr_buf[outer_chlen], other_octets); + } else { + new_buf = s->ext.ech.innerch; + new_buflen = s->ext.ech.innerch_len; + } + } else { + new_buf = s->ext.ech.innerch; + new_buflen = s->ext.ech.innerch_len; + } + /* + * And now reset the handshake transcript to our buffer + * Note ssl3_finish_mac isn't that great a name - that one just + * adds to the transcript but doesn't actually "finish" anything + */ + if (ssl3_init_finished_mac(s) == 0) { + OPENSSL_free(new_buf); + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + return 0; + } + if (ssl3_finish_mac(s, new_buf, new_buflen) == 0) { + OPENSSL_free(new_buf); + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + return 0; + } + OPENSSL_free(new_buf); + } +# ifdef OSSL_ECH_SUPERVERBOSE + ossl_ech_ptranscript(s, "ech_swaperoo, after"); +# endif + /* Declare victory! */ + s->ext.ech.attempted = 1; + s->ext.ech.success = 1; + s->ext.ech.done = 1; + s->ext.ech.grease = OSSL_ECH_NOT_GREASE; + /* time to call an ECH callback, if there's one */ + if (s->ext.ech.es != NULL && s->ext.ech.done == 1 + && s->hello_retry_request != SSL_HRR_PENDING + && s->ext.ech.cb != NULL) { + char pstr[OSSL_ECH_PBUF_SIZE + 1] = { 0 }; + BIO *biom = BIO_new(BIO_s_mem()); + unsigned int cbrv = 0; + + if (biom == NULL) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + return 0; + } + ech_status_print(biom, s, OSSL_ECHSTORE_ALL); + BIO_read(biom, pstr, OSSL_ECH_PBUF_SIZE); + cbrv = s->ext.ech.cb(&s->ssl, pstr); + BIO_free(biom); + if (cbrv != 1) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + return 0; + } + } + return 1; +} + +/* + * do the HKDF for ECH acceptance checking + * md is the h/s hash + * for_hrr is 1 if we're doing a HRR + * hashval/hashlen is the transcript hash + * hoval is the output, with the ECH acceptance signal + * return 1 for good, 0 for error + */ +static int ech_hkdf_extract_wrap(SSL_CONNECTION *s, EVP_MD *md, int for_hrr, + unsigned char *hashval, size_t hashlen, + unsigned char hoval[OSSL_ECH_SIGNAL_LEN]) +{ + int rv = 0; + unsigned char notsecret[EVP_MAX_MD_SIZE], zeros[EVP_MAX_MD_SIZE]; + size_t retlen = 0, labellen = 0; + EVP_PKEY_CTX *pctx = NULL; + const char *label = NULL; + unsigned char *p = NULL; + + if (for_hrr == 1) { + label = OSSL_ECH_HRR_CONFIRM_STRING; + } else { + label = OSSL_ECH_ACCEPT_CONFIRM_STRING; + } + labellen = strlen(label); +# ifdef OSSL_ECH_SUPERVERBOSE + ossl_ech_pbuf("cc: label", (unsigned char *)label, labellen); +# endif + memset(zeros, 0, EVP_MAX_MD_SIZE); + /* We don't seem to have an hkdf-extract that's exposed by libcrypto */ + pctx = EVP_PKEY_CTX_new_id(EVP_PKEY_HKDF, NULL); + if (pctx == NULL + || EVP_PKEY_derive_init(pctx) != 1 + || EVP_PKEY_CTX_hkdf_mode(pctx, + EVP_PKEY_HKDEF_MODE_EXTRACT_ONLY) != 1 + || EVP_PKEY_CTX_hkdf_mode(pctx, + EVP_PKEY_HKDEF_MODE_EXTRACT_ONLY) != 1 + || EVP_PKEY_CTX_set_hkdf_md(pctx, md) != 1) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + goto err; + } + /* pick correct client_random */ + if (s->server) + p = s->s3.client_random; + else + p = s->ext.ech.client_random; +# ifdef OSSL_ECH_SUPERVERBOSE + ossl_ech_pbuf("cc: client_random", p, SSL3_RANDOM_SIZE); +# endif + if (EVP_PKEY_CTX_set1_hkdf_key(pctx, p, SSL3_RANDOM_SIZE) != 1 + || EVP_PKEY_CTX_set1_hkdf_salt(pctx, zeros, hashlen) != 1 + || EVP_PKEY_derive(pctx, NULL, &retlen) != 1 + || hashlen != retlen + || EVP_PKEY_derive(pctx, notsecret, &retlen) != 1) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + goto err; + } +# ifdef OSSL_ECH_SUPERVERBOSE + ossl_ech_pbuf("cc: notsecret", notsecret, hashlen); +# endif + if (hashlen < OSSL_ECH_SIGNAL_LEN + || !tls13_hkdf_expand(s, md, notsecret, + (const unsigned char *)label, labellen, + hashval, hashlen, hoval, + OSSL_ECH_SIGNAL_LEN, 1)) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + goto err; + } + rv = 1; +err: + EVP_PKEY_CTX_free(pctx); + return rv; +} + +/* + * ECH accept_confirmation calculation + * for_hrr is 1 if this is for an HRR, otherwise for SH + * ac is (a caller allocated) 8 octet buffer + * shbuf is a pointer to the SH buffer (incl. the type+3-octet length) + * shlen is the length of the SH buf + * return: 1 for success, 0 otherwise + * + * This is a magic value in the ServerHello.random lower 8 octets + * that is used to signal that the inner worked. + * + * As per spec: + * + * accept_confirmation = HKDF-Expand-Label( + * HKDF-Extract(0, ClientHelloInner.random), + * "ech accept confirmation", + * transcript_ech_conf, + * 8) + * + * transcript_ech_conf = ClientHelloInner..ServerHello + * with last 8 octets of ServerHello.random==0x00 + * + * and with differences due to HRR + */ +int ossl_ech_calc_confirm(SSL_CONNECTION *s, int for_hrr, + unsigned char acbuf[OSSL_ECH_SIGNAL_LEN], + const unsigned char *shbuf, const size_t shlen) +{ + int rv = 0; + EVP_MD_CTX *ctx = NULL; + EVP_MD *md = NULL; + unsigned char *tbuf = NULL, *conf_loc = NULL; + unsigned char *fixedshbuf = NULL; + size_t fixedshbuf_len = 0, tlen = 0, chend = 0; + size_t shoffset = 6 + 24, extoffset = 0, echoffset = 0; + uint16_t echtype; + unsigned int hashlen = 0; + unsigned char hashval[EVP_MAX_MD_SIZE], hoval[EVP_MAX_MD_SIZE]; + + if ((md = (EVP_MD *)ssl_handshake_md(s)) == NULL) + goto err; + if (ossl_ech_make_transcript_buffer(s, for_hrr, shbuf, shlen, &tbuf, &tlen, + &chend, &fixedshbuf_len) != 1) + goto err; /* SSLfatal called already */ +# ifdef OSSL_ECH_SUPERVERBOSE + ossl_ech_pbuf("cx: tbuf b4", tbuf, tlen); +# endif + /* put zeros in correct place */ + if (for_hrr == 0) { /* zap magic octets at fixed place for SH */ + conf_loc = tbuf + chend + shoffset; + } else { + if (s->server == 1) { /* we get to say where we put ECH:-) */ + conf_loc = tbuf + tlen - OSSL_ECH_SIGNAL_LEN; + } else { + if (ossl_ech_get_sh_offsets(shbuf, shlen, &extoffset, + &echoffset, &echtype) != 1) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, SSL_R_ECH_REQUIRED); + goto err; + } + if (echoffset == 0 || extoffset == 0 || echtype == 0 + || tlen < (chend + 4 + echoffset + 4 + OSSL_ECH_SIGNAL_LEN)) { + /* No ECH found so we'll exit, but set random output */ + if (RAND_bytes_ex(s->ssl.ctx->libctx, acbuf, + OSSL_ECH_SIGNAL_LEN, + RAND_DRBG_STRENGTH) <= 0) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, SSL_R_ECH_REQUIRED); + goto err; + } + rv = 1; + goto err; + } + conf_loc = tbuf + chend + 4 + echoffset + 4; + } + } + memset(conf_loc, 0, OSSL_ECH_SIGNAL_LEN); +# ifdef OSSL_ECH_SUPERVERBOSE + ossl_ech_pbuf("cx: tbuf after", tbuf, tlen); +# endif + if ((ctx = EVP_MD_CTX_new()) == NULL + || EVP_DigestInit_ex(ctx, md, NULL) <= 0 + || EVP_DigestUpdate(ctx, tbuf, tlen) <= 0 + || EVP_DigestFinal_ex(ctx, hashval, &hashlen) <= 0) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + goto err; + } + EVP_MD_CTX_free(ctx); + ctx = NULL; +# ifdef OSSL_ECH_SUPERVERBOSE + ossl_ech_pbuf("cx: hashval", hashval, hashlen); +# endif + if (ech_hkdf_extract_wrap(s, md, for_hrr, hashval, hashlen, hoval) != 1) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + goto err; + } + memcpy(acbuf, hoval, OSSL_ECH_SIGNAL_LEN); /* Finally, set the output */ +# ifdef OSSL_ECH_SUPERVERBOSE + ossl_ech_pbuf("cx: result", acbuf, OSSL_ECH_SIGNAL_LEN); +# endif + /* put confirm value back into transcript vars */ + if (s->hello_retry_request != SSL_HRR_NONE && s->ext.ech.kepthrr != NULL + && for_hrr == 1 && s->server == 1) + memcpy(s->ext.ech.kepthrr + s->ext.ech.kepthrr_len + - OSSL_ECH_SIGNAL_LEN, acbuf, OSSL_ECH_SIGNAL_LEN); + memcpy(conf_loc, acbuf, OSSL_ECH_SIGNAL_LEN); + /* on a server, we need to reset the hs buffer now */ + if (s->server && s->hello_retry_request == SSL_HRR_NONE) + ossl_ech_reset_hs_buffer(s, s->ext.ech.innerch, s->ext.ech.innerch_len); + if (s->server && s->hello_retry_request == SSL_HRR_COMPLETE) + ossl_ech_reset_hs_buffer(s, tbuf, tlen - fixedshbuf_len); + rv = 1; +err: + OPENSSL_free(fixedshbuf); + OPENSSL_free(tbuf); + EVP_MD_CTX_free(ctx); + return rv; +} #endif diff --git a/ssl/ech/ech_local.h b/ssl/ech/ech_local.h index 387fe713b60c9..fdf7d45b0f66e 100644 --- a/ssl/ech/ech_local.h +++ b/ssl/ech/ech_local.h @@ -41,7 +41,22 @@ /* value for not yet set ECH config_id */ # define OSSL_ECH_config_id_unset -1 +# define OSSL_ECH_OUTER_CH_TYPE 0 /* outer ECHClientHello enum */ +# define OSSL_ECH_INNER_CH_TYPE 1 /* inner ECHClientHello enum */ + # define OSSL_ECH_CIPHER_LEN 4 /* ECHCipher length (2 for kdf, 2 for aead) */ + +# define OSSL_ECH_SIGNAL_LEN 8 /* length of ECH acceptance signal */ + +# ifndef CLIENT_VERSION_LEN +/* + * This is the legacy version length, i.e. len(0x0303). The same + * label is used in e.g. test/sslapitest.c and elsewhere but not + * defined in a header file I could find. + */ +# define CLIENT_VERSION_LEN 2 +# endif + /* * Reminder of what goes in DNS for ECH RFC XXXX * @@ -84,7 +99,7 @@ typedef struct ossl_echext_st { DEFINE_STACK_OF(OSSL_ECHEXT) typedef struct ossl_echstore_entry_st { - uint16_t version; /* 0xff0d for draft-13 */ + uint16_t version; /* 0xfe0d for RFC XXXX */ char *public_name; size_t pub_len; unsigned char *pub; @@ -133,14 +148,19 @@ typedef struct ossl_ech_conn_st { */ char *former_inner; /* - * TODO(ECH): The next 4 buffers (and lengths) may change later - * if a better way to handle the mutiple transcripts needed is - * suggested/invented. I'd suggest we review these when that code - * is part of a PR (which won't be for a few PR's yet.) + * TODO(ECH): The next 4 buffers (and lengths) may change if a + * better way to handle the mutiple transcripts needed is + * suggested/invented. I suggest re-factoring transcript handling + * (which is probably needed) after/with the PR that includes the + * server-side ECH code. That should be much easier as at that point + * the full set of tests can be run, whereas for now, we're limited + * to testing the client side really works via bodged s_client + * scripts, so there'd be a bigger risk of breaking something + * subtly if we try re-factor now. */ /* * encoded inner ClientHello before/after ECH compression, which` - * is nitty/complex (to avoid repeating the same extenstion value + * is nitty/complex (to avoid repeating the same extension value * in outer and inner, thus saving bandwidth) but (re-)calculating * the compression is a pain, so we'll store those as we make them */ @@ -174,7 +194,7 @@ typedef struct ossl_ech_conn_st { * to avoid the need to change a couple of extension APIs. * TODO(ECH): check if there's another way to get that value */ - size_t ext_ind; + int ext_ind; /* ECH status vars */ int ch_depth; /* set during CH creation, 0: doing outer, 1: doing inner */ int attempted; /* 1 if ECH was or is being attempted, 0 otherwise */ @@ -207,6 +227,39 @@ typedef struct ossl_ech_conn_st { unsigned char client_random[SSL3_RANDOM_SIZE]; /* CH random */ } OSSL_ECH_CONN; +/* Return values from ossl_ech_same_ext */ +# define OSSL_ECH_SAME_EXT_ERR 0 /* bummer something wrong */ +# define OSSL_ECH_SAME_EXT_DONE 1 /* proceed with same value in inner/outer */ +# define OSSL_ECH_SAME_EXT_CONTINUE 2 /* generate a new value for outer CH */ + +/* + * During extension construction (in extensions_clnt.c and surprisingly also in + * extensions.c), we need to handle inner/outer CH cloning - ossl_ech_same_ext + * will (depending on compile time handling options) copy the value from + * CH.inner to CH.outer or else processing will continue, for a 2nd call, + * likely generating a fresh value for the outer CH. The fresh value could well + * be the same as in the inner. + * + * This macro should be called in each _ctos_ function that doesn't explicitly + * have special ECH handling. + * + * Note that the placement of this macro needs a bit of thought - it has to go + * after declarations (to keep the ansi-c compile happy) and also after any + * checks that result in the extension not being sent but before any relevant + * state changes that would affect a possible 2nd call to the constructor. + * Luckily, that's usually not too hard, but it's not mechanical. + */ +# define ECH_SAME_EXT(s, pkt) \ + if (s->ext.ech.es != NULL && s->ext.ech.grease == 0) { \ + int ech_iosame_rv = ossl_ech_same_ext(s, pkt); \ + \ + if (ech_iosame_rv == OSSL_ECH_SAME_EXT_ERR) \ + return EXT_RETURN_FAIL; \ + if (ech_iosame_rv == OSSL_ECH_SAME_EXT_DONE) \ + return EXT_RETURN_SENT; \ + /* otherwise continue as normal */ \ + } + /* Internal ECH APIs */ OSSL_ECHSTORE *ossl_echstore_dup(const OSSL_ECHSTORE *old); @@ -217,6 +270,36 @@ int ossl_ech_conn_init(SSL_CONNECTION *s, SSL_CTX *ctx, void ossl_ech_conn_clear(OSSL_ECH_CONN *ec); void ossl_echext_free(OSSL_ECHEXT *e); OSSL_ECHEXT *ossl_echext_dup(const OSSL_ECHEXT *src); +# ifdef OSSL_ECH_SUPERVERBOSE +void ossl_ech_pbuf(const char *msg, + const unsigned char *buf, const size_t blen); +void ossl_ech_ptranscript(SSL_CONNECTION *s, const char *msg); +# endif +int ossl_ech_get_retry_configs(SSL_CONNECTION *s, unsigned char **rcfgs, + size_t *rcfgslen); +int ossl_ech_send_grease(SSL_CONNECTION *s, WPACKET *pkt); +int ossl_ech_pick_matching_cfg(SSL_CONNECTION *s, OSSL_ECHSTORE_ENTRY **ee, + OSSL_HPKE_SUITE *suite); +int ossl_ech_encode_inner(SSL_CONNECTION *s); +int ossl_ech_find_confirm(SSL_CONNECTION *s, int hrr, + unsigned char acbuf[OSSL_ECH_SIGNAL_LEN], + const unsigned char *shbuf, const size_t shlen); +int ossl_ech_make_transcript_buffer(SSL_CONNECTION *s, int for_hrr, + const unsigned char *shbuf, size_t shlen, + unsigned char **tbuf, size_t *tlen, + size_t *chend, size_t *fixedshbuf_len); +int ossl_ech_reset_hs_buffer(SSL_CONNECTION *s, const unsigned char *buf, + size_t blen); +int ossl_ech_aad_and_encrypt(SSL_CONNECTION *s, WPACKET *pkt); +int ossl_ech_swaperoo(SSL_CONNECTION *s); +int ossl_ech_calc_confirm(SSL_CONNECTION *s, int for_hrr, + unsigned char acbuf[OSSL_ECH_SIGNAL_LEN], + const unsigned char *shbuf, const size_t shlen); + +/* these are internal but located in ssl/statem/extensions.c */ +int ossl_ech_same_ext(SSL_CONNECTION *s, WPACKET *pkt); +int ossl_ech_same_key_share(void); +int ossl_ech_2bcompressed(int ind); # endif #endif diff --git a/ssl/ech/ech_store.c b/ssl/ech/ech_store.c index 653b99f3494da..5d2781fbf4bcf 100644 --- a/ssl/ech/ech_store.c +++ b/ssl/ech/ech_store.c @@ -1124,7 +1124,7 @@ int OSSL_ECHSTORE_flush_keys(OSSL_ECHSTORE *es, time_t age) ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_INVALID_ARGUMENT); return 0; } - if (ee->keyshare != NULL && ((ee->loadtime + age) > now)) { + if (ee->keyshare != NULL && ee->loadtime + age >= now) { ossl_echstore_entry_free(ee); sk_OSSL_ECHSTORE_ENTRY_delete(es->entries, i); } diff --git a/ssl/ssl_ciph.c b/ssl/ssl_ciph.c index 2c35f31065cf2..eee0dd14e61d8 100644 --- a/ssl/ssl_ciph.c +++ b/ssl/ssl_ciph.c @@ -2262,3 +2262,107 @@ const char *OSSL_default_ciphersuites(void) "TLS_CHACHA20_POLY1305_SHA256:" "TLS_AES_128_GCM_SHA256"; } + +int ssl_cipher_list_to_bytes(SSL_CONNECTION *s, STACK_OF(SSL_CIPHER) *sk, + WPACKET *pkt) +{ + int i; + size_t totlen = 0, len, maxlen, maxverok = 0; + int empty_reneg_info_scsv = !s->renegotiate + && !SSL_CONNECTION_IS_DTLS(s) + && ssl_security(s, SSL_SECOP_VERSION, 0, TLS1_VERSION, NULL) + && s->min_proto_version <= TLS1_VERSION; + SSL *ssl = SSL_CONNECTION_GET_SSL(s); + + /* Set disabled masks for this session */ + if (!ssl_set_client_disabled(s)) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, SSL_R_NO_PROTOCOLS_AVAILABLE); + return 0; + } + + if (sk == NULL) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + return 0; + } + +#ifdef OPENSSL_MAX_TLS1_2_CIPHER_LENGTH +# if OPENSSL_MAX_TLS1_2_CIPHER_LENGTH < 6 +# error Max cipher length too short +# endif + /* + * Some servers hang if client hello > 256 bytes as hack workaround + * chop number of supported ciphers to keep it well below this if we + * use TLS v1.2 + */ + if (TLS1_get_version(ssl) >= TLS1_2_VERSION) + maxlen = OPENSSL_MAX_TLS1_2_CIPHER_LENGTH & ~1; + else +#endif + /* Maximum length that can be stored in 2 bytes. Length must be even */ + maxlen = 0xfffe; + + if (empty_reneg_info_scsv) + maxlen -= 2; + if (s->mode & SSL_MODE_SEND_FALLBACK_SCSV) + maxlen -= 2; + + for (i = 0; i < sk_SSL_CIPHER_num(sk) && totlen < maxlen; i++) { + const SSL_CIPHER *c; + + c = sk_SSL_CIPHER_value(sk, i); + /* Skip disabled ciphers */ + if (ssl_cipher_disabled(s, c, SSL_SECOP_CIPHER_SUPPORTED, 0)) + continue; + + if (!ssl->method->put_cipher_by_char(c, pkt, &len)) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + return 0; + } + + /* Sanity check that the maximum version we offer has ciphers enabled */ + if (!maxverok) { + int minproto = SSL_CONNECTION_IS_DTLS(s) ? c->min_dtls : c->min_tls; + int maxproto = SSL_CONNECTION_IS_DTLS(s) ? c->max_dtls : c->max_tls; + + if (ssl_version_cmp(s, maxproto, s->s3.tmp.max_ver) >= 0 + && ssl_version_cmp(s, minproto, s->s3.tmp.max_ver) <= 0) + maxverok = 1; + } + + totlen += len; + } + + if (totlen == 0 || !maxverok) { + const char *maxvertext = + !maxverok + ? "No ciphers enabled for max supported SSL/TLS version" + : NULL; + + SSLfatal_data(s, SSL_AD_INTERNAL_ERROR, SSL_R_NO_CIPHERS_AVAILABLE, + maxvertext); + return 0; + } + + if (totlen != 0) { + if (empty_reneg_info_scsv) { + static const SSL_CIPHER scsv = { + 0, NULL, NULL, SSL3_CK_SCSV, 0, 0, 0, 0, 0, 0, 0, 0, 0 + }; + if (!ssl->method->put_cipher_by_char(&scsv, pkt, &len)) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + return 0; + } + } + if (s->mode & SSL_MODE_SEND_FALLBACK_SCSV) { + static const SSL_CIPHER scsv = { + 0, NULL, NULL, SSL3_CK_FALLBACK_SCSV, 0, 0, 0, 0, 0, 0, 0, 0, 0 + }; + if (!ssl->method->put_cipher_by_char(&scsv, pkt, &len)) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + return 0; + } + } + } + + return 1; +} diff --git a/ssl/ssl_local.h b/ssl/ssl_local.h index 66a5cad13ac9d..5cb16d1b7536d 100644 --- a/ssl/ssl_local.h +++ b/ssl/ssl_local.h @@ -691,6 +691,8 @@ typedef enum tlsext_index_en { TLSEXT_IDX_compress_certificate, TLSEXT_IDX_early_data, TLSEXT_IDX_certificate_authorities, + TLSEXT_IDX_ech, + TLSEXT_IDX_outer_extensions, TLSEXT_IDX_padding, TLSEXT_IDX_psk, /* Dummy index - must always be the last entry */ @@ -2633,6 +2635,8 @@ __owur STACK_OF(SSL_CIPHER) *ssl_get_ciphers_by_id(SSL_CONNECTION *sc); __owur int ssl_x509err2alert(int type); void ssl_sort_cipher_list(void); int ssl_load_ciphers(SSL_CTX *ctx); +int ssl_cipher_list_to_bytes(SSL_CONNECTION *s, STACK_OF(SSL_CIPHER) *sk, + WPACKET *pkt); __owur int ssl_setup_sigalgs(SSL_CTX *ctx); int ssl_load_groups(SSL_CTX *ctx); int ssl_load_sigalgs(SSL_CTX *ctx); diff --git a/ssl/ssl_stat.c b/ssl/ssl_stat.c index d6ba000c65d45..01e0fcd3b0550 100644 --- a/ssl/ssl_stat.c +++ b/ssl/ssl_stat.c @@ -333,6 +333,10 @@ const char *SSL_alert_desc_string(int value) return "BH"; case TLS1_AD_UNKNOWN_PSK_IDENTITY: return "UP"; +#ifndef OPENSSL_NO_ECH + case TLS1_AD_ECH_REQUIRED: + return "RR"; +#endif default: return "UK"; } @@ -403,6 +407,10 @@ const char *SSL_alert_desc_string_long(int value) return "unknown PSK identity"; case TLS1_AD_NO_APPLICATION_PROTOCOL: return "no application protocol"; +#ifndef OPENSSL_NO_ECH + case TLS1_AD_ECH_REQUIRED: + return "ECH required"; +#endif default: return "unknown"; } diff --git a/ssl/statem/extensions.c b/ssl/statem/extensions.c index 59587fedbe9b6..5e8fc4cb9d471 100644 --- a/ssl/statem/extensions.c +++ b/ssl/statem/extensions.c @@ -21,6 +21,40 @@ #include #include +/* + * values for ext_defs ech_handling field + * exceptionally, we don't conditionally compile that field to avoid a pile of + * fndefs all over the ext_defs values + */ +#define OSSL_ECH_HANDLING_CALL_BOTH 1 /* call constructor both times */ +#define OSSL_ECH_HANDLING_COMPRESS 2 /* compress outer value into inner */ +#define OSSL_ECH_HANDLING_DUPLICATE 3 /* same value in inner and outer */ +/* + * DUPLICATE isn't really useful other than to show we can, + * and for debugging/tests/coverage so may disappear. Changes mostly + * won't affect the outer CH size, due to padding, but might for some + * larger extensions. + * + * Note there is a co-dependency with test/recipies/75-test_quicapi.t: + * If you change an |ech_handling| value, that may well affect the order + * of extensions in a ClientHello, which is reflected in the test data + * in test/recipies/75-test_quicapi_data/\*.txt files. To fix, you need + * to look in test-runs/test_quicapi for the "new" files and then edit + * (replacing actual octets with "?" in relevant places), and copy the + * result back over to test/recipies/75-test_quicapi_data/. The reason + * this happens is the ECH COMPRESS'd extensions need to be contiguous + * in the ClientHello, so changes to/from COMPRESS affect extension + * order, in inner and outer CH. There doesn't seem to be an easy, + * generic, way to reconcile these compile-time changes with having + * fixed value test files. Likely the best option is to decide on the + * disposition of ECH COMPRESS or not and consider that an at least + * medium-term thing. (But still allow other builds to vary at + * compile time if they need something different.) + */ +#ifndef OPENSSL_NO_ECH +static int init_ech(SSL_CONNECTION *s, unsigned int context); +#endif /* OPENSSL_NO_ECH */ + static int final_renegotiate(SSL_CONNECTION *s, unsigned int context, int sent); static int init_server_name(SSL_CONNECTION *s, unsigned int context); static int final_server_name(SSL_CONNECTION *s, unsigned int context, int sent); @@ -86,6 +120,11 @@ typedef struct extensions_definition_st { * protocol versions */ unsigned int context; + /* + * exceptionally, we don't conditionally compile this field to avoid a pile of + * fndefs all over the ext_defs values + */ + int ech_handling; /* how to handle ECH for this extension type */ /* * Initialise extension before parsing. Always called for relevant contexts * even if extension not present @@ -140,12 +179,14 @@ typedef struct extensions_definition_st { * NOTE: WebSphere Application Server 7+ cannot handle empty extensions at * the end, keep these extensions before signature_algorithm. */ -#define INVALID_EXTENSION { TLSEXT_TYPE_invalid, 0, NULL, NULL, NULL, NULL, NULL, NULL } +#define INVALID_EXTENSION { TLSEXT_TYPE_invalid, 0, 0, NULL, NULL, NULL, NULL, NULL, NULL } + static const EXTENSION_DEFINITION ext_defs[] = { { TLSEXT_TYPE_renegotiate, SSL_EXT_CLIENT_HELLO | SSL_EXT_TLS1_2_SERVER_HELLO | SSL_EXT_SSL3_ALLOWED | SSL_EXT_TLS1_2_AND_BELOW_ONLY, + OSSL_ECH_HANDLING_COMPRESS, NULL, tls_parse_ctos_renegotiate, tls_parse_stoc_renegotiate, tls_construct_stoc_renegotiate, tls_construct_ctos_renegotiate, final_renegotiate @@ -154,6 +195,7 @@ static const EXTENSION_DEFINITION ext_defs[] = { TLSEXT_TYPE_server_name, SSL_EXT_CLIENT_HELLO | SSL_EXT_TLS1_2_SERVER_HELLO | SSL_EXT_TLS1_3_ENCRYPTED_EXTENSIONS, + OSSL_ECH_HANDLING_CALL_BOTH, init_server_name, tls_parse_ctos_server_name, tls_parse_stoc_server_name, tls_construct_stoc_server_name, tls_construct_ctos_server_name, @@ -163,6 +205,7 @@ static const EXTENSION_DEFINITION ext_defs[] = { TLSEXT_TYPE_max_fragment_length, SSL_EXT_CLIENT_HELLO | SSL_EXT_TLS1_2_SERVER_HELLO | SSL_EXT_TLS1_3_ENCRYPTED_EXTENSIONS, + OSSL_ECH_HANDLING_COMPRESS, NULL, tls_parse_ctos_maxfragmentlen, tls_parse_stoc_maxfragmentlen, tls_construct_stoc_maxfragmentlen, tls_construct_ctos_maxfragmentlen, final_maxfragmentlen @@ -171,6 +214,7 @@ static const EXTENSION_DEFINITION ext_defs[] = { { TLSEXT_TYPE_srp, SSL_EXT_CLIENT_HELLO | SSL_EXT_TLS1_2_AND_BELOW_ONLY, + OSSL_ECH_HANDLING_COMPRESS, init_srp, tls_parse_ctos_srp, NULL, NULL, tls_construct_ctos_srp, NULL }, #else @@ -180,6 +224,7 @@ static const EXTENSION_DEFINITION ext_defs[] = { TLSEXT_TYPE_ec_point_formats, SSL_EXT_CLIENT_HELLO | SSL_EXT_TLS1_2_SERVER_HELLO | SSL_EXT_TLS1_2_AND_BELOW_ONLY, + OSSL_ECH_HANDLING_COMPRESS, init_ec_point_formats, tls_parse_ctos_ec_pt_formats, tls_parse_stoc_ec_pt_formats, tls_construct_stoc_ec_pt_formats, tls_construct_ctos_ec_pt_formats, final_ec_pt_formats @@ -213,6 +258,7 @@ static const EXTENSION_DEFINITION ext_defs[] = { TLSEXT_TYPE_supported_groups, SSL_EXT_CLIENT_HELLO | SSL_EXT_TLS1_3_ENCRYPTED_EXTENSIONS | SSL_EXT_TLS1_2_SERVER_HELLO, + OSSL_ECH_HANDLING_COMPRESS, NULL, tls_parse_ctos_supported_groups, NULL, tls_construct_stoc_supported_groups, tls_construct_ctos_supported_groups, NULL @@ -221,6 +267,7 @@ static const EXTENSION_DEFINITION ext_defs[] = { TLSEXT_TYPE_session_ticket, SSL_EXT_CLIENT_HELLO | SSL_EXT_TLS1_2_SERVER_HELLO | SSL_EXT_TLS1_2_AND_BELOW_ONLY, + OSSL_ECH_HANDLING_COMPRESS, init_session_ticket, tls_parse_ctos_session_ticket, tls_parse_stoc_session_ticket, tls_construct_stoc_session_ticket, tls_construct_ctos_session_ticket, NULL @@ -230,6 +277,7 @@ static const EXTENSION_DEFINITION ext_defs[] = { TLSEXT_TYPE_status_request, SSL_EXT_CLIENT_HELLO | SSL_EXT_TLS1_2_SERVER_HELLO | SSL_EXT_TLS1_3_CERTIFICATE | SSL_EXT_TLS1_3_CERTIFICATE_REQUEST, + OSSL_ECH_HANDLING_COMPRESS, init_status_request, tls_parse_ctos_status_request, tls_parse_stoc_status_request, tls_construct_stoc_status_request, tls_construct_ctos_status_request, NULL @@ -242,6 +290,7 @@ static const EXTENSION_DEFINITION ext_defs[] = { TLSEXT_TYPE_next_proto_neg, SSL_EXT_CLIENT_HELLO | SSL_EXT_TLS1_2_SERVER_HELLO | SSL_EXT_TLS1_2_AND_BELOW_ONLY, + OSSL_ECH_HANDLING_COMPRESS, init_npn, tls_parse_ctos_npn, tls_parse_stoc_npn, tls_construct_stoc_next_proto_neg, tls_construct_ctos_npn, NULL }, @@ -256,6 +305,7 @@ static const EXTENSION_DEFINITION ext_defs[] = { TLSEXT_TYPE_application_layer_protocol_negotiation, SSL_EXT_CLIENT_HELLO | SSL_EXT_TLS1_2_SERVER_HELLO | SSL_EXT_TLS1_3_ENCRYPTED_EXTENSIONS, + OSSL_ECH_HANDLING_CALL_BOTH, init_alpn, tls_parse_ctos_alpn, tls_parse_stoc_alpn, tls_construct_stoc_alpn, tls_construct_ctos_alpn, final_alpn }, @@ -264,6 +314,7 @@ static const EXTENSION_DEFINITION ext_defs[] = { TLSEXT_TYPE_use_srtp, SSL_EXT_CLIENT_HELLO | SSL_EXT_TLS1_2_SERVER_HELLO | SSL_EXT_TLS1_3_ENCRYPTED_EXTENSIONS | SSL_EXT_DTLS_ONLY, + OSSL_ECH_HANDLING_COMPRESS, init_srtp, tls_parse_ctos_use_srtp, tls_parse_stoc_use_srtp, tls_construct_stoc_use_srtp, tls_construct_ctos_use_srtp, NULL }, @@ -274,6 +325,15 @@ static const EXTENSION_DEFINITION ext_defs[] = { TLSEXT_TYPE_encrypt_then_mac, SSL_EXT_CLIENT_HELLO | SSL_EXT_TLS1_2_SERVER_HELLO | SSL_EXT_TLS1_2_AND_BELOW_ONLY, + /* + * If you want to demonstrate/exercise duplicate, then + * this does that and has no effect on sizes, but it + * will break the quicapi test (see above). Probably + * best done in local tests and not comitted to any + * upstream. + * OSSL_ECH_HANDLING_DUPLICATE, + */ + OSSL_ECH_HANDLING_COMPRESS, init_etm, tls_parse_ctos_etm, tls_parse_stoc_etm, tls_construct_stoc_etm, tls_construct_ctos_etm, NULL }, @@ -282,6 +342,7 @@ static const EXTENSION_DEFINITION ext_defs[] = { TLSEXT_TYPE_signed_certificate_timestamp, SSL_EXT_CLIENT_HELLO | SSL_EXT_TLS1_2_SERVER_HELLO | SSL_EXT_TLS1_3_CERTIFICATE | SSL_EXT_TLS1_3_CERTIFICATE_REQUEST, + OSSL_ECH_HANDLING_COMPRESS, NULL, /* * No server side support for this, but can be provided by a custom @@ -297,12 +358,14 @@ static const EXTENSION_DEFINITION ext_defs[] = { TLSEXT_TYPE_extended_master_secret, SSL_EXT_CLIENT_HELLO | SSL_EXT_TLS1_2_SERVER_HELLO | SSL_EXT_TLS1_2_AND_BELOW_ONLY, + OSSL_ECH_HANDLING_COMPRESS, init_ems, tls_parse_ctos_ems, tls_parse_stoc_ems, tls_construct_stoc_ems, tls_construct_ctos_ems, final_ems }, { TLSEXT_TYPE_signature_algorithms_cert, SSL_EXT_CLIENT_HELLO | SSL_EXT_TLS1_3_CERTIFICATE_REQUEST, + OSSL_ECH_HANDLING_COMPRESS, init_sig_algs_cert, tls_parse_ctos_sig_algs_cert, tls_parse_ctos_sig_algs_cert, /* We do not generate signature_algorithms_cert at present. */ @@ -311,6 +374,7 @@ static const EXTENSION_DEFINITION ext_defs[] = { { TLSEXT_TYPE_post_handshake_auth, SSL_EXT_CLIENT_HELLO | SSL_EXT_TLS1_3_ONLY, + OSSL_ECH_HANDLING_COMPRESS, init_post_handshake_auth, tls_parse_ctos_post_handshake_auth, NULL, NULL, tls_construct_ctos_post_handshake_auth, @@ -320,6 +384,7 @@ static const EXTENSION_DEFINITION ext_defs[] = { TLSEXT_TYPE_client_cert_type, SSL_EXT_CLIENT_HELLO | SSL_EXT_TLS1_3_ENCRYPTED_EXTENSIONS | SSL_EXT_TLS1_2_SERVER_HELLO, + OSSL_ECH_HANDLING_CALL_BOTH, init_client_cert_type, tls_parse_ctos_client_cert_type, tls_parse_stoc_client_cert_type, tls_construct_stoc_client_cert_type, tls_construct_ctos_client_cert_type, @@ -329,6 +394,7 @@ static const EXTENSION_DEFINITION ext_defs[] = { TLSEXT_TYPE_server_cert_type, SSL_EXT_CLIENT_HELLO | SSL_EXT_TLS1_3_ENCRYPTED_EXTENSIONS | SSL_EXT_TLS1_2_SERVER_HELLO, + OSSL_ECH_HANDLING_CALL_BOTH, init_server_cert_type, tls_parse_ctos_server_cert_type, tls_parse_stoc_server_cert_type, tls_construct_stoc_server_cert_type, tls_construct_ctos_server_cert_type, @@ -337,6 +403,7 @@ static const EXTENSION_DEFINITION ext_defs[] = { { TLSEXT_TYPE_signature_algorithms, SSL_EXT_CLIENT_HELLO | SSL_EXT_TLS1_3_CERTIFICATE_REQUEST, + OSSL_ECH_HANDLING_COMPRESS, init_sig_algs, tls_parse_ctos_sig_algs, tls_parse_ctos_sig_algs, tls_construct_ctos_sig_algs, tls_construct_ctos_sig_algs, final_sig_algs @@ -345,6 +412,7 @@ static const EXTENSION_DEFINITION ext_defs[] = { TLSEXT_TYPE_supported_versions, SSL_EXT_CLIENT_HELLO | SSL_EXT_TLS1_3_SERVER_HELLO | SSL_EXT_TLS1_3_HELLO_RETRY_REQUEST | SSL_EXT_TLS_IMPLEMENTATION_ONLY, + OSSL_ECH_HANDLING_COMPRESS, NULL, /* Processed inline as part of version selection */ NULL, tls_parse_stoc_supported_versions, @@ -355,6 +423,7 @@ static const EXTENSION_DEFINITION ext_defs[] = { TLSEXT_TYPE_psk_kex_modes, SSL_EXT_CLIENT_HELLO | SSL_EXT_TLS_IMPLEMENTATION_ONLY | SSL_EXT_TLS1_3_ONLY, + OSSL_ECH_HANDLING_COMPRESS, init_psk_kex_modes, tls_parse_ctos_psk_kex_modes, NULL, NULL, tls_construct_ctos_psk_kex_modes, NULL }, @@ -367,6 +436,7 @@ static const EXTENSION_DEFINITION ext_defs[] = { SSL_EXT_CLIENT_HELLO | SSL_EXT_TLS1_3_SERVER_HELLO | SSL_EXT_TLS1_3_HELLO_RETRY_REQUEST | SSL_EXT_TLS_IMPLEMENTATION_ONLY | SSL_EXT_TLS1_3_ONLY, + OSSL_ECH_HANDLING_COMPRESS, NULL, tls_parse_ctos_key_share, tls_parse_stoc_key_share, tls_construct_stoc_key_share, tls_construct_ctos_key_share, final_key_share @@ -376,6 +446,7 @@ static const EXTENSION_DEFINITION ext_defs[] = { TLSEXT_TYPE_cookie, SSL_EXT_CLIENT_HELLO | SSL_EXT_TLS1_3_HELLO_RETRY_REQUEST | SSL_EXT_TLS_IMPLEMENTATION_ONLY | SSL_EXT_TLS1_3_ONLY, + OSSL_ECH_HANDLING_COMPRESS, NULL, tls_parse_ctos_cookie, tls_parse_stoc_cookie, tls_construct_stoc_cookie, tls_construct_ctos_cookie, NULL }, @@ -388,12 +459,14 @@ static const EXTENSION_DEFINITION ext_defs[] = { TLSEXT_TYPE_cryptopro_bug, SSL_EXT_CLIENT_HELLO | SSL_EXT_TLS1_2_SERVER_HELLO | SSL_EXT_TLS1_2_AND_BELOW_ONLY, + OSSL_ECH_HANDLING_COMPRESS, NULL, NULL, NULL, tls_construct_stoc_cryptopro_bug, NULL, NULL }, { TLSEXT_TYPE_compress_certificate, SSL_EXT_CLIENT_HELLO | SSL_EXT_TLS1_3_CERTIFICATE_REQUEST | SSL_EXT_TLS_IMPLEMENTATION_ONLY | SSL_EXT_TLS1_3_ONLY, + OSSL_ECH_HANDLING_COMPRESS, tls_init_compress_certificate, tls_parse_compress_certificate, tls_parse_compress_certificate, tls_construct_compress_certificate, tls_construct_compress_certificate, @@ -403,6 +476,7 @@ static const EXTENSION_DEFINITION ext_defs[] = { TLSEXT_TYPE_early_data, SSL_EXT_CLIENT_HELLO | SSL_EXT_TLS1_3_ENCRYPTED_EXTENSIONS | SSL_EXT_TLS1_3_NEW_SESSION_TICKET | SSL_EXT_TLS1_3_ONLY, + OSSL_ECH_HANDLING_CALL_BOTH, NULL, tls_parse_ctos_early_data, tls_parse_stoc_early_data, tls_construct_stoc_early_data, tls_construct_ctos_early_data, final_early_data @@ -411,15 +485,47 @@ static const EXTENSION_DEFINITION ext_defs[] = { TLSEXT_TYPE_certificate_authorities, SSL_EXT_CLIENT_HELLO | SSL_EXT_TLS1_3_CERTIFICATE_REQUEST | SSL_EXT_TLS1_3_ONLY, + OSSL_ECH_HANDLING_COMPRESS, init_certificate_authorities, tls_parse_certificate_authorities, tls_parse_certificate_authorities, tls_construct_certificate_authorities, tls_construct_certificate_authorities, NULL, }, +#ifndef OPENSSL_NO_ECH + { + TLSEXT_TYPE_ech, + SSL_EXT_CLIENT_HELLO | SSL_EXT_TLS1_3_ONLY | + SSL_EXT_TLS1_3_ENCRYPTED_EXTENSIONS | + SSL_EXT_TLS1_3_HELLO_RETRY_REQUEST, + OSSL_ECH_HANDLING_CALL_BOTH, + init_ech, + /* + * TODO(ECH): add server calls as per below in a bit + * tls_parse_ctos_ech, tls_parse_stoc_ech, + * tls_construct_stoc_ech, tls_construct_ctos_ech, + */ + NULL, tls_parse_stoc_ech, + NULL, tls_construct_ctos_ech, + NULL + }, + { + TLSEXT_TYPE_outer_extensions, + SSL_EXT_CLIENT_HELLO | SSL_EXT_TLS1_3_ONLY, + OSSL_ECH_HANDLING_CALL_BOTH, + NULL, + NULL, NULL, + NULL, NULL, + NULL + }, +#else /* OPENSSL_NO_ECH */ + INVALID_EXTENSION, + INVALID_EXTENSION, +#endif /* END_OPENSSL_NO_ECH */ { /* Must be immediately before pre_shared_key */ TLSEXT_TYPE_padding, SSL_EXT_CLIENT_HELLO, + OSSL_ECH_HANDLING_CALL_BOTH, NULL, /* We send this, but don't read it */ NULL, NULL, NULL, tls_construct_ctos_padding, NULL @@ -429,11 +535,136 @@ static const EXTENSION_DEFINITION ext_defs[] = { TLSEXT_TYPE_psk, SSL_EXT_CLIENT_HELLO | SSL_EXT_TLS1_3_SERVER_HELLO | SSL_EXT_TLS_IMPLEMENTATION_ONLY | SSL_EXT_TLS1_3_ONLY, + OSSL_ECH_HANDLING_CALL_BOTH, NULL, tls_parse_ctos_psk, tls_parse_stoc_psk, tls_construct_stoc_psk, tls_construct_ctos_psk, final_psk } }; +#ifndef OPENSSL_NO_ECH +/* + * Copy an inner extension value to outer. + * inner CH must have been pre-decoded into s->clienthello->pre_proc_exts + * already. + */ +static int ech_copy_inner2outer(SSL_CONNECTION *s, uint16_t ext_type, + int ind, WPACKET *pkt) +{ + RAW_EXTENSION *myext = NULL, *raws = NULL; + + if (s == NULL || s->clienthello == NULL) + return OSSL_ECH_SAME_EXT_ERR; + raws = s->clienthello->pre_proc_exts; + if (raws == NULL) + return OSSL_ECH_SAME_EXT_ERR; + myext = &raws[ind]; + OSSL_TRACE_BEGIN(TLS) { + BIO_printf(trc_out, "inner2outer: Copying ext type %d to outer\n", + ext_type); + } OSSL_TRACE_END(TLS); + /* + * This one wasn't in inner, so re-do processing. We don't + * actually do this currently, but could. + */ + if (myext == NULL) + return OSSL_ECH_SAME_EXT_CONTINUE; + /* copy inner value to outer */ + if (PACKET_data(&myext->data) != NULL + && PACKET_remaining(&myext->data) > 0) { + if (!WPACKET_put_bytes_u16(pkt, ext_type) + || !WPACKET_sub_memcpy_u16(pkt, PACKET_data(&myext->data), + PACKET_remaining(&myext->data))) + return OSSL_ECH_SAME_EXT_ERR; + } else { + /* empty extension */ + if (!WPACKET_put_bytes_u16(pkt, ext_type) + || !WPACKET_put_bytes_u16(pkt, 0)) + return OSSL_ECH_SAME_EXT_ERR; + } + return 1; +} + +/* + * DUPEMALL is useful for testing - this turns off compression and + * causes two calls to each extension constructor, which'd be the same + * as making all entries in ext_tab use the CALL_BOTH value + */ +# undef DUPEMALL + +/* + * Check if we're using the same/different key shares + * return 1 if same key share in inner and outer, 0 otherwise + */ +int ossl_ech_same_key_share(void) +{ +# ifdef DUPEMALL + return 0; +# endif + return ext_defs[TLSEXT_IDX_key_share].ech_handling + != OSSL_ECH_HANDLING_CALL_BOTH; +} + +/* + * say if extension at index |ind| in ext_defs is to be ECH compressed + * return 1 if this one is to be compressed, 0 if not, -1 for error + */ +int ossl_ech_2bcompressed(int ind) +{ + const int nexts = OSSL_NELEM(ext_defs); + +# ifdef DUPEMALL + return 0; +# endif + if (ind < 0 || ind >= nexts) + return -1; + return ext_defs[ind].ech_handling == OSSL_ECH_HANDLING_COMPRESS; +} + +/* as needed, repeat extension from inner in outer handling compression */ +int ossl_ech_same_ext(SSL_CONNECTION *s, WPACKET *pkt) +{ + unsigned int type = 0; + int tind = 0, nexts = OSSL_NELEM(ext_defs); + +# ifdef DUPEMALL + return OSSL_ECH_SAME_EXT_CONTINUE; +# endif + if (s == NULL || s->ext.ech.es == NULL) + return OSSL_ECH_SAME_EXT_CONTINUE; /* nothing to do */ + tind = s->ext.ech.ext_ind; + /* If this index'd extension won't be compressed, we're done */ + if (tind < 0 || tind >= nexts) + return OSSL_ECH_SAME_EXT_ERR; + type = ext_defs[tind].type; + if (s->ext.ech.ch_depth == 1) { + /* inner CH - just note compression as configured */ + if (ext_defs[tind].ech_handling != OSSL_ECH_HANDLING_COMPRESS) + return OSSL_ECH_SAME_EXT_CONTINUE; + /* mark this one to be "compressed" */ + if (s->ext.ech.n_outer_only >= OSSL_ECH_OUTERS_MAX) + return OSSL_ECH_SAME_EXT_ERR; + s->ext.ech.outer_only[s->ext.ech.n_outer_only] = type; + s->ext.ech.n_outer_only++; + OSSL_TRACE_BEGIN(TLS) { + BIO_printf(trc_out, "ech_same_ext: Marking (type %u, ind %d " + "tot-comp %d) for compression\n", type, tind, + (int) s->ext.ech.n_outer_only); + } OSSL_TRACE_END(TLS); + return OSSL_ECH_SAME_EXT_CONTINUE; + } else { + /* Copy value from inner to outer, or indicate a new value needed */ + if (s->clienthello == NULL || pkt == NULL) + return OSSL_ECH_SAME_EXT_ERR; + if (ext_defs[tind].ech_handling == OSSL_ECH_HANDLING_CALL_BOTH) + return OSSL_ECH_SAME_EXT_CONTINUE; + else + return ech_copy_inner2outer(s, type, tind, pkt); + } + /* just in case - shouldn't happen */ + return OSSL_ECH_SAME_EXT_ERR; +} +#endif + /* Returns a TLSEXT_TYPE for the given index */ unsigned int ossl_get_extension_type(size_t idx) { @@ -858,6 +1089,9 @@ int tls_construct_extensions(SSL_CONNECTION *s, WPACKET *pkt, int min_version, max_version = 0, reason; const EXTENSION_DEFINITION *thisexd; int for_comp = (context & SSL_EXT_TLS1_3_CERTIFICATE_COMPRESSION) != 0; +#ifndef OPENSSL_NO_ECH + int pass; +#endif if (!WPACKET_start_sub_packet_u16(pkt) /* @@ -893,40 +1127,77 @@ int tls_construct_extensions(SSL_CONNECTION *s, WPACKET *pkt, return 0; } - for (i = 0, thisexd = ext_defs; i < OSSL_NELEM(ext_defs); i++, thisexd++) { - EXT_RETURN (*construct)(SSL_CONNECTION *s, WPACKET *pkt, - unsigned int context, - X509 *x, size_t chainidx); - EXT_RETURN ret; +#ifndef OPENSSL_NO_ECH + /* + * Two passes - we first construct the to-be-ECH-compressed + * extensions, and then go around again constructing those that + * aren't to be ECH-compressed. We need to ensure this ordering + * so that all the ECH-compressed extensions are contiguous + * in the encoding. The actual compression happens later in + * ech_encode_inner(). + */ + for (pass = 0; pass <= 1; pass++) +#endif - /* Skip if not relevant for our context */ - if (!should_add_extension(s, thisexd->context, context, max_version)) - continue; + for (i = 0, thisexd = ext_defs; i < OSSL_NELEM(ext_defs); + i++, thisexd++) { + EXT_RETURN (*construct)(SSL_CONNECTION *s, WPACKET *pkt, + unsigned int context, + X509 *x, size_t chainidx); + EXT_RETURN ret; + +#ifndef OPENSSL_NO_ECH + /* do compressed in pass 0, non-compressed in pass 1 */ + if (ossl_ech_2bcompressed(i) == pass) + continue; + /* stash index - needed for COMPRESS ECH handling */ + s->ext.ech.ext_ind = i; +#endif + /* Skip if not relevant for our context */ + if (!should_add_extension(s, thisexd->context, context, max_version)) + continue; - construct = s->server ? thisexd->construct_stoc - : thisexd->construct_ctos; + construct = s->server ? thisexd->construct_stoc + : thisexd->construct_ctos; - if (construct == NULL) - continue; + if (construct == NULL) + continue; - ret = construct(s, pkt, context, x, chainidx); - if (ret == EXT_RETURN_FAIL) { - /* SSLfatal() already called */ + ret = construct(s, pkt, context, x, chainidx); + if (ret == EXT_RETURN_FAIL) { + /* SSLfatal() already called */ + return 0; + } + if (ret == EXT_RETURN_SENT + && (context & (SSL_EXT_CLIENT_HELLO + | SSL_EXT_TLS1_3_CERTIFICATE_REQUEST + | SSL_EXT_TLS1_3_NEW_SESSION_TICKET)) != 0) + s->ext.extflags[i] |= SSL_EXT_FLAG_SENT; + } + +#ifndef OPENSSL_NO_ECH + /* + * don't close yet if client in the middle of doing ECH, we'll + * eventually close this in ech_aad_and_encrypt() after we add + * the real ECH extension value + */ + if (s->server + || s->ext.ech.attempted == 0 + || s->ext.ech.ch_depth == 1 + || s->ext.ech.grease == OSSL_ECH_IS_GREASE) { + if (!WPACKET_close(pkt)) { + if (!for_comp) + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); return 0; } - if (ret == EXT_RETURN_SENT - && (context & (SSL_EXT_CLIENT_HELLO - | SSL_EXT_TLS1_3_CERTIFICATE_REQUEST - | SSL_EXT_TLS1_3_NEW_SESSION_TICKET)) != 0) - s->ext.extflags[i] |= SSL_EXT_FLAG_SENT; } - +#else if (!WPACKET_close(pkt)) { if (!for_comp) SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); return 0; } - +#endif return 1; } @@ -989,6 +1260,26 @@ static int init_server_name(SSL_CONNECTION *s, unsigned int context) return 1; } +#ifndef OPENSSL_NO_ECH +/* + * Just note that ech is not yet done + * return 1 for good, 0 otherwise + */ +static int init_ech(SSL_CONNECTION *s, unsigned int context) +{ + const int nexts = OSSL_NELEM(ext_defs); + + /* we don't need this assert everywhere - anywhere is fine */ + if (!ossl_assert(TLSEXT_IDX_num_builtins == nexts)) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + return 0; + } + if ((context & SSL_EXT_CLIENT_HELLO) != 0) + s->ext.ech.done = 0; + return 1; +} +#endif /* OPENSSL_NO_ECH */ + static int final_server_name(SSL_CONNECTION *s, unsigned int context, int sent) { int ret = SSL_TLSEXT_ERR_NOACK; @@ -1050,7 +1341,7 @@ static int final_server_name(SSL_CONNECTION *s, unsigned int context, int sent) && was_ticket && (SSL_get_options(ssl) & SSL_OP_NO_TICKET) != 0) { s->ext.ticket_expected = 0; if (!s->hit) { - SSL_SESSION* ss = SSL_get_session(ssl); + SSL_SESSION *ss = SSL_get_session(ssl); if (ss != NULL) { OPENSSL_free(ss->ext.tick); @@ -1538,6 +1829,13 @@ int tls_psk_do_binder(SSL_CONNECTION *s, const EVP_MD *md, int usepskfored = 0; SSL_CTX *sctx = SSL_CONNECTION_GET_CTX(s); OSSL_PARAM params[2] = { OSSL_PARAM_END, OSSL_PARAM_END }; +#ifndef OPENSSL_NO_ECH + unsigned char hashval[EVP_MAX_MD_SIZE]; + unsigned int hashlen = 0; + EVP_MD_CTX *ctx = NULL; + WPACKET tpkt; + BUF_MEM *tpkt_mem = NULL; +#endif /* Ensure cast to size_t is safe */ if (!ossl_assert(hashsizei > 0)) { @@ -1619,12 +1917,47 @@ int tls_psk_do_binder(SSL_CONNECTION *s, const EVP_MD *md, long hdatalen_l; void *hdata; - hdatalen = hdatalen_l = - BIO_get_mem_data(s->s3.handshake_buffer, &hdata); - if (hdatalen_l <= 0) { - SSLfatal(s, SSL_AD_INTERNAL_ERROR, SSL_R_BAD_HANDSHAKE_LENGTH); - goto err; +#ifndef OPENSSL_NO_ECH + /* handle the hashing as per ECH needs (on client) */ + if (s->ext.ech.attempted == 1 && s->ext.ech.ch_depth == 1) { + if ((tpkt_mem = BUF_MEM_new()) == NULL + || !BUF_MEM_grow(tpkt_mem, SSL3_RT_MAX_PLAIN_LENGTH) + || !WPACKET_init(&tpkt, tpkt_mem)) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + goto err; + } + hashlen = EVP_MD_size(md); + if ((ctx = EVP_MD_CTX_new()) == NULL + || EVP_DigestInit_ex(ctx, md, NULL) <= 0 + || EVP_DigestUpdate(ctx, s->ext.ech.innerch1, + s->ext.ech.innerch1_len) <= 0 + || EVP_DigestFinal_ex(ctx, hashval, &hashlen) <= 0) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + goto err; + } + EVP_MD_CTX_free(ctx); + ctx = NULL; + if (!WPACKET_put_bytes_u8(&tpkt, SSL3_MT_MESSAGE_HASH) + || !WPACKET_put_bytes_u24(&tpkt, hashlen) + || !WPACKET_memcpy(&tpkt, hashval, hashlen) + || !WPACKET_memcpy(&tpkt, s->ext.ech.kepthrr, + s->ext.ech.kepthrr_len) + || !WPACKET_get_length(&tpkt, &hdatalen)) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + goto err; + } + hdata = WPACKET_get_curr(&tpkt) - hdatalen; + } else { +#endif + hdatalen = hdatalen_l = + BIO_get_mem_data(s->s3.handshake_buffer, &hdata); + if (hdatalen_l <= 0) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, SSL_R_BAD_HANDSHAKE_LENGTH); + goto err; + } +#ifndef OPENSSL_NO_ECH } +#endif /* * For servers the handshake buffer data will include the second @@ -1695,6 +2028,13 @@ int tls_psk_do_binder(SSL_CONNECTION *s, const EVP_MD *md, OPENSSL_cleanse(finishedkey, sizeof(finishedkey)); EVP_PKEY_free(mackey); EVP_MD_CTX_free(mctx); +#ifndef OPENSSL_NO_ECH + EVP_MD_CTX_free(ctx); + if (tpkt_mem != NULL) { + WPACKET_cleanup(&tpkt); + BUF_MEM_free(tpkt_mem); + } +#endif return ret; } @@ -1822,6 +2162,9 @@ static EXT_RETURN tls_construct_compress_certificate(SSL_CONNECTION *sc, WPACKET if (sc->cert_comp_prefs[0] == TLSEXT_comp_cert_none) return EXT_RETURN_NOT_SENT; +# ifndef OPENSSL_NO_ECH + ECH_SAME_EXT(sc, pkt); +# endif if (!WPACKET_put_bytes_u16(pkt, TLSEXT_TYPE_compress_certificate) || !WPACKET_start_sub_packet_u16(pkt) diff --git a/ssl/statem/extensions_clnt.c b/ssl/statem/extensions_clnt.c index 4e89c963ddc13..0342ce92e71af 100644 --- a/ssl/statem/extensions_clnt.c +++ b/ssl/statem/extensions_clnt.c @@ -12,6 +12,9 @@ #include "internal/cryptlib.h" #include "internal/ssl_unwrap.h" #include "statem_local.h" +#ifndef OPENSSL_NO_ECH +# include +#endif EXT_RETURN tls_construct_ctos_renegotiate(SSL_CONNECTION *s, WPACKET *pkt, unsigned int context, X509 *x, @@ -35,6 +38,9 @@ EXT_RETURN tls_construct_ctos_renegotiate(SSL_CONNECTION *s, WPACKET *pkt, return EXT_RETURN_NOT_SENT; } +#ifndef OPENSSL_NO_ECH + ECH_SAME_EXT(s, pkt) +#endif if (!WPACKET_put_bytes_u16(pkt, TLSEXT_TYPE_renegotiate) || !WPACKET_start_sub_packet_u16(pkt) @@ -47,6 +53,10 @@ EXT_RETURN tls_construct_ctos_renegotiate(SSL_CONNECTION *s, WPACKET *pkt, return EXT_RETURN_SENT; } +#ifndef OPENSSL_NO_ECH + ECH_SAME_EXT(s, pkt) +#endif + /* Add a complete RI extension if renegotiating */ if (!WPACKET_put_bytes_u16(pkt, TLSEXT_TYPE_renegotiate) || !WPACKET_start_sub_packet_u16(pkt) @@ -64,6 +74,43 @@ EXT_RETURN tls_construct_ctos_server_name(SSL_CONNECTION *s, WPACKET *pkt, unsigned int context, X509 *x, size_t chainidx) { +#ifndef OPENSSL_NO_ECH + char *chosen = s->ext.hostname; + OSSL_HPKE_SUITE suite; + OSSL_ECHSTORE_ENTRY *ee = NULL; + + if (s->ext.ech.es != NULL) { + if (ossl_ech_pick_matching_cfg(s, &ee, &suite) != 1) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + return EXT_RETURN_NOT_SENT; + } + /* Don't send outer SNI if external API says so */ + if (s->ext.ech.ch_depth == 0 && s->ext.ech.no_outer == 1) + return EXT_RETURN_NOT_SENT; + if (s->ext.ech.ch_depth == 1) /* inner */ + chosen = s->ext.hostname; + if (s->ext.ech.ch_depth == 0) { /* outer */ + if (s->ext.ech.outer_hostname != NULL) /* prefer API */ + chosen = s->ext.ech.outer_hostname; + else /* use name from ECHConfig */ + chosen = ee->public_name; + } + } + if (chosen == NULL) + return EXT_RETURN_NOT_SENT; + /* Add TLS extension servername to the Client Hello message */ + if (!WPACKET_put_bytes_u16(pkt, TLSEXT_TYPE_server_name) + || !WPACKET_start_sub_packet_u16(pkt) + || !WPACKET_start_sub_packet_u16(pkt) + || !WPACKET_put_bytes_u8(pkt, TLSEXT_NAMETYPE_host_name) + || !WPACKET_sub_memcpy_u16(pkt, chosen, strlen(chosen)) + || !WPACKET_close(pkt) + || !WPACKET_close(pkt)) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + return EXT_RETURN_FAIL; + } + return EXT_RETURN_SENT; +#else if (s->ext.hostname == NULL) return EXT_RETURN_NOT_SENT; @@ -83,6 +130,7 @@ EXT_RETURN tls_construct_ctos_server_name(SSL_CONNECTION *s, WPACKET *pkt, } return EXT_RETURN_SENT; +#endif } /* Push a Max Fragment Len extension into ClientHello */ @@ -92,6 +140,9 @@ EXT_RETURN tls_construct_ctos_maxfragmentlen(SSL_CONNECTION *s, WPACKET *pkt, { if (s->ext.max_fragment_len_mode == TLSEXT_max_fragment_length_DISABLED) return EXT_RETURN_NOT_SENT; +#ifndef OPENSSL_NO_ECH + ECH_SAME_EXT(s, pkt) +#endif /* Add Max Fragment Length extension if client enabled it. */ /*- @@ -118,6 +169,9 @@ EXT_RETURN tls_construct_ctos_srp(SSL_CONNECTION *s, WPACKET *pkt, /* Add SRP username if there is one */ if (s->srp_ctx.login == NULL) return EXT_RETURN_NOT_SENT; +# ifndef OPENSSL_NO_ECH + ECH_SAME_EXT(s, pkt) +# endif if (!WPACKET_put_bytes_u16(pkt, TLSEXT_TYPE_srp) /* Sub-packet for SRP extension */ @@ -196,6 +250,9 @@ EXT_RETURN tls_construct_ctos_ec_pt_formats(SSL_CONNECTION *s, WPACKET *pkt, } if (!use_ecc(s, min_version, max_version)) return EXT_RETURN_NOT_SENT; +#ifndef OPENSSL_NO_ECH + ECH_SAME_EXT(s, pkt) +#endif /* Add TLS extension ECPointFormats to the ClientHello message */ tls1_get_formatlist(s, &pformats, &num_formats); @@ -233,6 +290,9 @@ EXT_RETURN tls_construct_ctos_supported_groups(SSL_CONNECTION *s, WPACKET *pkt, if (!use_ecc(s, min_version, max_version) && (SSL_CONNECTION_IS_DTLS(s) || max_version < TLS1_3_VERSION)) return EXT_RETURN_NOT_SENT; +#ifndef OPENSSL_NO_ECH + ECH_SAME_EXT(s, pkt) +#endif /* * Add TLS extension supported_groups to the ClientHello message @@ -289,6 +349,9 @@ EXT_RETURN tls_construct_ctos_session_ticket(SSL_CONNECTION *s, WPACKET *pkt, if (!tls_use_ticket(s)) return EXT_RETURN_NOT_SENT; +#ifndef OPENSSL_NO_ECH + ECH_SAME_EXT(s, pkt) +#endif if (!s->new_session && s->session != NULL && s->session->ext.tick != NULL @@ -346,6 +409,10 @@ EXT_RETURN tls_construct_ctos_sig_algs(SSL_CONNECTION *s, WPACKET *pkt, return EXT_RETURN_NOT_SENT; } +#ifndef OPENSSL_NO_ECH + ECH_SAME_EXT(s, pkt) +#endif + salglen = tls12_get_psigalgs(s, 1, &salg); if (!WPACKET_put_bytes_u16(pkt, TLSEXT_TYPE_signature_algorithms) /* Sub-packet for sig-algs extension */ @@ -375,6 +442,9 @@ EXT_RETURN tls_construct_ctos_status_request(SSL_CONNECTION *s, WPACKET *pkt, if (s->ext.status_type != TLSEXT_STATUSTYPE_ocsp) return EXT_RETURN_NOT_SENT; +# ifndef OPENSSL_NO_ECH + ECH_SAME_EXT(s, pkt) +# endif if (!WPACKET_put_bytes_u16(pkt, TLSEXT_TYPE_status_request) /* Sub-packet for status request extension */ @@ -435,6 +505,9 @@ EXT_RETURN tls_construct_ctos_npn(SSL_CONNECTION *s, WPACKET *pkt, if (SSL_CONNECTION_GET_CTX(s)->ext.npn_select_cb == NULL || !SSL_IS_FIRST_HANDSHAKE(s)) return EXT_RETURN_NOT_SENT; +# ifndef OPENSSL_NO_ECH + ECH_SAME_EXT(s, pkt) +# endif /* * The client advertises an empty extension to indicate its support @@ -454,8 +527,46 @@ EXT_RETURN tls_construct_ctos_alpn(SSL_CONNECTION *s, WPACKET *pkt, unsigned int context, X509 *x, size_t chainidx) { - s->s3.alpn_sent = 0; +#ifndef OPENSSL_NO_ECH + unsigned char *aval = NULL; + size_t alen = 0; +#endif + s->s3.alpn_sent = 0; +#ifndef OPENSSL_NO_ECH + /* + * If we have different alpn and alpn_outer values, then we set + * the appropriate one for inner and outer. + * If no alpn is set (for inner or outer), we don't send any. + * If only an inner is set then we send the same in both. + * Logic above is on the basis that alpn's aren't that sensitive, + * usually, so special action is needed to do better. + * We also don't support a way to send alpn only in the inner. + * If you don't want the inner value in the outer, you have to + * pick what to send in the outer and send that. + */ + if (!SSL_IS_FIRST_HANDSHAKE(s)) + return EXT_RETURN_NOT_SENT; + aval = s->ext.alpn; + alen = s->ext.alpn_len; + if (s->ext.ech.ch_depth == 1 && s->ext.alpn == NULL) /* inner */ + return EXT_RETURN_NOT_SENT; + if (s->ext.ech.ch_depth == 0 && s->ext.alpn == NULL + && s->ext.ech.alpn_outer == NULL) /* outer */ + return EXT_RETURN_NOT_SENT; + if (s->ext.ech.ch_depth == 0 && s->ext.ech.alpn_outer != NULL) { + aval = s->ext.ech.alpn_outer; + alen = s->ext.ech.alpn_outer_len; + } + if (!WPACKET_put_bytes_u16(pkt, + TLSEXT_TYPE_application_layer_protocol_negotiation) + || !WPACKET_start_sub_packet_u16(pkt) + || !WPACKET_sub_memcpy_u16(pkt, aval, alen) + || !WPACKET_close(pkt)) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + return EXT_RETURN_FAIL; + } +#else if (s->ext.alpn == NULL || !SSL_IS_FIRST_HANDSHAKE(s)) return EXT_RETURN_NOT_SENT; @@ -468,6 +579,7 @@ EXT_RETURN tls_construct_ctos_alpn(SSL_CONNECTION *s, WPACKET *pkt, SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); return EXT_RETURN_FAIL; } +#endif s->s3.alpn_sent = 1; return EXT_RETURN_SENT; @@ -485,6 +597,9 @@ EXT_RETURN tls_construct_ctos_use_srtp(SSL_CONNECTION *s, WPACKET *pkt, if (clnt == NULL) return EXT_RETURN_NOT_SENT; +# ifndef OPENSSL_NO_ECH + ECH_SAME_EXT(s, pkt) +# endif if (!WPACKET_put_bytes_u16(pkt, TLSEXT_TYPE_use_srtp) /* Sub-packet for SRTP extension */ @@ -523,6 +638,9 @@ EXT_RETURN tls_construct_ctos_etm(SSL_CONNECTION *s, WPACKET *pkt, { if (s->options & SSL_OP_NO_ENCRYPT_THEN_MAC) return EXT_RETURN_NOT_SENT; +#ifndef OPENSSL_NO_ECH + ECH_SAME_EXT(s, pkt) +#endif if (!WPACKET_put_bytes_u16(pkt, TLSEXT_TYPE_encrypt_then_mac) || !WPACKET_put_bytes_u16(pkt, 0)) { @@ -544,6 +662,9 @@ EXT_RETURN tls_construct_ctos_sct(SSL_CONNECTION *s, WPACKET *pkt, /* Not defined for client Certificates */ if (x != NULL) return EXT_RETURN_NOT_SENT; +# ifndef OPENSSL_NO_ECH + ECH_SAME_EXT(s, pkt) +# endif if (!WPACKET_put_bytes_u16(pkt, TLSEXT_TYPE_signed_certificate_timestamp) || !WPACKET_put_bytes_u16(pkt, 0)) { @@ -561,6 +682,9 @@ EXT_RETURN tls_construct_ctos_ems(SSL_CONNECTION *s, WPACKET *pkt, { if (s->options & SSL_OP_NO_EXTENDED_MASTER_SECRET) return EXT_RETURN_NOT_SENT; +#ifndef OPENSSL_NO_ECH + ECH_SAME_EXT(s, pkt) +#endif if (!WPACKET_put_bytes_u16(pkt, TLSEXT_TYPE_extended_master_secret) || !WPACKET_put_bytes_u16(pkt, 0)) { @@ -589,6 +713,9 @@ EXT_RETURN tls_construct_ctos_supported_versions(SSL_CONNECTION *s, WPACKET *pkt */ if (max_version < TLS1_3_VERSION) return EXT_RETURN_NOT_SENT; +#ifndef OPENSSL_NO_ECH + ECH_SAME_EXT(s, pkt) +#endif if (!WPACKET_put_bytes_u16(pkt, TLSEXT_TYPE_supported_versions) || !WPACKET_start_sub_packet_u16(pkt) @@ -621,6 +748,10 @@ EXT_RETURN tls_construct_ctos_psk_kex_modes(SSL_CONNECTION *s, WPACKET *pkt, #ifndef OPENSSL_NO_TLS1_3 int nodhe = s->options & SSL_OP_ALLOW_NO_DHE_KEX; +# ifndef OPENSSL_NO_ECH + ECH_SAME_EXT(s, pkt) +# endif + if (!WPACKET_put_bytes_u16(pkt, TLSEXT_TYPE_psk_kex_modes) || !WPACKET_start_sub_packet_u16(pkt) || !WPACKET_start_sub_packet_u8(pkt) @@ -680,6 +811,15 @@ static int add_key_share(SSL_CONNECTION *s, WPACKET *pkt, unsigned int group_id, goto err; } +# ifndef OPENSSL_NO_ECH + if (s->ext.ech.ch_depth == 1) { /* stash inner */ + EVP_PKEY_up_ref(key_share_key); + EVP_PKEY_free(s->ext.ech.tmp_pkey); + s->ext.ech.tmp_pkey = key_share_key; + s->ext.ech.group_id = group_id; + } +# endif + /* For backward compatibility, we use the first valid group to add a key share */ if (loop_num == 0) { s->s3.tmp.pkey = key_share_key; @@ -713,6 +853,10 @@ EXT_RETURN tls_construct_ctos_key_share(SSL_CONNECTION *s, WPACKET *pkt, int add_only_one = 0; size_t valid_keyshare = 0; +# ifndef OPENSSL_NO_ECH + ECH_SAME_EXT(s, pkt) +# endif + /* key_share extension */ if (!WPACKET_put_bytes_u16(pkt, TLSEXT_TYPE_key_share) /* Extension data sub-packet */ @@ -798,6 +942,9 @@ EXT_RETURN tls_construct_ctos_cookie(SSL_CONNECTION *s, WPACKET *pkt, /* Should only be set if we've had an HRR */ if (s->ext.tls13_cookie_len == 0) return EXT_RETURN_NOT_SENT; +#ifndef OPENSSL_NO_ECH + ECH_SAME_EXT(s, pkt) +#endif if (!WPACKET_put_bytes_u16(pkt, TLSEXT_TYPE_cookie) /* Extension data sub-packet */ @@ -832,6 +979,33 @@ EXT_RETURN tls_construct_ctos_early_data(SSL_CONNECTION *s, WPACKET *pkt, const EVP_MD *handmd = NULL; SSL *ussl = SSL_CONNECTION_GET_USER_SSL(s); +#ifndef OPENSSL_NO_ECH + /* + * If we're attempting ECH and processing the outer CH + * then we only need to check if the extension is to be + * sent or not - any other processing (with side effects) + * happened already for the inner CH. + */ + if (s->ext.ech.es != NULL && s->ext.ech.ch_depth == 0) { + /* + * if we called this for inner and did send then + * the following two things should be set, if so, + * then send again in the outer CH. + */ + if (s->ext.early_data == SSL_EARLY_DATA_REJECTED + && s->ext.early_data_ok == 1) { + if (!WPACKET_put_bytes_u16(pkt, TLSEXT_TYPE_early_data) + || !WPACKET_start_sub_packet_u16(pkt) + || !WPACKET_close(pkt)) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + return EXT_RETURN_FAIL; + } + return EXT_RETURN_SENT; + } else { + return EXT_RETURN_NOT_SENT; + } + } +#endif if (s->hello_retry_request == SSL_HRR_PENDING) handmd = ssl_handshake_md(s); @@ -1212,6 +1386,89 @@ EXT_RETURN tls_construct_ctos_psk(SSL_CONNECTION *s, WPACKET *pkt, return EXT_RETURN_FAIL; } +# ifndef OPENSSL_NO_ECH + /* + * For ECH if we're processing the outer CH and the inner CH + * has a PSK, then we want to send a GREASE PSK in the outer. + * We'll do that by just replacing the ticket value itself + * with random values of the same length. + */ + if (s->ext.ech.es != NULL && s->ext.ech.ch_depth == 0) { + unsigned char *rndbuf = NULL; + size_t totalrndsize = 0; + + if (s->session == NULL) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + return EXT_RETURN_FAIL; + } + totalrndsize = s->session->ext.ticklen + + 4 /* agems */ + + s->psksession_id_len + + reshashsize + + pskhashsize; + rndbuf = OPENSSL_malloc(totalrndsize); + if (rndbuf == NULL) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + return EXT_RETURN_FAIL; + } + /* outer CH allocate a similar sized random value */ + if (RAND_bytes_ex(s->ssl.ctx->libctx, rndbuf, totalrndsize, + RAND_DRBG_STRENGTH) <= 0) { + OPENSSL_free(rndbuf); + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + return EXT_RETURN_FAIL; + } + /* set agems from random buffer */ + agems = *((uint32_t *)(rndbuf + s->session->ext.ticklen)); + if (dores != 0) { + if (!WPACKET_sub_memcpy_u16(pkt, rndbuf, + s->session->ext.ticklen) + || !WPACKET_put_bytes_u32(pkt, agems)) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + OPENSSL_free(rndbuf); + return EXT_RETURN_FAIL; + } + } + if (s->psksession != NULL) { + if (!WPACKET_sub_memcpy_u16(pkt, + rndbuf + s->session->ext.ticklen + 4, + s->psksession_id_len) + || !WPACKET_put_bytes_u32(pkt, 0)) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + OPENSSL_free(rndbuf); + return EXT_RETURN_FAIL; + } + } + if (!WPACKET_close(pkt) + || !WPACKET_get_total_written(pkt, &binderoffset) + || !WPACKET_start_sub_packet_u16(pkt) + || (dores == 1 + && !WPACKET_sub_memcpy_u8(pkt, + rndbuf + s->session->ext.ticklen + + 4 + s->psksession_id_len, + reshashsize)) + || (s->psksession != NULL + && !WPACKET_sub_memcpy_u8(pkt, + rndbuf + s->session->ext.ticklen + + 4 + s->psksession_id_len + + reshashsize, + pskhashsize)) + || !WPACKET_close(pkt) + || !WPACKET_close(pkt) + || !WPACKET_get_total_written(pkt, &msglen) + /* + * We need to fill in all the sub-packet lengths now so we can + * calculate the HMAC of the message up to the binders + */ + || !WPACKET_fill_lengths(pkt)) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + OPENSSL_free(rndbuf); + return EXT_RETURN_FAIL; + } + OPENSSL_free(rndbuf); + return EXT_RETURN_SENT; + } +# endif /* OPENSSL_NO_ECH */ if (dores) { if (!WPACKET_sub_memcpy_u16(pkt, s->session->ext.tick, s->session->ext.ticklen) @@ -1280,6 +1537,9 @@ EXT_RETURN tls_construct_ctos_post_handshake_auth(SSL_CONNECTION *s, WPACKET *pk #ifndef OPENSSL_NO_TLS1_3 if (!s->pha_enabled) return EXT_RETURN_NOT_SENT; +# ifndef OPENSSL_NO_ECH + ECH_SAME_EXT(s, pkt) +# endif /* construct extension - 0 length, no contents */ if (!WPACKET_put_bytes_u16(pkt, TLSEXT_TYPE_post_handshake_auth) @@ -1399,6 +1659,32 @@ int tls_parse_stoc_server_name(SSL_CONNECTION *s, PACKET *pkt, unsigned int context, X509 *x, size_t chainidx) { +#ifndef OPENSSL_NO_ECH + char *eff_sni = s->ext.hostname; + + /* if we tried ECH and failed, the outer is what's expected */ + if (s->ext.ech.es != NULL && s->ext.ech.success == 0) + eff_sni = s->ext.ech.outer_hostname; + if (eff_sni == NULL) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + return 0; + } + if (PACKET_remaining(pkt) > 0) { + SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_BAD_EXTENSION); + return 0; + } + if (!s->hit) { + if (s->session->ext.hostname != NULL) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + return 0; + } + s->session->ext.hostname = OPENSSL_strdup(eff_sni); + if (s->session->ext.hostname == NULL) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + return 0; + } + } +#else if (s->ext.hostname == NULL) { SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); return 0; @@ -1420,7 +1706,7 @@ int tls_parse_stoc_server_name(SSL_CONNECTION *s, PACKET *pkt, return 0; } } - +#endif return 1; } @@ -2170,6 +2456,9 @@ EXT_RETURN tls_construct_ctos_client_cert_type(SSL_CONNECTION *sc, WPACKET *pkt, sc->ext.client_cert_type_ctos = OSSL_CERT_TYPE_CTOS_NONE; if (sc->client_cert_type == NULL) return EXT_RETURN_NOT_SENT; +#ifndef OPENSSL_NO_ECH + ECH_SAME_EXT(sc, pkt) +#endif if (!WPACKET_put_bytes_u16(pkt, TLSEXT_TYPE_client_cert_type) || !WPACKET_start_sub_packet_u16(pkt) @@ -2222,6 +2511,9 @@ EXT_RETURN tls_construct_ctos_server_cert_type(SSL_CONNECTION *sc, WPACKET *pkt, sc->ext.server_cert_type_ctos = OSSL_CERT_TYPE_CTOS_NONE; if (sc->server_cert_type == NULL) return EXT_RETURN_NOT_SENT; +#ifndef OPENSSL_NO_ECH + ECH_SAME_EXT(sc, pkt) +#endif if (!WPACKET_put_bytes_u16(pkt, TLSEXT_TYPE_server_cert_type) || !WPACKET_start_sub_packet_u16(pkt) @@ -2266,3 +2558,96 @@ int tls_parse_stoc_server_cert_type(SSL_CONNECTION *sc, PACKET *pkt, sc->ext.server_cert_type = type; return 1; } + +#ifndef OPENSSL_NO_ECH +EXT_RETURN tls_construct_ctos_ech(SSL_CONNECTION *s, WPACKET *pkt, + unsigned int context, X509 *x, + size_t chainidx) +{ + if (s->ext.ech.attempted_type != TLSEXT_TYPE_ech + && s->ext.ech.grease != OSSL_ECH_IS_GREASE + && !(s->options & SSL_OP_ECH_GREASE)) + return EXT_RETURN_NOT_SENT; + /* send grease if not really attempting ECH */ + if (s->ext.ech.attempted == 0 + && (s->ext.ech.grease == OSSL_ECH_IS_GREASE + || (s->options & SSL_OP_ECH_GREASE))) { + if (s->hello_retry_request == SSL_HRR_PENDING + && s->ext.ech.sent != NULL) { + /* re-tx already sent GREASEy ECH */ + if (WPACKET_memcpy(pkt, s->ext.ech.sent, + s->ext.ech.sent_len) != 1) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + return EXT_RETURN_FAIL; + } + return EXT_RETURN_SENT; + } + /* if nobody set a type, use the defaulf */ + if (s->ext.ech.attempted_type == OSSL_ECH_type_unknown) + s->ext.ech.attempted_type = TLSEXT_TYPE_ech; + if (ossl_ech_send_grease(s, pkt) != 1) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + return EXT_RETURN_NOT_SENT; + } + return EXT_RETURN_SENT; + } + /* + * If not GREASEing we fake sending the outer value - after the + * entire thing has been constructed we only then finally encode + * and encrypt - need to do it that way as we need the rest of + * the outer CH as AAD input to the encryption. + */ + if (s->ext.ech.ch_depth == 0) + return EXT_RETURN_NOT_SENT; + /* For the inner CH - we simply include one of these saying "inner" */ + if (s->ext.ech.ch_depth == 1) { + if (!WPACKET_put_bytes_u16(pkt, TLSEXT_TYPE_ech) + || !WPACKET_start_sub_packet_u16(pkt) + || !WPACKET_put_bytes_u8(pkt, OSSL_ECH_INNER_CH_TYPE) + || !WPACKET_close(pkt)) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + return EXT_RETURN_FAIL; + } + return EXT_RETURN_SENT; + } + return EXT_RETURN_FAIL; +} + +/* if the server thinks we GREASE'd then we may get an ECHConfigList */ +int tls_parse_stoc_ech(SSL_CONNECTION *s, PACKET *pkt, unsigned int context, + X509 *x, size_t chainidx) +{ + unsigned int rlen = 0; + const unsigned char *rval = NULL; + unsigned char *srval = NULL; + + /* + * An HRR will have an ECH extension with the + * 8-octet confirmation value, already handled + */ + if (context == SSL_EXT_TLS1_3_HELLO_RETRY_REQUEST) + return 1; + /* othewise we expect retry-configs */ + if (!PACKET_get_net_2(pkt, &rlen)) { + SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_LENGTH_MISMATCH); + return 0; + } + if (!PACKET_get_bytes(pkt, &rval, rlen)) { + SSLfatal(s, SSL_AD_ILLEGAL_PARAMETER, SSL_R_LENGTH_MISMATCH); + return 0; + } + OPENSSL_free(s->ext.ech.returned); + s->ext.ech.returned = NULL; + srval = OPENSSL_malloc(rlen + 2); + if (srval == NULL) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + return 0; + } + srval[0] = (rlen >> 8) & 0xff; + srval[1] = rlen & 0xff; + memcpy(srval + 2, rval, rlen); + s->ext.ech.returned = srval; + s->ext.ech.returned_len = rlen + 2; + return 1; +} +#endif /* END_OPENSSL_NO_ECH */ diff --git a/ssl/statem/statem_clnt.c b/ssl/statem/statem_clnt.c index 5639048a8e6be..53a8fcc46117b 100644 --- a/ssl/statem/statem_clnt.c +++ b/ssl/statem/statem_clnt.c @@ -38,8 +38,6 @@ static MSG_PROCESS_RETURN tls_process_encrypted_extensions(SSL_CONNECTION *s, static ossl_inline int cert_req_allowed(SSL_CONNECTION *s); static int key_exchange_expected(SSL_CONNECTION *s); -static int ssl_cipher_list_to_bytes(SSL_CONNECTION *s, STACK_OF(SSL_CIPHER) *sk, - WPACKET *pkt); static ossl_inline int received_server_cert(SSL_CONNECTION *sc) { @@ -1175,7 +1173,252 @@ WORK_STATE ossl_statem_client_post_process_message(SSL_CONNECTION *s, } } -CON_FUNC_RETURN tls_construct_client_hello(SSL_CONNECTION *s, WPACKET *pkt) +#ifndef OPENSSL_NO_ECH +/* + * Wrap the existing ClientHello construction with ECH code. + * + * As needed, we'll call the existing CH constructor twice, + * first for inner, and then for outer. + * + * So the old tls_construct_client_hello is renamed to the _aux + * variant, and the new tls_construct_client_hello just calls + * that if there's no ECH involved, but otherwise does ECH + * things around calls to the _aux variant. + * + * Our basic model is that, when really attempting ECH we + * indicate via the ch_depth field whether we're dealing + * with inner or outer CH (1 for inner, 0 for outer). + * + * After creating the fields for the inner CH, we encode + * those (so we can re-use existing code) then decode again + * (using the existing tls_process_client_hello previously + * only used on servers), again to maximise code re-use. + * + * We next re-encode inner but this time including the + * optimisations for inner CH "compression" (outer exts etc.) + * to produce our plaintext for encrypting. + * + * We then process the outer CH in more or less the + * usual manner. + * + * We lastly form up the AAD etc and encrypt to give us + * the ciphertext for inclusion in the value of the outer + * CH ECH extension. + * + * It may seem odd to form up the outer CH before + * encrypting, but we need to do it that way so we get + * the octets for the AAD used in encryption. + * + * Phew! + */ +static int tls_construct_client_hello_aux(SSL_CONNECTION *s, WPACKET *pkt); + +__owur CON_FUNC_RETURN tls_construct_client_hello(SSL_CONNECTION *s, + WPACKET *pkt) +{ + unsigned char *innerch_full = NULL, *innerch_end = NULL; + WPACKET inner; /* "fake" pkt for inner */ + BUF_MEM *inner_mem = NULL; + PACKET rpkt; /* we'll decode back the inner ch to help make the outer */ + SSL_SESSION *sess = NULL; + size_t sess_id_len = 0, innerlen = 0; + int mt = SSL3_MT_CLIENT_HELLO, rv = 0; + OSSL_HPKE_SUITE suite; + OSSL_ECHSTORE_ENTRY *ee = NULL; + /* Work out what SSL/TLS/DTLS version to use */ + int protverr = ssl_set_client_hello_version(s); + + if (protverr != 0) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, protverr); + return 0; + } + /* If we're not really attempting ECH, just call existing code. */ + if (s->ext.ech.es == NULL) + return tls_construct_client_hello_aux(s, pkt); + /* note version we're attempting and that an attempt is being made */ + if (s->ext.ech.es->entries != NULL) { + if (ossl_ech_pick_matching_cfg(s, &ee, &suite) != 1 || ee == NULL) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, protverr); + return 0; + } + if (ee->version != OSSL_ECH_RFCXXXX_VERSION) { + /* we only support that version for now */ + SSLfatal(s, SSL_AD_INTERNAL_ERROR, protverr); + return 0; + } + s->ext.ech.attempted_type = TLSEXT_TYPE_ech; + s->ext.ech.attempted_cid = ee->config_id; + s->ext.ech.attempted = 1; + if (s->ext.ech.outer_hostname == NULL && ee->public_name != NULL) { + s->ext.ech.outer_hostname = OPENSSL_strdup((char *)ee->public_name); + if (s->ext.ech.outer_hostname == NULL) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + return 0; + } + } + } + /* If doing real ECH and application requested GREASE too, over-ride that */ + if (s->ext.ech.grease == OSSL_ECH_IS_GREASE && s->ext.ech.attempted == 1) + s->ext.ech.grease = OSSL_ECH_NOT_GREASE; + /* + * Session ID is handled "oddly" by not being encoded into inner CH (an + * optimisation) so is the same for both inner and outer. + */ + sess = s->session; + if (sess == NULL + || !ssl_version_supported(s, sess->ssl_version, NULL) + || !SSL_SESSION_is_resumable(sess)) { + if (s->hello_retry_request == SSL_HRR_NONE + && !ssl_get_new_session(s, 0)) + return 0; /* SSLfatal() already called */ + } + if (s->new_session || s->session->ssl_version == TLS1_3_VERSION) { + if (s->version == TLS1_3_VERSION + && (s->options & SSL_OP_ENABLE_MIDDLEBOX_COMPAT) != 0) { + sess_id_len = sizeof(s->tmp_session_id); + s->tmp_session_id_len = sess_id_len; + if (s->hello_retry_request == SSL_HRR_NONE + && RAND_bytes_ex(s->ssl.ctx->libctx, s->tmp_session_id, + sess_id_len, RAND_DRBG_STRENGTH) <= 0) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + return 0; + } + memcpy(s->session->session_id, s->tmp_session_id, sess_id_len); + s->session->session_id_length = sess_id_len; + } else { + sess_id_len = 0; + } + } else { + assert(s->session->session_id_length <= sizeof(s->session->session_id)); + sess_id_len = s->session->session_id_length; + if (s->version == TLS1_3_VERSION) { + s->tmp_session_id_len = sess_id_len; + memcpy(s->tmp_session_id, s->session->session_id, sess_id_len); + } + } + if (s->hello_retry_request != SSL_HRR_NONE) { + s->ext.ech.n_outer_only = 0; /* reset count of "compressed" exts */ + OPENSSL_free(s->ext.ech.encoded_innerch); + s->ext.ech.encoded_innerch = NULL; + s->ext.ech.encoded_innerch_len = 0; + if (s->ext.ech.innerch != NULL) { + OPENSSL_free(s->ext.ech.innerch1); + s->ext.ech.innerch1 = s->ext.ech.innerch; + s->ext.ech.innerch1_len = s->ext.ech.innerch_len; + s->ext.ech.innerch_len = 0; + s->ext.ech.innerch = NULL; + } + } + /* + * Set CH depth flag so that other code (e.g. extension handlers) + * know where we're at: 1 is "inner CH", 0 is "outer CH" + */ + s->ext.ech.ch_depth = 1; + if ((inner_mem = BUF_MEM_new()) == NULL + || !WPACKET_init(&inner, inner_mem) + || !ssl_set_handshake_header(s, &inner, mt) + || tls_construct_client_hello_aux(s, &inner) != 1 + || !WPACKET_close(&inner) + || !WPACKET_get_length(&inner, &innerlen)) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, protverr); + goto err; + } + innerch_full = OPENSSL_malloc(innerlen); + if (innerch_full == NULL) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + goto err; + } + innerch_end = WPACKET_get_curr(&inner); + memcpy(innerch_full, innerch_end - innerlen, innerlen); + OPENSSL_free(s->ext.ech.innerch); + s->ext.ech.innerch = innerch_full; + s->ext.ech.innerch_len = innerlen; + WPACKET_cleanup(&inner); + BUF_MEM_free(inner_mem); + inner_mem = NULL; +# ifdef OSSL_ECH_SUPERVERBOSE + ossl_ech_pbuf("inner CH", s->ext.ech.innerch, s->ext.ech.innerch_len); + ossl_ech_pbuf("inner, client_random", s->ext.ech.client_random, + SSL3_RANDOM_SIZE); + ossl_ech_pbuf("inner, session_id", s->session->session_id, + s->session->session_id_length); +# endif + /* Decode inner so that we can make up encoded inner */ + if (!PACKET_buf_init(&rpkt, (unsigned char *)s->ext.ech.innerch + 4, + s->ext.ech.innerch_len - 4)) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + goto err; + } + /* + * Parse the full inner CH (usually done on server). This gets us + * individually encoded extensions so we can choose to compress + * and/or to re-use the same value in outer. + */ + if (!tls_process_client_hello(s, &rpkt)) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + goto err; + } + /* Make ClientHelloInner and EncodedClientHelloInner as per spec. */ + if (ossl_ech_encode_inner(s) != 1) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + goto err; + } +# ifdef OSSL_ECH_SUPERVERBOSE + ossl_ech_pbuf("encoded inner CH", s->ext.ech.encoded_innerch, + s->ext.ech.encoded_innerch_len); +# endif + s->ext.ech.ch_depth = 0; /* set depth for outer CH */ + /* + * If we want different key shares for inner and outer, then + * zap the one for the inner. The inner key_share is stashed + * in s.ext.ech.tmp_pkey already. + */ + if (ossl_ech_same_key_share() == 0) { + EVP_PKEY_free(s->s3.tmp.pkey); + s->s3.tmp.pkey = NULL; + } + /* Make second call into CH construction for outer CH. */ + rv = tls_construct_client_hello_aux(s, pkt); + if (rv != 1) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, protverr); + goto err; + } +# ifdef OSSL_ECH_SUPERVERBOSE + ossl_ech_pbuf("outer, client_random", s->s3.client_random, + SSL3_RANDOM_SIZE); + ossl_ech_pbuf("outer, session_id", s->session->session_id, + s->session->session_id_length); +# endif + /* Finally, calculate AAD and encrypt using HPKE */ + if (ossl_ech_aad_and_encrypt(s, pkt) != 1) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + goto err; + } + /* Free up raw exts as needed (happens like this on real server) */ + if (s->clienthello != NULL + && s->clienthello->pre_proc_exts != NULL) { + OPENSSL_free(s->clienthello->pre_proc_exts); + OPENSSL_free(s->clienthello); + s->clienthello = NULL; + } + return 1; +err: + if (inner_mem != NULL) { + WPACKET_cleanup(&inner); + BUF_MEM_free(inner_mem); + } + if (s->clienthello != NULL) { + OPENSSL_free(s->clienthello->pre_proc_exts); + OPENSSL_free(s->clienthello); + s->clienthello = NULL; + } + return 0; +} + +static int tls_construct_client_hello_aux(SSL_CONNECTION *s, WPACKET *pkt) +#else +__owur CON_FUNC_RETURN tls_construct_client_hello(SSL_CONNECTION *s, WPACKET *pkt) +#endif { unsigned char *p; size_t sess_id_len; @@ -1194,18 +1437,30 @@ CON_FUNC_RETURN tls_construct_client_hello(SSL_CONNECTION *s, WPACKET *pkt) return CON_FUNC_ERROR; } - if (sess == NULL +#ifndef OPENSSL_NO_ECH + /* if we're doing ECH, re-use session ID setup earlier */ + if (s->ext.ech.es == NULL) +#endif + if (sess == NULL || !ssl_version_supported(s, sess->ssl_version, NULL) || !SSL_SESSION_is_resumable(sess)) { - if (s->hello_retry_request == SSL_HRR_NONE - && !ssl_get_new_session(s, 0)) { - /* SSLfatal() already called */ - return CON_FUNC_ERROR; + if (s->hello_retry_request == SSL_HRR_NONE + && !ssl_get_new_session(s, 0)) { + /* SSLfatal() already called */ + return CON_FUNC_ERROR; + } } - } - /* else use the pre-loaded session */ + /* else use the pre-loaded session */ +#ifndef OPENSSL_NO_ECH + /* use different client_random fields for inner and outer */ + if (s->ext.ech.es != NULL && s->ext.ech.ch_depth == 1) + p = s->ext.ech.client_random; + else + p = s->s3.client_random; +#else p = s->s3.client_random; +#endif /* * for DTLS if client_random is initialized, reuse it, we are @@ -1263,37 +1518,55 @@ CON_FUNC_RETURN tls_construct_client_hello(SSL_CONNECTION *s, WPACKET *pkt) * For TLS 1.3 we always set the ClientHello version to 1.2 and rely on the * supported_versions extension for the real supported versions. */ +#ifndef OPENSSL_NO_ECH + if (!WPACKET_put_bytes_u16(pkt, s->client_version) + || !WPACKET_memcpy(pkt, p, SSL3_RANDOM_SIZE)) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + return CON_FUNC_ERROR; + } +#else if (!WPACKET_put_bytes_u16(pkt, s->client_version) || !WPACKET_memcpy(pkt, s->s3.client_random, SSL3_RANDOM_SIZE)) { SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); return CON_FUNC_ERROR; } +#endif /* Session ID */ session_id = s->session->session_id; - if (s->new_session || s->session->ssl_version == TLS1_3_VERSION) { - if (s->version == TLS1_3_VERSION - && (s->options & SSL_OP_ENABLE_MIDDLEBOX_COMPAT) != 0) { - sess_id_len = sizeof(s->tmp_session_id); - s->tmp_session_id_len = sess_id_len; - session_id = s->tmp_session_id; - if (s->hello_retry_request == SSL_HRR_NONE - && RAND_bytes_ex(sctx->libctx, s->tmp_session_id, - sess_id_len, 0) <= 0) { - SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); - return CON_FUNC_ERROR; +#ifndef OPENSSL_NO_ECH + /* same session ID is used for inner/outer when doing ECH */ + if (s->ext.ech.es != NULL) { + sess_id_len = sizeof(s->tmp_session_id); + } else { +#endif + if (s->new_session || s->session->ssl_version == TLS1_3_VERSION) { + if (s->version == TLS1_3_VERSION + && (s->options & SSL_OP_ENABLE_MIDDLEBOX_COMPAT) != 0) { + sess_id_len = sizeof(s->tmp_session_id); + s->tmp_session_id_len = sess_id_len; + session_id = s->tmp_session_id; + if (s->hello_retry_request == SSL_HRR_NONE + && RAND_bytes_ex(sctx->libctx, s->tmp_session_id, + sess_id_len, 0) <= 0) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + return CON_FUNC_ERROR; + } + } else { + sess_id_len = 0; } } else { - sess_id_len = 0; - } - } else { - assert(s->session->session_id_length <= sizeof(s->session->session_id)); - sess_id_len = s->session->session_id_length; - if (s->version == TLS1_3_VERSION) { - s->tmp_session_id_len = sess_id_len; - memcpy(s->tmp_session_id, s->session->session_id, sess_id_len); + assert(s->session->session_id_length <= sizeof(s->session->session_id)); + sess_id_len = s->session->session_id_length; + if (s->version == TLS1_3_VERSION) { + s->tmp_session_id_len = sess_id_len; + memcpy(s->tmp_session_id, s->session->session_id, sess_id_len); + } } +#ifndef OPENSSL_NO_ECH } +#endif + if (!WPACKET_start_sub_packet_u8(pkt) || (sess_id_len != 0 && !WPACKET_memcpy(pkt, session_id, sess_id_len)) @@ -1482,6 +1755,24 @@ MSG_PROCESS_RETURN tls_process_server_hello(SSL_CONNECTION *s, PACKET *pkt) #ifndef OPENSSL_NO_COMP SSL_COMP *comp; #endif +#ifndef OPENSSL_NO_ECH + const unsigned char *shbuf = NULL; + size_t shlen, chend, fixedshbuf_len, alen; + /* + * client and server accept signal buffers, initialise in case of + * e.g. memory fail when calculating, only really applies when + * SUPERVERBOSE is defined and we trace these. + */ + unsigned char c_signal[OSSL_ECH_SIGNAL_LEN] = { 0 }; + unsigned char s_signal[OSSL_ECH_SIGNAL_LEN] = { 0xff }; + unsigned char *abuf = NULL; + + shlen = PACKET_remaining(pkt); + if (PACKET_peek_bytes(pkt, &shbuf, shlen) != 1) { + SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_LENGTH_MISMATCH); + goto err; + } +#endif if (!PACKET_get_net_2(pkt, &sversion)) { SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_LENGTH_MISMATCH); @@ -1537,6 +1828,71 @@ MSG_PROCESS_RETURN tls_process_server_hello(SSL_CONNECTION *s, PACKET *pkt) goto err; } +#ifndef OPENSSL_NO_ECH + /* + * If we sent an ECH then check if that worked based on the + * ServerHello.random confirmation trick. If that is good + * then we'll swap over the inner and outer contexts and + * proceed with inner. There are some HRR wrinkles too + * though. + */ + if (s->ext.ech.es != NULL + && s->ext.ech.done != 1 && s->ext.ech.ch_depth == 0 + && s->ext.ech.grease == OSSL_ECH_NOT_GREASE + && s->ext.ech.attempted_type == TLSEXT_TYPE_ech) { + /* try set this earlier see what happens */ + if (!set_client_ciphersuite(s, cipherchars)) { + /* SSLfatal() already called */ + goto err; + } + /* check the ECH accept signal */ + if (ossl_ech_calc_confirm(s, hrr, c_signal, shbuf, shlen) != 1 + || ossl_ech_find_confirm(s, hrr, s_signal, shbuf, shlen) != 1 + || memcmp(s_signal, c_signal, sizeof(c_signal)) != 0) { + OSSL_TRACE(TLS, "ECH accept check failed\n"); +# ifdef OSSL_ECH_SUPERVERBOSE + ossl_ech_pbuf("ECH client accept val:", c_signal, sizeof(c_signal)); + ossl_ech_pbuf("ECH server accept val:", s_signal, sizeof(s_signal)); +# endif + s->ext.ech.success = 0; + } else { /* match, ECH worked */ + OSSL_TRACE_BEGIN(TLS) { + BIO_printf(trc_out, "ECH accept check ok\n"); + BIO_printf(trc_out, "ECH set session hostname to %s\n", + s->ext.hostname ? s->ext.hostname : "NULL"); + } OSSL_TRACE_END(TLS); + s->ext.ech.success = 1; + } + if (!hrr && s->ext.ech.success == 1) { + if (ossl_ech_swaperoo(s) != 1 + || ossl_ech_make_transcript_buffer(s, hrr, shbuf, shlen, + &abuf, &alen, + &chend, &fixedshbuf_len) != 1 + || ossl_ech_reset_hs_buffer(s, abuf, alen) != 1) { + OPENSSL_free(abuf); + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + goto err; + } + OPENSSL_free(abuf); + } else if (!hrr) { + /* + * If we got retry_configs then we should be validating + * the outer CH, so we better set the hostname for the + * connection accordingly. + */ + s->ext.ech.former_inner = s->ext.hostname; + s->ext.hostname = NULL; + if (s->ext.ech.outer_hostname != NULL) { + s->ext.hostname = OPENSSL_strdup(s->ext.ech.outer_hostname); + if (s->ext.hostname == NULL) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + goto err; + } + } + } + } +#endif + /* TLS extensions */ if (PACKET_remaining(pkt) == 0 && !hrr) { PACKET_null_init(&extpkt); @@ -3031,6 +3387,18 @@ int tls_process_initial_server_flight(SSL_CONNECTION *s) } #endif +#ifndef OPENSSL_NO_ECH + /* check result of ech and return error if needed */ + if (!s->server + && s->ext.ech.es != NULL + && s->ext.ech.attempted == 1 + && s->ext.ech.success != 1 + && s->ext.ech.grease != OSSL_ECH_IS_GREASE) { + SSLfatal(s, SSL_AD_ECH_REQUIRED, SSL_R_ECH_REQUIRED); + return 0; + } +#endif /* OPENSSL_NO_ECH */ + return 1; } @@ -4138,110 +4506,6 @@ int ssl_do_client_cert_cb(SSL_CONNECTION *s, X509 **px509, EVP_PKEY **ppkey) return i; } -int ssl_cipher_list_to_bytes(SSL_CONNECTION *s, STACK_OF(SSL_CIPHER) *sk, - WPACKET *pkt) -{ - int i; - size_t totlen = 0, len, maxlen, maxverok = 0; - int empty_reneg_info_scsv = !s->renegotiate - && !SSL_CONNECTION_IS_DTLS(s) - && ssl_security(s, SSL_SECOP_VERSION, 0, TLS1_VERSION, NULL) - && s->min_proto_version <= TLS1_VERSION; - SSL *ssl = SSL_CONNECTION_GET_SSL(s); - - /* Set disabled masks for this session */ - if (!ssl_set_client_disabled(s)) { - SSLfatal(s, SSL_AD_INTERNAL_ERROR, SSL_R_NO_PROTOCOLS_AVAILABLE); - return 0; - } - - if (sk == NULL) { - SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); - return 0; - } - -#ifdef OPENSSL_MAX_TLS1_2_CIPHER_LENGTH -# if OPENSSL_MAX_TLS1_2_CIPHER_LENGTH < 6 -# error Max cipher length too short -# endif - /* - * Some servers hang if client hello > 256 bytes as hack workaround - * chop number of supported ciphers to keep it well below this if we - * use TLS v1.2 - */ - if (TLS1_get_version(ssl) >= TLS1_2_VERSION) - maxlen = OPENSSL_MAX_TLS1_2_CIPHER_LENGTH & ~1; - else -#endif - /* Maximum length that can be stored in 2 bytes. Length must be even */ - maxlen = 0xfffe; - - if (empty_reneg_info_scsv) - maxlen -= 2; - if (s->mode & SSL_MODE_SEND_FALLBACK_SCSV) - maxlen -= 2; - - for (i = 0; i < sk_SSL_CIPHER_num(sk) && totlen < maxlen; i++) { - const SSL_CIPHER *c; - - c = sk_SSL_CIPHER_value(sk, i); - /* Skip disabled ciphers */ - if (ssl_cipher_disabled(s, c, SSL_SECOP_CIPHER_SUPPORTED, 0)) - continue; - - if (!ssl->method->put_cipher_by_char(c, pkt, &len)) { - SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); - return 0; - } - - /* Sanity check that the maximum version we offer has ciphers enabled */ - if (!maxverok) { - int minproto = SSL_CONNECTION_IS_DTLS(s) ? c->min_dtls : c->min_tls; - int maxproto = SSL_CONNECTION_IS_DTLS(s) ? c->max_dtls : c->max_tls; - - if (ssl_version_cmp(s, maxproto, s->s3.tmp.max_ver) >= 0 - && ssl_version_cmp(s, minproto, s->s3.tmp.max_ver) <= 0) - maxverok = 1; - } - - totlen += len; - } - - if (totlen == 0 || !maxverok) { - const char *maxvertext = - !maxverok - ? "No ciphers enabled for max supported SSL/TLS version" - : NULL; - - SSLfatal_data(s, SSL_AD_INTERNAL_ERROR, SSL_R_NO_CIPHERS_AVAILABLE, - maxvertext); - return 0; - } - - if (totlen != 0) { - if (empty_reneg_info_scsv) { - static const SSL_CIPHER scsv = { - 0, NULL, NULL, SSL3_CK_SCSV, 0, 0, 0, 0, 0, 0, 0, 0, 0 - }; - if (!ssl->method->put_cipher_by_char(&scsv, pkt, &len)) { - SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); - return 0; - } - } - if (s->mode & SSL_MODE_SEND_FALLBACK_SCSV) { - static const SSL_CIPHER scsv = { - 0, NULL, NULL, SSL3_CK_FALLBACK_SCSV, 0, 0, 0, 0, 0, 0, 0, 0, 0 - }; - if (!ssl->method->put_cipher_by_char(&scsv, pkt, &len)) { - SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); - return 0; - } - } - } - - return 1; -} - CON_FUNC_RETURN tls_construct_end_of_early_data(SSL_CONNECTION *s, WPACKET *pkt) { if (s->early_data_state != SSL_EARLY_DATA_WRITE_RETRY diff --git a/ssl/statem/statem_local.h b/ssl/statem/statem_local.h index 48870683c3426..6328e1ee828bd 100644 --- a/ssl/statem/statem_local.h +++ b/ssl/statem/statem_local.h @@ -570,3 +570,13 @@ int tls_parse_ctos_server_cert_type(SSL_CONNECTION *sc, PACKET *pkt, int tls_parse_stoc_server_cert_type(SSL_CONNECTION *s, PACKET *pkt, unsigned int context, X509 *x, size_t chainidx); +#ifndef OPENSSL_NO_ECH +EXT_RETURN tls_construct_ctos_ech(SSL_CONNECTION *s, WPACKET *pkt, + unsigned int context, X509 *x, + size_t chainidx); +EXT_RETURN tls_construct_ctos_ech(SSL_CONNECTION *s, WPACKET *pkt, + unsigned int context, X509 *x, + size_t chainidx); +int tls_parse_stoc_ech(SSL_CONNECTION *s, PACKET *pkt, unsigned int context, + X509 *x, size_t chainidx); +#endif diff --git a/ssl/t1_enc.c b/ssl/t1_enc.c index 941455bc8aeb2..bf7a7a516f7e6 100644 --- a/ssl/t1_enc.c +++ b/ssl/t1_enc.c @@ -585,6 +585,10 @@ int tls1_alert_code(int code) return SSL_AD_HANDSHAKE_FAILURE; case TLS13_AD_MISSING_EXTENSION: return SSL_AD_HANDSHAKE_FAILURE; +#ifndef OPENSSL_NO_ECH + case SSL_AD_ECH_REQUIRED: + return TLS1_AD_ECH_REQUIRED; +#endif default: return -1; } diff --git a/ssl/t1_trce.c b/ssl/t1_trce.c index 640552c0ba0db..e6dcf28db301d 100644 --- a/ssl/t1_trce.c +++ b/ssl/t1_trce.c @@ -500,6 +500,10 @@ static const ssl_trace_tbl ssl_exts_tbl[] = { # ifndef OPENSSL_NO_NEXTPROTONEG {TLSEXT_TYPE_next_proto_neg, "next_proto_neg"}, # endif +# ifndef OPENSSL_NO_ECH + {TLSEXT_TYPE_ech, "encrypted_client_hello"}, + {TLSEXT_TYPE_outer_extensions, "outer_extension"}, +# endif }; static const ssl_trace_tbl ssl_groups_tbl[] = { diff --git a/test/ech_test.c b/test/ech_test.c index ef71d158a571a..743ab90208e32 100644 --- a/test/ech_test.c +++ b/test/ech_test.c @@ -827,7 +827,7 @@ static int ech_ingest_test(int run) BIO *in = NULL, *out = NULL; int i, rv = 0, keysb4, keysaftr, actual_ents = 0, has_priv, for_retry; ingest_tv_t *tv = &ingest_tvs[run]; - time_t now = 0, secs = 0; + time_t secs = 0, add_time = 0, flush_time = 0; char *pn = NULL, *ec = NULL; if ((in = BIO_new(BIO_s_mem())) == NULL @@ -842,6 +842,7 @@ static int ech_ingest_test(int run) TEST_info("Bad test vector entry"); goto end; } + add_time = time(0); if (tv->pemenc == 1 && !TEST_int_eq(OSSL_ECHSTORE_read_pem(es, in, OSSL_ECH_NO_RETRY), tv->read)) @@ -885,10 +886,10 @@ static int ech_ingest_test(int run) || !TEST_true(OSSL_ECHSTORE_write_pem(es, OSSL_ECHSTORE_ALL, out)) || !TEST_false(OSSL_ECHSTORE_write_pem(es, 100, out))) goto end; - now = time(0); - if (!TEST_true(OSSL_ECHSTORE_flush_keys(es, now)) - || !TEST_true(OSSL_ECHSTORE_num_keys(es, &keysaftr)) - || !TEST_false(keysaftr)) + flush_time = time(0); + if (!TEST_true(OSSL_ECHSTORE_flush_keys(es, flush_time - add_time)) + || !TEST_int_eq(OSSL_ECHSTORE_num_keys(es, &keysaftr), 1) + || !TEST_int_eq(keysaftr, 0)) goto end; rv = 1; end: @@ -1128,6 +1129,7 @@ static int ech_boring_compat(void) # define OSSL_ECH_TEST_HRR 1 # define OSSL_ECH_TEST_EARLY 2 # define OSSL_ECH_TEST_CUSTOM 3 +# define OSSL_ECH_TEST_ENOE 4 /* early + no-ech */ /* * @brief ECH roundtrip test helper @@ -1153,22 +1155,19 @@ static int ech_boring_compat(void) static int test_ech_roundtrip_helper(int idx, int combo) { int res = 0, kemind, kdfind, aeadind, kemsz, kdfsz, aeadsz; - char suitestr[100]; + int clientstatus, serverstatus, server = 1, client = 0; + unsigned int context; OSSL_ECHSTORE *es = NULL; OSSL_HPKE_SUITE hpke_suite = OSSL_HPKE_SUITE_DEFAULT; uint16_t ech_version = OSSL_ECH_CURRENT_VERSION; uint8_t max_name_length = 0; - char *public_name = "example.com"; + char *public_name = "example.com", suitestr[100]; SSL_CTX *cctx = NULL, *sctx = NULL; SSL *clientssl = NULL, *serverssl = NULL; - int clientstatus, serverstatus; char *cinner = NULL, *couter = NULL, *sinner = NULL, *souter = NULL; SSL_SESSION *sess = NULL; - unsigned char ed[21]; size_t written = 0, readbytes = 0; - unsigned char buf[1024]; - unsigned int context; - int server = 1, client = 0; + unsigned char ed[21], buf[1024]; /* split idx into kemind, kdfind, aeadind */ kemsz = OSSL_NELEM(kem_str_list); @@ -1192,21 +1191,17 @@ static int test_ech_roundtrip_helper(int idx, int combo) TLS1_3_VERSION, TLS1_3_VERSION, &sctx, &cctx, cert, privkey))) goto end; - if (combo == OSSL_ECH_TEST_EARLY) { - /* just to keep the format checker happy :-) */ - int lrv = 0; - + if (combo == OSSL_ECH_TEST_EARLY || combo == OSSL_ECH_TEST_ENOE) { if (!TEST_true(SSL_CTX_set_options(sctx, SSL_OP_NO_ANTI_REPLAY)) || !TEST_true(SSL_CTX_set_max_early_data(sctx, SSL3_RT_MAX_PLAIN_LENGTH))) goto end; - lrv = SSL_CTX_set_recv_max_early_data(sctx, SSL3_RT_MAX_PLAIN_LENGTH); - if (!TEST_true(lrv)) + if (!TEST_true(SSL_CTX_set_recv_max_early_data(sctx, + SSL3_RT_MAX_PLAIN_LENGTH))) goto end; } if (combo == OSSL_ECH_TEST_CUSTOM) { - /* add custom CH ext to client and server */ - context = SSL_EXT_CLIENT_HELLO; + context = SSL_EXT_CLIENT_HELLO; /* add custom CH ext to client/server */ if (!TEST_true(SSL_CTX_add_custom_ext(cctx, TEST_EXT_TYPE1, context, new_add_cb, new_free_cb, &client, new_parse_cb, &client)) @@ -1221,18 +1216,40 @@ static int test_ech_roundtrip_helper(int idx, int combo) &server, NULL, &server))) goto end; } - if (!TEST_true(SSL_CTX_set1_echstore(cctx, es)) - || !TEST_true(SSL_CTX_set1_echstore(sctx, es)) + if (combo != OSSL_ECH_TEST_ENOE + && !TEST_true(SSL_CTX_set1_echstore(cctx, es))) + goto end; + if (!TEST_true(SSL_CTX_set1_echstore(sctx, es)) || !TEST_true(create_ssl_objects(sctx, cctx, &serverssl, &clientssl, NULL, NULL))) goto end; if (combo == OSSL_ECH_TEST_HRR && !TEST_true(SSL_set1_groups_list(serverssl, "P-384"))) goto end; - if (!TEST_true(SSL_set_tlsext_host_name(clientssl, "server.example")) - || !TEST_true(create_ssl_connection(serverssl, clientssl, + if (!TEST_true(SSL_set_tlsext_host_name(clientssl, "server.example"))) + goto end; +# undef DROPFORNOW +# ifdef DROPFORNOW + /* TODO(ECH): we'll re-instate this once server-side ECH code is in */ + if (!TEST_true(create_ssl_connection(serverssl, clientssl, + SSL_ERROR_NONE))) + goto end; +# else + /* + * For this PR, check connections fail when client does ECH + * and server doesn't, but work if client doesn't do ECH. + * Added in early data for the no-ECH case because an + * intermediate state of the code had an issue. + */ + if (combo != OSSL_ECH_TEST_ENOE + && !TEST_false(create_ssl_connection(serverssl, clientssl, + SSL_ERROR_NONE))) + goto end; + if (combo == OSSL_ECH_TEST_ENOE + && !TEST_true(create_ssl_connection(serverssl, clientssl, SSL_ERROR_NONE))) goto end; +# endif serverstatus = SSL_ech_get1_status(serverssl, &sinner, &souter); if (verbose) TEST_info("server status %d, %s, %s", serverstatus, sinner, souter); @@ -1253,8 +1270,16 @@ static int test_ech_roundtrip_helper(int idx, int combo) goto end; } /* continue for EARLY test */ - if (combo != OSSL_ECH_TEST_EARLY) +# ifdef DROPFORNOW + /* TODO(ECH): turn back on later */ + if (combo != OSSL_ECH_TEST_EARLY && combo != OSSL_ECH_TEST_ENOE) + goto end; +# else + if (combo != OSSL_ECH_TEST_ENOE) { + res = 1; goto end; + } +# endif /* shutdown for start over */ sess = SSL_get1_session(clientssl); OPENSSL_free(sinner); @@ -1348,6 +1373,14 @@ static int ech_custom_test(int idx) return test_ech_roundtrip_helper(idx, OSSL_ECH_TEST_CUSTOM); } +/* Test a roundtrip with No ECH, and early data */ +static int ech_enoe_test(int idx) +{ + if (verbose) + TEST_info("Doing: ech_no ech + early test "); + return test_ech_roundtrip_helper(idx, OSSL_ECH_TEST_ENOE); +} + #endif int setup_tests(void) @@ -1390,6 +1423,7 @@ int setup_tests(void) ADD_ALL_TESTS(test_ech_hrr, suite_combos); ADD_ALL_TESTS(test_ech_early, suite_combos); ADD_ALL_TESTS(ech_custom_test, suite_combos); + ADD_ALL_TESTS(ech_enoe_test, suite_combos); /* TODO(ECH): add more test code as other PRs done */ return 1; err: diff --git a/test/ext_internal_test.c b/test/ext_internal_test.c index 20cf708de27a3..c7d07d930ee1b 100644 --- a/test/ext_internal_test.c +++ b/test/ext_internal_test.c @@ -72,6 +72,13 @@ static EXT_LIST ext_list[] = { EXT_ENTRY(compress_certificate), EXT_ENTRY(early_data), EXT_ENTRY(certificate_authorities), +#ifndef OPENSSL_NO_ECH + EXT_ENTRY(ech), + EXT_ENTRY(outer_extensions), +#else + EXT_EXCEPTION(ech), + EXT_EXCEPTION(outer_extensions), +#endif EXT_ENTRY(padding), EXT_ENTRY(psk), EXT_END(num_builtins) diff --git a/test/recipes/75-test_quicapi_data/ssltraceref-zlib.txt b/test/recipes/75-test_quicapi_data/ssltraceref-zlib.txt index d36d58772b876..e7cf0b4e9e614 100644 --- a/test/recipes/75-test_quicapi_data/ssltraceref-zlib.txt +++ b/test/recipes/75-test_quicapi_data/ssltraceref-zlib.txt @@ -31,8 +31,6 @@ Header: ffdhe2048 (256) ffdhe3072 (257) extension_type=session_ticket(35), length=0 - extension_type=application_layer_protocol_negotiation(16), length=11 - ossltest extension_type=encrypt_then_mac(22), length=0 extension_type=extended_master_secret(23), length=0 extension_type=signature_algorithms(13), length=? @@ -67,6 +65,8 @@ Header: key_exchange: (len=32): ? extension_type=compress_certificate(27), length=3 zlib (1) + extension_type=application_layer_protocol_negotiation(16), length=11 + ossltest Sent Frame: Crypto Offset: 0 diff --git a/test/recipes/75-test_quicapi_data/ssltraceref.txt b/test/recipes/75-test_quicapi_data/ssltraceref.txt index 7b7fa28c08871..b6daa53ae1e0c 100644 --- a/test/recipes/75-test_quicapi_data/ssltraceref.txt +++ b/test/recipes/75-test_quicapi_data/ssltraceref.txt @@ -31,8 +31,6 @@ Header: ffdhe2048 (256) ffdhe3072 (257) extension_type=session_ticket(35), length=0 - extension_type=application_layer_protocol_negotiation(16), length=11 - ossltest extension_type=encrypt_then_mac(22), length=0 extension_type=extended_master_secret(23), length=0 extension_type=signature_algorithms(13), length=? @@ -65,6 +63,8 @@ Header: key_exchange: (len=1216): ? NamedGroup: ecdh_x25519 (29) key_exchange: (len=32): ? + extension_type=application_layer_protocol_negotiation(16), length=11 + ossltest Sent Frame: Crypto Offset: 0 From 03590aa25bf28d0d2c6226a6f60005b9af41bff4 Mon Sep 17 00:00:00 2001 From: sftcd Date: Sat, 28 Dec 2024 02:49:12 +0000 Subject: [PATCH 07/24] ECH client side transcript refactor Reviewed-by: Tomas Mraz Reviewed-by: Matt Caswell (Merged from https://github.com/openssl/openssl/pull/26011) --- include/internal/ech_helpers.h | 6 +- ssl/ech/ech_helper.c | 111 +----- ssl/ech/ech_internal.c | 687 +++++++++++---------------------- ssl/ech/ech_local.h | 86 +++-- ssl/ech/ech_ssl_apis.c | 11 +- ssl/ech/ech_store.c | 5 +- ssl/statem/extensions.c | 65 +--- ssl/statem/extensions_clnt.c | 346 +++++++++++------ ssl/statem/extensions_cust.c | 75 ++++ ssl/statem/statem_clnt.c | 180 +++++---- ssl/tls13_enc.c | 23 +- test/ech_test.c | 13 +- 12 files changed, 716 insertions(+), 892 deletions(-) diff --git a/include/internal/ech_helpers.h b/include/internal/ech_helpers.h index b71029857b156..3e13936e2e9cf 100644 --- a/include/internal/ech_helpers.h +++ b/include/internal/ech_helpers.h @@ -17,10 +17,8 @@ # ifndef OPENSSL_NO_ECH -int ossl_ech_get_sh_offsets(const unsigned char *sh, size_t sh_len, - size_t *exts, size_t *echoffset, - uint16_t *echtype); -int ossl_ech_make_enc_info(unsigned char *encoding, size_t encoding_length, +int ossl_ech_make_enc_info(const unsigned char *encoding, + size_t encoding_length, unsigned char *info, size_t *info_len); # endif diff --git a/ssl/ech/ech_helper.c b/ssl/ech/ech_helper.c index 6f900408ef553..4303a1b6eb082 100644 --- a/ssl/ech/ech_helper.c +++ b/ssl/ech/ech_helper.c @@ -20,116 +20,22 @@ static const char OSSL_ECH_CONTEXT_STRING[] = "\x74\x6c\x73\x20\x65\x63\x68"; /* - * Given a SH (or HRR) find the offsets of the ECH (if any) - * sh is the SH buffer - * sh_len is the length of the SH - * exts points to offset of extensions - * echoffset points to offset of ECH - * echtype points to the ext type of the ECH - * return 1 for success, zero otherwise - * - * Offsets are returned to the type or length field in question. - * Offsets are set to zero if relevant thing not found. - * - * Note: input here is untrusted! - */ -int ossl_ech_get_sh_offsets(const unsigned char *sh, size_t sh_len, - size_t *exts, size_t *echoffset, - uint16_t *echtype) -{ - unsigned int etype = 0, pi_tmp = 0; - const unsigned char *pp_tmp = NULL, *shstart = NULL; - PACKET pkt, session_id, extpkt, oneext; - size_t extlens = 0; - int done = 0; -#ifdef OSSL_ECH_SUPERVERBOSE - size_t echlen = 0; /* length of ECH, including type & ECH-internal length */ - size_t sessid_offset = 0, sessid_len = 0; -#endif - - if (sh == NULL || sh_len == 0 || exts == NULL || echoffset == NULL - || echtype == NULL) - return 0; - *exts = *echoffset = *echtype = 0; - if (!PACKET_buf_init(&pkt, sh, sh_len)) - return 0; - shstart = PACKET_data(&pkt); - if (!PACKET_get_net_2(&pkt, &pi_tmp)) - return 0; - /* - * TODO(ECH): we've had a TLSv1.2 test in the past where we add an - * ECH to a TLSv1.2 CH to ensure server code ignores that properly. - * We might or might not keep that, if we don't then the test below - * should allow TLSv1.3 only. - */ - /* if we're not TLSv1.2+ then we can bail, but it's not an error */ - if (pi_tmp != TLS1_2_VERSION && pi_tmp != TLS1_3_VERSION) - return 1; - if (!PACKET_get_bytes(&pkt, &pp_tmp, SSL3_RANDOM_SIZE) -#ifdef OSSL_ECH_SUPERVERBOSE - || (sessid_offset = PACKET_data(&pkt) - shstart) == 0 -#endif - || !PACKET_get_length_prefixed_1(&pkt, &session_id) -#ifdef OSSL_ECH_SUPERVERBOSE - || (sessid_len = PACKET_remaining(&session_id)) == 0 -#endif - || !PACKET_get_net_2(&pkt, &pi_tmp) /* ciphersuite */ - || !PACKET_get_1(&pkt, &pi_tmp) /* compression */ - || (*exts = PACKET_data(&pkt) - shstart) == 0 - || !PACKET_as_length_prefixed_2(&pkt, &extpkt) - || PACKET_remaining(&pkt) != 0) - return 0; - extlens = PACKET_remaining(&extpkt); - if (extlens == 0) /* not an error, in theory */ - return 1; - while (PACKET_remaining(&extpkt) > 0 && done < 1) { - if (!PACKET_get_net_2(&extpkt, &etype) - || !PACKET_get_length_prefixed_2(&extpkt, &oneext)) - return 0; - if (etype == TLSEXT_TYPE_ech) { - if (PACKET_remaining(&oneext) != 8) - return 0; - *echoffset = PACKET_data(&oneext) - shstart - 4; - *echtype = etype; -#ifdef OSSL_ECH_SUPERVERBOSE - echlen = PACKET_remaining(&oneext) + 4; /* type/length included */ -#endif - done++; - } - } -#ifdef OSSL_ECH_SUPERVERBOSE - OSSL_TRACE_BEGIN(TLS) { - BIO_printf(trc_out, "orig SH/ECH type: %4x\n", *echtype); - } OSSL_TRACE_END(TLS); - ossl_ech_pbuf("orig SH", (unsigned char *)sh, sh_len); - ossl_ech_pbuf("orig SH session_id", (unsigned char *)sh + sessid_offset, - sessid_len); - ossl_ech_pbuf("orig SH exts", (unsigned char *)sh + *exts, extlens); - ossl_ech_pbuf("orig SH/ECH ", (unsigned char *)sh + *echoffset, echlen); -#endif - return 1; -} - -/* - * make up HPKE "info" input as per spec + * Construct HPKE "info" input as per spec * encoding is the ECHconfig being used - * encodinglen is the length of ECHconfig being used + * encoding_length is the length of ECHconfig being used * info is a caller-allocated buffer for results * info_len is the buffer size on input, used-length on output * return 1 for success, zero otherwise */ -int ossl_ech_make_enc_info(unsigned char *encoding, size_t encoding_length, +int ossl_ech_make_enc_info(const unsigned char *encoding, + size_t encoding_length, unsigned char *info, size_t *info_len) { WPACKET ipkt = { 0 }; - BUF_MEM *ipkt_mem = NULL; if (encoding == NULL || info == NULL || info_len == NULL) return 0; - if (*info_len < (sizeof(OSSL_ECH_CONTEXT_STRING) + encoding_length)) - return 0; - if ((ipkt_mem = BUF_MEM_new()) == NULL - || !WPACKET_init(&ipkt, ipkt_mem) + if (!WPACKET_init_static_len(&ipkt, info, *info_len, 0) || !WPACKET_memcpy(&ipkt, OSSL_ECH_CONTEXT_STRING, sizeof(OSSL_ECH_CONTEXT_STRING) - 1) /* @@ -138,14 +44,11 @@ int ossl_ech_make_enc_info(unsigned char *encoding, size_t encoding_length, * the context string being NUL terminated */ || !WPACKET_put_bytes_u8(&ipkt, 0) - || !WPACKET_memcpy(&ipkt, encoding, encoding_length)) { + || !WPACKET_memcpy(&ipkt, encoding, encoding_length) + || !WPACKET_get_total_written(&ipkt, info_len)) { WPACKET_cleanup(&ipkt); - BUF_MEM_free(ipkt_mem); return 0; } - *info_len = sizeof(OSSL_ECH_CONTEXT_STRING) + encoding_length; - memcpy(info, WPACKET_get_curr(&ipkt) - *info_len, *info_len); WPACKET_cleanup(&ipkt); - BUF_MEM_free(ipkt_mem); return 1; } diff --git a/ssl/ech/ech_internal.c b/ssl/ech/ech_internal.c index a8d60102046c3..ae71656546e32 100644 --- a/ssl/ech/ech_internal.c +++ b/ssl/ech/ech_internal.c @@ -12,7 +12,8 @@ #include "../ssl_local.h" #include "ech_local.h" #include -#include +#include "../statem/statem_local.h" +#include "internal/ech_helpers.h" #include #ifndef OPENSSL_NO_ECH @@ -46,26 +47,29 @@ void ossl_ech_pbuf(const char *msg, const unsigned char *buf, const size_t blen) } /* trace out transcript */ -void ossl_ech_ptranscript(SSL_CONNECTION *s, const char *msg) +static void ossl_ech_ptranscript(SSL_CONNECTION *s, const char *msg) { - OSSL_TRACE_BEGIN(TLS) { - size_t hdatalen = 0; - unsigned char *hdata = NULL; - unsigned char ddata[EVP_MAX_MD_SIZE]; - size_t ddatalen; + size_t hdatalen = 0; + unsigned char *hdata = NULL; + unsigned char ddata[EVP_MAX_MD_SIZE]; + size_t ddatalen; - if (s == NULL) - return; - hdatalen = BIO_get_mem_data(s->s3.handshake_buffer, &hdata); - ossl_ech_pbuf(msg, hdata, hdatalen); - if (s->s3.handshake_dgst != NULL) { - if (ssl_handshake_hash(s, ddata, sizeof(ddata), &ddatalen) == 0) + if (s == NULL) + return; + hdatalen = BIO_get_mem_data(s->s3.handshake_buffer, &hdata); + ossl_ech_pbuf(msg, hdata, hdatalen); + if (s->s3.handshake_dgst != NULL) { + if (ssl_handshake_hash(s, ddata, sizeof(ddata), &ddatalen) == 0) { + OSSL_TRACE_BEGIN(TLS) { BIO_printf(trc_out, "ssl_handshake_hash failed\n"); + } OSSL_TRACE_END(TLS); ossl_ech_pbuf(msg, ddata, ddatalen); - } else { - BIO_printf(trc_out, "handshake_dgst is NULL\n"); } + } + OSSL_TRACE_BEGIN(TLS) { + BIO_printf(trc_out, "new transbuf:\n"); } OSSL_TRACE_END(TLS); + ossl_ech_pbuf(msg, s->ext.ech.transbuf, s->ext.ech.transbuf_len); return; } # endif @@ -168,16 +172,15 @@ void ossl_ech_conn_clear(OSSL_ECH_CONN *ec) OPENSSL_free(ec->outer_hostname); OPENSSL_free(ec->alpn_outer); OPENSSL_free(ec->former_inner); + OPENSSL_free(ec->transbuf); OPENSSL_free(ec->innerch); - OPENSSL_free(ec->encoded_innerch); - OPENSSL_free(ec->innerch1); - OPENSSL_free(ec->kepthrr); OPENSSL_free(ec->grease_suite); OPENSSL_free(ec->sent); OPENSSL_free(ec->returned); OPENSSL_free(ec->pub); OSSL_HPKE_CTX_free(ec->hpke_ctx); EVP_PKEY_free(ec->tmp_pkey); + OPENSSL_free(ec->encoded_inner); return; } @@ -226,8 +229,8 @@ int ossl_ech_get_retry_configs(SSL_CONNECTION *s, unsigned char **rcfgs, OSSL_ECHSTORE *es = NULL; OSSL_ECHSTORE_ENTRY *ee = NULL; int i, num = 0; - size_t retslen = 0, encilen = 0; - unsigned char *tmp = NULL, *enci = NULL, *rets = NULL; + size_t retslen = 0; + unsigned char *tmp = NULL, *rets = NULL; if (s == NULL || rcfgs == NULL || rcfgslen == NULL) return 0; @@ -237,17 +240,13 @@ int ossl_ech_get_retry_configs(SSL_CONNECTION *s, unsigned char **rcfgs, for (i = 0; i != num; i++) { ee = sk_OSSL_ECHSTORE_ENTRY_value(es->entries, i); if (ee != NULL && ee->for_retry == OSSL_ECH_FOR_RETRY) { - encilen = ee->encoded_len; - if (encilen < 2) - goto err; - encilen -= 2; - enci = ee->encoded + 2; - tmp = (unsigned char *)OPENSSL_realloc(rets, retslen + encilen); + tmp = (unsigned char *)OPENSSL_realloc(rets, + retslen + ee->encoded_len); if (tmp == NULL) goto err; rets = tmp; - memcpy(rets + retslen, enci, encilen); - retslen += encilen; + memcpy(rets + retslen, ee->encoded, ee->encoded_len); + retslen += ee->encoded_len; } } *rcfgs = rets; @@ -285,7 +284,6 @@ int ossl_ech_send_grease(SSL_CONNECTION *s, WPACKET *pkt) size_t cipher_len = 0, cipher_len_jitter = 0; unsigned char cid, senderpub[OSSL_ECH_MAX_GREASE_PUB]; unsigned char cipher[OSSL_ECH_MAX_GREASE_CT]; - unsigned char *pp = WPACKET_get_curr(pkt); if (s == NULL) return 0; @@ -295,8 +293,7 @@ int ossl_ech_send_grease(SSL_CONNECTION *s, WPACKET *pkt) } WPACKET_get_total_written(pkt, &pp_at_start); /* randomly select cipher_len to be one of 144, 176, 208, 244 */ - if (RAND_bytes_ex(s->ssl.ctx->libctx, &cid, 1, - RAND_DRBG_STRENGTH) <= 0) { + if (RAND_bytes_ex(s->ssl.ctx->libctx, &cid, 1, 0) <= 0) { SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); return 0; } @@ -304,8 +301,7 @@ int ossl_ech_send_grease(SSL_CONNECTION *s, WPACKET *pkt) cipher_len = 144; cipher_len += 32 * cipher_len_jitter; /* generate a random (1 octet) client id */ - if (RAND_bytes_ex(s->ssl.ctx->libctx, &cid, 1, - RAND_DRBG_STRENGTH) <= 0) { + if (RAND_bytes_ex(s->ssl.ctx->libctx, &cid, 1, 0) <= 0) { SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); return 0; } @@ -348,7 +344,8 @@ int ossl_ech_send_grease(SSL_CONNECTION *s, WPACKET *pkt) SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); return 0; } - memcpy(s->ext.ech.sent, pp, s->ext.ech.sent_len); + memcpy(s->ext.ech.sent, WPACKET_get_curr(pkt) - s->ext.ech.sent_len, + s->ext.ech.sent_len); s->ext.ech.grease = OSSL_ECH_IS_GREASE; OSSL_TRACE_BEGIN(TLS) { BIO_printf(trc_out, "ECH - sending GREASE\n"); @@ -373,6 +370,7 @@ int ossl_ech_pick_matching_cfg(SSL_CONNECTION *s, OSSL_ECHSTORE_ENTRY **ee, if (s == NULL || s->ext.ech.es == NULL || ee == NULL || suite == NULL) return 0; + *ee = NULL; es = s->ext.ech.es; if (es->entries == NULL) return 0; @@ -387,7 +385,7 @@ int ossl_ech_pick_matching_cfg(SSL_CONNECTION *s, OSSL_ECHSTORE_ENTRY **ee, hnlen = 0; nameoverride = 1; } - for (cind = 0; cind != num && (suitematch == 0 || namematch == 0); cind++) { + for (cind = 0; cind < num && (suitematch == 0 || namematch == 0); cind++) { lee = sk_OSSL_ECHSTORE_ENTRY_value(es->entries, cind); if (lee == NULL || lee->version != OSSL_ECH_RFCXXXX_VERSION) continue; @@ -398,8 +396,8 @@ int ossl_ech_pick_matching_cfg(SSL_CONNECTION *s, OSSL_ECHSTORE_ENTRY **ee, if (hnlen == 0 || (lee->public_name != NULL && strlen(lee->public_name) == hnlen - && !OPENSSL_strncasecmp(hn, (char *)lee->public_name, - hnlen))) + && OPENSSL_strncasecmp(hn, (char *)lee->public_name, + hnlen) == 0)) namematch = 1; } suitematch = 0; @@ -418,7 +416,8 @@ int ossl_ech_pick_matching_cfg(SSL_CONNECTION *s, OSSL_ECHSTORE_ENTRY **ee, } } } - if (nameoverride == 1 && (namematch == 0 || suitematch == 0)) { + if (tee != NULL && nameoverride == 1 + && (namematch == 0 || suitematch == 0)) { *suite = tee->suites[tsuite]; *ee = tee; } else if (namematch == 0 || suitematch == 0) { @@ -431,11 +430,11 @@ int ossl_ech_pick_matching_cfg(SSL_CONNECTION *s, OSSL_ECHSTORE_ENTRY **ee, } /* Make up the ClientHelloInner and EncodedClientHelloInner buffers */ -int ossl_ech_encode_inner(SSL_CONNECTION *s) +int ossl_ech_encode_inner(SSL_CONNECTION *s, unsigned char **encoded, + size_t *encoded_len) { int rv = 0; size_t nraws = 0, ind = 0, innerlen = 0; - unsigned char *innerch_full = NULL; WPACKET inner = { 0 }; /* "fake" pkt for inner */ BUF_MEM *inner_mem = NULL; RAW_EXTENSION *raws = NULL; @@ -517,15 +516,9 @@ int ossl_ech_encode_inner(SSL_CONNECTION *s) SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); goto err; } - innerch_full = OPENSSL_malloc(innerlen); - if (innerch_full == NULL) { - SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); - goto err; - } - memcpy(innerch_full, inner_mem->data, innerlen); - OPENSSL_free(s->ext.ech.encoded_innerch); - s->ext.ech.encoded_innerch = innerch_full; - s->ext.ech.encoded_innerch_len = innerlen; + *encoded = (unsigned char *)inner_mem->data; + inner_mem->data = NULL; /* keep BUF_MEM_free happy */ + *encoded_len = innerlen; /* and clean up */ rv = 1; err: @@ -543,192 +536,19 @@ int ossl_ech_encode_inner(SSL_CONNECTION *s) * return: 1 for success, 0 otherwise */ int ossl_ech_find_confirm(SSL_CONNECTION *s, int hrr, - unsigned char acbuf[OSSL_ECH_SIGNAL_LEN], - const unsigned char *shbuf, const size_t shlen) + unsigned char acbuf[OSSL_ECH_SIGNAL_LEN]) { - PACKET pkt; - const unsigned char *acp = NULL, *pp_tmp; - unsigned int pi_tmp, etype, elen; - int done = 0; + unsigned char *acp = NULL; if (hrr == 0) { acp = s->s3.server_random + SSL3_RANDOM_SIZE - OSSL_ECH_SIGNAL_LEN; - memcpy(acbuf, acp, OSSL_ECH_SIGNAL_LEN); - return 1; - } else { - if (!PACKET_buf_init(&pkt, shbuf, shlen) - || !PACKET_get_net_2(&pkt, &pi_tmp) - || !PACKET_get_bytes(&pkt, &pp_tmp, SSL3_RANDOM_SIZE) - || !PACKET_get_1(&pkt, &pi_tmp) /* sessid len */ - || !PACKET_get_bytes(&pkt, &pp_tmp, pi_tmp) /* sessid */ - || !PACKET_get_net_2(&pkt, &pi_tmp) /* ciphersuite */ - || !PACKET_get_1(&pkt, &pi_tmp) /* compression */ - || !PACKET_get_net_2(&pkt, &pi_tmp)) /* len(extensions) */ + } else { /* was set in extension handler */ + if (s->ext.ech.hrrsignal_p == NULL) return 0; - while (PACKET_remaining(&pkt) > 0 && done < 1) { - if (!PACKET_get_net_2(&pkt, &etype) - || !PACKET_get_net_2(&pkt, &elen)) - return 0; - if (etype == TLSEXT_TYPE_ech) { - if (elen != OSSL_ECH_SIGNAL_LEN - || !PACKET_get_bytes(&pkt, &acp, elen)) - return 0; - memcpy(acbuf, acp, elen); - done++; - } else { - if (!PACKET_get_bytes(&pkt, &pp_tmp, elen)) - return 0; - } - } - return done; - } - return 0; -} - -/* - * make up a buffer to use to reset transcript - * for_hrr is 1 if we've just seen HRR, 0 otherwise - * shbuf is the output buffer - * shlen is the length of that buffer - * tbuf is the output buffer - * tlen is the length of that buffer - * chend returns the offset of the end of the last CH in the buffer - * fixedshbuf_len returns the fixed up length of the SH - * return 1 for good, 0 otherwise - */ -int ossl_ech_make_transcript_buffer(SSL_CONNECTION *s, int for_hrr, - const unsigned char *shbuf, size_t shlen, - unsigned char **tbuf, size_t *tlen, - size_t *chend, size_t *fixedshbuf_len) -{ - unsigned char *fixedshbuf = NULL, *hashin = NULL, hashval[EVP_MAX_MD_SIZE]; - unsigned int hashlen = 0, hashin_len = 0; - EVP_MD_CTX *ctx = NULL; - EVP_MD *md = NULL; - WPACKET tpkt = { 0 }, shpkt = { 0 }; - BUF_MEM *tpkt_mem = NULL, *shpkt_mem = NULL; - - /* - * SH preamble has bad length at this point on server - * and is missing on client so we'll fix - */ - if ((shpkt_mem = BUF_MEM_new()) == NULL - || !WPACKET_init(&shpkt, shpkt_mem)) { - BUF_MEM_free(shpkt_mem); - SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); - goto err; - } - if (!WPACKET_put_bytes_u8(&shpkt, SSL3_MT_SERVER_HELLO) - || (s->server == 1 && !WPACKET_put_bytes_u24(&shpkt, shlen - 4)) - || (s->server == 1 && !WPACKET_memcpy(&shpkt, shbuf + 4, shlen -4)) - || (s->server == 0 && !WPACKET_put_bytes_u24(&shpkt, shlen)) - || (s->server == 0 && !WPACKET_memcpy(&shpkt, shbuf, shlen)) - || !WPACKET_get_length(&shpkt, fixedshbuf_len)) { - BUF_MEM_free(shpkt_mem); - WPACKET_cleanup(&shpkt); - SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); - goto err; - } - fixedshbuf = (unsigned char *)shpkt_mem->data; - WPACKET_cleanup(&shpkt); -# ifdef OSSL_ECH_SUPERVERBOSE - ossl_ech_pbuf("cx: fixed sh buf", fixedshbuf, *fixedshbuf_len); -# endif - if ((tpkt_mem = BUF_MEM_new()) == NULL - || !WPACKET_init(&tpkt, tpkt_mem)) { - SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); - goto err; - } - if (s->hello_retry_request == SSL_HRR_NONE) { - if (!WPACKET_memcpy(&tpkt, s->ext.ech.innerch, - s->ext.ech.innerch_len) - || !WPACKET_get_length(&tpkt, chend) - || !WPACKET_memcpy(&tpkt, fixedshbuf, *fixedshbuf_len) - || !WPACKET_get_length(&tpkt, tlen)) { - SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); - goto err; - } - *tbuf = OPENSSL_malloc(*tlen); - if (*tbuf == NULL) { - SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); - goto err; - } - memcpy(*tbuf, WPACKET_get_curr(&tpkt) - *tlen, *tlen); - BUF_MEM_free(shpkt_mem); - WPACKET_cleanup(&tpkt); - BUF_MEM_free(tpkt_mem); - return 1; - } - /* everything below only applies if we're at some stage in doing HRR */ - if ((md = (EVP_MD *)ssl_handshake_md(s)) == NULL) { - SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); - goto err; - } - if (for_hrr == 0) { /* after 2nd SH rx'd */ - hashin = s->ext.ech.innerch1; - hashin_len = s->ext.ech.innerch1_len; - } else { /* after HRR rx'd */ - hashin = s->ext.ech.innerch; - hashin_len = s->ext.ech.innerch_len; - OPENSSL_free(s->ext.ech.kepthrr); - /* stash this SH/HRR for later */ - s->ext.ech.kepthrr = OPENSSL_malloc(*fixedshbuf_len); - if (s->ext.ech.kepthrr == NULL) { - SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); - goto err; - } - memcpy(s->ext.ech.kepthrr, fixedshbuf, *fixedshbuf_len); - s->ext.ech.kepthrr_len = *fixedshbuf_len; - } -# ifdef OSSL_ECH_SUPERVERBOSE - ossl_ech_pbuf("cx: ch2hash", hashin, hashin_len); -# endif - if ((ctx = EVP_MD_CTX_new()) == NULL - || EVP_DigestInit_ex(ctx, md, NULL) <= 0 - || EVP_DigestUpdate(ctx, hashin, hashin_len) <= 0 - || EVP_DigestFinal_ex(ctx, hashval, &hashlen) <= 0) { - SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); - goto err; - } - EVP_MD_CTX_free(ctx); - ctx = NULL; - if (!WPACKET_put_bytes_u8(&tpkt, SSL3_MT_MESSAGE_HASH) - || !WPACKET_put_bytes_u24(&tpkt, hashlen) - || !WPACKET_memcpy(&tpkt, hashval, hashlen)) { - SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); - goto err; - } - if (for_hrr == 0) { /* after 2nd SH */ - if (!WPACKET_memcpy(&tpkt, s->ext.ech.kepthrr, - s->ext.ech.kepthrr_len) - || !WPACKET_memcpy(&tpkt, s->ext.ech.innerch, - s->ext.ech.innerch_len)) { - SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); - goto err; - } - } - if (!WPACKET_get_length(&tpkt, chend) - || !WPACKET_memcpy(&tpkt, fixedshbuf, *fixedshbuf_len) - || !WPACKET_get_length(&tpkt, tlen)) { - SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); - goto err; + acp = s->ext.ech.hrrsignal; } - *tbuf = OPENSSL_malloc(*tlen); - if (*tbuf == NULL) { - SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); - goto err; - } - memcpy(*tbuf, WPACKET_get_curr(&tpkt) - *tlen, *tlen); - BUF_MEM_free(shpkt_mem); - WPACKET_cleanup(&tpkt); - BUF_MEM_free(tpkt_mem); + memcpy(acbuf, acp, OSSL_ECH_SIGNAL_LEN); return 1; -err: - BUF_MEM_free(shpkt_mem); - BUF_MEM_free(tpkt_mem); - WPACKET_cleanup(&tpkt); - EVP_MD_CTX_free(ctx); - return 0; } /* @@ -744,15 +564,16 @@ int ossl_ech_reset_hs_buffer(SSL_CONNECTION *s, const unsigned char *buf, ossl_ech_pbuf("RESET transcript to", buf, blen); # endif if (s->s3.handshake_buffer != NULL) { + if (BIO_reset(s->s3.handshake_buffer) < 0) + return 0; + } else { + s->s3.handshake_buffer = BIO_new(BIO_s_mem()); + if (s->s3.handshake_buffer == NULL) + return 0; (void)BIO_set_close(s->s3.handshake_buffer, BIO_CLOSE); - BIO_free(s->s3.handshake_buffer); - s->s3.handshake_buffer = NULL; } EVP_MD_CTX_free(s->s3.handshake_dgst); s->s3.handshake_dgst = NULL; - s->s3.handshake_buffer = BIO_new(BIO_s_mem()); - if (s->s3.handshake_buffer == NULL) - return 0; /* providing nothing at all is a real use (mid-HRR) */ if (buf != NULL && blen > 0) BIO_write(s->s3.handshake_buffer, (void *)buf, (int)blen); @@ -778,7 +599,7 @@ int ossl_ech_reset_hs_buffer(SSL_CONNECTION *s, const unsigned char *buf, * better really, BUT... we might want to keep this if others (e.g. * browsers) do it so as to not stand out compared to them. * - * The "+ 9" constant below is from the specifiation and is the + * The "+ 9" constant below is from the specification and is the * expansion comparing a string length to an encoded SNI extension. * Same is true of the 31/32 formula below. * @@ -786,11 +607,12 @@ int ossl_ech_reset_hs_buffer(SSL_CONNECTION *s, const unsigned char *buf, * a padded cleartext of 128 octets, the ciphertext will be 144 * octets. */ -static size_t ech_calc_padding(SSL_CONNECTION *s, OSSL_ECHSTORE_ENTRY *ee) +size_t ossl_ech_calc_padding(SSL_CONNECTION *s, OSSL_ECHSTORE_ENTRY *ee, + size_t encoded_len) { int length_of_padding = 0, length_with_snipadding = 0; int innersnipadding = 0, length_with_padding = 0; - size_t mnl = 0, clear_len = 0, isnilen = 0; + size_t mnl = 0, isnilen = 0; if (s == NULL || ee == NULL) return 0; @@ -805,10 +627,9 @@ static size_t ech_calc_padding(SSL_CONNECTION *s, OSSL_ECHSTORE_ENTRY *ee) } } /* padding is after the inner client hello has been encoded */ - length_with_snipadding = innersnipadding + s->ext.ech.encoded_innerch_len; + length_with_snipadding = innersnipadding + encoded_len; length_of_padding = 31 - ((length_with_snipadding - 1) % 32); - length_with_padding = s->ext.ech.encoded_innerch_len - + length_of_padding + innersnipadding; + length_with_padding = encoded_len + length_of_padding + innersnipadding; /* * Finally - make sure final result is longer than padding target * and a multiple of our padding increment. @@ -816,20 +637,18 @@ static size_t ech_calc_padding(SSL_CONNECTION *s, OSSL_ECHSTORE_ENTRY *ee) * it makes us stick out; or if we take out the above more (uselessly:-) * complicated scheme, we may only need this in the end. */ - if (length_with_padding % OSSL_ECH_PADDING_INCREMENT) + if ((length_with_padding % OSSL_ECH_PADDING_INCREMENT) != 0) length_with_padding += OSSL_ECH_PADDING_INCREMENT - (length_with_padding % OSSL_ECH_PADDING_INCREMENT); while (length_with_padding < OSSL_ECH_PADDING_TARGET) length_with_padding += OSSL_ECH_PADDING_INCREMENT; - clear_len = length_with_padding; OSSL_TRACE_BEGIN(TLS) { BIO_printf(trc_out, "EAAE: padding: mnl: %zu, lws: %d " - "lop: %d, lwp: %d, clear_len: %zu, orig: %zu\n", + "lop: %d, clear_len (len with padding): %d, orig: %zu\n", mnl, length_with_snipadding, length_of_padding, - length_with_padding, clear_len, - s->ext.ech.encoded_innerch_len); + length_with_padding, encoded_len); } OSSL_TRACE_END(TLS); - return clear_len; + return (size_t)length_with_padding; } /* @@ -844,13 +663,11 @@ static size_t ech_calc_padding(SSL_CONNECTION *s, OSSL_ECHSTORE_ENTRY *ee) */ int ossl_ech_aad_and_encrypt(SSL_CONNECTION *s, WPACKET *pkt) { - int rv = 0, hpke_mode = OSSL_HPKE_MODE_BASE; - OSSL_ECHSTORE_ENTRY *ee = NULL; - OSSL_HPKE_SUITE hpke_suite = OSSL_HPKE_SUITE_DEFAULT; - unsigned char config_id_to_use = 0x00, info[SSL3_RT_MAX_PLAIN_LENGTH]; - unsigned char *clear = NULL, *cipher = NULL, *aad = NULL, *mypub = NULL; - size_t cipherlen = 0, aad_len = 0, lenclen = 0, mypub_len = 0; - size_t info_len = SSL3_RT_MAX_PLAIN_LENGTH, clear_len = 0; + int rv = 0; + size_t cipherlen = 0, aad_len = 0, mypub_len = 0, clear_len = 0; + size_t encoded_inner_len = 0; + unsigned char *clear = NULL, *aad = NULL, *mypub = NULL; + unsigned char *encoded_inner = NULL, *cipher_loc = NULL; if (s == NULL) return 0; @@ -859,124 +676,19 @@ int ossl_ech_aad_and_encrypt(SSL_CONNECTION *s, WPACKET *pkt) SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); goto err; } - rv = ossl_ech_pick_matching_cfg(s, &ee, &hpke_suite); - if (rv != 1 || ee == NULL) { + /* values calculated in tls_construct_ctos_ech */ + encoded_inner = s->ext.ech.encoded_inner; + encoded_inner_len = s->ext.ech.encoded_inner_len; + clear_len = s->ext.ech.clearlen; + cipherlen = s->ext.ech.cipherlen; + if (!WPACKET_get_total_written(pkt, &aad_len) || aad_len < 4) { SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); goto err; } - s->ext.ech.attempted_type = ee->version; - OSSL_TRACE_BEGIN(TLS) { - BIO_printf(trc_out, "EAAE: selected: version: %4x, config %2x\n", - ee->version, ee->config_id); - } OSSL_TRACE_END(TLS); - config_id_to_use = ee->config_id; /* if requested, use a random config_id instead */ - if ((s->ssl.ctx->options & SSL_OP_ECH_IGNORE_CID) != 0 - || (s->options & SSL_OP_ECH_IGNORE_CID) != 0) { - if (RAND_bytes_ex(s->ssl.ctx->libctx, &config_id_to_use, 1, - RAND_DRBG_STRENGTH) <= 0) { - SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); - return 0; - } -# ifdef OSSL_ECH_SUPERVERBOSE - ossl_ech_pbuf("EAAE: random config_id", &config_id_to_use, 1); -# endif - } - s->ext.ech.attempted_cid = config_id_to_use; -# ifdef OSSL_ECH_SUPERVERBOSE - ossl_ech_pbuf("EAAE: peer pub", ee->pub, ee->pub_len); - ossl_ech_pbuf("EAAE: clear", s->ext.ech.encoded_innerch, - s->ext.ech.encoded_innerch_len); - ossl_ech_pbuf("EAAE: ECHConfig", ee->encoded, ee->encoded_len); -# endif - /* - * The AAD is the full outer client hello but with the correct number of - * zeros for where the ECH ciphertext octets will later be placed. So we - * add the ECH extension to the |pkt| but with zeros for ciphertext, that - * forms up the AAD, then after we've encrypted, we'll splice in the actual - * ciphertext. - * Watch out for the "4" offsets that remove the type and 3-octet length - * from the encoded CH as per the spec. - */ - clear_len = ech_calc_padding(s, ee); - if (clear_len == 0) { - SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); - goto err; - } - lenclen = OSSL_HPKE_get_public_encap_size(hpke_suite); - if (s->ext.ech.hpke_ctx == NULL) { /* 1st CH */ - if (ossl_ech_make_enc_info(ee->encoded, ee->encoded_len, - info, &info_len) != 1) { - SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); - goto err; - } -# ifdef OSSL_ECH_SUPERVERBOSE - ossl_ech_pbuf("EAAE info", info, info_len); -# endif - s->ext.ech.hpke_ctx = OSSL_HPKE_CTX_new(hpke_mode, hpke_suite, - OSSL_HPKE_ROLE_SENDER, - NULL, NULL); - if (s->ext.ech.hpke_ctx == NULL) { - SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); - goto err; - } - mypub = OPENSSL_malloc(lenclen); - if (mypub == NULL) { - SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); - goto err; - } - mypub_len = lenclen; - rv = OSSL_HPKE_encap(s->ext.ech.hpke_ctx, mypub, &mypub_len, - ee->pub, ee->pub_len, info, info_len); - if (rv != 1) { - OPENSSL_free(mypub); - mypub = NULL; - SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); - goto err; - } - s->ext.ech.pub = mypub; - s->ext.ech.pub_len = mypub_len; - } else { /* HRR - retrieve public */ - mypub = s->ext.ech.pub; - mypub_len = s->ext.ech.pub_len; - if (mypub == NULL || mypub_len == 0) { - SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); - goto err; - } - } -# ifdef OSSL_ECH_SUPERVERBOSE - ossl_ech_pbuf("EAAE: mypub", mypub, mypub_len); - WPACKET_get_total_written(pkt, &aad_len); /* use aad_len for tracing */ - ossl_ech_pbuf("EAAE pkt b4", WPACKET_get_curr(pkt) - aad_len, aad_len); -# endif - cipherlen = OSSL_HPKE_get_ciphertext_size(hpke_suite, clear_len); - if (cipherlen <= clear_len || cipherlen > OSSL_ECH_MAX_PAYLOAD_LEN) { - SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_BAD_EXTENSION); - goto err; - } - cipher = OPENSSL_zalloc(cipherlen); - if (cipher == NULL) { - SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); - goto err; - } - if (!WPACKET_put_bytes_u16(pkt, TLSEXT_TYPE_ech) - || !WPACKET_start_sub_packet_u16(pkt) - || !WPACKET_put_bytes_u8(pkt, OSSL_ECH_OUTER_CH_TYPE) - || !WPACKET_put_bytes_u16(pkt, hpke_suite.kdf_id) - || !WPACKET_put_bytes_u16(pkt, hpke_suite.aead_id) - || !WPACKET_put_bytes_u8(pkt, config_id_to_use) - || (s->hello_retry_request == SSL_HRR_PENDING - && !WPACKET_put_bytes_u16(pkt, 0x00)) /* no pub */ - || (s->hello_retry_request != SSL_HRR_PENDING - && !WPACKET_sub_memcpy_u16(pkt, mypub, mypub_len)) - || !WPACKET_sub_memcpy_u16(pkt, cipher, cipherlen) - || !WPACKET_close(pkt) - || !WPACKET_get_total_written(pkt, &aad_len) - || aad_len < 4) { - SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); - goto err; - } - aad_len -= 4; /* aad starts after type + 3-octet len */ + aad_len -= 4; /* ECH/HPKE aad starts after type + 3-octet len */ aad = WPACKET_get_curr(pkt) - aad_len; + /* where we'll replace zeros with ciphertext */ + cipher_loc = aad + s->ext.ech.cipher_offset; /* * close the extensions of the CH - we skipped doing this * earlier when encoding extensions, to allow for adding the @@ -995,32 +707,29 @@ int ossl_ech_aad_and_encrypt(SSL_CONNECTION *s, WPACKET *pkt) SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); goto err; } - memcpy(clear, s->ext.ech.encoded_innerch, s->ext.ech.encoded_innerch_len); + memcpy(clear, encoded_inner, encoded_inner_len); # ifdef OSSL_ECH_SUPERVERBOSE ossl_ech_pbuf("EAAE: padded clear", clear, clear_len); # endif - rv = OSSL_HPKE_seal(s->ext.ech.hpke_ctx, cipher, &cipherlen, - aad, aad_len, clear, clear_len); + /* we're done with this now */ + OPENSSL_free(s->ext.ech.encoded_inner); + s->ext.ech.encoded_inner = NULL; + rv = OSSL_HPKE_seal(s->ext.ech.hpke_ctx, cipher_loc, + &cipherlen, aad, aad_len, clear, clear_len); OPENSSL_free(clear); if (rv != 1) { SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); goto err; } # ifdef OSSL_ECH_SUPERVERBOSE - ossl_ech_pbuf("EAAE: cipher", cipher, cipherlen); + ossl_ech_pbuf("EAAE: cipher", cipher_loc, cipherlen); ossl_ech_pbuf("EAAE: hpke mypub", mypub, mypub_len); -# endif - /* splice real ciphertext back in now */ - memcpy(aad + aad_len - cipherlen, cipher, cipherlen); -# ifdef OSSL_ECH_SUPERVERBOSE /* re-use aad_len for tracing */ WPACKET_get_total_written(pkt, &aad_len); ossl_ech_pbuf("EAAE pkt aftr", WPACKET_get_curr(pkt) - aad_len, aad_len); # endif - OPENSSL_free(cipher); return 1; err: - OPENSSL_free(cipher); return 0; } @@ -1029,7 +738,7 @@ int ossl_ech_aad_and_encrypt(SSL_CONNECTION *s, WPACKET *pkt) * out is the BIO to use (e.g. stdout/whatever) * selector OSSL_ECH_SELECT_ALL or just one of the SSL_ECH values */ -static void ech_status_print(BIO *out, SSL_CONNECTION *s, int selector) +void ossl_ech_status_print(BIO *out, SSL_CONNECTION *s, int selector) { int num = 0, i, has_priv, for_retry; size_t j; @@ -1091,17 +800,14 @@ static void ech_status_print(BIO *out, SSL_CONNECTION *s, int selector) return; } -/* size of string buffer returned via ECH callback */ -# define OSSL_ECH_PBUF_SIZE 8 * 1024 - /* * Swap the inner and outer after ECH success on the client * return 0 for error, 1 for success */ int ossl_ech_swaperoo(SSL_CONNECTION *s) { - unsigned char *curr_buf = NULL, *new_buf = NULL; - size_t curr_buflen = 0, new_buflen = 0, outer_chlen = 0, other_octets = 0; + unsigned char *curr_buf = NULL; + size_t curr_buflen = 0; if (s == NULL) return 0; @@ -1118,69 +824,49 @@ int ossl_ech_swaperoo(SSL_CONNECTION *s) s->s3.group_id = s->ext.ech.group_id; s->ext.ech.tmp_pkey = NULL; /* - * TODO(ECH): I suggest re-factoring transcript handling (which - * is probably needed) after/with the PR that includes the server- - * side ECH code. That should be much easier as at that point the - * full set of tests can be run, whereas for now, we're limited - * to testing the client side really works via bodged s_client - * scripts, so there'd be a bigger risk of breaking something - * subtly if we try re-factor now. - * * When not doing HRR... fix up the transcript to reflect the inner CH. * If there's a client hello at the start of the buffer, then that's * the outer CH and we want to replace that with the inner. We need to - * be careful that there could be a server hello following and can't - * lose that. + * be careful that there could be early data or a server hello following + * and we can't lose that. * * For HRR... HRR processing code has already done the necessary. */ if (s->hello_retry_request == SSL_HRR_NONE) { - curr_buflen = BIO_get_mem_data(s->s3.handshake_buffer, - &curr_buf); - if (curr_buflen > 4 && curr_buf[0] == SSL3_MT_CLIENT_HELLO) { - outer_chlen = 1 + curr_buf[1] * 256 * 256 - + curr_buf[2] * 256 + curr_buf[3]; - if (outer_chlen > curr_buflen) { + BIO *handbuf = s->s3.handshake_buffer; + PACKET pkt, subpkt; + unsigned int mt; + + s->s3.handshake_buffer = NULL; + if (ssl3_init_finished_mac(s) == 0) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + BIO_free(handbuf); + return 0; + } + if (ssl3_finish_mac(s, s->ext.ech.innerch, s->ext.ech.innerch_len) == 0) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + BIO_free(handbuf); + return 0; + } + curr_buflen = BIO_get_mem_data(handbuf, &curr_buf); + if (PACKET_buf_init(&pkt, curr_buf, curr_buflen) + && PACKET_get_1(&pkt, &mt) + && mt == SSL3_MT_CLIENT_HELLO + && PACKET_remaining(&pkt) >= 3) { + if (!PACKET_get_length_prefixed_3(&pkt, &subpkt)) { SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + BIO_free(handbuf); return 0; } - other_octets = curr_buflen - outer_chlen; - if (other_octets > 0) { - new_buflen = s->ext.ech.innerch_len + other_octets; - new_buf = OPENSSL_malloc(new_buflen); - if (new_buf == NULL) { + if (PACKET_remaining(&pkt) > 0) { + if (ssl3_finish_mac(s, PACKET_data(&pkt), PACKET_remaining(&pkt)) == 0) { SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + BIO_free(handbuf); return 0; } - if (s->ext.ech.innerch != NULL) /* asan check added */ - memcpy(new_buf, s->ext.ech.innerch, - s->ext.ech.innerch_len); - memcpy(new_buf + s->ext.ech.innerch_len, - &curr_buf[outer_chlen], other_octets); - } else { - new_buf = s->ext.ech.innerch; - new_buflen = s->ext.ech.innerch_len; } - } else { - new_buf = s->ext.ech.innerch; - new_buflen = s->ext.ech.innerch_len; + BIO_free(handbuf); } - /* - * And now reset the handshake transcript to our buffer - * Note ssl3_finish_mac isn't that great a name - that one just - * adds to the transcript but doesn't actually "finish" anything - */ - if (ssl3_init_finished_mac(s) == 0) { - OPENSSL_free(new_buf); - SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); - return 0; - } - if (ssl3_finish_mac(s, new_buf, new_buflen) == 0) { - OPENSSL_free(new_buf); - SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); - return 0; - } - OPENSSL_free(new_buf); } # ifdef OSSL_ECH_SUPERVERBOSE ossl_ech_ptranscript(s, "ech_swaperoo, after"); @@ -1202,7 +888,7 @@ int ossl_ech_swaperoo(SSL_CONNECTION *s) SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); return 0; } - ech_status_print(biom, s, OSSL_ECHSTORE_ALL); + ossl_ech_status_print(biom, s, OSSL_ECHSTORE_ALL); BIO_read(biom, pstr, OSSL_ECH_PBUF_SIZE); cbrv = s->ext.ech.cb(&s->ssl, pstr); BIO_free(biom); @@ -1235,10 +921,11 @@ static int ech_hkdf_extract_wrap(SSL_CONNECTION *s, EVP_MD *md, int for_hrr, if (for_hrr == 1) { label = OSSL_ECH_HRR_CONFIRM_STRING; + labellen = sizeof(OSSL_ECH_HRR_CONFIRM_STRING) - 1; } else { label = OSSL_ECH_ACCEPT_CONFIRM_STRING; + labellen = sizeof(OSSL_ECH_ACCEPT_CONFIRM_STRING) - 1; } - labellen = strlen(label); # ifdef OSSL_ECH_SUPERVERBOSE ossl_ech_pbuf("cc: label", (unsigned char *)label, labellen); # endif @@ -1247,8 +934,6 @@ static int ech_hkdf_extract_wrap(SSL_CONNECTION *s, EVP_MD *md, int for_hrr, pctx = EVP_PKEY_CTX_new_id(EVP_PKEY_HKDF, NULL); if (pctx == NULL || EVP_PKEY_derive_init(pctx) != 1 - || EVP_PKEY_CTX_hkdf_mode(pctx, - EVP_PKEY_HKDEF_MODE_EXTRACT_ONLY) != 1 || EVP_PKEY_CTX_hkdf_mode(pctx, EVP_PKEY_HKDEF_MODE_EXTRACT_ONLY) != 1 || EVP_PKEY_CTX_set_hkdf_md(pctx, md) != 1) { @@ -1291,9 +976,8 @@ static int ech_hkdf_extract_wrap(SSL_CONNECTION *s, EVP_MD *md, int for_hrr, /* * ECH accept_confirmation calculation * for_hrr is 1 if this is for an HRR, otherwise for SH - * ac is (a caller allocated) 8 octet buffer - * shbuf is a pointer to the SH buffer (incl. the type+3-octet length) - * shlen is the length of the SH buf + * acbuf is an 8 octet buffer for the confirmation value + * shlen is the server hello length * return: 1 for success, 0 otherwise * * This is a magic value in the ServerHello.random lower 8 octets @@ -1314,7 +998,7 @@ static int ech_hkdf_extract_wrap(SSL_CONNECTION *s, EVP_MD *md, int for_hrr, */ int ossl_ech_calc_confirm(SSL_CONNECTION *s, int for_hrr, unsigned char acbuf[OSSL_ECH_SIGNAL_LEN], - const unsigned char *shbuf, const size_t shlen) + const size_t shlen) { int rv = 0; EVP_MD_CTX *ctx = NULL; @@ -1322,18 +1006,28 @@ int ossl_ech_calc_confirm(SSL_CONNECTION *s, int for_hrr, unsigned char *tbuf = NULL, *conf_loc = NULL; unsigned char *fixedshbuf = NULL; size_t fixedshbuf_len = 0, tlen = 0, chend = 0; - size_t shoffset = 6 + 24, extoffset = 0, echoffset = 0; - uint16_t echtype; + /* shoffset is: 4 + 2 + 32 - 8 */ + size_t shoffset = SSL3_HM_HEADER_LENGTH + sizeof(uint16_t) + + SSL3_RANDOM_SIZE - OSSL_ECH_SIGNAL_LEN; unsigned int hashlen = 0; - unsigned char hashval[EVP_MAX_MD_SIZE], hoval[EVP_MAX_MD_SIZE]; + unsigned char hashval[EVP_MAX_MD_SIZE]; - if ((md = (EVP_MD *)ssl_handshake_md(s)) == NULL) - goto err; - if (ossl_ech_make_transcript_buffer(s, for_hrr, shbuf, shlen, &tbuf, &tlen, - &chend, &fixedshbuf_len) != 1) - goto err; /* SSLfatal called already */ + if ((md = (EVP_MD *)ssl_handshake_md(s)) == NULL) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, SSL_R_ECH_REQUIRED); + goto end; + } + if (ossl_ech_intbuf_fetch(s, &tbuf, &tlen) != 1) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, SSL_R_ECH_REQUIRED); + goto end; + } + chend = tlen - shlen - 4; + fixedshbuf_len = shlen + 4; + if (s->server) { + chend = tlen - shlen; + fixedshbuf_len = shlen; + } # ifdef OSSL_ECH_SUPERVERBOSE - ossl_ech_pbuf("cx: tbuf b4", tbuf, tlen); + ossl_ech_pbuf("cx: tbuf b4-b4", tbuf, tlen); # endif /* put zeros in correct place */ if (for_hrr == 0) { /* zap magic octets at fixed place for SH */ @@ -1342,24 +1036,17 @@ int ossl_ech_calc_confirm(SSL_CONNECTION *s, int for_hrr, if (s->server == 1) { /* we get to say where we put ECH:-) */ conf_loc = tbuf + tlen - OSSL_ECH_SIGNAL_LEN; } else { - if (ossl_ech_get_sh_offsets(shbuf, shlen, &extoffset, - &echoffset, &echtype) != 1) { - SSLfatal(s, SSL_AD_INTERNAL_ERROR, SSL_R_ECH_REQUIRED); - goto err; - } - if (echoffset == 0 || extoffset == 0 || echtype == 0 - || tlen < (chend + 4 + echoffset + 4 + OSSL_ECH_SIGNAL_LEN)) { + if (s->ext.ech.hrrsignal_p == NULL) { /* No ECH found so we'll exit, but set random output */ if (RAND_bytes_ex(s->ssl.ctx->libctx, acbuf, - OSSL_ECH_SIGNAL_LEN, - RAND_DRBG_STRENGTH) <= 0) { + OSSL_ECH_SIGNAL_LEN, 0) <= 0) { SSLfatal(s, SSL_AD_INTERNAL_ERROR, SSL_R_ECH_REQUIRED); - goto err; + goto end; } rv = 1; - goto err; + goto end; } - conf_loc = tbuf + chend + 4 + echoffset + 4; + conf_loc = s->ext.ech.hrrsignal_p; } } memset(conf_loc, 0, OSSL_ECH_SIGNAL_LEN); @@ -1371,37 +1058,95 @@ int ossl_ech_calc_confirm(SSL_CONNECTION *s, int for_hrr, || EVP_DigestUpdate(ctx, tbuf, tlen) <= 0 || EVP_DigestFinal_ex(ctx, hashval, &hashlen) <= 0) { SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); - goto err; + goto end; } EVP_MD_CTX_free(ctx); ctx = NULL; # ifdef OSSL_ECH_SUPERVERBOSE ossl_ech_pbuf("cx: hashval", hashval, hashlen); # endif - if (ech_hkdf_extract_wrap(s, md, for_hrr, hashval, hashlen, hoval) != 1) { + /* calculate and set the final output */ + if (ech_hkdf_extract_wrap(s, md, for_hrr, hashval, hashlen, acbuf) != 1) { SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); - goto err; + goto end; } - memcpy(acbuf, hoval, OSSL_ECH_SIGNAL_LEN); /* Finally, set the output */ # ifdef OSSL_ECH_SUPERVERBOSE ossl_ech_pbuf("cx: result", acbuf, OSSL_ECH_SIGNAL_LEN); # endif - /* put confirm value back into transcript vars */ - if (s->hello_retry_request != SSL_HRR_NONE && s->ext.ech.kepthrr != NULL - && for_hrr == 1 && s->server == 1) - memcpy(s->ext.ech.kepthrr + s->ext.ech.kepthrr_len - - OSSL_ECH_SIGNAL_LEN, acbuf, OSSL_ECH_SIGNAL_LEN); - memcpy(conf_loc, acbuf, OSSL_ECH_SIGNAL_LEN); + /* put confirm value back into transcript */ + if (s->ext.ech.hrrsignal_p == NULL) + memcpy(conf_loc, acbuf, OSSL_ECH_SIGNAL_LEN); + else + memcpy(conf_loc, s->ext.ech.hrrsignal, OSSL_ECH_SIGNAL_LEN); /* on a server, we need to reset the hs buffer now */ if (s->server && s->hello_retry_request == SSL_HRR_NONE) ossl_ech_reset_hs_buffer(s, s->ext.ech.innerch, s->ext.ech.innerch_len); if (s->server && s->hello_retry_request == SSL_HRR_COMPLETE) ossl_ech_reset_hs_buffer(s, tbuf, tlen - fixedshbuf_len); rv = 1; -err: +end: OPENSSL_free(fixedshbuf); - OPENSSL_free(tbuf); EVP_MD_CTX_free(ctx); return rv; } + +int ossl_ech_intbuf_add(SSL_CONNECTION *s, const unsigned char *buf, + size_t blen, int hash_existing) +{ + EVP_MD_CTX *ctx = NULL; + EVP_MD *md = NULL; + unsigned int rv = 0, hashlen = 0; + unsigned char hashval[EVP_MAX_MD_SIZE], *t1; + size_t tlen; + WPACKET tpkt = { 0 }; + BUF_MEM *tpkt_mem = NULL; + + if (s == NULL || buf == NULL || blen == 0) + goto err; + if (hash_existing == 1) { + /* hash existing buffer, needed during HRR */ + if (s->ext.ech.transbuf == NULL + || (md = (EVP_MD *)ssl_handshake_md(s)) == NULL + || (ctx = EVP_MD_CTX_new()) == NULL + || EVP_DigestInit_ex(ctx, md, NULL) <= 0 + || EVP_DigestUpdate(ctx, s->ext.ech.transbuf, + s->ext.ech.transbuf_len) <= 0 + || EVP_DigestFinal_ex(ctx, hashval, &hashlen) <= 0 + || (tpkt_mem = BUF_MEM_new()) == NULL + || !WPACKET_init(&tpkt, tpkt_mem) + || !WPACKET_put_bytes_u8(&tpkt, SSL3_MT_MESSAGE_HASH) + || !WPACKET_put_bytes_u24(&tpkt, hashlen) + || !WPACKET_memcpy(&tpkt, hashval, hashlen) + || !WPACKET_get_length(&tpkt, &tlen) + || (t1 = OPENSSL_realloc(s->ext.ech.transbuf, tlen + blen)) == NULL) + goto err; + s->ext.ech.transbuf = t1; + memcpy(s->ext.ech.transbuf, tpkt_mem->data, tlen); + memcpy(s->ext.ech.transbuf + tlen, buf, blen); + s->ext.ech.transbuf_len = tlen + blen; + } else { + /* just add new octets */ + if ((t1 = OPENSSL_realloc(s->ext.ech.transbuf, + s->ext.ech.transbuf_len + blen)) == NULL) + goto err; + s->ext.ech.transbuf = t1; + memcpy(s->ext.ech.transbuf + s->ext.ech.transbuf_len, buf, blen); + s->ext.ech.transbuf_len += blen; + } + rv = 1; +err: + BUF_MEM_free(tpkt_mem); + WPACKET_cleanup(&tpkt); + EVP_MD_CTX_free(ctx); + return rv; +} + +int ossl_ech_intbuf_fetch(SSL_CONNECTION *s, unsigned char **buf, size_t *blen) +{ + if (s == NULL || buf == NULL || blen == NULL || s->ext.ech.transbuf == NULL) + return 0; + *buf = s->ext.ech.transbuf; + *blen = s->ext.ech.transbuf_len; + return 1; +} #endif diff --git a/ssl/ech/ech_local.h b/ssl/ech/ech_local.h index fdf7d45b0f66e..f2ecb4aee2005 100644 --- a/ssl/ech/ech_local.h +++ b/ssl/ech/ech_local.h @@ -48,6 +48,9 @@ # define OSSL_ECH_SIGNAL_LEN 8 /* length of ECH acceptance signal */ +/* size of string buffer returned via ECH callback */ +# define OSSL_ECH_PBUF_SIZE 8 * 1024 + # ifndef CLIENT_VERSION_LEN /* * This is the legacy version length, i.e. len(0x0303). The same @@ -55,6 +58,7 @@ * defined in a header file I could find. */ # define CLIENT_VERSION_LEN 2 + # endif /* @@ -147,37 +151,20 @@ typedef struct ossl_ech_conn_st { * the value we tried as the inner SNI for debug purposes */ char *former_inner; - /* - * TODO(ECH): The next 4 buffers (and lengths) may change if a - * better way to handle the mutiple transcripts needed is - * suggested/invented. I suggest re-factoring transcript handling - * (which is probably needed) after/with the PR that includes the - * server-side ECH code. That should be much easier as at that point - * the full set of tests can be run, whereas for now, we're limited - * to testing the client side really works via bodged s_client - * scripts, so there'd be a bigger risk of breaking something - * subtly if we try re-factor now. - */ - /* - * encoded inner ClientHello before/after ECH compression, which` - * is nitty/complex (to avoid repeating the same extension value - * in outer and inner, thus saving bandwidth) but (re-)calculating - * the compression is a pain, so we'll store those as we make them - */ - unsigned char *innerch; /* before compression */ + /* inner CH transcript buffer */ + unsigned char *transbuf; + size_t transbuf_len; + /* inner ClientHello before ECH compression */ + unsigned char *innerch; size_t innerch_len; - unsigned char *encoded_innerch; /* after compression */ - size_t encoded_innerch_len; - /* - * in case of HRR, we need to record the 1st inner client hello, and - * the first server hello (aka the HRR) so we can independently - * generate the transcript and accept confirmation when making the - * 2nd server hello - */ - unsigned char *innerch1; - size_t innerch1_len; - unsigned char *kepthrr; - size_t kepthrr_len; + /* encoded inner CH */ + unsigned char *encoded_inner; + size_t encoded_inner_len; + /* lengths calculated early, used when encrypting at end of processing */ + size_t clearlen; + size_t cipherlen; + /* location to put ciphertext, initially filled with zeros */ + size_t cipher_offset; /* * Extensions are "outer-only" if the value is only sent in the * outer CH and only the type is sent in the inner CH. @@ -202,6 +189,8 @@ typedef struct ossl_ech_conn_st { uint16_t attempted_type; /* ECH version used */ int attempted_cid; /* ECH config id sent/rx'd */ int backend; /* 1 if we're a server backend in split-mode, 0 otherwise */ + /* When using a PSK stash the tick_identity from inner, for outer */ + int tick_identity; /* * success is 1 if ECH succeeded, 0 otherwise, on the server this * is known early, on the client we need to wait for the ECH confirm @@ -217,6 +206,14 @@ typedef struct ossl_ech_conn_st { unsigned char *pub; /* client ephemeral public kept by server in case HRR */ size_t pub_len; OSSL_HPKE_CTX *hpke_ctx; /* HPKE context, needed for HRR */ + /* + * A pointer to, and copy of, the hrrsignal from an HRR message. + * We need both, as we zero-out the octets when re-calculating and + * may need to put back what the server included so the transcript + * is correct when ECH acceptance failed. + */ + unsigned char *hrrsignal_p; + unsigned char hrrsignal[OSSL_ECH_SIGNAL_LEN]; /* * Fields that differ on client between inner and outer that we need to * keep and swap over IFF ECH has succeeded. Same names chosen as are @@ -233,7 +230,7 @@ typedef struct ossl_ech_conn_st { # define OSSL_ECH_SAME_EXT_CONTINUE 2 /* generate a new value for outer CH */ /* - * During extension construction (in extensions_clnt.c and surprisingly also in + * During extension construction (in extensions_clnt.c, and surprisingly also in * extensions.c), we need to handle inner/outer CH cloning - ossl_ech_same_ext * will (depending on compile time handling options) copy the value from * CH.inner to CH.outer or else processing will continue, for a 2nd call, @@ -273,33 +270,42 @@ OSSL_ECHEXT *ossl_echext_dup(const OSSL_ECHEXT *src); # ifdef OSSL_ECH_SUPERVERBOSE void ossl_ech_pbuf(const char *msg, const unsigned char *buf, const size_t blen); -void ossl_ech_ptranscript(SSL_CONNECTION *s, const char *msg); # endif int ossl_ech_get_retry_configs(SSL_CONNECTION *s, unsigned char **rcfgs, size_t *rcfgslen); int ossl_ech_send_grease(SSL_CONNECTION *s, WPACKET *pkt); int ossl_ech_pick_matching_cfg(SSL_CONNECTION *s, OSSL_ECHSTORE_ENTRY **ee, OSSL_HPKE_SUITE *suite); -int ossl_ech_encode_inner(SSL_CONNECTION *s); +int ossl_ech_encode_inner(SSL_CONNECTION *s, unsigned char **encoded, + size_t *encoded_len); int ossl_ech_find_confirm(SSL_CONNECTION *s, int hrr, - unsigned char acbuf[OSSL_ECH_SIGNAL_LEN], - const unsigned char *shbuf, const size_t shlen); -int ossl_ech_make_transcript_buffer(SSL_CONNECTION *s, int for_hrr, - const unsigned char *shbuf, size_t shlen, - unsigned char **tbuf, size_t *tlen, - size_t *chend, size_t *fixedshbuf_len); + unsigned char acbuf[OSSL_ECH_SIGNAL_LEN]); int ossl_ech_reset_hs_buffer(SSL_CONNECTION *s, const unsigned char *buf, size_t blen); int ossl_ech_aad_and_encrypt(SSL_CONNECTION *s, WPACKET *pkt); int ossl_ech_swaperoo(SSL_CONNECTION *s); int ossl_ech_calc_confirm(SSL_CONNECTION *s, int for_hrr, unsigned char acbuf[OSSL_ECH_SIGNAL_LEN], - const unsigned char *shbuf, const size_t shlen); + const size_t shlen); /* these are internal but located in ssl/statem/extensions.c */ int ossl_ech_same_ext(SSL_CONNECTION *s, WPACKET *pkt); int ossl_ech_same_key_share(void); int ossl_ech_2bcompressed(int ind); +int ossl_ech_copy_inner2outer(SSL_CONNECTION *s, uint16_t ext_type, int ind, + WPACKET *pkt); + +int ossl_ech_get_ch_offsets(SSL_CONNECTION *s, PACKET *pkt, size_t *sessid, + size_t *exts, size_t *echoffset, uint16_t *echtype, + int *inner, size_t *snioffset); +int ossl_ech_early_decrypt(SSL_CONNECTION *s, PACKET *outerpkt, PACKET *newpkt); +void ossl_ech_status_print(BIO *out, SSL_CONNECTION *s, int selector); + +int ossl_ech_intbuf_add(SSL_CONNECTION *s, const unsigned char *buf, + size_t blen, int hash_existing); +int ossl_ech_intbuf_fetch(SSL_CONNECTION *s, unsigned char **buf, size_t *blen); +size_t ossl_ech_calc_padding(SSL_CONNECTION *s, OSSL_ECHSTORE_ENTRY *ee, + size_t encoded_len); # endif #endif diff --git a/ssl/ech/ech_ssl_apis.c b/ssl/ech/ech_ssl_apis.c index 7e38ac0e036bc..601a8ad16213e 100644 --- a/ssl/ech/ech_ssl_apis.c +++ b/ssl/ech/ech_ssl_apis.c @@ -185,7 +185,7 @@ int SSL_ech_get1_status(SSL *ssl, char **inner_sni, char **outer_sni) return SSL_ECH_STATUS_GREASE_ECH; return SSL_ECH_STATUS_GREASE; } - if (s->options & SSL_OP_ECH_GREASE) + if ((s->options & SSL_OP_ECH_GREASE) !=0 && s->ext.ech.attempted != 1) return SSL_ECH_STATUS_GREASE; if (s->ext.ech.backend == 1) { if (s->ext.hostname != NULL @@ -292,6 +292,8 @@ int SSL_ech_get1_retry_config(SSL *ssl, unsigned char **ec, size_t *eclen) OSSL_ECHSTORE *ve = NULL; BIO *in = NULL; int rv = 0; + OSSL_LIB_CTX *libctx = NULL; + const char *propq = NULL; s = SSL_CONNECTION_FROM_SSL(ssl); if (s == NULL || ec == NULL || eclen == NULL) @@ -309,10 +311,13 @@ int SSL_ech_get1_retry_config(SSL *ssl, unsigned char **ec, size_t *eclen) * and letting the application see that might cause someone to do an * upgrade. */ + if (s->ext.ech.es != NULL) { + libctx = s->ext.ech.es->libctx; + propq = s->ext.ech.es->propq; + } if ((in = BIO_new(BIO_s_mem())) == NULL || BIO_write(in, s->ext.ech.returned, s->ext.ech.returned_len) <= 0 - || (ve = OSSL_ECHSTORE_new(s->ext.ech.es->libctx, - s->ext.ech.es->propq)) == NULL) { + || (ve = OSSL_ECHSTORE_new(libctx, propq)) == NULL) { ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR); goto err; } diff --git a/ssl/ech/ech_store.c b/ssl/ech/ech_store.c index 5d2781fbf4bcf..c52c124568d3c 100644 --- a/ssl/ech/ech_store.c +++ b/ssl/ech/ech_store.c @@ -703,8 +703,7 @@ int OSSL_ECHSTORE_new_config(OSSL_ECHSTORE *es, goto err; } /* random config_id */ - if (RAND_bytes_ex(es->libctx, (unsigned char *)&config_id, 1, - RAND_DRBG_STRENGTH) <= 0) { + if (RAND_bytes_ex(es->libctx, (unsigned char *)&config_id, 1, 0) <= 0) { ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR); goto err; } @@ -1124,7 +1123,7 @@ int OSSL_ECHSTORE_flush_keys(OSSL_ECHSTORE *es, time_t age) ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_INVALID_ARGUMENT); return 0; } - if (ee->keyshare != NULL && ee->loadtime + age >= now) { + if (ee->keyshare != NULL && ee->loadtime + age <= now) { ossl_echstore_entry_free(ee); sk_OSSL_ECHSTORE_ENTRY_delete(es->entries, i); } diff --git a/ssl/statem/extensions.c b/ssl/statem/extensions.c index 5e8fc4cb9d471..670620631642a 100644 --- a/ssl/statem/extensions.c +++ b/ssl/statem/extensions.c @@ -24,7 +24,7 @@ /* * values for ext_defs ech_handling field * exceptionally, we don't conditionally compile that field to avoid a pile of - * fndefs all over the ext_defs values + * ifndefs all over the ext_defs values */ #define OSSL_ECH_HANDLING_CALL_BOTH 1 /* call constructor both times */ #define OSSL_ECH_HANDLING_COMPRESS 2 /* compress outer value into inner */ @@ -121,8 +121,8 @@ typedef struct extensions_definition_st { */ unsigned int context; /* - * exceptionally, we don't conditionally compile this field to avoid a pile of - * fndefs all over the ext_defs values + * exceptionally, we don't conditionally compile this field to avoid a + * pile of ifndefs all over the ext_defs values */ int ech_handling; /* how to handle ECH for this extension type */ /* @@ -547,8 +547,8 @@ static const EXTENSION_DEFINITION ext_defs[] = { * inner CH must have been pre-decoded into s->clienthello->pre_proc_exts * already. */ -static int ech_copy_inner2outer(SSL_CONNECTION *s, uint16_t ext_type, - int ind, WPACKET *pkt) +int ossl_ech_copy_inner2outer(SSL_CONNECTION *s, uint16_t ext_type, + int ind, WPACKET *pkt) { RAW_EXTENSION *myext = NULL, *raws = NULL; @@ -631,6 +631,7 @@ int ossl_ech_same_ext(SSL_CONNECTION *s, WPACKET *pkt) # endif if (s == NULL || s->ext.ech.es == NULL) return OSSL_ECH_SAME_EXT_CONTINUE; /* nothing to do */ + /* TODO(ECH): we need a better way to handle indexing exts */ tind = s->ext.ech.ext_ind; /* If this index'd extension won't be compressed, we're done */ if (tind < 0 || tind >= nexts) @@ -658,7 +659,7 @@ int ossl_ech_same_ext(SSL_CONNECTION *s, WPACKET *pkt) if (ext_defs[tind].ech_handling == OSSL_ECH_HANDLING_CALL_BOTH) return OSSL_ECH_SAME_EXT_CONTINUE; else - return ech_copy_inner2outer(s, type, tind, pkt); + return ossl_ech_copy_inner2outer(s, type, tind, pkt); } /* just in case - shouldn't happen */ return OSSL_ECH_SAME_EXT_ERR; @@ -1129,12 +1130,12 @@ int tls_construct_extensions(SSL_CONNECTION *s, WPACKET *pkt, #ifndef OPENSSL_NO_ECH /* - * Two passes - we first construct the to-be-ECH-compressed - * extensions, and then go around again constructing those that - * aren't to be ECH-compressed. We need to ensure this ordering - * so that all the ECH-compressed extensions are contiguous - * in the encoding. The actual compression happens later in - * ech_encode_inner(). + * Two passes if doing real ECH - we first construct the + * to-be-ECH-compressed extensions, and then go around again + * constructing those that aren't to be ECH-compressed. We + * need to ensure this ordering so that all the ECH-compressed + * extensions are contiguous in the encoding. The actual + * compression happens later in ech_encode_inner(). */ for (pass = 0; pass <= 1; pass++) #endif @@ -1829,13 +1830,6 @@ int tls_psk_do_binder(SSL_CONNECTION *s, const EVP_MD *md, int usepskfored = 0; SSL_CTX *sctx = SSL_CONNECTION_GET_CTX(s); OSSL_PARAM params[2] = { OSSL_PARAM_END, OSSL_PARAM_END }; -#ifndef OPENSSL_NO_ECH - unsigned char hashval[EVP_MAX_MD_SIZE]; - unsigned int hashlen = 0; - EVP_MD_CTX *ctx = NULL; - WPACKET tpkt; - BUF_MEM *tpkt_mem = NULL; -#endif /* Ensure cast to size_t is safe */ if (!ossl_assert(hashsizei > 0)) { @@ -1920,33 +1914,10 @@ int tls_psk_do_binder(SSL_CONNECTION *s, const EVP_MD *md, #ifndef OPENSSL_NO_ECH /* handle the hashing as per ECH needs (on client) */ if (s->ext.ech.attempted == 1 && s->ext.ech.ch_depth == 1) { - if ((tpkt_mem = BUF_MEM_new()) == NULL - || !BUF_MEM_grow(tpkt_mem, SSL3_RT_MAX_PLAIN_LENGTH) - || !WPACKET_init(&tpkt, tpkt_mem)) { - SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); - goto err; - } - hashlen = EVP_MD_size(md); - if ((ctx = EVP_MD_CTX_new()) == NULL - || EVP_DigestInit_ex(ctx, md, NULL) <= 0 - || EVP_DigestUpdate(ctx, s->ext.ech.innerch1, - s->ext.ech.innerch1_len) <= 0 - || EVP_DigestFinal_ex(ctx, hashval, &hashlen) <= 0) { - SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); - goto err; - } - EVP_MD_CTX_free(ctx); - ctx = NULL; - if (!WPACKET_put_bytes_u8(&tpkt, SSL3_MT_MESSAGE_HASH) - || !WPACKET_put_bytes_u24(&tpkt, hashlen) - || !WPACKET_memcpy(&tpkt, hashval, hashlen) - || !WPACKET_memcpy(&tpkt, s->ext.ech.kepthrr, - s->ext.ech.kepthrr_len) - || !WPACKET_get_length(&tpkt, &hdatalen)) { + if (ossl_ech_intbuf_fetch(s, (unsigned char **)&hdata, &hdatalen) != 1) { SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); goto err; } - hdata = WPACKET_get_curr(&tpkt) - hdatalen; } else { #endif hdatalen = hdatalen_l = @@ -2028,14 +1999,6 @@ int tls_psk_do_binder(SSL_CONNECTION *s, const EVP_MD *md, OPENSSL_cleanse(finishedkey, sizeof(finishedkey)); EVP_PKEY_free(mackey); EVP_MD_CTX_free(mctx); -#ifndef OPENSSL_NO_ECH - EVP_MD_CTX_free(ctx); - if (tpkt_mem != NULL) { - WPACKET_cleanup(&tpkt); - BUF_MEM_free(tpkt_mem); - } -#endif - return ret; } diff --git a/ssl/statem/extensions_clnt.c b/ssl/statem/extensions_clnt.c index 0342ce92e71af..5e5b6321adaca 100644 --- a/ssl/statem/extensions_clnt.c +++ b/ssl/statem/extensions_clnt.c @@ -14,6 +14,12 @@ #include "statem_local.h" #ifndef OPENSSL_NO_ECH # include +#include "internal/ech_helpers.h" +/* + * the max HPKE 'info' we'll process is the max ECHConfig size + * (OSSL_ECH_MAX_ECHCONFIG_LEN) plus OSSL_ECH_CONTEXT_STRING(len=7) + 1 + */ +#define OSSL_ECH_MAX_INFO_LEN (OSSL_ECH_MAX_ECHCONFIG_LEN + 8) #endif EXT_RETURN tls_construct_ctos_renegotiate(SSL_CONNECTION *s, WPACKET *pkt, @@ -74,8 +80,8 @@ EXT_RETURN tls_construct_ctos_server_name(SSL_CONNECTION *s, WPACKET *pkt, unsigned int context, X509 *x, size_t chainidx) { -#ifndef OPENSSL_NO_ECH char *chosen = s->ext.hostname; +#ifndef OPENSSL_NO_ECH OSSL_HPKE_SUITE suite; OSSL_ECHSTORE_ENTRY *ee = NULL; @@ -96,41 +102,23 @@ EXT_RETURN tls_construct_ctos_server_name(SSL_CONNECTION *s, WPACKET *pkt, chosen = ee->public_name; } } +#endif if (chosen == NULL) return EXT_RETURN_NOT_SENT; /* Add TLS extension servername to the Client Hello message */ - if (!WPACKET_put_bytes_u16(pkt, TLSEXT_TYPE_server_name) - || !WPACKET_start_sub_packet_u16(pkt) - || !WPACKET_start_sub_packet_u16(pkt) - || !WPACKET_put_bytes_u8(pkt, TLSEXT_NAMETYPE_host_name) - || !WPACKET_sub_memcpy_u16(pkt, chosen, strlen(chosen)) - || !WPACKET_close(pkt) - || !WPACKET_close(pkt)) { - SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); - return EXT_RETURN_FAIL; - } - return EXT_RETURN_SENT; -#else - if (s->ext.hostname == NULL) - return EXT_RETURN_NOT_SENT; - - /* Add TLS extension servername to the Client Hello message */ if (!WPACKET_put_bytes_u16(pkt, TLSEXT_TYPE_server_name) /* Sub-packet for server_name extension */ || !WPACKET_start_sub_packet_u16(pkt) /* Sub-packet for servername list (always 1 hostname)*/ || !WPACKET_start_sub_packet_u16(pkt) || !WPACKET_put_bytes_u8(pkt, TLSEXT_NAMETYPE_host_name) - || !WPACKET_sub_memcpy_u16(pkt, s->ext.hostname, - strlen(s->ext.hostname)) + || !WPACKET_sub_memcpy_u16(pkt, chosen, strlen(chosen)) || !WPACKET_close(pkt) || !WPACKET_close(pkt)) { SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); return EXT_RETURN_FAIL; } - return EXT_RETURN_SENT; -#endif } /* Push a Max Fragment Len extension into ClientHello */ @@ -527,12 +515,12 @@ EXT_RETURN tls_construct_ctos_alpn(SSL_CONNECTION *s, WPACKET *pkt, unsigned int context, X509 *x, size_t chainidx) { -#ifndef OPENSSL_NO_ECH - unsigned char *aval = NULL; - size_t alen = 0; -#endif + unsigned char *aval = s->ext.alpn; + size_t alen = s->ext.alpn_len; s->s3.alpn_sent = 0; + if (!SSL_IS_FIRST_HANDSHAKE(s)) + return EXT_RETURN_NOT_SENT; #ifndef OPENSSL_NO_ECH /* * If we have different alpn and alpn_outer values, then we set @@ -545,10 +533,6 @@ EXT_RETURN tls_construct_ctos_alpn(SSL_CONNECTION *s, WPACKET *pkt, * If you don't want the inner value in the outer, you have to * pick what to send in the outer and send that. */ - if (!SSL_IS_FIRST_HANDSHAKE(s)) - return EXT_RETURN_NOT_SENT; - aval = s->ext.alpn; - alen = s->ext.alpn_len; if (s->ext.ech.ch_depth == 1 && s->ext.alpn == NULL) /* inner */ return EXT_RETURN_NOT_SENT; if (s->ext.ech.ch_depth == 0 && s->ext.alpn == NULL @@ -558,30 +542,19 @@ EXT_RETURN tls_construct_ctos_alpn(SSL_CONNECTION *s, WPACKET *pkt, aval = s->ext.ech.alpn_outer; alen = s->ext.ech.alpn_outer_len; } +#endif + if (aval == NULL) + return EXT_RETURN_NOT_SENT; if (!WPACKET_put_bytes_u16(pkt, - TLSEXT_TYPE_application_layer_protocol_negotiation) + TLSEXT_TYPE_application_layer_protocol_negotiation) + /* Sub-packet ALPN extension */ || !WPACKET_start_sub_packet_u16(pkt) || !WPACKET_sub_memcpy_u16(pkt, aval, alen) || !WPACKET_close(pkt)) { SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); return EXT_RETURN_FAIL; } -#else - if (s->ext.alpn == NULL || !SSL_IS_FIRST_HANDSHAKE(s)) - return EXT_RETURN_NOT_SENT; - - if (!WPACKET_put_bytes_u16(pkt, - TLSEXT_TYPE_application_layer_protocol_negotiation) - /* Sub-packet ALPN extension */ - || !WPACKET_start_sub_packet_u16(pkt) - || !WPACKET_sub_memcpy_u16(pkt, s->ext.alpn, s->ext.alpn_len) - || !WPACKET_close(pkt)) { - SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); - return EXT_RETURN_FAIL; - } -#endif s->s3.alpn_sent = 1; - return EXT_RETURN_SENT; } @@ -813,7 +786,10 @@ static int add_key_share(SSL_CONNECTION *s, WPACKET *pkt, unsigned int group_id, # ifndef OPENSSL_NO_ECH if (s->ext.ech.ch_depth == 1) { /* stash inner */ - EVP_PKEY_up_ref(key_share_key); + if (EVP_PKEY_up_ref(key_share_key) != 1) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + goto err; + } EVP_PKEY_free(s->ext.ech.tmp_pkey); s->ext.ech.tmp_pkey = key_share_key; s->ext.ech.group_id = group_id; @@ -1173,6 +1149,9 @@ EXT_RETURN tls_construct_ctos_padding(SSL_CONNECTION *s, WPACKET *pkt, if ((s->options & SSL_OP_TLSEXT_PADDING) == 0) return EXT_RETURN_NOT_SENT; +#ifndef OPENSSL_NO_ECH + ECH_SAME_EXT(s, pkt); +#endif /* * Add padding to workaround bugs in F5 terminators. See RFC7685. @@ -1294,6 +1273,19 @@ EXT_RETURN tls_construct_ctos_psk(SSL_CONNECTION *s, WPACKET *pkt, goto dopsksess; } +#ifndef OPENSSL_NO_ECH + /* + * When doing ECH, we get here twice (for inner then outer). The + * 2nd time (for outer) we can skip some checks as we know how + * those went last time. + */ + if (s->ext.ech.es != NULL && s->ext.ech.ch_depth == 0) { + s->ext.tick_identity = s->ext.ech.tick_identity; + dores = (s->ext.tick_identity > 0); + goto dopsksess; + } +#endif + /* * Technically the C standard just says time() returns a time_t and says * nothing about the encoding of that type. In practice most @@ -1304,6 +1296,7 @@ EXT_RETURN tls_construct_ctos_psk(SSL_CONNECTION *s, WPACKET *pkt, */ t = ossl_time_subtract(ossl_time_now(), s->session->time); agesec = (uint32_t)ossl_time2seconds(t); + /* * We calculate the age in seconds but the server may work in ms. Due to * rounding errors we could overestimate the age by up to 1s. It is @@ -1344,6 +1337,11 @@ EXT_RETURN tls_construct_ctos_psk(SSL_CONNECTION *s, WPACKET *pkt, if (reshashsize <= 0) goto dopsksess; s->ext.tick_identity++; +#ifndef OPENSSL_NO_ECH + /* stash this for re-use in outer CH */ + if (s->ext.ech.es != NULL && s->ext.ech.ch_depth == 1) + s->ext.ech.tick_identity = s->ext.tick_identity; +#endif dores = 1; } @@ -1394,7 +1392,8 @@ EXT_RETURN tls_construct_ctos_psk(SSL_CONNECTION *s, WPACKET *pkt, * with random values of the same length. */ if (s->ext.ech.es != NULL && s->ext.ech.ch_depth == 0) { - unsigned char *rndbuf = NULL; + /* TODO(ECH): changes here need testing with server-side code PR */ + unsigned char *rndbuf = NULL, *rndbufp = NULL; size_t totalrndsize = 0; if (s->session == NULL) { @@ -1402,7 +1401,7 @@ EXT_RETURN tls_construct_ctos_psk(SSL_CONNECTION *s, WPACKET *pkt, return EXT_RETURN_FAIL; } totalrndsize = s->session->ext.ticklen - + 4 /* agems */ + + sizeof(agems) + s->psksession_id_len + reshashsize + pskhashsize; @@ -1411,56 +1410,43 @@ EXT_RETURN tls_construct_ctos_psk(SSL_CONNECTION *s, WPACKET *pkt, SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); return EXT_RETURN_FAIL; } - /* outer CH allocate a similar sized random value */ - if (RAND_bytes_ex(s->ssl.ctx->libctx, rndbuf, totalrndsize, - RAND_DRBG_STRENGTH) <= 0) { + /* for outer CH allocate a similar sized random value */ + if (RAND_bytes_ex(s->ssl.ctx->libctx, rndbuf, totalrndsize, 0) <= 0) { OPENSSL_free(rndbuf); SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); return EXT_RETURN_FAIL; } /* set agems from random buffer */ - agems = *((uint32_t *)(rndbuf + s->session->ext.ticklen)); + rndbufp = rndbuf; + agems = *((uint32_t *)(rndbufp)); + rndbufp += sizeof(agems); if (dores != 0) { - if (!WPACKET_sub_memcpy_u16(pkt, rndbuf, + if (!WPACKET_sub_memcpy_u16(pkt, rndbufp, s->session->ext.ticklen) || !WPACKET_put_bytes_u32(pkt, agems)) { SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); OPENSSL_free(rndbuf); return EXT_RETURN_FAIL; } + rndbufp += s->session->ext.ticklen; } if (s->psksession != NULL) { - if (!WPACKET_sub_memcpy_u16(pkt, - rndbuf + s->session->ext.ticklen + 4, - s->psksession_id_len) + if (!WPACKET_sub_memcpy_u16(pkt, rndbufp, s->psksession_id_len) || !WPACKET_put_bytes_u32(pkt, 0)) { SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); OPENSSL_free(rndbuf); return EXT_RETURN_FAIL; } + rndbufp += s->psksession_id_len; } if (!WPACKET_close(pkt) - || !WPACKET_get_total_written(pkt, &binderoffset) || !WPACKET_start_sub_packet_u16(pkt) || (dores == 1 - && !WPACKET_sub_memcpy_u8(pkt, - rndbuf + s->session->ext.ticklen - + 4 + s->psksession_id_len, - reshashsize)) + && !WPACKET_sub_memcpy_u8(pkt, rndbufp, reshashsize)) || (s->psksession != NULL - && !WPACKET_sub_memcpy_u8(pkt, - rndbuf + s->session->ext.ticklen - + 4 + s->psksession_id_len - + reshashsize, - pskhashsize)) + && !WPACKET_sub_memcpy_u8(pkt, rndbufp, pskhashsize)) || !WPACKET_close(pkt) - || !WPACKET_close(pkt) - || !WPACKET_get_total_written(pkt, &msglen) - /* - * We need to fill in all the sub-packet lengths now so we can - * calculate the HMAC of the message up to the binders - */ - || !WPACKET_fill_lengths(pkt)) { + || !WPACKET_close(pkt)) { SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); OPENSSL_free(rndbuf); return EXT_RETURN_FAIL; @@ -1659,12 +1645,13 @@ int tls_parse_stoc_server_name(SSL_CONNECTION *s, PACKET *pkt, unsigned int context, X509 *x, size_t chainidx) { -#ifndef OPENSSL_NO_ECH char *eff_sni = s->ext.hostname; +#ifndef OPENSSL_NO_ECH /* if we tried ECH and failed, the outer is what's expected */ if (s->ext.ech.es != NULL && s->ext.ech.success == 0) eff_sni = s->ext.ech.outer_hostname; +#endif if (eff_sni == NULL) { SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); return 0; @@ -1684,29 +1671,6 @@ int tls_parse_stoc_server_name(SSL_CONNECTION *s, PACKET *pkt, return 0; } } -#else - if (s->ext.hostname == NULL) { - SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); - return 0; - } - - if (PACKET_remaining(pkt) > 0) { - SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_BAD_EXTENSION); - return 0; - } - - if (!s->hit) { - if (s->session->ext.hostname != NULL) { - SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); - return 0; - } - s->session->ext.hostname = OPENSSL_strdup(s->ext.hostname); - if (s->session->ext.hostname == NULL) { - SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); - return 0; - } - } -#endif return 1; } @@ -2564,14 +2528,23 @@ EXT_RETURN tls_construct_ctos_ech(SSL_CONNECTION *s, WPACKET *pkt, unsigned int context, X509 *x, size_t chainidx) { - if (s->ext.ech.attempted_type != TLSEXT_TYPE_ech - && s->ext.ech.grease != OSSL_ECH_IS_GREASE - && !(s->options & SSL_OP_ECH_GREASE)) + int rv = 0, hpke_mode = OSSL_HPKE_MODE_BASE; + OSSL_ECHSTORE_ENTRY *ee = NULL; + OSSL_HPKE_SUITE hpke_suite = OSSL_HPKE_SUITE_DEFAULT; + unsigned char config_id_to_use = 0x00, info[OSSL_ECH_MAX_INFO_LEN]; + unsigned char *encoded = NULL, *mypub = NULL; + size_t cipherlen = 0, aad_len = 0, lenclen = 0, mypub_len = 0; + size_t info_len = OSSL_ECH_MAX_INFO_LEN, clear_len = 0, encoded_len = 0; + + /* whether or not we've been asked to GREASE, one way or another */ + int grease_opt_set = (s->ext.ech.grease == OSSL_ECH_IS_GREASE + || ((s->options & SSL_OP_ECH_GREASE) != 0)); + + /* if we're not doing real ECH and not GREASEing then exit */ + if (s->ext.ech.attempted_type != TLSEXT_TYPE_ech && grease_opt_set == 0) return EXT_RETURN_NOT_SENT; /* send grease if not really attempting ECH */ - if (s->ext.ech.attempted == 0 - && (s->ext.ech.grease == OSSL_ECH_IS_GREASE - || (s->options & SSL_OP_ECH_GREASE))) { + if (s->ext.ech.attempted == 0 && grease_opt_set == 1) { if (s->hello_retry_request == SSL_HRR_PENDING && s->ext.ech.sent != NULL) { /* re-tx already sent GREASEy ECH */ @@ -2591,14 +2564,7 @@ EXT_RETURN tls_construct_ctos_ech(SSL_CONNECTION *s, WPACKET *pkt, } return EXT_RETURN_SENT; } - /* - * If not GREASEing we fake sending the outer value - after the - * entire thing has been constructed we only then finally encode - * and encrypt - need to do it that way as we need the rest of - * the outer CH as AAD input to the encryption. - */ - if (s->ext.ech.ch_depth == 0) - return EXT_RETURN_NOT_SENT; + /* For the inner CH - we simply include one of these saying "inner" */ if (s->ext.ech.ch_depth == 1) { if (!WPACKET_put_bytes_u16(pkt, TLSEXT_TYPE_ech) @@ -2610,6 +2576,141 @@ EXT_RETURN tls_construct_ctos_ech(SSL_CONNECTION *s, WPACKET *pkt, } return EXT_RETURN_SENT; } + + /* + * If not GREASEing we prepare sending the outer value - after the + * entire thing has been constructed, putting in zeros for now where + * we'd otherwise include ECH ciphertext, we later encode and encrypt. + * We need to do it that way as we need the rest of the outer CH to + * be known and used as AAD input before we do encryption. + */ + if (s->ext.ech.ch_depth != 0) + return EXT_RETURN_NOT_SENT; + /* Make ClientHelloInner and EncodedClientHelloInner as per spec. */ + if (ossl_ech_encode_inner(s, &encoded, &encoded_len) != 1) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + goto err; + } + s->ext.ech.encoded_inner = encoded; + s->ext.ech.encoded_inner_len = encoded_len; +# ifdef OSSL_ECH_SUPERVERBOSE + ossl_ech_pbuf("encoded inner CH", encoded, encoded_len); +# endif + rv = ossl_ech_pick_matching_cfg(s, &ee, &hpke_suite); + if (rv != 1 || ee == NULL) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + goto err; + } + s->ext.ech.attempted_type = ee->version; + OSSL_TRACE_BEGIN(TLS) { + BIO_printf(trc_out, "EAAE: selected: version: %4x, config %2x\n", + ee->version, ee->config_id); + } OSSL_TRACE_END(TLS); + config_id_to_use = ee->config_id; /* if requested, use a random config_id instead */ + if ((s->options & SSL_OP_ECH_IGNORE_CID) != 0) { + if (RAND_bytes_ex(s->ssl.ctx->libctx, &config_id_to_use, 1, 0) <= 0) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + return 0; + } +# ifdef OSSL_ECH_SUPERVERBOSE + ossl_ech_pbuf("EAAE: random config_id", &config_id_to_use, 1); +# endif + } + s->ext.ech.attempted_cid = config_id_to_use; +# ifdef OSSL_ECH_SUPERVERBOSE + ossl_ech_pbuf("EAAE: peer pub", ee->pub, ee->pub_len); + ossl_ech_pbuf("EAAE: clear", encoded, encoded_len); + ossl_ech_pbuf("EAAE: ECHConfig", ee->encoded, ee->encoded_len); +# endif + /* + * The AAD is the full outer client hello but with the correct number of + * zeros for where the ECH ciphertext octets will later be placed. So we + * add the ECH extension to the |pkt| but with zeros for ciphertext, that + * forms up the AAD, then after we've encrypted, we'll splice in the actual + * ciphertext. + * Watch out for the "4" offsets that remove the type and 3-octet length + * from the encoded CH as per the spec. + */ + clear_len = ossl_ech_calc_padding(s, ee, encoded_len); + if (clear_len == 0) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + goto err; + } + lenclen = OSSL_HPKE_get_public_encap_size(hpke_suite); + if (s->ext.ech.hpke_ctx == NULL) { /* 1st CH */ + if (ossl_ech_make_enc_info(ee->encoded, ee->encoded_len, + info, &info_len) != 1) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + goto err; + } +# ifdef OSSL_ECH_SUPERVERBOSE + ossl_ech_pbuf("EAAE info", info, info_len); +# endif + s->ext.ech.hpke_ctx = OSSL_HPKE_CTX_new(hpke_mode, hpke_suite, + OSSL_HPKE_ROLE_SENDER, + NULL, NULL); + if (s->ext.ech.hpke_ctx == NULL) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + goto err; + } + mypub = OPENSSL_malloc(lenclen); + if (mypub == NULL) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + goto err; + } + mypub_len = lenclen; + rv = OSSL_HPKE_encap(s->ext.ech.hpke_ctx, mypub, &mypub_len, + ee->pub, ee->pub_len, info, info_len); + if (rv != 1) { + OPENSSL_free(mypub); + mypub = NULL; + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + goto err; + } + s->ext.ech.pub = mypub; + s->ext.ech.pub_len = mypub_len; + } else { /* HRR - retrieve public */ + mypub = s->ext.ech.pub; + mypub_len = s->ext.ech.pub_len; + if (mypub == NULL || mypub_len == 0) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + goto err; + } + } +# ifdef OSSL_ECH_SUPERVERBOSE + ossl_ech_pbuf("EAAE: mypub", mypub, mypub_len); + WPACKET_get_total_written(pkt, &aad_len); /* use aad_len for tracing */ + ossl_ech_pbuf("EAAE pkt b4", WPACKET_get_curr(pkt) - aad_len, aad_len); +# endif + cipherlen = OSSL_HPKE_get_ciphertext_size(hpke_suite, clear_len); + if (cipherlen <= clear_len || cipherlen > OSSL_ECH_MAX_PAYLOAD_LEN) { + SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_BAD_EXTENSION); + goto err; + } + s->ext.ech.clearlen = clear_len; + s->ext.ech.cipherlen = cipherlen; + if (!WPACKET_put_bytes_u16(pkt, TLSEXT_TYPE_ech) + || !WPACKET_start_sub_packet_u16(pkt) + || !WPACKET_put_bytes_u8(pkt, OSSL_ECH_OUTER_CH_TYPE) + || !WPACKET_put_bytes_u16(pkt, hpke_suite.kdf_id) + || !WPACKET_put_bytes_u16(pkt, hpke_suite.aead_id) + || !WPACKET_put_bytes_u8(pkt, config_id_to_use) + || (s->hello_retry_request == SSL_HRR_PENDING + && !WPACKET_put_bytes_u16(pkt, 0x00)) /* no pub */ + || (s->hello_retry_request != SSL_HRR_PENDING + && !WPACKET_sub_memcpy_u16(pkt, mypub, mypub_len)) + || !WPACKET_start_sub_packet_u16(pkt) + || !WPACKET_get_total_written(pkt, &s->ext.ech.cipher_offset) + || !WPACKET_memset(pkt, 0, cipherlen) + || !WPACKET_close(pkt) + || !WPACKET_close(pkt)) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + goto err; + } + /* don't count the type + 3-octet length */ + s->ext.ech.cipher_offset -= 4; + return EXT_RETURN_SENT; +err: return EXT_RETURN_FAIL; } @@ -2620,22 +2721,29 @@ int tls_parse_stoc_ech(SSL_CONNECTION *s, PACKET *pkt, unsigned int context, unsigned int rlen = 0; const unsigned char *rval = NULL; unsigned char *srval = NULL; + PACKET rcfgs_pkt; /* - * An HRR will have an ECH extension with the - * 8-octet confirmation value, already handled + * An HRR will have an ECH extension with the 8-octet confirmation value. + * Store it away for when we check it later */ - if (context == SSL_EXT_TLS1_3_HELLO_RETRY_REQUEST) + if (context == SSL_EXT_TLS1_3_HELLO_RETRY_REQUEST) { + if (PACKET_remaining(pkt) != OSSL_ECH_SIGNAL_LEN) { + SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_LENGTH_MISMATCH); + return 0; + } + s->ext.ech.hrrsignal_p = (unsigned char *)PACKET_data(pkt); + memcpy(s->ext.ech.hrrsignal, s->ext.ech.hrrsignal_p, + OSSL_ECH_SIGNAL_LEN); return 1; + } /* othewise we expect retry-configs */ - if (!PACKET_get_net_2(pkt, &rlen)) { + if (!PACKET_get_length_prefixed_2(pkt, &rcfgs_pkt)) { SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_LENGTH_MISMATCH); return 0; } - if (!PACKET_get_bytes(pkt, &rval, rlen)) { - SSLfatal(s, SSL_AD_ILLEGAL_PARAMETER, SSL_R_LENGTH_MISMATCH); - return 0; - } + rval = PACKET_data(&rcfgs_pkt); + rlen = PACKET_remaining(&rcfgs_pkt); OPENSSL_free(s->ext.ech.returned); s->ext.ech.returned = NULL; srval = OPENSSL_malloc(rlen + 2); diff --git a/ssl/statem/extensions_cust.c b/ssl/statem/extensions_cust.c index aa352529c4cc7..bda7e460b9d05 100644 --- a/ssl/statem/extensions_cust.c +++ b/ssl/statem/extensions_cust.c @@ -200,6 +200,65 @@ int custom_ext_add(SSL_CONNECTION *s, int context, WPACKET *pkt, X509 *x, if (!(meth->ext_flags & SSL_EXT_FLAG_RECEIVED)) continue; } + +#ifndef OPENSSL_NO_ECH + if ((context & SSL_EXT_CLIENT_HELLO) != 0 + && s->ext.ech.attempted == 1) { + if (s->ext.ech.ch_depth == 1) { + /* mark custom CH ext for ECH compression, if doing ECH */ + if (s->ext.ech.n_outer_only >= OSSL_ECH_OUTERS_MAX) { + OSSL_TRACE_BEGIN(TLS) { + BIO_printf(trc_out, + "Too many outers to compress (max=%d)\n", + OSSL_ECH_OUTERS_MAX); + } OSSL_TRACE_END(TLS); + SSLfatal(s, SSL_AD_INTERNAL_ERROR, SSL_R_BAD_EXTENSION); + return 0; + } + s->ext.ech.outer_only[s->ext.ech.n_outer_only] = meth->ext_type; + s->ext.ech.n_outer_only++; + OSSL_TRACE_BEGIN(TLS) { + BIO_printf(trc_out, "ECH compressing type " + "0x%04x (tot: %d)\n", + (int) meth->ext_type, + (int) s->ext.ech.n_outer_only); + } OSSL_TRACE_END(TLS); + } + if (s->ext.ech.ch_depth == 0) { + /* TODO(ECH): we need a better way to handle indexing exts */ + /* copy over the extension octets (if any) to outer */ + int j, tind = -1; + RAW_EXTENSION *raws = NULL; + + /* we gotta find the relevant index to copy over this ext */ + if (s->clienthello == NULL + || s->clienthello->pre_proc_exts == NULL) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, SSL_R_BAD_EXTENSION); + return 0; + } + raws = s->clienthello->pre_proc_exts; + for (j = 0; j != (int) s->clienthello->pre_proc_exts_len; j++) { + if (raws[j].type == meth->ext_type) { + tind = j; + break; + } + } + if (tind == -1) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, SSL_R_BAD_EXTENSION); + return 0; + } + if (ossl_ech_copy_inner2outer(s, meth->ext_type, tind, pkt) + != OSSL_ECH_SAME_EXT_DONE) { + /* for custom exts, we really should have found it */ + SSLfatal(s, SSL_AD_INTERNAL_ERROR, SSL_R_BAD_EXTENSION); + return 0; + } + /* we're done with that one now */ + continue; + } + } +#endif + /* * We skip it if the callback is absent - except for a ClientHello where * we add an empty extension. @@ -446,7 +505,19 @@ int ossl_tls_add_custom_ext_intern(SSL_CTX *ctx, custom_ext_methods *exts, * for extension types that previously were not supported, but now are. */ if (SSL_extension_supported(ext_type) +#if !defined(OPENSSL_NO_ECH) && defined(OPENSSL_ECH_ALLOW_CUST_INJECT) + /* + * Do this conditionally so we can test an ECH in TLSv1.2 + * via the custom extensions API. + * OPENSSL_ECH_ALLOW_CUST_INJECT is defined (or not) in + * include/openssl/ech.h and if defined enables a test in + * test/ech_test.c + */ + && ext_type != TLSEXT_TYPE_ech && ext_type != TLSEXT_TYPE_signed_certificate_timestamp) +#else + && ext_type != TLSEXT_TYPE_signed_certificate_timestamp) +#endif return 0; /* Extension type must fit in 16 bits */ @@ -600,6 +671,10 @@ int SSL_extension_supported(unsigned int ext_type) case TLSEXT_TYPE_compress_certificate: case TLSEXT_TYPE_client_cert_type: case TLSEXT_TYPE_server_cert_type: +#ifndef OPENSSL_NO_ECH + case TLSEXT_TYPE_ech: + case TLSEXT_TYPE_outer_extensions: +#endif return 1; default: return 0; diff --git a/ssl/statem/statem_clnt.c b/ssl/statem/statem_clnt.c index 53a8fcc46117b..18daa3200c56a 100644 --- a/ssl/statem/statem_clnt.c +++ b/ssl/statem/statem_clnt.c @@ -32,7 +32,7 @@ #include static MSG_PROCESS_RETURN tls_process_as_hello_retry_request(SSL_CONNECTION *s, - PACKET *pkt); + RAW_EXTENSION *extensions); static MSG_PROCESS_RETURN tls_process_encrypted_extensions(SSL_CONNECTION *s, PACKET *pkt); @@ -1175,13 +1175,13 @@ WORK_STATE ossl_statem_client_post_process_message(SSL_CONNECTION *s, #ifndef OPENSSL_NO_ECH /* - * Wrap the existing ClientHello construction with ECH code. + * Wrap ClientHello construction with ECH code. * - * As needed, we'll call the existing CH constructor twice, - * first for inner, and then for outer. + * As needed, we'll call the CH constructor twice, first for + * inner, and then for outer. * - * So the old tls_construct_client_hello is renamed to the _aux - * variant, and the new tls_construct_client_hello just calls + * `tls_construct_client_hello_aux` is the pre-ECH code + * and the ECH-aware tls_construct_client_hello just calls * that if there's no ECH involved, but otherwise does ECH * things around calls to the _aux variant. * @@ -1216,7 +1216,6 @@ static int tls_construct_client_hello_aux(SSL_CONNECTION *s, WPACKET *pkt); __owur CON_FUNC_RETURN tls_construct_client_hello(SSL_CONNECTION *s, WPACKET *pkt) { - unsigned char *innerch_full = NULL, *innerch_end = NULL; WPACKET inner; /* "fake" pkt for inner */ BUF_MEM *inner_mem = NULL; PACKET rpkt; /* we'll decode back the inner ch to help make the outer */ @@ -1238,12 +1237,12 @@ __owur CON_FUNC_RETURN tls_construct_client_hello(SSL_CONNECTION *s, /* note version we're attempting and that an attempt is being made */ if (s->ext.ech.es->entries != NULL) { if (ossl_ech_pick_matching_cfg(s, &ee, &suite) != 1 || ee == NULL) { - SSLfatal(s, SSL_AD_INTERNAL_ERROR, protverr); + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_UNSUPPORTED); return 0; } if (ee->version != OSSL_ECH_RFCXXXX_VERSION) { /* we only support that version for now */ - SSLfatal(s, SSL_AD_INTERNAL_ERROR, protverr); + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_UNSUPPORTED); return 0; } s->ext.ech.attempted_type = TLSEXT_TYPE_ech; @@ -1279,7 +1278,7 @@ __owur CON_FUNC_RETURN tls_construct_client_hello(SSL_CONNECTION *s, s->tmp_session_id_len = sess_id_len; if (s->hello_retry_request == SSL_HRR_NONE && RAND_bytes_ex(s->ssl.ctx->libctx, s->tmp_session_id, - sess_id_len, RAND_DRBG_STRENGTH) <= 0) { + sess_id_len, 0) <= 0) { SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); return 0; } @@ -1296,19 +1295,8 @@ __owur CON_FUNC_RETURN tls_construct_client_hello(SSL_CONNECTION *s, memcpy(s->tmp_session_id, s->session->session_id, sess_id_len); } } - if (s->hello_retry_request != SSL_HRR_NONE) { + if (s->hello_retry_request != SSL_HRR_NONE) s->ext.ech.n_outer_only = 0; /* reset count of "compressed" exts */ - OPENSSL_free(s->ext.ech.encoded_innerch); - s->ext.ech.encoded_innerch = NULL; - s->ext.ech.encoded_innerch_len = 0; - if (s->ext.ech.innerch != NULL) { - OPENSSL_free(s->ext.ech.innerch1); - s->ext.ech.innerch1 = s->ext.ech.innerch; - s->ext.ech.innerch1_len = s->ext.ech.innerch_len; - s->ext.ech.innerch_len = 0; - s->ext.ech.innerch = NULL; - } - } /* * Set CH depth flag so that other code (e.g. extension handlers) * know where we're at: 1 is "inner CH", 0 is "outer CH" @@ -1320,19 +1308,18 @@ __owur CON_FUNC_RETURN tls_construct_client_hello(SSL_CONNECTION *s, || tls_construct_client_hello_aux(s, &inner) != 1 || !WPACKET_close(&inner) || !WPACKET_get_length(&inner, &innerlen)) { - SSLfatal(s, SSL_AD_INTERNAL_ERROR, protverr); - goto err; - } - innerch_full = OPENSSL_malloc(innerlen); - if (innerch_full == NULL) { SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); goto err; } - innerch_end = WPACKET_get_curr(&inner); - memcpy(innerch_full, innerch_end - innerlen, innerlen); OPENSSL_free(s->ext.ech.innerch); - s->ext.ech.innerch = innerch_full; + s->ext.ech.innerch = (unsigned char*)inner_mem->data; + inner_mem->data = NULL; s->ext.ech.innerch_len = innerlen; + /* add inner to transcript */ + if (ossl_ech_intbuf_add(s, s->ext.ech.innerch, innerlen, 0) != 1) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + goto err; + } WPACKET_cleanup(&inner); BUF_MEM_free(inner_mem); inner_mem = NULL; @@ -1358,15 +1345,7 @@ __owur CON_FUNC_RETURN tls_construct_client_hello(SSL_CONNECTION *s, SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); goto err; } - /* Make ClientHelloInner and EncodedClientHelloInner as per spec. */ - if (ossl_ech_encode_inner(s) != 1) { - SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); - goto err; - } -# ifdef OSSL_ECH_SUPERVERBOSE - ossl_ech_pbuf("encoded inner CH", s->ext.ech.encoded_innerch, - s->ext.ech.encoded_innerch_len); -# endif + s->ext.ech.ch_depth = 0; /* set depth for outer CH */ /* * If we want different key shares for inner and outer, then @@ -1380,7 +1359,7 @@ __owur CON_FUNC_RETURN tls_construct_client_hello(SSL_CONNECTION *s, /* Make second call into CH construction for outer CH. */ rv = tls_construct_client_hello_aux(s, pkt); if (rv != 1) { - SSLfatal(s, SSL_AD_INTERNAL_ERROR, protverr); + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); goto err; } # ifdef OSSL_ECH_SUPERVERBOSE @@ -1457,10 +1436,8 @@ __owur CON_FUNC_RETURN tls_construct_client_hello(SSL_CONNECTION *s, WPACKET *pk if (s->ext.ech.es != NULL && s->ext.ech.ch_depth == 1) p = s->ext.ech.client_random; else - p = s->s3.client_random; -#else - p = s->s3.client_random; #endif + p = s->s3.client_random; /* * for DTLS if client_random is initialized, reuse it, we are @@ -1518,19 +1495,11 @@ __owur CON_FUNC_RETURN tls_construct_client_hello(SSL_CONNECTION *s, WPACKET *pk * For TLS 1.3 we always set the ClientHello version to 1.2 and rely on the * supported_versions extension for the real supported versions. */ -#ifndef OPENSSL_NO_ECH if (!WPACKET_put_bytes_u16(pkt, s->client_version) || !WPACKET_memcpy(pkt, p, SSL3_RANDOM_SIZE)) { SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); return CON_FUNC_ERROR; } -#else - if (!WPACKET_put_bytes_u16(pkt, s->client_version) - || !WPACKET_memcpy(pkt, s->s3.client_random, SSL3_RANDOM_SIZE)) { - SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); - return CON_FUNC_ERROR; - } -#endif /* Session ID */ session_id = s->session->session_id; @@ -1757,13 +1726,13 @@ MSG_PROCESS_RETURN tls_process_server_hello(SSL_CONNECTION *s, PACKET *pkt) #endif #ifndef OPENSSL_NO_ECH const unsigned char *shbuf = NULL; - size_t shlen, chend, fixedshbuf_len, alen; + size_t shlen, alen; /* * client and server accept signal buffers, initialise in case of * e.g. memory fail when calculating, only really applies when * SUPERVERBOSE is defined and we trace these. */ - unsigned char c_signal[OSSL_ECH_SIGNAL_LEN] = { 0 }; + unsigned char c_signal[OSSL_ECH_SIGNAL_LEN] = { 0 }; unsigned char s_signal[OSSL_ECH_SIGNAL_LEN] = { 0xff }; unsigned char *abuf = NULL; @@ -1828,6 +1797,34 @@ MSG_PROCESS_RETURN tls_process_server_hello(SSL_CONNECTION *s, PACKET *pkt) goto err; } + /* TLS extensions */ + if (PACKET_remaining(pkt) == 0 && !hrr) { + PACKET_null_init(&extpkt); + } else if (!PACKET_as_length_prefixed_2(pkt, &extpkt) + || PACKET_remaining(pkt) != 0) { + SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_BAD_LENGTH); + goto err; + } + + if (hrr) { + if (!tls_collect_extensions(s, &extpkt, SSL_EXT_TLS1_3_HELLO_RETRY_REQUEST, + &extensions, NULL, 1) + || !tls_parse_extension(s, TLSEXT_IDX_ech, + SSL_EXT_TLS1_3_HELLO_RETRY_REQUEST, + extensions, NULL, 0)) { + /* SSLfatal() already called */ + goto err; + } + } else { + if (!tls_collect_extensions(s, &extpkt, + SSL_EXT_TLS1_2_SERVER_HELLO + | SSL_EXT_TLS1_3_SERVER_HELLO, + &extensions, NULL, 1)) { + /* SSLfatal() already called */ + goto err; + } + } + #ifndef OPENSSL_NO_ECH /* * If we sent an ECH then check if that worked based on the @@ -1840,14 +1837,31 @@ MSG_PROCESS_RETURN tls_process_server_hello(SSL_CONNECTION *s, PACKET *pkt) && s->ext.ech.done != 1 && s->ext.ech.ch_depth == 0 && s->ext.ech.grease == OSSL_ECH_NOT_GREASE && s->ext.ech.attempted_type == TLSEXT_TYPE_ech) { - /* try set this earlier see what happens */ if (!set_client_ciphersuite(s, cipherchars)) { /* SSLfatal() already called */ goto err; } + /* add any SH/HRR to inner transcript if we tried ECH */ + if (s->ext.ech.attempted == 1) { + unsigned char prelude[4]; + + prelude[0] = SSL3_MT_SERVER_HELLO; + prelude[1] = (shlen >> 16) & 0xff; + prelude[2] = (shlen >> 8) & 0xff; + prelude[3] = shlen & 0xff; + if (ossl_ech_intbuf_add(s, prelude, sizeof(prelude), hrr) != 1 + || ossl_ech_intbuf_add(s, shbuf, shlen, 0) != 1) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + goto err; + } + } /* check the ECH accept signal */ - if (ossl_ech_calc_confirm(s, hrr, c_signal, shbuf, shlen) != 1 - || ossl_ech_find_confirm(s, hrr, s_signal, shbuf, shlen) != 1 + if (ossl_ech_calc_confirm(s, hrr, c_signal, shlen) != 1) { + /* SSLfatal() already called */ + OSSL_TRACE(TLS, "ECH calc confim failed\n"); + goto err; + } + if (ossl_ech_find_confirm(s, hrr, s_signal) != 1 || memcmp(s_signal, c_signal, sizeof(c_signal)) != 0) { OSSL_TRACE(TLS, "ECH accept check failed\n"); # ifdef OSSL_ECH_SUPERVERBOSE @@ -1863,17 +1877,15 @@ MSG_PROCESS_RETURN tls_process_server_hello(SSL_CONNECTION *s, PACKET *pkt) } OSSL_TRACE_END(TLS); s->ext.ech.success = 1; } + /* we're done with that hrrsignal (if we got one) */ + s->ext.ech.hrrsignal_p = NULL; if (!hrr && s->ext.ech.success == 1) { if (ossl_ech_swaperoo(s) != 1 - || ossl_ech_make_transcript_buffer(s, hrr, shbuf, shlen, - &abuf, &alen, - &chend, &fixedshbuf_len) != 1 + || ossl_ech_intbuf_fetch(s, &abuf, &alen) != 1 || ossl_ech_reset_hs_buffer(s, abuf, alen) != 1) { - OPENSSL_free(abuf); SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); goto err; } - OPENSSL_free(abuf); } else if (!hrr) { /* * If we got retry_configs then we should be validating @@ -1893,24 +1905,7 @@ MSG_PROCESS_RETURN tls_process_server_hello(SSL_CONNECTION *s, PACKET *pkt) } #endif - /* TLS extensions */ - if (PACKET_remaining(pkt) == 0 && !hrr) { - PACKET_null_init(&extpkt); - } else if (!PACKET_as_length_prefixed_2(pkt, &extpkt) - || PACKET_remaining(pkt) != 0) { - SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_BAD_LENGTH); - goto err; - } - if (!hrr) { - if (!tls_collect_extensions(s, &extpkt, - SSL_EXT_TLS1_2_SERVER_HELLO - | SSL_EXT_TLS1_3_SERVER_HELLO, - &extensions, NULL, 1)) { - /* SSLfatal() already called */ - goto err; - } - if (!ssl_choose_client_version(s, sversion, extensions)) { /* SSLfatal() already called */ goto err; @@ -1933,12 +1928,17 @@ MSG_PROCESS_RETURN tls_process_server_hello(SSL_CONNECTION *s, PACKET *pkt) } if (hrr) { + int ret; + if (!set_client_ciphersuite(s, cipherchars)) { /* SSLfatal() already called */ goto err; } - return tls_process_as_hello_retry_request(s, &extpkt); + ret = tls_process_as_hello_retry_request(s, extensions); + OPENSSL_free(extensions); + + return ret; } /* @@ -2193,10 +2193,8 @@ MSG_PROCESS_RETURN tls_process_server_hello(SSL_CONNECTION *s, PACKET *pkt) } static MSG_PROCESS_RETURN tls_process_as_hello_retry_request(SSL_CONNECTION *s, - PACKET *extpkt) + RAW_EXTENSION *extensions) { - RAW_EXTENSION *extensions = NULL; - /* * If we were sending early_data then any alerts should not be sent using * the old wrlmethod. @@ -2214,17 +2212,12 @@ static MSG_PROCESS_RETURN tls_process_as_hello_retry_request(SSL_CONNECTION *s, /* We are definitely going to be using TLSv1.3 */ s->rlayer.wrlmethod->set_protocol_version(s->rlayer.wrl, TLS1_3_VERSION); - if (!tls_collect_extensions(s, extpkt, SSL_EXT_TLS1_3_HELLO_RETRY_REQUEST, - &extensions, NULL, 1) - || !tls_parse_all_extensions(s, SSL_EXT_TLS1_3_HELLO_RETRY_REQUEST, - extensions, NULL, 0, 1)) { + if (!tls_parse_all_extensions(s, SSL_EXT_TLS1_3_HELLO_RETRY_REQUEST, + extensions, NULL, 0, 1)) { /* SSLfatal() already called */ goto err; } - OPENSSL_free(extensions); - extensions = NULL; - if (s->ext.tls13_cookie_len == 0 && s->s3.tmp.pkey != NULL) { /* * We didn't receive a cookie or a new key_share so the next @@ -2257,7 +2250,6 @@ static MSG_PROCESS_RETURN tls_process_as_hello_retry_request(SSL_CONNECTION *s, return MSG_PROCESS_FINISHED_READING; err: - OPENSSL_free(extensions); return MSG_PROCESS_ERROR; } @@ -3389,8 +3381,12 @@ int tls_process_initial_server_flight(SSL_CONNECTION *s) #ifndef OPENSSL_NO_ECH /* check result of ech and return error if needed */ - if (!s->server - && s->ext.ech.es != NULL + /* + * TODO(ECH): check that we never get here in a server + * during split-mode or test cases - there used be a + * check of !s->server added to the below. + */ + if (s->ext.ech.es != NULL && s->ext.ech.attempted == 1 && s->ext.ech.success != 1 && s->ext.ech.grease != OSSL_ECH_IS_GREASE) { diff --git a/ssl/tls13_enc.c b/ssl/tls13_enc.c index 489fafd521447..2ff0b9437b73c 100644 --- a/ssl/tls13_enc.c +++ b/ssl/tls13_enc.c @@ -529,10 +529,25 @@ int tls13_change_cipher_state(SSL_CONNECTION *s, int which) labellen = sizeof(client_early_traffic) - 1; log_label = CLIENT_EARLY_LABEL; - handlen = BIO_get_mem_data(s->s3.handshake_buffer, &hdata); - if (handlen <= 0) { - SSLfatal(s, SSL_AD_INTERNAL_ERROR, SSL_R_BAD_HANDSHAKE_LENGTH); - goto err; +#ifndef OPENSSL_NO_ECH + /* if ECH worked then use the innerch and not the h/s buffer here */ + if (((which & SSL3_CC_SERVER) && s->ext.ech.success == 1) + || ((which & SSL3_CC_CLIENT) && s->ext.ech.attempted == 1)) { + if (s->ext.ech.innerch == NULL) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + goto err; + } + handlen = s->ext.ech.innerch_len; + hdata = s->ext.ech.innerch; + } else +#endif + { + handlen = BIO_get_mem_data(s->s3.handshake_buffer, &hdata); + if (handlen <= 0) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, + SSL_R_BAD_HANDSHAKE_LENGTH); + goto err; + } } if (s->early_data_state == SSL_EARLY_DATA_CONNECTING diff --git a/test/ech_test.c b/test/ech_test.c index 743ab90208e32..968fca896d1c2 100644 --- a/test/ech_test.c +++ b/test/ech_test.c @@ -24,6 +24,8 @@ static char *cert = NULL; static char *privkey = NULL; static char *rootcert = NULL; +/* TODO(ECH): add some testing of SSL_OP_ECH_IGNORE_CID */ + /* callback */ static unsigned int test_cb(SSL *s, const char *str) { @@ -887,10 +889,19 @@ static int ech_ingest_test(int run) || !TEST_false(OSSL_ECHSTORE_write_pem(es, 100, out))) goto end; flush_time = time(0); + /* + * Occasionally, flush_time will be 1 more than add_time. We'll + * check for that as that should catch a few more code paths + * in the flush_keys API. + */ if (!TEST_true(OSSL_ECHSTORE_flush_keys(es, flush_time - add_time)) || !TEST_int_eq(OSSL_ECHSTORE_num_keys(es, &keysaftr), 1) - || !TEST_int_eq(keysaftr, 0)) + || ((flush_time <= add_time) && !TEST_int_eq(keysaftr, 0)) + || ((flush_time > add_time) && !TEST_int_eq(keysaftr, 1))) { + TEST_info("Flush time: %lld, add_time: %lld", (long long)flush_time, + (long long)add_time); goto end; + } rv = 1; end: OPENSSL_free(pn); From ed55939b6caff11ef79a5c504157c47065093dc9 Mon Sep 17 00:00:00 2001 From: sftcd Date: Fri, 2 May 2025 12:58:30 +0100 Subject: [PATCH 08/24] ECH client support for sending multiple key shares Reviewed-by: Matt Caswell Reviewed-by: Tomas Mraz (Merged from https://github.com/openssl/openssl/pull/27540) --- include/internal/ssl.h | 5 ++++ ssl/ech/ech_internal.c | 58 +++++++++++++++++++++++++++++++----- ssl/ech/ech_local.h | 9 ++++-- ssl/ssl_local.h | 5 ---- ssl/statem/extensions_clnt.c | 21 +++++-------- 5 files changed, 70 insertions(+), 28 deletions(-) diff --git a/include/internal/ssl.h b/include/internal/ssl.h index 689f3484ff7b8..5b55cd0e3a918 100644 --- a/include/internal/ssl.h +++ b/include/internal/ssl.h @@ -23,4 +23,9 @@ int ossl_ssl_get_error(const SSL *s, int i, int check_err); /* Set if this is our QUIC handshake layer */ # define TLS1_FLAGS_QUIC_INTERNAL 0x4000 +/* We limit the number of key shares sent */ +# ifndef OPENSSL_CLIENT_MAX_KEY_SHARES +# define OPENSSL_CLIENT_MAX_KEY_SHARES 4 +# endif + #endif diff --git a/ssl/ech/ech_internal.c b/ssl/ech/ech_internal.c index ae71656546e32..017872d60ccf4 100644 --- a/ssl/ech/ech_internal.c +++ b/ssl/ech/ech_internal.c @@ -164,6 +164,20 @@ void ossl_ech_ctx_clear(OSSL_ECH_CTX *ce) return; } +static void ech_free_stashed_key_shares(OSSL_ECH_CONN *ec) +{ + size_t i; + + if (ec == NULL) + return; + for (i = 0; i != ec->num_ks_pkey; i++) { + EVP_PKEY_free(ec->ks_pkey[i]); + ec->ks_pkey[i] = NULL; + } + ec->num_ks_pkey = 0; + return; +} + void ossl_ech_conn_clear(OSSL_ECH_CONN *ec) { if (ec == NULL) @@ -179,8 +193,8 @@ void ossl_ech_conn_clear(OSSL_ECH_CONN *ec) OPENSSL_free(ec->returned); OPENSSL_free(ec->pub); OSSL_HPKE_CTX_free(ec->hpke_ctx); - EVP_PKEY_free(ec->tmp_pkey); OPENSSL_free(ec->encoded_inner); + ech_free_stashed_key_shares(ec); return; } @@ -814,15 +828,11 @@ int ossl_ech_swaperoo(SSL_CONNECTION *s) # ifdef OSSL_ECH_SUPERVERBOSE ossl_ech_ptranscript(s, "ech_swaperoo, b4"); # endif - /* un-stash inner key share */ - if (s->ext.ech.tmp_pkey == NULL) { + /* un-stash inner key share(s) */ + if (ossl_ech_unstash_keyshares(s) != 1) { SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); return 0; } - EVP_PKEY_free(s->s3.tmp.pkey); - s->s3.tmp.pkey = s->ext.ech.tmp_pkey; - s->s3.group_id = s->ext.ech.group_id; - s->ext.ech.tmp_pkey = NULL; /* * When not doing HRR... fix up the transcript to reflect the inner CH. * If there's a client hello at the start of the buffer, then that's @@ -1149,4 +1159,38 @@ int ossl_ech_intbuf_fetch(SSL_CONNECTION *s, unsigned char **buf, size_t *blen) *blen = s->ext.ech.transbuf_len; return 1; } + +int ossl_ech_stash_keyshares(SSL_CONNECTION *s) +{ + size_t i; + + ech_free_stashed_key_shares(&s->ext.ech); + for (i = 0; i != s->s3.tmp.num_ks_pkey; i++) { + s->ext.ech.ks_pkey[i] = s->s3.tmp.ks_pkey[i]; + if (EVP_PKEY_up_ref(s->ext.ech.ks_pkey[i]) != 1) + return 0; + s->ext.ech.ks_group_id[i] = s->s3.tmp.ks_group_id[i]; + } + s->ext.ech.num_ks_pkey = s->s3.tmp.num_ks_pkey; + return 1; +} + +int ossl_ech_unstash_keyshares(SSL_CONNECTION *s) +{ + size_t i; + + for (i = 0; i != s->s3.tmp.num_ks_pkey; i++) { + EVP_PKEY_free(s->s3.tmp.ks_pkey[i]); + s->s3.tmp.ks_pkey[i] = NULL; + } + for (i = 0; i != s->ext.ech.num_ks_pkey; i++) { + s->s3.tmp.ks_pkey[i] = s->ext.ech.ks_pkey[i]; + if (EVP_PKEY_up_ref(s->s3.tmp.ks_pkey[i]) != 1) + return 0; + s->s3.tmp.ks_group_id[i] = s->ext.ech.ks_group_id[i]; + } + s->s3.tmp.num_ks_pkey = s->ext.ech.num_ks_pkey; + ech_free_stashed_key_shares(&s->ext.ech); + return 1; +} #endif diff --git a/ssl/ech/ech_local.h b/ssl/ech/ech_local.h index f2ecb4aee2005..6ae0b42648900 100644 --- a/ssl/ech/ech_local.h +++ b/ssl/ech/ech_local.h @@ -58,7 +58,6 @@ * defined in a header file I could find. */ # define CLIENT_VERSION_LEN 2 - # endif /* @@ -219,8 +218,10 @@ typedef struct ossl_ech_conn_st { * keep and swap over IFF ECH has succeeded. Same names chosen as are * used in SSL_CONNECTION */ - EVP_PKEY *tmp_pkey; /* client's key share for inner */ - int group_id; /* key share group */ + EVP_PKEY *ks_pkey[OPENSSL_CLIENT_MAX_KEY_SHARES]; + /* The IDs of the keyshare keys */ + uint16_t ks_group_id[OPENSSL_CLIENT_MAX_KEY_SHARES]; + size_t num_ks_pkey; /* how many keyshares are there */ unsigned char client_random[SSL3_RANDOM_SIZE]; /* CH random */ } OSSL_ECH_CONN; @@ -306,6 +307,8 @@ int ossl_ech_intbuf_add(SSL_CONNECTION *s, const unsigned char *buf, int ossl_ech_intbuf_fetch(SSL_CONNECTION *s, unsigned char **buf, size_t *blen); size_t ossl_ech_calc_padding(SSL_CONNECTION *s, OSSL_ECHSTORE_ENTRY *ee, size_t encoded_len); +int ossl_ech_stash_keyshares(SSL_CONNECTION *s); +int ossl_ech_unstash_keyshares(SSL_CONNECTION *s); # endif #endif diff --git a/ssl/ssl_local.h b/ssl/ssl_local.h index 5cb16d1b7536d..36410ea806a9f 100644 --- a/ssl/ssl_local.h +++ b/ssl/ssl_local.h @@ -792,11 +792,6 @@ typedef struct { # define TLS_GROUP_FFDHE_FOR_TLS1_3 (TLS_GROUP_FFDHE|TLS_GROUP_ONLY_FOR_TLS1_3) -/* We limit the number of key shares sent */ -# ifndef OPENSSL_CLIENT_MAX_KEY_SHARES -# define OPENSSL_CLIENT_MAX_KEY_SHARES 4 -# endif - struct ssl_ctx_st { OSSL_LIB_CTX *libctx; diff --git a/ssl/statem/extensions_clnt.c b/ssl/statem/extensions_clnt.c index 5e5b6321adaca..566ee08dfe640 100644 --- a/ssl/statem/extensions_clnt.c +++ b/ssl/statem/extensions_clnt.c @@ -784,18 +784,6 @@ static int add_key_share(SSL_CONNECTION *s, WPACKET *pkt, unsigned int group_id, goto err; } -# ifndef OPENSSL_NO_ECH - if (s->ext.ech.ch_depth == 1) { /* stash inner */ - if (EVP_PKEY_up_ref(key_share_key) != 1) { - SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); - goto err; - } - EVP_PKEY_free(s->ext.ech.tmp_pkey); - s->ext.ech.tmp_pkey = key_share_key; - s->ext.ech.group_id = group_id; - } -# endif - /* For backward compatibility, we use the first valid group to add a key share */ if (loop_num == 0) { s->s3.tmp.pkey = key_share_key; @@ -808,7 +796,6 @@ static int add_key_share(SSL_CONNECTION *s, WPACKET *pkt, unsigned int group_id, s->s3.tmp.num_ks_pkey++; OPENSSL_free(encoded_pubkey); - return 1; err: if (key_share_key != s->s3.tmp.ks_pkey[loop_num]) @@ -899,6 +886,14 @@ EXT_RETURN tls_construct_ctos_key_share(SSL_CONNECTION *s, WPACKET *pkt, return EXT_RETURN_FAIL; } +# ifndef OPENSSL_NO_ECH + /* stash inner key shares */ + if (s->ext.ech.ch_depth == 1 && ossl_ech_stash_keyshares(s) != 1) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + return EXT_RETURN_FAIL; + } +# endif + if (!WPACKET_close(pkt) || !WPACKET_close(pkt)) { SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); return EXT_RETURN_FAIL; From 86677f61d3e5321b1e5cbaf00dd7b7688335d681 Mon Sep 17 00:00:00 2001 From: Matt Caswell Date: Thu, 5 Jun 2025 14:41:55 +0100 Subject: [PATCH 09/24] Introduce the PACKET_msg_start() function This gives us the start of the buffer in use for the PACKET. We then use this information when calculating the TLS PSK binder. Previously we were assuming knowledge about where the buffer starts. However, with ECH, we may be using a different buffer to normal so it is better to ask the PACKET where the start of the buffer is. Reviewed-by: Tomas Mraz Reviewed-by: Neil Horman (Merged from https://github.com/openssl/openssl/pull/27776) --- include/internal/packet.h | 26 +++++++++++++++++++++++--- ssl/statem/extensions_srvr.c | 7 +++---- ssl/statem/statem.c | 17 +++++++++++++++-- 3 files changed, 41 insertions(+), 9 deletions(-) diff --git a/include/internal/packet.h b/include/internal/packet.h index 2051cabd99d02..cce3bf5c9ff4e 100644 --- a/include/internal/packet.h +++ b/include/internal/packet.h @@ -22,6 +22,8 @@ typedef struct { /* Pointer to where we are currently reading from */ const unsigned char *curr; + /* Pointer to the start of the message */ + const unsigned char *msgstart; /* Number of bytes remaining */ size_t remaining; } PACKET; @@ -52,6 +54,15 @@ static ossl_inline const unsigned char *PACKET_end(const PACKET *pkt) return pkt->curr + pkt->remaining; } +/* + * Returns a pointer to the very start of the buffer. If this is a sub packet + * this will be the start of the buffer for the top of the PACKET tree. + */ +static ossl_inline const unsigned char *PACKET_msg_start(const PACKET *pkt) +{ + return pkt->msgstart; +} + /* * Returns a pointer to the PACKET's current position. * For use in non-PACKETized APIs. @@ -74,7 +85,7 @@ __owur static ossl_inline int PACKET_buf_init(PACKET *pkt, if (len > (size_t)(SIZE_MAX / 2)) return 0; - pkt->curr = buf; + pkt->curr = pkt->msgstart = buf; pkt->remaining = len; return 1; } @@ -82,7 +93,7 @@ __owur static ossl_inline int PACKET_buf_init(PACKET *pkt, /* Initialize a PACKET to hold zero bytes. */ static ossl_inline void PACKET_null_init(PACKET *pkt) { - pkt->curr = NULL; + pkt->curr = pkt->msgstart = NULL; pkt->remaining = 0; } @@ -110,7 +121,11 @@ __owur static ossl_inline int PACKET_peek_sub_packet(const PACKET *pkt, if (PACKET_remaining(pkt) < len) return 0; - return PACKET_buf_init(subpkt, pkt->curr, len); + if (!PACKET_buf_init(subpkt, pkt->curr, len)) + return 0; + + subpkt->msgstart = pkt->msgstart; + return 1; } /* @@ -543,6 +558,7 @@ __owur static ossl_inline int PACKET_get_length_prefixed_1(PACKET *pkt, *pkt = tmp; subpkt->curr = data; + subpkt->msgstart = pkt->msgstart; subpkt->remaining = length; return 1; @@ -566,6 +582,7 @@ __owur static ossl_inline int PACKET_as_length_prefixed_1(PACKET *pkt, *pkt = tmp; subpkt->curr = data; + subpkt->msgstart = pkt->msgstart; subpkt->remaining = length; return 1; @@ -592,6 +609,7 @@ __owur static ossl_inline int PACKET_get_length_prefixed_2(PACKET *pkt, *pkt = tmp; subpkt->curr = data; + subpkt->msgstart = pkt->msgstart; subpkt->remaining = length; return 1; @@ -616,6 +634,7 @@ __owur static ossl_inline int PACKET_as_length_prefixed_2(PACKET *pkt, *pkt = tmp; subpkt->curr = data; + subpkt->msgstart = pkt->msgstart; subpkt->remaining = length; return 1; @@ -641,6 +660,7 @@ __owur static ossl_inline int PACKET_get_length_prefixed_3(PACKET *pkt, *pkt = tmp; subpkt->curr = data; + subpkt->msgstart = pkt->msgstart; subpkt->remaining = length; return 1; diff --git a/ssl/statem/extensions_srvr.c b/ssl/statem/extensions_srvr.c index ac2bddde3b0c7..0fc03e9acca6b 100644 --- a/ssl/statem/extensions_srvr.c +++ b/ssl/statem/extensions_srvr.c @@ -1517,7 +1517,7 @@ int tls_parse_ctos_psk(SSL_CONNECTION *s, PACKET *pkt, unsigned int context, if (sess == NULL) return 1; - binderoffset = PACKET_data(pkt) - (const unsigned char *)s->init_buf->data; + binderoffset = PACKET_data(pkt) - PACKET_msg_start(pkt); hashsize = EVP_MD_get_size(md); if (hashsize <= 0) goto err; @@ -1538,9 +1538,8 @@ int tls_parse_ctos_psk(SSL_CONNECTION *s, PACKET *pkt, unsigned int context, SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_BAD_EXTENSION); goto err; } - if (tls_psk_do_binder(s, md, (const unsigned char *)s->init_buf->data, - binderoffset, PACKET_data(&binder), NULL, sess, 0, - ext) != 1) { + if (tls_psk_do_binder(s, md, PACKET_msg_start(pkt), binderoffset, + PACKET_data(&binder), NULL, sess, 0, ext) != 1) { /* SSLfatal() already called */ goto err; } diff --git a/ssl/statem/statem.c b/ssl/statem/statem.c index 864a2f1a339f0..97df583f5dcde 100644 --- a/ssl/statem/statem.c +++ b/ssl/statem/statem.c @@ -588,7 +588,7 @@ static SUB_STATE_RETURN read_state_machine(SSL_CONNECTION *s) { OSSL_STATEM *st = &s->statem; int ret, mt; - size_t len = 0; + size_t len = 0, headerlen; int (*transition) (SSL_CONNECTION *s, int mt); PACKET pkt; MSG_PROCESS_RETURN(*process_message) (SSL_CONNECTION *s, PACKET *pkt); @@ -682,10 +682,23 @@ static SUB_STATE_RETURN read_state_machine(SSL_CONNECTION *s) } s->first_packet = 0; - if (!PACKET_buf_init(&pkt, s->init_msg, len)) { + /* + * We initialise the buffer including the message header, and + * then skip over header ready to process the message. This + * ensures that calls to PACKET_msg_start() gives us the whole + * message + */ + headerlen = (char *)s->init_msg - s->init_buf->data; + if (!PACKET_buf_init(&pkt, (unsigned char *)s->init_buf->data, + len + headerlen)) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + return SUB_STATE_ERROR; + } + if (!PACKET_forward(&pkt, headerlen)) { SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); return SUB_STATE_ERROR; } + ret = process_message(s, &pkt); /* Discard the packet data */ From 3138079d81a38353e8ebb30843c9b5ed8aa4fcd0 Mon Sep 17 00:00:00 2001 From: Matt Caswell Date: Thu, 5 Jun 2025 15:29:01 +0100 Subject: [PATCH 10/24] Add a test for the new PACKET_msg_start() function Reviewed-by: Tomas Mraz Reviewed-by: Neil Horman (Merged from https://github.com/openssl/openssl/pull/27776) --- test/packettest.c | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/test/packettest.c b/test/packettest.c index 40b68d310a724..3eb0f73f044c7 100644 --- a/test/packettest.c +++ b/test/packettest.c @@ -570,9 +570,39 @@ static int test_PACKET_get_quic_length_prefixed(void) return 1; } - #endif +static int test_PACKET_msg_start(void) +{ + unsigned char buf[16] = { 0 }; + PACKET pkt, subpkt; + + if (!TEST_true(PACKET_buf_init(&pkt, buf, sizeof(buf)))) + return 0; + + if (!TEST_ptr_eq(PACKET_msg_start(&pkt), buf)) + return 0; + + if (!TEST_true(PACKET_forward(&pkt, 1)) + || !TEST_ptr_eq(PACKET_msg_start(&pkt), buf)) + return 0; + + if (!TEST_true(PACKET_get_sub_packet(&pkt, &subpkt, 1)) + || !TEST_ptr_eq(PACKET_msg_start(&subpkt), buf) + || !TEST_ptr_eq(PACKET_msg_start(&pkt), buf)) + return 0; + + if (!TEST_true(PACKET_forward(&subpkt, 1)) + || !TEST_ptr_eq(PACKET_msg_start(&pkt), buf)) + return 0; + + PACKET_null_init(&pkt); + if (!TEST_ptr_null(PACKET_msg_start(&pkt))) + return 0; + + return 1; +} + int setup_tests(void) { unsigned int i; @@ -607,5 +637,6 @@ int setup_tests(void) ADD_TEST(test_PACKET_get_quic_vlint); ADD_TEST(test_PACKET_get_quic_length_prefixed); #endif + ADD_TEST(test_PACKET_msg_start); return 1; } From d18068bfa5630438ad6022cb15709a7f26dcd3bd Mon Sep 17 00:00:00 2001 From: sftcd Date: Mon, 5 May 2025 14:23:55 +0100 Subject: [PATCH 11/24] Add server-side handling of Encrypted Client Hello Reviewed-by: Matt Caswell Reviewed-by: Tomas Mraz (Merged from https://github.com/openssl/openssl/pull/27561) --- doc/man3/SSL_CTX_set_client_hello_cb.pod | 2 + include/internal/ech_helpers.h | 33 + ssl/ech/ech_helper.c | 100 +- ssl/ech/ech_internal.c | 1025 ++++++++++++++++++++- ssl/ech/ech_local.h | 43 + ssl/ech/ech_ssl_apis.c | 2 +- ssl/statem/extensions.c | 9 +- ssl/statem/extensions_clnt.c | 8 +- ssl/statem/extensions_cust.c | 26 +- ssl/statem/extensions_srvr.c | 156 +++- ssl/statem/statem_clnt.c | 2 +- ssl/statem/statem_local.h | 4 +- ssl/statem/statem_srvr.c | 252 ++++- test/ech_test.c | 95 +- util/platform_symbols/windows-symbols.txt | 1 + 15 files changed, 1647 insertions(+), 111 deletions(-) diff --git a/doc/man3/SSL_CTX_set_client_hello_cb.pod b/doc/man3/SSL_CTX_set_client_hello_cb.pod index 6367c68a62502..8f129ce7fa7f1 100644 --- a/doc/man3/SSL_CTX_set_client_hello_cb.pod +++ b/doc/man3/SSL_CTX_set_client_hello_cb.pod @@ -111,6 +111,8 @@ The SSL_client_hello_get0_*() functions return raw ClientHello data, whereas SSL_client_hello_get1_extensions_present() returns only recognized extensions (so unknown/GREASE-extensions are not included). +TODO(ECH): How ECH is handled here needs to be documented. + =head1 RETURN VALUES The application's supplied ClientHello callback returns diff --git a/include/internal/ech_helpers.h b/include/internal/ech_helpers.h index 3e13936e2e9cf..946ad2f1dcff4 100644 --- a/include/internal/ech_helpers.h +++ b/include/internal/ech_helpers.h @@ -17,9 +17,42 @@ # ifndef OPENSSL_NO_ECH +/* + * the max HPKE 'info' we'll process is the max ECHConfig size + * (OSSL_ECH_MAX_ECHCONFIG_LEN) plus OSSL_ECH_CONTEXT_STRING(len=7) + 1 + */ +# define OSSL_ECH_MAX_INFO_LEN (OSSL_ECH_MAX_ECHCONFIG_LEN + 8) + int ossl_ech_make_enc_info(const unsigned char *encoding, size_t encoding_length, unsigned char *info, size_t *info_len); +/* + * Given a CH find the offsets of the session id, extensions and ECH + * ch is the encoded client hello + * ch_len is the length of ch + * sessid_off returns offset of session_id length + * exts_off points to offset of extensions + * exts_len returns length of extensions + * ech_off returns offset of ECH + * echtype returns the ext type of the ECH + * ech_len returns the length of the ECH + * sni_off returns offset of (outer) SNI + * sni_len returns the length of the SNI + * inner 1 if the ECH is marked as an inner, 0 for outer + * return 1 for success, other otherwise + * + * Offsets are set to zero if relevant thing not found. + * Offsets are returned to the type or length field in question. + * + * Note: input here is untrusted! + */ +int ossl_ech_helper_get_ch_offsets(const unsigned char *ch, size_t ch_len, + size_t *sessid_off, size_t *exts_off, + size_t *exts_len, + size_t *ech_off, uint16_t *echtype, + size_t *ech_len, size_t *sni_off, + size_t *sni_len, int *inner); + # endif #endif diff --git a/ssl/ech/ech_helper.c b/ssl/ech/ech_helper.c index 4303a1b6eb082..b2c2a87aa4f8c 100644 --- a/ssl/ech/ech_helper.c +++ b/ssl/ech/ech_helper.c @@ -13,8 +13,6 @@ #include "ech_local.h" #include "internal/ech_helpers.h" -/* TODO(ECH): move more code that's used by internals and test here */ - /* used in ECH crypto derivations (odd format for EBCDIC goodness) */ /* "tls ech" */ static const char OSSL_ECH_CONTEXT_STRING[] = "\x74\x6c\x73\x20\x65\x63\x68"; @@ -52,3 +50,101 @@ int ossl_ech_make_enc_info(const unsigned char *encoding, WPACKET_cleanup(&ipkt); return 1; } + +/* + * Given a CH find the offsets of the session id, extensions and ECH + * ch is the encoded client hello + * ch_len is the length of ch + * sessid_off returns offset of session_id length + * exts_off points to offset of extensions + * exts_len returns length of extensions + * ech_off returns offset of ECH + * echtype returns the ext type of the ECH + * ech_len returns the length of the ECH + * sni_off returns offset of (outer) SNI + * sni_len returns the length of the SNI + * inner 1 if the ECH is marked as an inner, 0 for outer + * return 1 for success, other otherwise + * + * Offsets are set to zero if relevant thing not found. + * Offsets are returned to the type or length field in question. + * + * Note: input here is untrusted! + */ +int ossl_ech_helper_get_ch_offsets(const unsigned char *ch, size_t ch_len, + size_t *sessid_off, size_t *exts_off, + size_t *exts_len, + size_t *ech_off, uint16_t *echtype, + size_t *ech_len, size_t *sni_off, + size_t *sni_len, int *inner) +{ + unsigned int elen = 0, etype = 0, pi_tmp = 0; + const unsigned char *pp_tmp = NULL, *chstart = NULL, *estart = NULL; + PACKET pkt; + int done = 0; + + if (ch == NULL || ch_len == 0 || sessid_off == NULL || exts_off == NULL + || ech_off == NULL || echtype == NULL || ech_len == NULL + || sni_off == NULL || inner == NULL) + return 0; + *sessid_off = *exts_off = *ech_off = *sni_off = *sni_len = *ech_len = 0; + *echtype = 0xffff; + if (!PACKET_buf_init(&pkt, ch, ch_len)) + return 0; + chstart = PACKET_data(&pkt); + if (!PACKET_get_net_2(&pkt, &pi_tmp)) + return 0; + /* if we're not TLSv1.2+ then we can bail, but it's not an error */ + if (pi_tmp != TLS1_2_VERSION && pi_tmp != TLS1_3_VERSION) + return 1; + /* chew up the packet to extensions */ + if (!PACKET_get_bytes(&pkt, &pp_tmp, SSL3_RANDOM_SIZE) + || (*sessid_off = PACKET_data(&pkt) - chstart) == 0 + || !PACKET_get_1(&pkt, &pi_tmp) /* sessid len */ + || !PACKET_get_bytes(&pkt, &pp_tmp, pi_tmp) /* sessid */ + || !PACKET_get_net_2(&pkt, &pi_tmp) /* ciphersuite len */ + || !PACKET_get_bytes(&pkt, &pp_tmp, pi_tmp) /* suites */ + || !PACKET_get_1(&pkt, &pi_tmp) /* compression meths */ + || !PACKET_get_bytes(&pkt, &pp_tmp, pi_tmp) /* comp meths */ + || (*exts_off = PACKET_data(&pkt) - chstart) == 0 + || !PACKET_get_net_2(&pkt, &pi_tmp) /* len(extensions) */ + || (*exts_len = (size_t) pi_tmp) == 0) + /* + * unexpectedly, we return 1 here, as doing otherwise will + * break some non-ECH test code that truncates CH messages + * The same is true below when looking through extensions. + * That's ok though, we'll only set those offsets we've + * found. + */ + return 1; + /* no extensions is theoretically ok, if uninteresting */ + if (*exts_len == 0) + return 1; + /* find what we want from extensions */ + estart = PACKET_data(&pkt); + while (PACKET_remaining(&pkt) > 0 + && (size_t)(PACKET_data(&pkt) - estart) < *exts_len + && done < 2) { + if (!PACKET_get_net_2(&pkt, &etype) + || !PACKET_get_net_2(&pkt, &elen)) + return 1; /* see note above */ + if (etype == TLSEXT_TYPE_ech) { + if (elen == 0) + return 0; + *ech_off = PACKET_data(&pkt) - chstart - 4; + *echtype = etype; + *ech_len = elen; + done++; + } + if (etype == TLSEXT_TYPE_server_name) { + *sni_off = PACKET_data(&pkt) - chstart - 4; + *sni_len = elen; + done++; + } + if (!PACKET_get_bytes(&pkt, &pp_tmp, elen)) + return 1; /* see note above */ + if (etype == TLSEXT_TYPE_ech) + *inner = pp_tmp[0]; + } + return 1; +} diff --git a/ssl/ech/ech_internal.c b/ssl/ech/ech_internal.c index 017872d60ccf4..7fdcf3c5c75b5 100644 --- a/ssl/ech/ech_internal.c +++ b/ssl/ech/ech_internal.c @@ -60,15 +60,11 @@ static void ossl_ech_ptranscript(SSL_CONNECTION *s, const char *msg) ossl_ech_pbuf(msg, hdata, hdatalen); if (s->s3.handshake_dgst != NULL) { if (ssl_handshake_hash(s, ddata, sizeof(ddata), &ddatalen) == 0) { - OSSL_TRACE_BEGIN(TLS) { - BIO_printf(trc_out, "ssl_handshake_hash failed\n"); - } OSSL_TRACE_END(TLS); + OSSL_TRACE(TLS, "ssl_handshake_hash failed\n"); ossl_ech_pbuf(msg, ddata, ddatalen); } } - OSSL_TRACE_BEGIN(TLS) { - BIO_printf(trc_out, "new transbuf:\n"); - } OSSL_TRACE_END(TLS); + OSSL_TRACE(TLS, "new transbuf:\n"); ossl_ech_pbuf(msg, s->ext.ech.transbuf, s->ext.ech.transbuf_len); return; } @@ -1018,7 +1014,7 @@ int ossl_ech_calc_confirm(SSL_CONNECTION *s, int for_hrr, size_t fixedshbuf_len = 0, tlen = 0, chend = 0; /* shoffset is: 4 + 2 + 32 - 8 */ size_t shoffset = SSL3_HM_HEADER_LENGTH + sizeof(uint16_t) - + SSL3_RANDOM_SIZE - OSSL_ECH_SIGNAL_LEN; + + SSL3_RANDOM_SIZE - OSSL_ECH_SIGNAL_LEN; unsigned int hashlen = 0; unsigned char hashval[EVP_MAX_MD_SIZE]; @@ -1100,6 +1096,1019 @@ int ossl_ech_calc_confirm(SSL_CONNECTION *s, int for_hrr, return rv; } +/*! + * Given a CH find the offsets of the session id, extensions and ECH + * pkt is the CH + * sessid_off points to offset of session_id length + * exts_off points to offset of extensions + * ech_off points to offset of ECH + * echtype points to the ext type of the ECH + * inner 1 if the ECH is marked as an inner, 0 for outer + * sni_off points to offset of (outer) SNI + * return 1 for success, other otherwise + * + * Offsets are set to zero if relevant thing not found. + * Offsets are returned to the type or length field in question. + * + * Note: input here is untrusted! + */ +int ossl_ech_get_ch_offsets(SSL_CONNECTION *s, PACKET *pkt, size_t *sessid_off, + size_t *exts_off, size_t *ech_off, uint16_t *echtype, + int *inner, size_t *sni_off) +{ + const unsigned char *ch = NULL; + size_t ch_len = 0, exts_len = 0, sni_len = 0, ech_len = 0; + + if (s == NULL || pkt == NULL || sessid_off == NULL || exts_off == NULL + || ech_off == NULL || echtype == NULL || inner == NULL + || sni_off == NULL) { + SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_BAD_EXTENSION); + return 0; + } + /* check if we've already done the work */ + if (s->ext.ech.ch_offsets_done == 1) { + *sessid_off = s->ext.ech.sessid_off; + *exts_off = s->ext.ech.exts_off; + *ech_off = s->ext.ech.ech_off; + *echtype = s->ext.ech.echtype; + *inner = s->ext.ech.inner; + *sni_off = s->ext.ech.sni_off; + return 1; + } + *sessid_off = 0; + *exts_off = 0; + *ech_off = 0; + *echtype = OSSL_ECH_type_unknown; + *sni_off = 0; + /* do the work */ + ch_len = PACKET_remaining(pkt); + if (PACKET_peek_bytes(pkt, &ch, ch_len) != 1) { + SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_BAD_EXTENSION); + return 0; + } + if (ossl_ech_helper_get_ch_offsets(ch, ch_len, sessid_off, exts_off, + &exts_len, ech_off, echtype, &ech_len, + sni_off, &sni_len, inner) != 1) { + SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_BAD_EXTENSION); + return 0; + } +# ifdef OSSL_ECH_SUPERVERBOSE + OSSL_TRACE_BEGIN(TLS) { + BIO_printf(trc_out, "orig CH/ECH type: %4x\n", *echtype); + } OSSL_TRACE_END(TLS); + ossl_ech_pbuf("orig CH", (unsigned char *)ch, ch_len); + ossl_ech_pbuf("orig CH exts", (unsigned char *)ch + *exts_off, exts_len); + ossl_ech_pbuf("orig CH/ECH", (unsigned char *)ch + *ech_off, ech_len); + ossl_ech_pbuf("orig CH SNI", (unsigned char *)ch + *sni_off, sni_len); +# endif + s->ext.ech.sessid_off = *sessid_off; + s->ext.ech.exts_off = *exts_off; + s->ext.ech.ech_off = *ech_off; + s->ext.ech.echtype = *echtype; + s->ext.ech.inner = *inner; + s->ext.ech.sni_off = *sni_off; + s->ext.ech.ch_offsets_done = 1; + return 1; +} + +static void ossl_ech_encch_free(OSSL_ECH_ENCCH *tbf) +{ + if (tbf == NULL) + return; + OPENSSL_free(tbf->enc); + OPENSSL_free(tbf->payload); + return; +} + +/* + * decode outer sni value so we can trace it + * osni_str is the string-form of the SNI + * opd is the outer CH buffer + * opl is the length of the above + * snioffset is where we find the outer SNI + * + * The caller doesn't have to free the osni_str. + */ +static int ech_get_outer_sni(SSL_CONNECTION *s, char **osni_str, + const unsigned char *opd, size_t opl, + size_t snioffset) +{ + PACKET wrap, osni; + unsigned int type, osnilen; + + if (snioffset >= opl + || !PACKET_buf_init(&wrap, opd + snioffset, opl - snioffset) + || !PACKET_get_net_2(&wrap, &type) + || type != 0 + || !PACKET_get_net_2(&wrap, &osnilen) + || !PACKET_get_sub_packet(&wrap, &osni, osnilen) + || tls_parse_ctos_server_name(s, &osni, 0, NULL, 0) != 1) + return 0; + OPENSSL_free(s->ext.ech.outer_hostname); + *osni_str = s->ext.ech.outer_hostname = s->ext.hostname; + /* clean up what the ECH-unaware parse func above left behind */ + s->ext.hostname = NULL; + s->servername_done = 0; + return 1; +} + +/* + * decode EncryptedClientHello extension value + * pkt contains the ECH value as a PACKET + * retext is the returned decoded structure + * payload_offset is the offset to the ciphertext + * return 1 for good, 0 for bad + * + * SSLfatal called from inside, as needed + */ +static int ech_decode_inbound_ech(SSL_CONNECTION *s, PACKET *pkt, + OSSL_ECH_ENCCH **retext, + size_t *payload_offset) +{ + unsigned int innerorouter = 0xff; + unsigned int pval_tmp; /* tmp placeholder of value from packet */ + OSSL_ECH_ENCCH *extval = NULL; + const unsigned char *startofech = NULL; + + /* + * Decode the inbound ECH value. + * enum { outer(0), inner(1) } ECHClientHelloType; + * struct { + * ECHClientHelloType type; + * select (ECHClientHello.type) { + * case outer: + * HpkeSymmetricCipherSuite cipher_suite; + * uint8 config_id; + * opaque enc<0..2^16-1>; + * opaque payload<1..2^16-1>; + * case inner: + * Empty; + * }; + * } ECHClientHello; + */ + startofech = PACKET_data(pkt); + extval = OPENSSL_zalloc(sizeof(OSSL_ECH_ENCCH)); + if (extval == NULL) + goto err; + if (!PACKET_get_1(pkt, &innerorouter)) { + SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_BAD_EXTENSION); + goto err; + } + if (innerorouter != OSSL_ECH_OUTER_CH_TYPE) { + SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_BAD_EXTENSION); + goto err; + } + if (!PACKET_get_net_2(pkt, &pval_tmp)) { + SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_BAD_EXTENSION); + goto err; + } + extval->kdf_id = pval_tmp & 0xffff; + if (!PACKET_get_net_2(pkt, &pval_tmp)) { + SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_BAD_EXTENSION); + goto err; + } + extval->aead_id = pval_tmp & 0xffff; + /* config id */ + if (!PACKET_copy_bytes(pkt, &extval->config_id, 1)) { + SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_BAD_EXTENSION); + goto err; + } +# ifdef OSSL_ECH_SUPERVERBOSE + ossl_ech_pbuf("EARLY config id", &extval->config_id, 1); +# endif + s->ext.ech.attempted_cid = extval->config_id; + /* enc - the client's public share */ + if (!PACKET_get_net_2(pkt, &pval_tmp)) { + SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_BAD_EXTENSION); + goto err; + } + if (pval_tmp > OSSL_ECH_MAX_GREASE_PUB) { + SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_BAD_EXTENSION); + goto err; + } + if (pval_tmp > PACKET_remaining(pkt)) { + SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_BAD_EXTENSION); + goto err; + } + if (pval_tmp == 0 && s->hello_retry_request != SSL_HRR_PENDING) { + SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_BAD_EXTENSION); + goto err; + } else if (pval_tmp > 0 && s->hello_retry_request == SSL_HRR_PENDING) { + unsigned char *tmpenc = NULL; + + /* + * if doing HRR, client should only send this when GREASEing + * and it should be the same value as 1st time, so we'll check + * that + */ + if (s->ext.ech.pub == NULL || s->ext.ech.pub_len == 0) { + SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_BAD_EXTENSION); + goto err; + } + if (pval_tmp != s->ext.ech.pub_len) { + SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_BAD_EXTENSION); + goto err; + } + tmpenc = OPENSSL_malloc(pval_tmp); + if (tmpenc == NULL) + goto err; + if (!PACKET_copy_bytes(pkt, tmpenc, pval_tmp)) { + SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_BAD_EXTENSION); + goto err; + } + if (memcmp(tmpenc, s->ext.ech.pub, pval_tmp) != 0) { + OPENSSL_free(tmpenc); + SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_BAD_EXTENSION); + goto err; + } + OPENSSL_free(tmpenc); + } else if (pval_tmp == 0 && s->hello_retry_request == SSL_HRR_PENDING) { + if (s->ext.ech.pub == NULL || s->ext.ech.pub_len == 0) { + SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_BAD_EXTENSION); + goto err; + } + extval->enc_len = s->ext.ech.pub_len; + extval->enc = OPENSSL_malloc(extval->enc_len); + if (extval->enc == NULL) + goto err; + memcpy(extval->enc, s->ext.ech.pub, extval->enc_len); + } else { + extval->enc_len = pval_tmp; + extval->enc = OPENSSL_malloc(pval_tmp); + if (extval->enc == NULL) + goto err; + if (!PACKET_copy_bytes(pkt, extval->enc, pval_tmp)) { + SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_BAD_EXTENSION); + goto err; + } + /* squirrel away that value in case of future HRR */ + OPENSSL_free(s->ext.ech.pub); + s->ext.ech.pub_len = extval->enc_len; + s->ext.ech.pub = OPENSSL_malloc(extval->enc_len); + if (s->ext.ech.pub == NULL) + goto err; + memcpy(s->ext.ech.pub, extval->enc, extval->enc_len); + } + /* payload - the encrypted CH */ + *payload_offset = PACKET_data(pkt) - startofech; + if (!PACKET_get_net_2(pkt, &pval_tmp)) { + SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_BAD_EXTENSION); + goto err; + } + if (pval_tmp > OSSL_ECH_MAX_PAYLOAD_LEN) { + SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_BAD_EXTENSION); + goto err; + } + if (pval_tmp > PACKET_remaining(pkt)) { + SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_BAD_EXTENSION); + goto err; + } + extval->payload_len = pval_tmp; + extval->payload = OPENSSL_malloc(pval_tmp); + if (extval->payload == NULL) + goto err; + if (!PACKET_copy_bytes(pkt, extval->payload, pval_tmp)) { + SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_BAD_EXTENSION); + goto err; + } + *retext = extval; + return 1; +err: + if (extval != NULL) { + ossl_ech_encch_free(extval); + OPENSSL_free(extval); + extval = NULL; + } + return 0; +} + +/* + * find outers if any, and do initial checks + * pkt is the encoded inner + * outers is the array of outer ext types + * n_outers is the number of outers found + * return 1 for good, 0 for error + * + * recall we're dealing with recovered ECH plaintext here so + * the content must be a TLSv1.3 ECH encoded inner + */ +static int ech_find_outers(SSL_CONNECTION *s, PACKET *pkt, + uint16_t *outers, size_t *n_outers) +{ + const unsigned char *pp_tmp; + unsigned int pi_tmp, extlens, etype, elen, olen; + int outers_found = 0; + size_t i; + PACKET op; + + PACKET_null_init(&op); + /* chew up the packet to extensions */ + if (!PACKET_get_net_2(pkt, &pi_tmp) + || pi_tmp != TLS1_2_VERSION + || !PACKET_get_bytes(pkt, &pp_tmp, SSL3_RANDOM_SIZE) + || !PACKET_get_1(pkt, &pi_tmp) + || pi_tmp != 0x00 /* zero'd session id */ + || !PACKET_get_net_2(pkt, &pi_tmp) /* ciphersuite len */ + || !PACKET_get_bytes(pkt, &pp_tmp, pi_tmp) /* suites */ + || !PACKET_get_1(pkt, &pi_tmp) /* compression meths */ + || pi_tmp != 0x01 /* 1 octet of comressions */ + || !PACKET_get_1(pkt, &pi_tmp) /* compression meths */ + || pi_tmp != 0x00 /* 1 octet of no comressions */ + || !PACKET_get_net_2(pkt, &extlens) /* len(extensions) */ + || extlens == 0) { /* no extensions! */ + SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_BAD_EXTENSION); + goto err; + } + while (PACKET_remaining(pkt) > 0 && outers_found == 0) { + if (!PACKET_get_net_2(pkt, &etype)) { + SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_BAD_EXTENSION); + goto err; + } + if (etype == TLSEXT_TYPE_outer_extensions) { + outers_found = 1; + if (!PACKET_get_length_prefixed_2(pkt, &op)) { + SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_BAD_EXTENSION); + goto err; + } + } else { /* skip over */ + if (!PACKET_get_net_2(pkt, &elen) + || !PACKET_get_bytes(pkt, &pp_tmp, elen)) { + SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_BAD_EXTENSION); + goto err; + } + } + } + + if (outers_found == 0) { /* which is fine! */ + *n_outers = 0; + return 1; + } + /* + * outers has a silly internal length as well and that better + * be one less than the extension length and an even number + * and we only support a certain max of outers + */ + if (!PACKET_get_1(&op, &olen) + || olen % 2 == 1 + || olen / 2 > OSSL_ECH_OUTERS_MAX) { + SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_BAD_EXTENSION); + goto err; + } + *n_outers = olen / 2; + for (i = 0; i != *n_outers; i++) { + if (!PACKET_get_net_2(&op, &pi_tmp) + || pi_tmp == TLSEXT_TYPE_outer_extensions) { + SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_BAD_EXTENSION); + goto err; + } + outers[i] = (uint16_t) pi_tmp; + } + return 1; +err: + return 0; +} + +/* + * copy one extension from outer to inner + * di is the reconstituted inner CH + * type2copy is the outer type to copy + * extsbuf is the outer extensions buffer + * extslen is the outer extensions buffer length + * return 1 for good 0 for error + */ +static int ech_copy_ext(SSL_CONNECTION *s, WPACKET *di, uint16_t type2copy, + const unsigned char *extsbuf, size_t extslen) +{ + PACKET exts; + unsigned int etype, elen; + const unsigned char *eval; + + if (PACKET_buf_init(&exts, extsbuf, extslen) != 1) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + goto err; + } + while (PACKET_remaining(&exts) > 0) { + if (!PACKET_get_net_2(&exts, &etype) + || !PACKET_get_net_2(&exts, &elen) + || !PACKET_get_bytes(&exts, &eval, elen)) { + SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_BAD_EXTENSION); + goto err; + } + if (etype == type2copy) { + if (!WPACKET_put_bytes_u16(di, etype) + || !WPACKET_put_bytes_u16(di, elen) + || !WPACKET_memcpy(di, eval, elen)) { + SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_BAD_EXTENSION); + goto err; + } + return 1; + } + } + /* we didn't find such an extension - that's an error */ + SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_BAD_EXTENSION); +err: + return 0; +} + +/* + * reconstitute the inner CH from encoded inner and outers + * di is the reconstituted inner CH + * ei is the encoded inner + * ob is the outer CH as a buffer + * ob_len is the size of the above + * outers is the array of outer ext types + * n_outers is the number of outers found + * return 1 for good, 0 for error + */ +static int ech_reconstitute_inner(SSL_CONNECTION *s, WPACKET *di, PACKET *ei, + const unsigned char *ob, size_t ob_len, + uint16_t *outers, size_t n_outers) +{ + const unsigned char *pp_tmp, *eval, *outer_exts; + unsigned int pi_tmp, etype, elen, outer_extslen; + PACKET outer, session_id; + size_t i; + + if (PACKET_buf_init(&outer, ob, ob_len) != 1) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + goto err; + } + /* read/write from encoded inner to decoded inner with help from outer */ + if (/* version */ + !PACKET_get_net_2(&outer, &pi_tmp) + || !PACKET_get_net_2(ei, &pi_tmp) + || !WPACKET_put_bytes_u16(di, pi_tmp) + + /* client random */ + || !PACKET_get_bytes(&outer, &pp_tmp, SSL3_RANDOM_SIZE) + || !PACKET_get_bytes(ei, &pp_tmp, SSL3_RANDOM_SIZE) + || !WPACKET_memcpy(di, pp_tmp, SSL3_RANDOM_SIZE) + + /* session ID */ + || !PACKET_get_1(ei, &pi_tmp) + || !PACKET_get_length_prefixed_1(&outer, &session_id) + || !WPACKET_start_sub_packet_u8(di) + || (PACKET_remaining(&session_id) != 0 + && !WPACKET_memcpy(di, PACKET_data(&session_id), + PACKET_remaining(&session_id))) + || !WPACKET_close(di) + + /* ciphersuites */ + || !PACKET_get_net_2(&outer, &pi_tmp) /* ciphersuite len */ + || !PACKET_get_bytes(&outer, &pp_tmp, pi_tmp) /* suites */ + || !PACKET_get_net_2(ei, &pi_tmp) /* ciphersuite len */ + || !PACKET_get_bytes(ei, &pp_tmp, pi_tmp) /* suites */ + || !WPACKET_put_bytes_u16(di, pi_tmp) + || !WPACKET_memcpy(di, pp_tmp, pi_tmp) + + /* compression len & meth */ + || !PACKET_get_net_2(ei, &pi_tmp) + || !PACKET_get_net_2(&outer, &pi_tmp) + || !WPACKET_put_bytes_u16(di, pi_tmp)) { + SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_BAD_EXTENSION); + goto err; + } + /* handle simple, but unlikely, case first */ + if (n_outers == 0) { + if (PACKET_remaining(ei) == 0) + return 1; /* no exts is theoretically possible */ + if (!PACKET_get_net_2(ei, &pi_tmp) /* len(extensions) */ + || !PACKET_get_bytes(ei, &pp_tmp, pi_tmp) + || !WPACKET_put_bytes_u16(di, pi_tmp) + || !WPACKET_memcpy(di, pp_tmp, pi_tmp)) { + SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_BAD_EXTENSION); + goto err; + } + WPACKET_close(di); + return 1; + } + /* + * general case, copy one by one from inner, 'till we hit + * the outers extension, then copy one by one from outer + */ + if (!PACKET_get_net_2(ei, &pi_tmp) /* len(extensions) */ + || !PACKET_get_net_2(&outer, &outer_extslen) + || !PACKET_get_bytes(&outer, &outer_exts, outer_extslen) + || !WPACKET_start_sub_packet_u16(di)) { + SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_BAD_EXTENSION); + goto err; + } + while (PACKET_remaining(ei) > 0) { + if (!PACKET_get_net_2(ei, &etype) + || !PACKET_get_net_2(ei, &elen) + || !PACKET_get_bytes(ei, &eval, elen)) { + SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_BAD_EXTENSION); + goto err; + } + if (etype == TLSEXT_TYPE_outer_extensions) { + for (i = 0; i != n_outers; i++) { + if (ech_copy_ext(s, di, outers[i], + outer_exts, outer_extslen) != 1) + /* SSLfatal called already */ + goto err; + } + } else { + if (!WPACKET_put_bytes_u16(di, etype) + || !WPACKET_put_bytes_u16(di, elen) + || !WPACKET_memcpy(di, eval, elen)) { + SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_BAD_EXTENSION); + goto err; + } + } + } + WPACKET_close(di); + return 1; +err: + WPACKET_cleanup(di); + return 0; +} + +/* + * After successful ECH decrypt, we decode, decompress etc. + * ob is the outer CH as a buffer + * ob_len is the size of the above + * return 1 for success, error otherwise + * + * We need the outer CH as a buffer (ob, below) so we can + * ECH-decompress. + * The plaintext we start from is in encoded_innerch + * and our final decoded, decompressed buffer will end up + * in innerch (which'll then be further processed). + * That further processing includes all existing decoding + * checks so we should be fine wrt fuzzing without having + * to make all checks here (e.g. we can assume that the + * protocol version, NULL compression etc are correct here - + * if not, those'll be caught later). + * Note: there are a lot of literal values here, but it's + * not clear that changing those to #define'd symbols will + * help much - a change to the length of a type or from a + * 2 octet length to longer would seem unlikely. + */ +static int ech_decode_inner(SSL_CONNECTION *s, const unsigned char *ob, + size_t ob_len, unsigned char *encoded_inner, + size_t encoded_inner_len) +{ + int rv = 0; + PACKET ei; /* encoded inner */ + BUF_MEM *di_mem = NULL; + uint16_t outers[OSSL_ECH_OUTERS_MAX]; /* compressed extension types */ + size_t n_outers = 0; + WPACKET di; + + if (encoded_inner == NULL || ob == NULL || ob_len == 0) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + return 0; + } + if ((di_mem = BUF_MEM_new()) == NULL + || !BUF_MEM_grow(di_mem, SSL3_RT_MAX_PLAIN_LENGTH) + || !WPACKET_init(&di, di_mem) + || !WPACKET_put_bytes_u8(&di, SSL3_MT_CLIENT_HELLO) + || !WPACKET_start_sub_packet_u24(&di) + || !PACKET_buf_init(&ei, encoded_inner, encoded_inner_len)) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + goto err; + } +# ifdef OSSL_ECH_SUPERVERBOSE + memset(outers, -1, sizeof(outers)); /* fill with known values for debug */ +# endif + + /* 1. check for outers and make inital checks of those */ + if (ech_find_outers(s, &ei, outers, &n_outers) != 1) + goto err; /* SSLfatal called already */ + + /* 2. reconstitute inner CH */ + /* reset ei */ + if (PACKET_buf_init(&ei, encoded_inner, encoded_inner_len) != 1) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + goto err; + } + if (ech_reconstitute_inner(s, &di, &ei, ob, ob_len, outers, n_outers) != 1) + goto err; /* SSLfatal called already */ + /* 3. store final inner CH in connection */ + WPACKET_close(&di); + if (!WPACKET_get_length(&di, &s->ext.ech.innerch_len)) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + goto err; + } + OPENSSL_free(s->ext.ech.innerch); + s->ext.ech.innerch = (unsigned char *)di_mem->data; + di_mem->data = NULL; + rv = 1; +err: + WPACKET_cleanup(&di); + BUF_MEM_free(di_mem); + return rv; +} + +/* + * wrapper for hpke_dec just to save code repetition + * ee is the selected ECH_STORE entry + * the_ech is the value sent by the client + * aad_len is the length of the AAD to use + * aad is the AAD to use + * forhrr is 0 if not hrr, 1 if this is for 2nd CH + * innerlen points to the size of the recovered plaintext + * return pointer to plaintext or NULL (if error) + * + * The plaintext returned is allocated here and must + * be freed by the caller later. + */ +static unsigned char *hpke_decrypt_encch(SSL_CONNECTION *s, + OSSL_ECHSTORE_ENTRY *ee, + OSSL_ECH_ENCCH *the_ech, + size_t aad_len, unsigned char *aad, + int forhrr, size_t *innerlen) +{ + size_t cipherlen = 0; + unsigned char *cipher = NULL; + size_t senderpublen = 0; + unsigned char *senderpub = NULL; + size_t clearlen = 0; + unsigned char *clear = NULL; + int hpke_mode = OSSL_HPKE_MODE_BASE; + OSSL_HPKE_SUITE hpke_suite = OSSL_HPKE_SUITE_DEFAULT; + unsigned char info[OSSL_ECH_MAX_INFO_LEN]; + size_t info_len = OSSL_ECH_MAX_INFO_LEN; + int rv = 0; + OSSL_HPKE_CTX *hctx = NULL; +# ifdef OSSL_ECH_SUPERVERBOSE + size_t publen = 0; + unsigned char *pub = NULL; +# endif + + if (ee == NULL || ee->nsuites == 0) + return NULL; + cipherlen = the_ech->payload_len; + cipher = the_ech->payload; + senderpublen = the_ech->enc_len; + senderpub = the_ech->enc; + hpke_suite.aead_id = the_ech->aead_id; + hpke_suite.kdf_id = the_ech->kdf_id; + clearlen = cipherlen; /* small overestimate */ + clear = OPENSSL_malloc(clearlen); + if (clear == NULL) + return NULL; + /* The kem_id will be the same for all suites in the entry */ + hpke_suite.kem_id = ee->suites[0].kem_id; +# ifdef OSSL_ECH_SUPERVERBOSE + publen = ee->pub_len; + pub = ee->pub; + ossl_ech_pbuf("aad", aad, aad_len); + ossl_ech_pbuf("my local pub", pub, publen); + ossl_ech_pbuf("senderpub", senderpub, senderpublen); + ossl_ech_pbuf("cipher", cipher, cipherlen); +# endif + if (ossl_ech_make_enc_info(ee->encoded, ee->encoded_len, + info, &info_len) != 1) { + OPENSSL_free(clear); + return NULL; + } +# ifdef OSSL_ECH_SUPERVERBOSE + ossl_ech_pbuf("info", info, info_len); +# endif + OSSL_TRACE_BEGIN(TLS) { + BIO_printf(trc_out, + "hpke_dec suite: kem: %04x, kdf: %04x, aead: %04x\n", + hpke_suite.kem_id, hpke_suite.kdf_id, hpke_suite.aead_id); + } OSSL_TRACE_END(TLS); + /* + * We may generate externally visible OpenSSL errors + * if decryption fails (which is normal) but we'll + * ignore those as we might be dealing with a GREASEd + * ECH. To do that we need to now ignore some errors + * so we use ERR_set_mark() then later ERR_pop_to_mark(). + */ + ERR_set_mark(); + /* Use OSSL_HPKE_* APIs */ + hctx = OSSL_HPKE_CTX_new(hpke_mode, hpke_suite, OSSL_HPKE_ROLE_RECEIVER, + NULL, NULL); + if (hctx == NULL) + goto clearerrs; + rv = OSSL_HPKE_decap(hctx, senderpub, senderpublen, ee->keyshare, + info, info_len); + if (rv != 1) + goto clearerrs; + if (forhrr == 1) { + rv = OSSL_HPKE_CTX_set_seq(hctx, 1); + if (rv != 1) { + /* don't clear this error - GREASE can't cause it */ + ERR_clear_last_mark(); + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + goto end; + } + } + rv = OSSL_HPKE_open(hctx, clear, &clearlen, aad, aad_len, + cipher, cipherlen); +clearerrs: + /* close off our error handling */ + ERR_pop_to_mark(); +end: + OSSL_HPKE_CTX_free(hctx); + if (rv != 1) { + OSSL_TRACE(TLS, "HPKE decryption failed somehow\n"); + OPENSSL_free(clear); + return NULL; + } +# ifdef OSSL_ECH_SUPERVERBOSE + ossl_ech_pbuf("padded clear", clear, clearlen); +# endif + /* we need to remove possible (actually, v. likely) padding */ + *innerlen = clearlen; + if (ee->version == OSSL_ECH_RFCXXXX_VERSION) { + /* draft-13 pads after the encoded CH with zeros */ + size_t extsoffset = 0; + size_t extslen = 0; + size_t ch_len = 0; + size_t startofsessid = 0; + size_t echoffset = 0; /* offset of start of ECH within CH */ + uint16_t echtype = OSSL_ECH_type_unknown; /* type of ECH seen */ + size_t outersnioffset = 0; /* offset to SNI in outer */ + int innerflag = -1; + PACKET innerchpkt; + + if (PACKET_buf_init(&innerchpkt, clear, clearlen) != 1) { + SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_BAD_EXTENSION); + goto paderr; + } + /* reset the offsets, as we move from outer to inner CH */ + s->ext.ech.ch_offsets_done = 0; + rv = ossl_ech_get_ch_offsets(s, &innerchpkt, &startofsessid, + &extsoffset, &echoffset, &echtype, + &innerflag, &outersnioffset); + if (rv != 1) { + SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_BAD_EXTENSION); + goto paderr; + } + /* odd form of check below just for emphasis */ + if ((extsoffset + 1) > clearlen) { + SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_BAD_EXTENSION); + goto paderr; + } + extslen = (unsigned char)(clear[extsoffset]) * 256 + + (unsigned char)(clear[extsoffset + 1]); + ch_len = extsoffset + 2 + extslen; + /* the check below protects us from bogus data */ + if (ch_len > clearlen) { + SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_BAD_EXTENSION); + goto paderr; + } + /* + * The RFC calls for that padding to be all zeros. I'm not so + * keen on that being a good idea to enforce, so we'll make it + * easy to not do so (but check by default) + */ +# define CHECKZEROS +# ifdef CHECKZEROS + { + size_t zind = 0; + + if (*innerlen < ch_len) + goto paderr; + for (zind = ch_len; zind != *innerlen; zind++) { + if (clear[zind] != 0x00) + goto paderr; + } + } +# endif + *innerlen = ch_len; +# ifdef OSSL_ECH_SUPERVERBOSE + ossl_ech_pbuf("unpadded clear", clear, *innerlen); +# endif + return clear; + } +paderr: + OPENSSL_free(clear); + return NULL; +} + +/* + * If an ECH is present, attempt decryption + * outerpkt is the packet with the outer CH + * newpkt is the packet with the decrypted inner CH + * return 1 for success, other otherwise + * + * If decryption succeeds, the caller can swap the inner and outer + * CHs so that all further processing will only take into account + * the inner CH. + * + * The fact that decryption worked is signalled to the caller + * via s->ext.ech.success + * + * This function is called early, (hence the name:-), before + * the outer CH decoding has really started, so we need to be + * careful peeking into the packet + * + * The plan: + * 1. check if there's an ECH + * 2. trial-decrypt or check if config matches one loaded + * 3. if decrypt fails tee-up GREASE + * 4. if decrypt worked, decode and de-compress cleartext to + * make up real inner CH for later processing + */ +int ossl_ech_early_decrypt(SSL_CONNECTION *s, PACKET *outerpkt, PACKET *newpkt) +{ + int num = 0, cfgind = -1, foundcfg = 0, forhrr = 0, innerflag = -1; + OSSL_ECH_ENCCH *extval = NULL; + PACKET echpkt; + const unsigned char *startofech = NULL, *opd = NULL; + size_t echlen = 0, clearlen = 0, aad_len = 0; + unsigned char *clear = NULL, *aad = NULL; + /* offsets of things within CH */ + size_t startofsessid = 0, startofexts = 0, echoffset = 0, opl = 0; + size_t outersnioffset = 0, startofciphertext = 0, lenofciphertext = 0; + uint16_t echtype = OSSL_ECH_type_unknown; /* type of ECH seen */ + char *osni_str = NULL; + OSSL_ECHSTORE *es = NULL; + OSSL_ECHSTORE_ENTRY *ee = NULL; + + if (s == NULL) + return 0; + if (outerpkt == NULL || newpkt == NULL) { + SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_BAD_EXTENSION); + return 0; + } + /* find offsets - on success, outputs are safe to use */ + if (ossl_ech_get_ch_offsets(s, outerpkt, &startofsessid, &startofexts, + &echoffset, &echtype, &innerflag, + &outersnioffset) != 1) { + SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_BAD_EXTENSION); + return 0; + } + if (echoffset == 0 || echtype != TLSEXT_TYPE_ech) + return 1; /* ECH not present or wrong version */ + if (innerflag == 1) { + SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_BAD_EXTENSION); + return 0; + } + s->ext.ech.attempted = 1; /* Remember that we got an ECH */ + s->ext.ech.attempted_type = echtype; + if (s->hello_retry_request == SSL_HRR_PENDING) + forhrr = 1; /* set forhrr if that's correct */ + opl = PACKET_remaining(outerpkt); + opd = PACKET_data(outerpkt); + s->tmp_session_id_len = opd[startofsessid]; /* grab the session id */ + if (s->tmp_session_id_len > SSL_MAX_SSL_SESSION_ID_LENGTH + || startofsessid + 1 + s->tmp_session_id_len > opl) { + SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_BAD_EXTENSION); + goto err; + } + memcpy(s->tmp_session_id, &opd[startofsessid + 1], s->tmp_session_id_len); + if (outersnioffset > 0) { /* Grab the outer SNI for tracing */ + if (ech_get_outer_sni(s, &osni_str, opd, opl, outersnioffset) != 1 + || osni_str == NULL) { + SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_BAD_EXTENSION); + goto err; + } + OSSL_TRACE1(TLS, "EARLY: outer SNI of %s\n", osni_str); + } else { + OSSL_TRACE(TLS, "EARLY: no sign of an outer SNI\n"); + } + if (echoffset > opl - 4) { + SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_BAD_EXTENSION); + goto err; + } + startofech = &opd[echoffset + 4]; + echlen = opd[echoffset + 2] * 256 + opd[echoffset + 3]; + if (echlen > opl - echoffset - 4) { + SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_BAD_EXTENSION); + goto err; + } + if (PACKET_buf_init(&echpkt, startofech, echlen) != 1) { + SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_BAD_EXTENSION); + goto err; + } + if (ech_decode_inbound_ech(s, &echpkt, &extval, &startofciphertext) != 1) + goto err; /* SSLfatal already called if needed */ + /* + * startofciphertext is within the ECH value and after the length of the + * ciphertext, so we need to bump it by the offset of ECH within the CH + * plus the ECH type (2 octets) and length (also 2 octets) and that + * ciphertext length (another 2 octets) for a total of 6 octets + */ + startofciphertext += echoffset + 6; + lenofciphertext = extval->payload_len; + aad_len = opl; + if (aad_len < startofciphertext + lenofciphertext) { + SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_BAD_EXTENSION); + goto err; + } + aad = OPENSSL_memdup(opd, aad_len); + if (aad == NULL) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + goto err; + } + memset(aad + startofciphertext, 0, lenofciphertext); +# ifdef OSSL_ECH_SUPERVERBOSE + ossl_ech_pbuf("EARLY aad", aad, aad_len); +# endif + s->ext.ech.grease = OSSL_ECH_GREASE_UNKNOWN; + if (s->ext.ech.es == NULL) { + SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_BAD_EXTENSION); + goto err; + } + es = s->ext.ech.es; + num = (es == NULL || es->entries == NULL ? 0 + : sk_OSSL_ECHSTORE_ENTRY_num(es->entries)); + for (cfgind = 0; cfgind != num; cfgind++) { + ee = sk_OSSL_ECHSTORE_ENTRY_value(es->entries, cfgind); + OSSL_TRACE_BEGIN(TLS) { + BIO_printf(trc_out, + "EARLY: rx'd config id (%x) ==? %d-th configured (%x)\n", + extval->config_id, cfgind, ee->config_id); + } OSSL_TRACE_END(TLS); + if (extval->config_id == ee->config_id) { + foundcfg = 1; + break; + } + } + if (foundcfg == 1) { + clear = hpke_decrypt_encch(s, ee, extval, aad_len, aad, + forhrr, &clearlen); + if (clear == NULL) + s->ext.ech.grease = OSSL_ECH_IS_GREASE; + } + /* if still needed, trial decryptions */ + if (clear == NULL && (s->options & SSL_OP_ECH_TRIALDECRYPT)) { + foundcfg = 0; /* reset as we're trying again */ + for (cfgind = 0; cfgind != num; cfgind++) { + ee = sk_OSSL_ECHSTORE_ENTRY_value(es->entries, cfgind); + clear = hpke_decrypt_encch(s, ee, extval, + aad_len, aad, forhrr, &clearlen); + if (clear != NULL) { + foundcfg = 1; + s->ext.ech.grease = OSSL_ECH_NOT_GREASE; + break; + } + } + } + OPENSSL_free(aad); + aad = NULL; + s->ext.ech.done = 1; /* decrypting worked or not, but we're done now */ + /* 3. if decrypt fails tee-up GREASE */ + s->ext.ech.grease = OSSL_ECH_IS_GREASE; + s->ext.ech.success = 0; + if (clear != NULL) { + s->ext.ech.grease = OSSL_ECH_NOT_GREASE; + s->ext.ech.success = 1; + } + OSSL_TRACE_BEGIN(TLS) { + BIO_printf(trc_out, "EARLY: success: %d, assume_grease: %d, " + "foundcfg: %d, cfgind: %d, clearlen: %zd, clear %p\n", + s->ext.ech.success, s->ext.ech.grease, foundcfg, + cfgind, clearlen, (void *)clear); + } OSSL_TRACE_END(TLS); +# ifdef OSSL_ECH_SUPERVERBOSE + if (foundcfg == 1 && clear != NULL) { /* Bit more logging */ + ossl_ech_pbuf("local config_id", &ee->config_id, 1); + ossl_ech_pbuf("remote config_id", &extval->config_id, 1); + ossl_ech_pbuf("clear", clear, clearlen); + } +# endif + if (extval != NULL) { + ossl_ech_encch_free(extval); + OPENSSL_free(extval); + extval = NULL; + } + if (s->ext.ech.grease == OSSL_ECH_IS_GREASE) { + OPENSSL_free(clear); + return 1; + } + /* 4. if decrypt worked, de-compress cleartext to make up real inner CH */ + if (ech_decode_inner(s, opd, opl, clear, clearlen) != 1) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + goto err; + } + OPENSSL_free(clear); +# ifdef OSSL_ECH_SUPERVERBOSE + ossl_ech_pbuf("Inner CH (decoded)", s->ext.ech.innerch, + s->ext.ech.innerch_len); +# endif + if (PACKET_buf_init(newpkt, s->ext.ech.innerch, + s->ext.ech.innerch_len) != 1) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + goto err; + } + /* tls_process_client_hello doesn't want the message header, so skip it */ + if (!PACKET_forward(newpkt, SSL3_HM_HEADER_LENGTH)) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + goto err; + } + if (ossl_ech_intbuf_add(s, s->ext.ech.innerch, + s->ext.ech.innerch_len, 0) != 1) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + goto err; + } + return 1; +err: + OPENSSL_free(aad); + if (extval != NULL) { + ossl_ech_encch_free(extval); + OPENSSL_free(extval); + } + OPENSSL_free(clear); + return 0; +} + int ossl_ech_intbuf_add(SSL_CONNECTION *s, const unsigned char *buf, size_t blen, int hash_existing) { @@ -1137,7 +2146,7 @@ int ossl_ech_intbuf_add(SSL_CONNECTION *s, const unsigned char *buf, } else { /* just add new octets */ if ((t1 = OPENSSL_realloc(s->ext.ech.transbuf, - s->ext.ech.transbuf_len + blen)) == NULL) + s->ext.ech.transbuf_len + blen)) == NULL) goto err; s->ext.ech.transbuf = t1; memcpy(s->ext.ech.transbuf + s->ext.ech.transbuf_len, buf, blen); diff --git a/ssl/ech/ech_local.h b/ssl/ech/ech_local.h index 6ae0b42648900..7dcd553de983c 100644 --- a/ssl/ech/ech_local.h +++ b/ssl/ech/ech_local.h @@ -118,6 +118,33 @@ typedef struct ossl_echstore_entry_st { unsigned char *encoded; /* overall encoded content */ } OSSL_ECHSTORE_ENTRY; +/* + * What we send in the ech CH extension: + * enum { outer(0), inner(1) } ECHClientHelloType; + * struct { + * ECHClientHelloType type; + * select (ECHClientHello.type) { + * case outer: + * HpkeSymmetricCipherSuite cipher_suite; + * uint8 config_id; + * opaque enc<0..2^16-1>; + * opaque payload<1..2^16-1>; + * case inner: + * Empty; + * }; + * } ECHClientHello; + * + */ +typedef struct ech_encch_st { + uint16_t kdf_id; /* ciphersuite */ + uint16_t aead_id; /* ciphersuite */ + uint8_t config_id; /* (maybe) identifies DNS RR value used */ + size_t enc_len; /* public share */ + unsigned char *enc; /* public share for sender */ + size_t payload_len; /* ciphertext */ + unsigned char *payload; /* ciphertext */ +} OSSL_ECH_ENCCH; + DEFINE_STACK_OF(OSSL_ECHSTORE_ENTRY) struct ossl_echstore_st { @@ -205,6 +232,22 @@ typedef struct ossl_ech_conn_st { unsigned char *pub; /* client ephemeral public kept by server in case HRR */ size_t pub_len; OSSL_HPKE_CTX *hpke_ctx; /* HPKE context, needed for HRR */ + /* + * Offsets of various things we need to know about in an inbound + * ClientHello (CH) plus the type of ECH and whether that CH is an inner or + * outer CH. We find these once for the outer CH, by roughly parsing the CH + * so store them for later re-use. We need to re-do this parsing when we + * get the 2nd CH in the case of HRR, and when we move to processing the + * inner CH after successful ECH decyption, so we have a flag to say if + * we've done the work or not. + */ + int ch_offsets_done; + size_t sessid_off; /* offset of session_id length */ + size_t exts_off; /* to offset of extensions */ + size_t ech_off; /* offset of ECH */ + size_t sni_off; /* offset of (outer) SNI */ + int echtype; /* ext type of the ECH */ + int inner; /* 1 if the ECH is marked as an inner, 0 for outer */ /* * A pointer to, and copy of, the hrrsignal from an HRR message. * We need both, as we zero-out the octets when re-calculating and diff --git a/ssl/ech/ech_ssl_apis.c b/ssl/ech/ech_ssl_apis.c index 601a8ad16213e..436e0fa71ccf9 100644 --- a/ssl/ech/ech_ssl_apis.c +++ b/ssl/ech/ech_ssl_apis.c @@ -185,7 +185,7 @@ int SSL_ech_get1_status(SSL *ssl, char **inner_sni, char **outer_sni) return SSL_ECH_STATUS_GREASE_ECH; return SSL_ECH_STATUS_GREASE; } - if ((s->options & SSL_OP_ECH_GREASE) !=0 && s->ext.ech.attempted != 1) + if ((s->options & SSL_OP_ECH_GREASE) != 0 && s->ext.ech.attempted != 1) return SSL_ECH_STATUS_GREASE; if (s->ext.ech.backend == 1) { if (s->ext.hostname != NULL diff --git a/ssl/statem/extensions.c b/ssl/statem/extensions.c index 670620631642a..55a5dd145a833 100644 --- a/ssl/statem/extensions.c +++ b/ssl/statem/extensions.c @@ -499,13 +499,8 @@ static const EXTENSION_DEFINITION ext_defs[] = { SSL_EXT_TLS1_3_HELLO_RETRY_REQUEST, OSSL_ECH_HANDLING_CALL_BOTH, init_ech, - /* - * TODO(ECH): add server calls as per below in a bit - * tls_parse_ctos_ech, tls_parse_stoc_ech, - * tls_construct_stoc_ech, tls_construct_ctos_ech, - */ - NULL, tls_parse_stoc_ech, - NULL, tls_construct_ctos_ech, + tls_parse_ctos_ech, tls_parse_stoc_ech, + tls_construct_stoc_ech, tls_construct_ctos_ech, NULL }, { diff --git a/ssl/statem/extensions_clnt.c b/ssl/statem/extensions_clnt.c index 566ee08dfe640..70825d9bd2521 100644 --- a/ssl/statem/extensions_clnt.c +++ b/ssl/statem/extensions_clnt.c @@ -14,12 +14,7 @@ #include "statem_local.h" #ifndef OPENSSL_NO_ECH # include -#include "internal/ech_helpers.h" -/* - * the max HPKE 'info' we'll process is the max ECHConfig size - * (OSSL_ECH_MAX_ECHCONFIG_LEN) plus OSSL_ECH_CONTEXT_STRING(len=7) + 1 - */ -#define OSSL_ECH_MAX_INFO_LEN (OSSL_ECH_MAX_ECHCONFIG_LEN + 8) +# include "internal/ech_helpers.h" #endif EXT_RETURN tls_construct_ctos_renegotiate(SSL_CONNECTION *s, WPACKET *pkt, @@ -2530,7 +2525,6 @@ EXT_RETURN tls_construct_ctos_ech(SSL_CONNECTION *s, WPACKET *pkt, unsigned char *encoded = NULL, *mypub = NULL; size_t cipherlen = 0, aad_len = 0, lenclen = 0, mypub_len = 0; size_t info_len = OSSL_ECH_MAX_INFO_LEN, clear_len = 0, encoded_len = 0; - /* whether or not we've been asked to GREASE, one way or another */ int grease_opt_set = (s->ext.ech.grease == OSSL_ECH_IS_GREASE || ((s->options & SSL_OP_ECH_GREASE) != 0)); diff --git a/ssl/statem/extensions_cust.c b/ssl/statem/extensions_cust.c index bda7e460b9d05..5ea06eb0a7271 100644 --- a/ssl/statem/extensions_cust.c +++ b/ssl/statem/extensions_cust.c @@ -209,8 +209,8 @@ int custom_ext_add(SSL_CONNECTION *s, int context, WPACKET *pkt, X509 *x, if (s->ext.ech.n_outer_only >= OSSL_ECH_OUTERS_MAX) { OSSL_TRACE_BEGIN(TLS) { BIO_printf(trc_out, - "Too many outers to compress (max=%d)\n", - OSSL_ECH_OUTERS_MAX); + "Too many outers to compress (max=%d)\n", + OSSL_ECH_OUTERS_MAX); } OSSL_TRACE_END(TLS); SSLfatal(s, SSL_AD_INTERNAL_ERROR, SSL_R_BAD_EXTENSION); return 0; @@ -219,9 +219,9 @@ int custom_ext_add(SSL_CONNECTION *s, int context, WPACKET *pkt, X509 *x, s->ext.ech.n_outer_only++; OSSL_TRACE_BEGIN(TLS) { BIO_printf(trc_out, "ECH compressing type " - "0x%04x (tot: %d)\n", - (int) meth->ext_type, - (int) s->ext.ech.n_outer_only); + "0x%04x (tot: %d)\n", + (int) meth->ext_type, + (int) s->ext.ech.n_outer_only); } OSSL_TRACE_END(TLS); } if (s->ext.ech.ch_depth == 0) { @@ -247,8 +247,8 @@ int custom_ext_add(SSL_CONNECTION *s, int context, WPACKET *pkt, X509 *x, SSLfatal(s, SSL_AD_INTERNAL_ERROR, SSL_R_BAD_EXTENSION); return 0; } - if (ossl_ech_copy_inner2outer(s, meth->ext_type, tind, pkt) - != OSSL_ECH_SAME_EXT_DONE) { + if (ossl_ech_copy_inner2outer(s, meth->ext_type, tind, + pkt) != OSSL_ECH_SAME_EXT_DONE) { /* for custom exts, we really should have found it */ SSLfatal(s, SSL_AD_INTERNAL_ERROR, SSL_R_BAD_EXTENSION); return 0; @@ -505,19 +505,7 @@ int ossl_tls_add_custom_ext_intern(SSL_CTX *ctx, custom_ext_methods *exts, * for extension types that previously were not supported, but now are. */ if (SSL_extension_supported(ext_type) -#if !defined(OPENSSL_NO_ECH) && defined(OPENSSL_ECH_ALLOW_CUST_INJECT) - /* - * Do this conditionally so we can test an ECH in TLSv1.2 - * via the custom extensions API. - * OPENSSL_ECH_ALLOW_CUST_INJECT is defined (or not) in - * include/openssl/ech.h and if defined enables a test in - * test/ech_test.c - */ - && ext_type != TLSEXT_TYPE_ech && ext_type != TLSEXT_TYPE_signed_certificate_timestamp) -#else - && ext_type != TLSEXT_TYPE_signed_certificate_timestamp) -#endif return 0; /* Extension type must fit in 16 bits */ diff --git a/ssl/statem/extensions_srvr.c b/ssl/statem/extensions_srvr.c index 0fc03e9acca6b..fd1ddde2f67c3 100644 --- a/ssl/statem/extensions_srvr.c +++ b/ssl/statem/extensions_srvr.c @@ -13,7 +13,12 @@ #include "internal/cryptlib.h" #include "internal/ssl_unwrap.h" -#define COOKIE_STATE_FORMAT_VERSION 1 +#ifndef OPENSSL_NO_ECH +# include +# include +#endif + +#define COOKIE_STATE_FORMAT_VERSION 1 /* * 2 bytes for packet length, 2 bytes for format version, 2 bytes for @@ -2427,3 +2432,152 @@ int tls_parse_ctos_server_cert_type(SSL_CONNECTION *sc, PACKET *pkt, SSLfatal(sc, SSL_AD_UNSUPPORTED_CERTIFICATE, SSL_R_BAD_EXTENSION); return 0; } + +#ifndef OPENSSL_NO_ECH +/* + * ECH handling for edge cases (GREASE/inner) and errors. + * return 1 for good, 0 otherwise + * + * Real ECH handling (i.e. decryption) happens before, via + * ech_early_decrypt(), but if that failed (e.g. decryption + * failed, which may be down to GREASE) then we end up here, + * processing the ECH from the outer CH. + * Otherwise, we only expect to see an inner ECH with a fixed + * value here. + */ +int tls_parse_ctos_ech(SSL_CONNECTION *s, PACKET *pkt, unsigned int context, + X509 *x, size_t chainidx) +{ + unsigned int echtype = 0; + + if (s->ext.ech.grease == OSSL_ECH_IS_GREASE) { + /* GREASE is fine */ + return 1; + } + if (s->ext.ech.es == NULL) { + /* If not configured for ECH then we ignore it */ + return 1; + } + if (s->ext.ech.attempted_type != TLSEXT_TYPE_ech) { + /* if/when new versions of ECH are added we'll update here */ + SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_BAD_EXTENSION); + return 0; + } + /* + * we only allow "inner" which is one octet, valued 0x01 + * and only if we decrypted ok or are a backend + */ + if (PACKET_get_1(pkt, &echtype) != 1 + || echtype != OSSL_ECH_INNER_CH_TYPE + || PACKET_remaining(pkt) != 0) { + SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_BAD_EXTENSION); + return 0; + } + if (s->ext.ech.success != 1 && s->ext.ech.backend != 1) { + SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_BAD_EXTENSION); + return 0; + } + /* yay - we're ok with this */ + OSSL_TRACE_BEGIN(TLS) { + BIO_printf(trc_out, "ECH seen in inner as exptected.\n"); + } OSSL_TRACE_END(TLS); + return 1; +} + +/* + * Answer an ECH, as needed + * return 1 for good, 0 otherwise + * + * Return most-recent ECH config for retry, as needed. + * If doing HRR we include the confirmation value, but + * for now, we'll just add the zeros - the real octets + * will be added later via ech_calc_ech_confirm() which + * is called when constructing the server hello. + */ +EXT_RETURN tls_construct_stoc_ech(SSL_CONNECTION *s, WPACKET *pkt, + unsigned int context, X509 *x, + size_t chainidx) +{ + unsigned char *rcfgs = NULL; + size_t rcfgslen = 0; + + if (context == SSL_EXT_TLS1_3_HELLO_RETRY_REQUEST + && (s->ext.ech.success == 1 || s->ext.ech.backend == 1) + && s->ext.ech.attempted_type == TLSEXT_TYPE_ech) { + unsigned char eightzeros[8] = {0, 0, 0, 0, 0, 0, 0, 0}; + + if (!WPACKET_put_bytes_u16(pkt, s->ext.ech.attempted_type) + || !WPACKET_sub_memcpy_u16(pkt, eightzeros, 8)) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + return 0; + } + OSSL_TRACE_BEGIN(TLS) { + BIO_printf(trc_out, "set 8 zeros for ECH accept confirm in HRR\n"); + } OSSL_TRACE_END(TLS); + return EXT_RETURN_SENT; + } + /* GREASE or error => random confirmation in HRR case */ + if (context == SSL_EXT_TLS1_3_HELLO_RETRY_REQUEST + && s->ext.ech.attempted_type == TLSEXT_TYPE_ech + && s->ext.ech.attempted == 1) { + unsigned char randomconf[8]; + + if (RAND_bytes_ex(s->ssl.ctx->libctx, randomconf, 8, + RAND_DRBG_STRENGTH) <= 0) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + return 0; + } + if (!WPACKET_put_bytes_u16(pkt, s->ext.ech.attempted_type) + || !WPACKET_sub_memcpy_u16(pkt, randomconf, 8)) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + return 0; + } + OSSL_TRACE_BEGIN(TLS) { + BIO_printf(trc_out, "set random for ECH acccpt confirm in HRR\n"); + } OSSL_TRACE_END(TLS); + return EXT_RETURN_SENT; + } + /* in other HRR circumstances: don't set */ + if (context == SSL_EXT_TLS1_3_HELLO_RETRY_REQUEST) + return EXT_RETURN_NOT_SENT; + /* If in some weird state we ignore and send nothing */ + if (s->ext.ech.grease != OSSL_ECH_IS_GREASE + || s->ext.ech.attempted_type != TLSEXT_TYPE_ech) + return EXT_RETURN_NOT_SENT; + /* + * If the client GREASEd, or we think it did, return the + * most-recently loaded ECHConfigList, as the value of the + * extension. Most-recently loaded can be anywhere in the + * list, depending on changing or non-changing file names. + */ + if (s->ext.ech.es == NULL) { + OSSL_TRACE_BEGIN(TLS) { + BIO_printf(trc_out, "ECH - not sending ECHConfigList to client " + "even though they GREASE'd as I've no loaded configs\n"); + } OSSL_TRACE_END(TLS); + return EXT_RETURN_NOT_SENT; + } + if (ossl_ech_get_retry_configs(s, &rcfgs, &rcfgslen) != 1) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + return 0; + } + if (rcfgslen == 0) { + OSSL_TRACE_BEGIN(TLS) { + BIO_printf(trc_out, "ECH - not sending ECHConfigList to client " + "even though they GREASE'd and I have configs but " + "I've no configs set to be returned\n"); + } OSSL_TRACE_END(TLS); + return EXT_RETURN_NOT_SENT; + } + if (!WPACKET_put_bytes_u16(pkt, TLSEXT_TYPE_ech) + || !WPACKET_start_sub_packet_u16(pkt) + || !WPACKET_sub_memcpy_u16(pkt, rcfgs, rcfgslen) + || !WPACKET_close(pkt)) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + OPENSSL_free(rcfgs); + return 0; + } + OPENSSL_free(rcfgs); + return EXT_RETURN_SENT; +} +#endif /* END OPENSSL_NO_ECH */ diff --git a/ssl/statem/statem_clnt.c b/ssl/statem/statem_clnt.c index 18daa3200c56a..9b21027062937 100644 --- a/ssl/statem/statem_clnt.c +++ b/ssl/statem/statem_clnt.c @@ -1312,7 +1312,7 @@ __owur CON_FUNC_RETURN tls_construct_client_hello(SSL_CONNECTION *s, goto err; } OPENSSL_free(s->ext.ech.innerch); - s->ext.ech.innerch = (unsigned char*)inner_mem->data; + s->ext.ech.innerch = (unsigned char *)inner_mem->data; inner_mem->data = NULL; s->ext.ech.innerch_len = innerlen; /* add inner to transcript */ diff --git a/ssl/statem/statem_local.h b/ssl/statem/statem_local.h index 6328e1ee828bd..8ba5ef242a8b2 100644 --- a/ssl/statem/statem_local.h +++ b/ssl/statem/statem_local.h @@ -574,7 +574,9 @@ int tls_parse_stoc_server_cert_type(SSL_CONNECTION *s, PACKET *pkt, EXT_RETURN tls_construct_ctos_ech(SSL_CONNECTION *s, WPACKET *pkt, unsigned int context, X509 *x, size_t chainidx); -EXT_RETURN tls_construct_ctos_ech(SSL_CONNECTION *s, WPACKET *pkt, +int tls_parse_ctos_ech(SSL_CONNECTION *s, PACKET *pkt, unsigned int context, + X509 *x, size_t chainidx); +EXT_RETURN tls_construct_stoc_ech(SSL_CONNECTION *s, WPACKET *pkt, unsigned int context, X509 *x, size_t chainidx); int tls_parse_stoc_ech(SSL_CONNECTION *s, PACKET *pkt, unsigned int context, diff --git a/ssl/statem/statem_srvr.c b/ssl/statem/statem_srvr.c index 5b6465bc23960..89944706701e0 100644 --- a/ssl/statem/statem_srvr.c +++ b/ssl/statem/statem_srvr.c @@ -35,6 +35,10 @@ #define TICKET_NONCE_SIZE 8 +#ifndef OPENSSL_NO_ECH +# include "../ech/ech_local.h" +#endif + typedef struct { ASN1_TYPE *kxBlob; ASN1_TYPE *opaqueBlob; @@ -1495,6 +1499,86 @@ MSG_PROCESS_RETURN tls_process_client_hello(SSL_CONNECTION *s, PACKET *pkt) static const unsigned char null_compression = 0; CLIENTHELLO_MSG *clienthello = NULL; +#ifndef OPENSSL_NO_ECH + /* + * For a split-mode backend we want to have a way to point at the CH octets + * for the accept-confirmation calculation. The split-mode backend does not + * need any ECH secrets, but it does need to see the inner CH and be the TLS + * endpoint with which the ECH encrypting client sets up the TLS session. + * The split-mode backend however does need to do an ECH confirm calculation + * so we need to tee that up. The result of that calculation will be put in + * the ServerHello.random (or ECH extension if HRR) to signal to the client + * that ECH "worked." + */ + if (s->server && PACKET_remaining(pkt) != 0) { + int rv = 0, innerflag = -1; + size_t startofsessid = 0, startofexts = 0, echoffset = 0; + size_t outersnioffset = 0; /* offset to SNI in outer */ + uint16_t echtype = OSSL_ECH_type_unknown; /* type of ECH seen */ + const unsigned char *pbuf = NULL; + + /* reset needed in case of HRR */ + s->ext.ech.ch_offsets_done = 0; + rv = ossl_ech_get_ch_offsets(s, pkt, &startofsessid, &startofexts, + &echoffset, &echtype, &innerflag, + &outersnioffset); + if (rv != 1) { + SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_BAD_EXTENSION); + goto err; + } + if (innerflag == OSSL_ECH_INNER_CH_TYPE) { + WPACKET inner; + + OSSL_TRACE(TLS, "Got inner ECH so setting backend\n"); + /* For backend, include msg type & 3 octet length */ + s->ext.ech.backend = 1; + s->ext.ech.attempted_type = TLSEXT_TYPE_ech; + OPENSSL_free(s->ext.ech.innerch); + s->ext.ech.innerch_len = PACKET_remaining(pkt); + if (PACKET_peek_bytes(pkt, &pbuf, s->ext.ech.innerch_len) != 1) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + goto err; + } + s->ext.ech.innerch_len += SSL3_HM_HEADER_LENGTH; /* 4 */ + s->ext.ech.innerch = OPENSSL_malloc(s->ext.ech.innerch_len); + if (s->ext.ech.innerch == NULL) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + goto err; + } + if (!WPACKET_init_static_len(&inner, s->ext.ech.innerch, + s->ext.ech.innerch_len, 0) + || !WPACKET_put_bytes_u8(&inner, SSL3_MT_CLIENT_HELLO) + || !WPACKET_put_bytes_u24(&inner, s->ext.ech.innerch_len + - SSL3_HM_HEADER_LENGTH) + || !WPACKET_memcpy(&inner, pbuf, s->ext.ech.innerch_len + - SSL3_HM_HEADER_LENGTH) + || !WPACKET_finish(&inner)) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + goto err; + } + } else if (s->ext.ech.es != NULL) { + PACKET newpkt; + + if (ossl_ech_early_decrypt(s, pkt, &newpkt) != 1) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + goto err; + } + if (s->ext.ech.success == 1) { + /* + * Replace the outer CH with the inner, as long as there's + * space, which there better be! (a bug triggered a bigger + * inner CH once;-) + */ + if (PACKET_remaining(&newpkt) > PACKET_remaining(pkt)) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + goto err; + } + *pkt = newpkt; + } + } + } +#endif + /* Check if this is actually an unexpected renegotiation ClientHello */ if (s->renegotiate == 0 && !SSL_IS_FIRST_HANDSHAKE(s)) { if (!ossl_assert(!SSL_CONNECTION_IS_TLS13(s))) { @@ -1696,6 +1780,12 @@ MSG_PROCESS_RETURN tls_process_client_hello(SSL_CONNECTION *s, PACKET *pkt) if (clienthello != NULL) OPENSSL_free(clienthello->pre_proc_exts); OPENSSL_free(clienthello); +#ifndef OPENSSL_NO_ECH + s->clienthello = NULL; + OPENSSL_free(s->ext.ech.innerch); + s->ext.ech.innerch = NULL; + s->ext.ech.innerch_len = 0; +#endif return MSG_PROCESS_ERROR; } @@ -1989,12 +2079,24 @@ static int tls_early_post_process_client_hello(SSL_CONNECTION *s) goto err; } - if (!s->hit - && s->version >= TLS1_VERSION - && !SSL_CONNECTION_IS_TLS13(s) - && !SSL_CONNECTION_IS_DTLS(s) - && s->ext.session_secret_cb != NULL) { + /* + * Unless ECH has worked or not been configured we won't call + * the session_secret_cb now because we'll need to calculate the + * server random later to include the ECH accept value. + * We can't do it now as we don't yet have the SH encoding. + */ + if ( +#ifndef OPENSSL_NO_ECH + ((s->ext.ech.es != NULL && s->ext.ech.success == 1) + || s->ext.ech.es == NULL) && +#endif + !s->hit + && s->version >= TLS1_VERSION + && !SSL_CONNECTION_IS_TLS13(s) + && !SSL_CONNECTION_IS_DTLS(s) + && s->ext.session_secret_cb != NULL) { const SSL_CIPHER *pref_cipher = NULL; + /* * s->session->master_key_length is a size_t, but this is an int for * backwards compat reasons @@ -2151,7 +2253,8 @@ static int tls_early_post_process_client_hello(SSL_CONNECTION *s) err: sk_SSL_CIPHER_free(ciphers); sk_SSL_CIPHER_free(scsvs); - OPENSSL_free(clienthello->pre_proc_exts); + if (clienthello != NULL) + OPENSSL_free(clienthello->pre_proc_exts); OPENSSL_free(s->clienthello); s->clienthello = NULL; @@ -2513,16 +2616,147 @@ CON_FUNC_RETURN tls_construct_server_hello(SSL_CONNECTION *s, WPACKET *pkt) * Re-initialise the Transcript Hash. We're going to prepopulate it with * a synthetic message_hash in place of ClientHello1. */ - if (!create_synthetic_message_hash(s, NULL, 0, NULL, 0)) { +#ifndef OPENSSL_NO_ECH + /* + * if we're sending 2nd SH after HRR and we did ECH + * then we want to inject the hash of the inner CH1 + * and not the outer (which is the default) + */ + OSSL_TRACE_BEGIN(TLS) { + BIO_printf(trc_out, "Checking success (%d)/innerCH (%p)\n", + s->ext.ech.success, (void *)s->ext.ech.innerch); + } OSSL_TRACE_END(TLS); + if ((s->ext.ech.backend == 1 || s->ext.ech.success == 1) + && s->ext.ech.innerch != NULL) { + /* do pre-existing HRR stuff */ + unsigned char hashval[EVP_MAX_MD_SIZE]; + unsigned int hashlen; + EVP_MD_CTX *ctx = EVP_MD_CTX_new(); + const EVP_MD *md = NULL; + + OSSL_TRACE(TLS, "Adding in digest of ClientHello\n"); +# ifdef OSSL_ECH_SUPERVERBOSE + ossl_ech_pbuf("innerch", s->ext.ech.innerch, + s->ext.ech.innerch_len); +# endif + if (ctx == NULL) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + return CON_FUNC_ERROR; + } + md = ssl_handshake_md(s); + if (md == NULL) { + EVP_MD_CTX_free(ctx); + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + return CON_FUNC_ERROR; + } + if (EVP_DigestInit_ex(ctx, md, NULL) <= 0 + || EVP_DigestUpdate(ctx, s->ext.ech.innerch, + s->ext.ech.innerch_len) <= 0 + || EVP_DigestFinal_ex(ctx, hashval, &hashlen) <= 0) { + EVP_MD_CTX_free(ctx); + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + return CON_FUNC_ERROR; + } +# ifdef OSSL_ECH_SUPERVERBOSE + ossl_ech_pbuf("digested CH", hashval, hashlen); +# endif + EVP_MD_CTX_free(ctx); + if (ossl_ech_reset_hs_buffer(s, NULL, 0) != 1) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + return CON_FUNC_ERROR; + } + if (!create_synthetic_message_hash(s, hashval, hashlen, NULL, 0)) { + /* SSLfatal() already called */ + return CON_FUNC_ERROR; + } + } else { + if (!create_synthetic_message_hash(s, NULL, 0, NULL, 0)) + return CON_FUNC_ERROR; /* SSLfatal() already called */ + } +#else + if (!create_synthetic_message_hash(s, NULL, 0, NULL, 0)) /* SSLfatal() already called */ return CON_FUNC_ERROR; - } +#endif /* OPENSSL_NO_ECH */ } else if (!(s->verify_mode & SSL_VERIFY_PEER) - && !ssl3_digest_cached_records(s, 0)) { + && !ssl3_digest_cached_records(s, 0)) { /* SSLfatal() already called */; return CON_FUNC_ERROR; } +#ifndef OPENSSL_NO_ECH + /* + * Calculate the ECH-accept server random to indicate that + * we're accepting ECH, if that's the case + */ + if (s->ext.ech.attempted_type == TLSEXT_TYPE_ech + && (s->ext.ech.backend == 1 + || (s->ext.ech.es != NULL && s->ext.ech.success == 1))) { + unsigned char acbuf[8]; + unsigned char *shbuf = NULL; + size_t shlen = 0; + size_t shoffset = 0; + int hrr = 0; + + if (s->hello_retry_request == SSL_HRR_PENDING) + hrr = 1; + memset(acbuf, 0, 8); + if (WPACKET_get_total_written(pkt, &shlen) != 1) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + return CON_FUNC_ERROR; + } + shbuf = WPACKET_get_curr(pkt) - shlen; + /* we need to fixup SH length here */ + shbuf[1] = ((shlen - 4)) >> 16 & 0xff; + shbuf[2] = ((shlen - 4)) >> 8 & 0xff; + shbuf[3] = (shlen - 4) & 0xff; + if (ossl_ech_intbuf_add(s, shbuf, shlen, hrr) != 1) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + return CON_FUNC_ERROR; + } + if (ossl_ech_calc_confirm(s, hrr, acbuf, shlen) != 1) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + return CON_FUNC_ERROR; + } + memcpy(s->s3.server_random + SSL3_RANDOM_SIZE - 8, acbuf, 8); + if (hrr == 0) { + /* confirm value hacked into SH.random rightmost octets */ + shoffset = SSL3_HM_HEADER_LENGTH /* 4 */ + + CLIENT_VERSION_LEN /* 2 */ + + SSL3_RANDOM_SIZE /* 32 */ + - 8; + memcpy(shbuf + shoffset, acbuf, 8); + } else { + /* + * confirm value is in extension in HRR case as the SH.random + * is already hacked to be a specific value in a HRR + */ + memcpy(WPACKET_get_curr(pkt) - 8, acbuf, 8); + } + } + /* call ECH callback, if appropriate */ + if (s->ext.ech.attempted == 1 && s->ext.ech.cb != NULL + && s->hello_retry_request != SSL_HRR_PENDING) { + char pstr[OSSL_ECH_PBUF_SIZE + 1]; + BIO *biom = BIO_new(BIO_s_mem()); + unsigned int cbrv = 0; + + if (biom == NULL) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + return CON_FUNC_ERROR; + } + memset(pstr, 0, OSSL_ECH_PBUF_SIZE + 1); + ossl_ech_status_print(biom, s, OSSL_ECHSTORE_ALL); + BIO_read(biom, pstr, OSSL_ECH_PBUF_SIZE); + cbrv = s->ext.ech.cb(&s->ssl, pstr); + BIO_free(biom); + if (cbrv != 1) { + OSSL_TRACE(TLS, "Error from tls_construct_server_hello/ech_cb\n"); + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + return CON_FUNC_ERROR; + } + } +#endif /* OPENSSL_NO_ECH */ return CON_FUNC_SUCCESS; } diff --git a/test/ech_test.c b/test/ech_test.c index 968fca896d1c2..2f3cddc27b1a7 100644 --- a/test/ech_test.c +++ b/test/ech_test.c @@ -893,11 +893,15 @@ static int ech_ingest_test(int run) * Occasionally, flush_time will be 1 more than add_time. We'll * check for that as that should catch a few more code paths * in the flush_keys API. + * When flush_time is 1 more, we may or may not have flushed + * the one and only key (depending on which "side" of the second + * it was generated, so we may be left with 0 or 1 keys. */ if (!TEST_true(OSSL_ECHSTORE_flush_keys(es, flush_time - add_time)) || !TEST_int_eq(OSSL_ECHSTORE_num_keys(es, &keysaftr), 1) || ((flush_time <= add_time) && !TEST_int_eq(keysaftr, 0)) - || ((flush_time > add_time) && !TEST_int_eq(keysaftr, 1))) { + || ((flush_time > add_time) && !TEST_int_eq(keysaftr, 1) + && !TEST_int_eq(keysaftr, 0))) { TEST_info("Flush time: %lld, add_time: %lld", (long long)flush_time, (long long)add_time); goto end; @@ -1141,6 +1145,7 @@ static int ech_boring_compat(void) # define OSSL_ECH_TEST_EARLY 2 # define OSSL_ECH_TEST_CUSTOM 3 # define OSSL_ECH_TEST_ENOE 4 /* early + no-ech */ +/* note: early-data is prohibited after HRR so no tests for that */ /* * @brief ECH roundtrip test helper @@ -1155,13 +1160,6 @@ static int ech_boring_compat(void) * * The combo input is one of the #define'd OSSL_ECH_TEST_* * values above. - * - * TODO(ECH): we're not yet really attempting ECH, but we currently - * set the inputs as if we were doing ECH, yet don't expect to see - * real ECH status outcomes, so while we do make calls to get that - * status outcome, we don't compare vs. real expected results. - * That's done via the "if (0 &&" clauses below which will be - * removed once ECH is really being attempted. */ static int test_ech_roundtrip_helper(int idx, int combo) { @@ -1205,10 +1203,9 @@ static int test_ech_roundtrip_helper(int idx, int combo) if (combo == OSSL_ECH_TEST_EARLY || combo == OSSL_ECH_TEST_ENOE) { if (!TEST_true(SSL_CTX_set_options(sctx, SSL_OP_NO_ANTI_REPLAY)) || !TEST_true(SSL_CTX_set_max_early_data(sctx, - SSL3_RT_MAX_PLAIN_LENGTH))) - goto end; - if (!TEST_true(SSL_CTX_set_recv_max_early_data(sctx, - SSL3_RT_MAX_PLAIN_LENGTH))) + SSL3_RT_MAX_PLAIN_LENGTH)) + || !TEST_true(SSL_CTX_set_recv_max_early_data(sctx, + SSL3_RT_MAX_PLAIN_LENGTH))) goto end; } if (combo == OSSL_ECH_TEST_CUSTOM) { @@ -1239,58 +1236,38 @@ static int test_ech_roundtrip_helper(int idx, int combo) goto end; if (!TEST_true(SSL_set_tlsext_host_name(clientssl, "server.example"))) goto end; -# undef DROPFORNOW -# ifdef DROPFORNOW - /* TODO(ECH): we'll re-instate this once server-side ECH code is in */ if (!TEST_true(create_ssl_connection(serverssl, clientssl, SSL_ERROR_NONE))) goto end; -# else - /* - * For this PR, check connections fail when client does ECH - * and server doesn't, but work if client doesn't do ECH. - * Added in early data for the no-ECH case because an - * intermediate state of the code had an issue. - */ - if (combo != OSSL_ECH_TEST_ENOE - && !TEST_false(create_ssl_connection(serverssl, clientssl, - SSL_ERROR_NONE))) - goto end; - if (combo == OSSL_ECH_TEST_ENOE - && !TEST_true(create_ssl_connection(serverssl, clientssl, - SSL_ERROR_NONE))) - goto end; -# endif - serverstatus = SSL_ech_get1_status(serverssl, &sinner, &souter); - if (verbose) - TEST_info("server status %d, %s, %s", serverstatus, sinner, souter); - if (0 && !TEST_int_eq(serverstatus, SSL_ECH_STATUS_SUCCESS)) - goto end; /* override cert verification */ SSL_set_verify_result(clientssl, X509_V_OK); clientstatus = SSL_ech_get1_status(clientssl, &cinner, &couter); if (verbose) TEST_info("client status %d, %s, %s", clientstatus, cinner, couter); - if (0 && !TEST_int_eq(clientstatus, SSL_ECH_STATUS_SUCCESS)) + serverstatus = SSL_ech_get1_status(serverssl, &sinner, &souter); + if (verbose) + TEST_info("server status %d, %s, %s", serverstatus, sinner, souter); + if (combo != OSSL_ECH_TEST_ENOE + && !TEST_int_eq(serverstatus, SSL_ECH_STATUS_SUCCESS)) + goto end; + if (combo == OSSL_ECH_TEST_ENOE + && !TEST_int_eq(serverstatus, SSL_ECH_STATUS_NOT_TRIED)) + goto end; + if (combo != OSSL_ECH_TEST_ENOE + && !TEST_int_eq(clientstatus, SSL_ECH_STATUS_SUCCESS)) + goto end; + if (combo == OSSL_ECH_TEST_ENOE + && !TEST_int_eq(clientstatus, SSL_ECH_STATUS_NOT_CONFIGURED)) goto end; /* all good */ - if (combo == OSSL_ECH_TEST_BASIC - || combo == OSSL_ECH_TEST_HRR + if (combo == OSSL_ECH_TEST_BASIC || combo == OSSL_ECH_TEST_HRR || combo == OSSL_ECH_TEST_CUSTOM) { res = 1; goto end; } /* continue for EARLY test */ -# ifdef DROPFORNOW - /* TODO(ECH): turn back on later */ if (combo != OSSL_ECH_TEST_EARLY && combo != OSSL_ECH_TEST_ENOE) goto end; -# else - if (combo != OSSL_ECH_TEST_ENOE) { - res = 1; - goto end; - } -# endif /* shutdown for start over */ sess = SSL_get1_session(clientssl); OPENSSL_free(sinner); @@ -1310,8 +1287,8 @@ static int test_ech_roundtrip_helper(int idx, int combo) || !TEST_true(SSL_set_session(clientssl, sess)) || !TEST_true(SSL_write_early_data(clientssl, ed, sizeof(ed), &written)) || !TEST_size_t_eq(written, sizeof(ed)) - || !TEST_int_eq(SSL_read_early_data(serverssl, buf, - sizeof(buf), &readbytes), + || !TEST_int_eq(SSL_read_early_data(serverssl, buf, sizeof(buf), + &readbytes), SSL_READ_EARLY_DATA_SUCCESS) || !TEST_size_t_eq(written, readbytes)) goto end; @@ -1324,17 +1301,25 @@ static int test_ech_roundtrip_helper(int idx, int combo) || !TEST_true(SSL_read_ex(clientssl, buf, sizeof(buf), &readbytes)) || !TEST_mem_eq(buf, readbytes, ed, sizeof(ed))) goto end; - serverstatus = SSL_ech_get1_status(serverssl, &sinner, &souter); - if (verbose) - TEST_info("server status %d, %s, %s", serverstatus, sinner, souter); - if (0 && !TEST_int_eq(serverstatus, SSL_ECH_STATUS_SUCCESS)) - goto end; /* override cert verification */ SSL_set_verify_result(clientssl, X509_V_OK); clientstatus = SSL_ech_get1_status(clientssl, &cinner, &couter); if (verbose) TEST_info("client status %d, %s, %s", clientstatus, cinner, couter); - if (0 && !TEST_int_eq(clientstatus, SSL_ECH_STATUS_SUCCESS)) + serverstatus = SSL_ech_get1_status(serverssl, &sinner, &souter); + if (verbose) + TEST_info("server status %d, %s, %s", serverstatus, sinner, souter); + if (combo != OSSL_ECH_TEST_ENOE + && !TEST_int_eq(serverstatus, SSL_ECH_STATUS_SUCCESS)) + goto end; + if (combo == OSSL_ECH_TEST_ENOE + && !TEST_int_eq(serverstatus, SSL_ECH_STATUS_NOT_TRIED)) + goto end; + if (combo != OSSL_ECH_TEST_ENOE + && !TEST_int_eq(clientstatus, SSL_ECH_STATUS_SUCCESS)) + goto end; + if (combo == OSSL_ECH_TEST_ENOE + && !TEST_int_eq(clientstatus, SSL_ECH_STATUS_NOT_CONFIGURED)) goto end; /* all good */ res = 1; diff --git a/util/platform_symbols/windows-symbols.txt b/util/platform_symbols/windows-symbols.txt index 69fb23bfc1e7d..fa14f51834dec 100644 --- a/util/platform_symbols/windows-symbols.txt +++ b/util/platform_symbols/windows-symbols.txt @@ -89,6 +89,7 @@ __current_exception_context strlen strstr strchr +strlen memmove strrchr memcmp From 9b7ed5689c96945a667c42507fe2bfded4e8df13 Mon Sep 17 00:00:00 2001 From: Tomas Mraz Date: Thu, 18 Sep 2025 17:13:28 +0200 Subject: [PATCH 12/24] Fix warnings about casts in ECH code Reviewed-by: Matt Caswell Reviewed-by: Paul Dale (Merged from https://github.com/openssl/openssl/pull/28611) --- apps/s_client.c | 5 +++-- ssl/ech/ech_internal.c | 17 +++++++++-------- ssl/ech/ech_local.h | 2 +- ssl/statem/extensions.c | 6 +++--- ssl/statem/extensions_clnt.c | 2 +- ssl/tls13_enc.c | 2 +- test/ech_test.c | 2 +- 7 files changed, 19 insertions(+), 17 deletions(-) diff --git a/apps/s_client.c b/apps/s_client.c index 89b473b7e69fb..f9f11f642797a 100644 --- a/apps/s_client.c +++ b/apps/s_client.c @@ -3432,8 +3432,9 @@ static void print_ech_retry_configs(BIO *bio, SSL *s) * print nicely, note that any non-supported versions * sent by server will have been filtered out by now */ - if ((biom = BIO_new(BIO_s_mem())) == NULL - || BIO_write(biom, rtval, rtlen) <= 0 + if (rtlen > INT_MAX + || (biom = BIO_new(BIO_s_mem())) == NULL + || BIO_write(biom, rtval, (int)rtlen) <= 0 || (es = OSSL_ECHSTORE_new(NULL, NULL)) == NULL || OSSL_ECHSTORE_read_echconfiglist(es, biom) != 1) { BIO_printf(bio, "ECH: Error loading retry-configs\n"); diff --git a/ssl/ech/ech_internal.c b/ssl/ech/ech_internal.c index 7fdcf3c5c75b5..89fc63d19b083 100644 --- a/ssl/ech/ech_internal.c +++ b/ssl/ech/ech_internal.c @@ -40,7 +40,7 @@ void ossl_ech_pbuf(const char *msg, const unsigned char *buf, const size_t blen) BIO_printf(trc_out, "%s: blen is %lu\n", msg, (unsigned long)blen); } else { BIO_printf(trc_out, "%s (%lu)\n", msg, (unsigned long)blen); - BIO_dump_indent(trc_out, buf, blen, 4); + BIO_dump_indent(trc_out, buf, (int)blen, 4); } } OSSL_TRACE_END(TLS); return; @@ -373,7 +373,8 @@ int ossl_ech_pick_matching_cfg(SSL_CONNECTION *s, OSSL_ECHSTORE_ENTRY **ee, OSSL_HPKE_SUITE *suite) { int namematch = 0, nameoverride = 0, suitematch = 0, num, cind = 0; - unsigned int csuite = 0, tsuite = 0, hnlen = 0; + unsigned int csuite = 0, tsuite = 0; + size_t hnlen = 0; OSSL_ECHSTORE_ENTRY *lee = NULL, *tee = NULL; OSSL_ECHSTORE *es = NULL; char *hn = NULL; @@ -620,8 +621,8 @@ int ossl_ech_reset_hs_buffer(SSL_CONNECTION *s, const unsigned char *buf, size_t ossl_ech_calc_padding(SSL_CONNECTION *s, OSSL_ECHSTORE_ENTRY *ee, size_t encoded_len) { - int length_of_padding = 0, length_with_snipadding = 0; - int innersnipadding = 0, length_with_padding = 0; + size_t length_of_padding = 0, length_with_snipadding = 0; + size_t innersnipadding = 0, length_with_padding = 0; size_t mnl = 0, isnilen = 0; if (s == NULL || ee == NULL) @@ -653,12 +654,12 @@ size_t ossl_ech_calc_padding(SSL_CONNECTION *s, OSSL_ECHSTORE_ENTRY *ee, while (length_with_padding < OSSL_ECH_PADDING_TARGET) length_with_padding += OSSL_ECH_PADDING_INCREMENT; OSSL_TRACE_BEGIN(TLS) { - BIO_printf(trc_out, "EAAE: padding: mnl: %zu, lws: %d " - "lop: %d, clear_len (len with padding): %d, orig: %zu\n", + BIO_printf(trc_out, "EAAE: padding: mnl: %zu, lws: %zu " + "lop: %zu, clear_len (len with padding): %zu, orig: %zu\n", mnl, length_with_snipadding, length_of_padding, length_with_padding, encoded_len); } OSSL_TRACE_END(TLS); - return (size_t)length_with_padding; + return length_with_padding; } /* @@ -955,7 +956,7 @@ static int ech_hkdf_extract_wrap(SSL_CONNECTION *s, EVP_MD *md, int for_hrr, ossl_ech_pbuf("cc: client_random", p, SSL3_RANDOM_SIZE); # endif if (EVP_PKEY_CTX_set1_hkdf_key(pctx, p, SSL3_RANDOM_SIZE) != 1 - || EVP_PKEY_CTX_set1_hkdf_salt(pctx, zeros, hashlen) != 1 + || EVP_PKEY_CTX_set1_hkdf_salt(pctx, zeros, (int)hashlen) != 1 || EVP_PKEY_derive(pctx, NULL, &retlen) != 1 || hashlen != retlen || EVP_PKEY_derive(pctx, notsecret, &retlen) != 1) { diff --git a/ssl/ech/ech_local.h b/ssl/ech/ech_local.h index 7dcd553de983c..9928d1a2844d4 100644 --- a/ssl/ech/ech_local.h +++ b/ssl/ech/ech_local.h @@ -335,7 +335,7 @@ int ossl_ech_calc_confirm(SSL_CONNECTION *s, int for_hrr, /* these are internal but located in ssl/statem/extensions.c */ int ossl_ech_same_ext(SSL_CONNECTION *s, WPACKET *pkt); int ossl_ech_same_key_share(void); -int ossl_ech_2bcompressed(int ind); +int ossl_ech_2bcompressed(size_t ind); int ossl_ech_copy_inner2outer(SSL_CONNECTION *s, uint16_t ext_type, int ind, WPACKET *pkt); diff --git a/ssl/statem/extensions.c b/ssl/statem/extensions.c index 55a5dd145a833..20b29f999ac1f 100644 --- a/ssl/statem/extensions.c +++ b/ssl/statem/extensions.c @@ -603,9 +603,9 @@ int ossl_ech_same_key_share(void) * say if extension at index |ind| in ext_defs is to be ECH compressed * return 1 if this one is to be compressed, 0 if not, -1 for error */ -int ossl_ech_2bcompressed(int ind) +int ossl_ech_2bcompressed(size_t ind) { - const int nexts = OSSL_NELEM(ext_defs); + const size_t nexts = OSSL_NELEM(ext_defs); # ifdef DUPEMALL return 0; @@ -1147,7 +1147,7 @@ int tls_construct_extensions(SSL_CONNECTION *s, WPACKET *pkt, if (ossl_ech_2bcompressed(i) == pass) continue; /* stash index - needed for COMPRESS ECH handling */ - s->ext.ech.ext_ind = i; + s->ext.ech.ext_ind = (int)i; #endif /* Skip if not relevant for our context */ if (!should_add_extension(s, thisexd->context, context, max_version)) diff --git a/ssl/statem/extensions_clnt.c b/ssl/statem/extensions_clnt.c index 70825d9bd2521..f777c22ff3f56 100644 --- a/ssl/statem/extensions_clnt.c +++ b/ssl/statem/extensions_clnt.c @@ -2707,7 +2707,7 @@ EXT_RETURN tls_construct_ctos_ech(SSL_CONNECTION *s, WPACKET *pkt, int tls_parse_stoc_ech(SSL_CONNECTION *s, PACKET *pkt, unsigned int context, X509 *x, size_t chainidx) { - unsigned int rlen = 0; + size_t rlen = 0; const unsigned char *rval = NULL; unsigned char *srval = NULL; PACKET rcfgs_pkt; diff --git a/ssl/tls13_enc.c b/ssl/tls13_enc.c index 2ff0b9437b73c..254aea063ea39 100644 --- a/ssl/tls13_enc.c +++ b/ssl/tls13_enc.c @@ -537,7 +537,7 @@ int tls13_change_cipher_state(SSL_CONNECTION *s, int which) SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); goto err; } - handlen = s->ext.ech.innerch_len; + handlen = (long)s->ext.ech.innerch_len; hdata = s->ext.ech.innerch; } else #endif diff --git a/test/ech_test.c b/test/ech_test.c index 2f3cddc27b1a7..07fd9bddf495b 100644 --- a/test/ech_test.c +++ b/test/ech_test.c @@ -833,7 +833,7 @@ static int ech_ingest_test(int run) char *pn = NULL, *ec = NULL; if ((in = BIO_new(BIO_s_mem())) == NULL - || BIO_write(in, tv->tv, tv->len) <= 0 + || BIO_write(in, tv->tv, (int)tv->len) <= 0 || (out = BIO_new(BIO_s_mem())) == NULL || (es = OSSL_ECHSTORE_new(NULL, NULL)) == NULL) goto end; From 9940be7879a87c838a7c65fe7e5a497ab37daf28 Mon Sep 17 00:00:00 2001 From: Tomas Mraz Date: Thu, 18 Sep 2025 18:01:49 +0200 Subject: [PATCH 13/24] ech_ssl_apis.c: Check some invalid argument passing Reviewed-by: Matt Caswell Reviewed-by: Paul Dale (Merged from https://github.com/openssl/openssl/pull/28611) --- ssl/ech/ech_ssl_apis.c | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/ssl/ech/ech_ssl_apis.c b/ssl/ech/ech_ssl_apis.c index 436e0fa71ccf9..45d04c616e964 100644 --- a/ssl/ech/ech_ssl_apis.c +++ b/ssl/ech/ech_ssl_apis.c @@ -296,8 +296,11 @@ int SSL_ech_get1_retry_config(SSL *ssl, unsigned char **ec, size_t *eclen) const char *propq = NULL; s = SSL_CONNECTION_FROM_SSL(ssl); - if (s == NULL || ec == NULL || eclen == NULL) + if (s == NULL || ec == NULL || eclen == NULL + || s->ext.ech.returned_len > INT_MAX) { + ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_INVALID_ARGUMENT); goto err; + } if (s->ext.ech.returned == NULL) { *ec = NULL; *eclen = 0; @@ -316,7 +319,7 @@ int SSL_ech_get1_retry_config(SSL *ssl, unsigned char **ec, size_t *eclen) propq = s->ext.ech.es->propq; } if ((in = BIO_new(BIO_s_mem())) == NULL - || BIO_write(in, s->ext.ech.returned, s->ext.ech.returned_len) <= 0 + || BIO_write(in, s->ext.ech.returned, (int)s->ext.ech.returned_len) <= 0 || (ve = OSSL_ECHSTORE_new(libctx, propq)) == NULL) { ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR); goto err; @@ -355,7 +358,7 @@ int SSL_CTX_ech_set1_outer_alpn_protos(SSL_CTX *ctx, if (protos == NULL) return 1; if (protos_len == 0) { - ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_NULL_PARAMETER); + ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_INVALID_ARGUMENT); return 0; } ctx->ext.ech.alpn_outer = OPENSSL_memdup(protos, protos_len); @@ -413,11 +416,11 @@ int SSL_set1_ech_config_list(SSL *ssl, const uint8_t *ecl, size_t ecl_len) s->ext.ech.es = NULL; return 1; } - if (ecl_len == 0) { - ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR); + if (ecl_len == 0 || ecl_len > INT_MAX) { + ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_INVALID_ARGUMENT); goto err; } - if ((es_in = BIO_new_mem_buf(ecl, ecl_len)) == NULL + if ((es_in = BIO_new_mem_buf(ecl, (int)ecl_len)) == NULL || (es = OSSL_ECHSTORE_new(NULL, NULL)) == NULL || OSSL_ECHSTORE_read_echconfiglist(es, es_in) != 1 || SSL_set1_echstore(ssl, es) != 1) { From 9716576033af43898f7bf201531f49cab7e172f3 Mon Sep 17 00:00:00 2001 From: Tomas Mraz Date: Thu, 18 Sep 2025 19:41:49 +0200 Subject: [PATCH 14/24] ech_store.c: Fix casts and avoid leaks on error return Reviewed-by: Matt Caswell Reviewed-by: Paul Dale (Merged from https://github.com/openssl/openssl/pull/28611) --- ssl/ech/ech_store.c | 33 +++++++++++++++++++++------------ 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/ssl/ech/ech_store.c b/ssl/ech/ech_store.c index c52c124568d3c..ee9fbd76feca0 100644 --- a/ssl/ech/ech_store.c +++ b/ssl/ech/ech_store.c @@ -248,11 +248,11 @@ static int ech_decode_echconfig_exts(OSSL_ECHSTORE_ENTRY *ee, PACKET *exts) static int ech_final_config_checks(OSSL_ECHSTORE_ENTRY *ee) { OSSL_HPKE_SUITE hpke_suite; - size_t ind, num; + int ind, num; int goodsuitefound = 0; /* check local support for some suite */ - for (ind = 0; ind != ee->nsuites; ind++) { + for (ind = 0; ind != (int)ee->nsuites; ind++) { /* * suite_check says yes to the pseudo-aead for export, but we don't * want to see it here coming from outside in an encoding @@ -298,12 +298,14 @@ static int ech_final_config_checks(OSSL_ECHSTORE_ENTRY *ee) static int ech_decode_one_entry(OSSL_ECHSTORE_ENTRY **rent, PACKET *pkt, EVP_PKEY *priv, int for_retry) { - unsigned int ech_content_length = 0, tmpi; + size_t ech_content_length = 0; + unsigned int tmpi; const unsigned char *tmpecp = NULL; size_t tmpeclen = 0, test_publen = 0; PACKET ver_pkt, pub_pkt, cipher_suites, public_name_pkt, exts; uint16_t thiskemid; - unsigned int suiteoctets = 0, ci = 0; + size_t suiteoctets = 0; + unsigned int ci = 0; unsigned char cipher[OSSL_ECH_CIPHER_LEN], max_name_len; unsigned char test_pub[OSSL_ECH_CRYPTO_VAR_SIZE]; OSSL_ECHSTORE_ENTRY *ee = NULL; @@ -350,12 +352,13 @@ static int ech_decode_one_entry(OSSL_ECHSTORE_ENTRY **rent, PACKET *pkt, || !PACKET_memdup(&pub_pkt, &ee->pub, &ee->pub_len) || !PACKET_get_length_prefixed_2(&ver_pkt, &cipher_suites) || (suiteoctets = PACKET_remaining(&cipher_suites)) <= 0 - || (suiteoctets % 2) == 1) { + || (suiteoctets % 2) == 1 + || suiteoctets / OSSL_ECH_CIPHER_LEN > UINT_MAX) { ERR_raise(ERR_LIB_SSL, SSL_R_ECH_DECODE_ERROR); goto err; } thiskemid = (uint16_t) tmpi; - ee->nsuites = suiteoctets / OSSL_ECH_CIPHER_LEN; + ee->nsuites = (unsigned int)(suiteoctets / OSSL_ECH_CIPHER_LEN); ee->suites = OPENSSL_malloc(ee->nsuites * sizeof(*ee->suites)); if (ee->suites == NULL) goto err; @@ -574,7 +577,7 @@ static int ech_read_priv_echconfiglist(OSSL_ECHSTORE *es, BIO *in, ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR); goto err; } - tdeclen = BIO_read(btmp, binbuf, encodedlen); + tdeclen = BIO_read(btmp, binbuf, (int)encodedlen); if (tdeclen <= 0) { /* need int for -1 return in failure case */ ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR); goto err; @@ -838,7 +841,7 @@ int OSSL_ECHSTORE_write_pem(OSSL_ECHSTORE *es, int index, BIO *out) goto err; } if (PEM_write_bio(out, PEM_STRING_ECHCONFIG, NULL, - ee->encoded, ee->encoded_len) <= 0) { + ee->encoded, (long)ee->encoded_len) <= 0) { ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR); goto err; } @@ -869,7 +872,7 @@ int OSSL_ECHSTORE_write_pem(OSSL_ECHSTORE *es, int index, BIO *out) WPACKET_get_total_written(&epkt, &allencoded_len); if (PEM_write_bio(out, PEM_STRING_ECHCONFIG, NULL, (unsigned char *)epkt_mem->data, - allencoded_len) <= 0) { + (long)allencoded_len) <= 0) { ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR); goto err; } @@ -914,12 +917,12 @@ int OSSL_ECHSTORE_get1_info(OSSL_ECHSTORE *es, int index, time_t *loaded_secs, return 0; } *loaded_secs = now - ee->loadtime; + *public_name = NULL; + *echconfig = NULL; if (ee->public_name != NULL) { *public_name = OPENSSL_strdup(ee->public_name); if (*public_name == NULL) goto err; - } else { - *public_name = NULL; } *has_private = (ee->keyshare == NULL ? 0 : 1); /* Now "print" the ECHConfigList */ @@ -951,10 +954,12 @@ int OSSL_ECHSTORE_get1_info(OSSL_ECHSTORE *es, int index, time_t *loaded_secs, ee->exts == NULL ? 0 : sk_OSSL_ECHEXT_num(ee->exts)); } ehlen = BIO_get_mem_data(out, &ignore); + if (ehlen > INT_MAX) + goto err; *echconfig = OPENSSL_malloc(ehlen + 1); if (*echconfig == NULL) goto err; - if (BIO_read(out, *echconfig, ehlen) <= 0) { + if (BIO_read(out, *echconfig, (int)ehlen) <= 0) { ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR); goto err; } @@ -963,6 +968,10 @@ int OSSL_ECHSTORE_get1_info(OSSL_ECHSTORE *es, int index, time_t *loaded_secs, return 1; err: BIO_free(out); + OPENSSL_free(*public_name); + *public_name = NULL; + OPENSSL_free(*echconfig); + *echconfig = NULL; return 0; } From 3daefc47907d5a7b0064af59d2ae8a23043dc9f9 Mon Sep 17 00:00:00 2001 From: sftcd Date: Mon, 15 Sep 2025 21:10:33 +0100 Subject: [PATCH 15/24] Fix a client-auth bug introduced by ECH code Reviewed-by: Tomas Mraz Reviewed-by: Matt Caswell (Merged from https://github.com/openssl/openssl/pull/28555) --- ssl/ech/ech_local.h | 9 +++++--- ssl/statem/extensions.c | 3 ++- ssl/statem/extensions_clnt.c | 44 ++++++++++++++++++------------------ 3 files changed, 30 insertions(+), 26 deletions(-) diff --git a/ssl/ech/ech_local.h b/ssl/ech/ech_local.h index 9928d1a2844d4..9edf349de1c73 100644 --- a/ssl/ech/ech_local.h +++ b/ssl/ech/ech_local.h @@ -282,7 +282,9 @@ typedef struct ossl_ech_conn_st { * be the same as in the inner. * * This macro should be called in each _ctos_ function that doesn't explicitly - * have special ECH handling. + * have special ECH handling. There are some _ctos_ functions that are called + * from a server, but we don't want to do anything in such cases. We also + * screen out cases where the context is not handling the ClientHello. * * Note that the placement of this macro needs a bit of thought - it has to go * after declarations (to keep the ansi-c compile happy) and also after any @@ -290,8 +292,9 @@ typedef struct ossl_ech_conn_st { * state changes that would affect a possible 2nd call to the constructor. * Luckily, that's usually not too hard, but it's not mechanical. */ -# define ECH_SAME_EXT(s, pkt) \ - if (s->ext.ech.es != NULL && s->ext.ech.grease == 0) { \ +# define ECH_SAME_EXT(s, context, pkt) \ + if (context == SSL_EXT_CLIENT_HELLO && !s->server \ + && s->ext.ech.es != NULL && s->ext.ech.grease == 0) { \ int ech_iosame_rv = ossl_ech_same_ext(s, pkt); \ \ if (ech_iosame_rv == OSSL_ECH_SAME_EXT_ERR) \ diff --git a/ssl/statem/extensions.c b/ssl/statem/extensions.c index 20b29f999ac1f..ba78bcedeabc7 100644 --- a/ssl/statem/extensions.c +++ b/ssl/statem/extensions.c @@ -1178,6 +1178,7 @@ int tls_construct_extensions(SSL_CONNECTION *s, WPACKET *pkt, * the real ECH extension value */ if (s->server + || context != SSL_EXT_CLIENT_HELLO || s->ext.ech.attempted == 0 || s->ext.ech.ch_depth == 1 || s->ext.ech.grease == OSSL_ECH_IS_GREASE) { @@ -2121,7 +2122,7 @@ static EXT_RETURN tls_construct_compress_certificate(SSL_CONNECTION *sc, WPACKET if (sc->cert_comp_prefs[0] == TLSEXT_comp_cert_none) return EXT_RETURN_NOT_SENT; # ifndef OPENSSL_NO_ECH - ECH_SAME_EXT(sc, pkt); + ECH_SAME_EXT(sc, context, pkt); # endif if (!WPACKET_put_bytes_u16(pkt, TLSEXT_TYPE_compress_certificate) diff --git a/ssl/statem/extensions_clnt.c b/ssl/statem/extensions_clnt.c index f777c22ff3f56..a77943ace01ac 100644 --- a/ssl/statem/extensions_clnt.c +++ b/ssl/statem/extensions_clnt.c @@ -40,7 +40,7 @@ EXT_RETURN tls_construct_ctos_renegotiate(SSL_CONNECTION *s, WPACKET *pkt, } #ifndef OPENSSL_NO_ECH - ECH_SAME_EXT(s, pkt) + ECH_SAME_EXT(s, context, pkt) #endif if (!WPACKET_put_bytes_u16(pkt, TLSEXT_TYPE_renegotiate) @@ -55,7 +55,7 @@ EXT_RETURN tls_construct_ctos_renegotiate(SSL_CONNECTION *s, WPACKET *pkt, } #ifndef OPENSSL_NO_ECH - ECH_SAME_EXT(s, pkt) + ECH_SAME_EXT(s, context, pkt) #endif /* Add a complete RI extension if renegotiating */ @@ -124,7 +124,7 @@ EXT_RETURN tls_construct_ctos_maxfragmentlen(SSL_CONNECTION *s, WPACKET *pkt, if (s->ext.max_fragment_len_mode == TLSEXT_max_fragment_length_DISABLED) return EXT_RETURN_NOT_SENT; #ifndef OPENSSL_NO_ECH - ECH_SAME_EXT(s, pkt) + ECH_SAME_EXT(s, context, pkt) #endif /* Add Max Fragment Length extension if client enabled it. */ @@ -153,7 +153,7 @@ EXT_RETURN tls_construct_ctos_srp(SSL_CONNECTION *s, WPACKET *pkt, if (s->srp_ctx.login == NULL) return EXT_RETURN_NOT_SENT; # ifndef OPENSSL_NO_ECH - ECH_SAME_EXT(s, pkt) + ECH_SAME_EXT(s, context, pkt) # endif if (!WPACKET_put_bytes_u16(pkt, TLSEXT_TYPE_srp) @@ -234,7 +234,7 @@ EXT_RETURN tls_construct_ctos_ec_pt_formats(SSL_CONNECTION *s, WPACKET *pkt, if (!use_ecc(s, min_version, max_version)) return EXT_RETURN_NOT_SENT; #ifndef OPENSSL_NO_ECH - ECH_SAME_EXT(s, pkt) + ECH_SAME_EXT(s, context, pkt) #endif /* Add TLS extension ECPointFormats to the ClientHello message */ @@ -274,7 +274,7 @@ EXT_RETURN tls_construct_ctos_supported_groups(SSL_CONNECTION *s, WPACKET *pkt, && (SSL_CONNECTION_IS_DTLS(s) || max_version < TLS1_3_VERSION)) return EXT_RETURN_NOT_SENT; #ifndef OPENSSL_NO_ECH - ECH_SAME_EXT(s, pkt) + ECH_SAME_EXT(s, context, pkt) #endif /* @@ -333,7 +333,7 @@ EXT_RETURN tls_construct_ctos_session_ticket(SSL_CONNECTION *s, WPACKET *pkt, if (!tls_use_ticket(s)) return EXT_RETURN_NOT_SENT; #ifndef OPENSSL_NO_ECH - ECH_SAME_EXT(s, pkt) + ECH_SAME_EXT(s, context, pkt) #endif if (!s->new_session && s->session != NULL @@ -393,7 +393,7 @@ EXT_RETURN tls_construct_ctos_sig_algs(SSL_CONNECTION *s, WPACKET *pkt, } #ifndef OPENSSL_NO_ECH - ECH_SAME_EXT(s, pkt) + ECH_SAME_EXT(s, context, pkt) #endif salglen = tls12_get_psigalgs(s, 1, &salg); @@ -426,7 +426,7 @@ EXT_RETURN tls_construct_ctos_status_request(SSL_CONNECTION *s, WPACKET *pkt, if (s->ext.status_type != TLSEXT_STATUSTYPE_ocsp) return EXT_RETURN_NOT_SENT; # ifndef OPENSSL_NO_ECH - ECH_SAME_EXT(s, pkt) + ECH_SAME_EXT(s, context, pkt) # endif if (!WPACKET_put_bytes_u16(pkt, TLSEXT_TYPE_status_request) @@ -489,7 +489,7 @@ EXT_RETURN tls_construct_ctos_npn(SSL_CONNECTION *s, WPACKET *pkt, || !SSL_IS_FIRST_HANDSHAKE(s)) return EXT_RETURN_NOT_SENT; # ifndef OPENSSL_NO_ECH - ECH_SAME_EXT(s, pkt) + ECH_SAME_EXT(s, context, pkt) # endif /* @@ -566,7 +566,7 @@ EXT_RETURN tls_construct_ctos_use_srtp(SSL_CONNECTION *s, WPACKET *pkt, if (clnt == NULL) return EXT_RETURN_NOT_SENT; # ifndef OPENSSL_NO_ECH - ECH_SAME_EXT(s, pkt) + ECH_SAME_EXT(s, context, pkt) # endif if (!WPACKET_put_bytes_u16(pkt, TLSEXT_TYPE_use_srtp) @@ -607,7 +607,7 @@ EXT_RETURN tls_construct_ctos_etm(SSL_CONNECTION *s, WPACKET *pkt, if (s->options & SSL_OP_NO_ENCRYPT_THEN_MAC) return EXT_RETURN_NOT_SENT; #ifndef OPENSSL_NO_ECH - ECH_SAME_EXT(s, pkt) + ECH_SAME_EXT(s, context, pkt) #endif if (!WPACKET_put_bytes_u16(pkt, TLSEXT_TYPE_encrypt_then_mac) @@ -631,7 +631,7 @@ EXT_RETURN tls_construct_ctos_sct(SSL_CONNECTION *s, WPACKET *pkt, if (x != NULL) return EXT_RETURN_NOT_SENT; # ifndef OPENSSL_NO_ECH - ECH_SAME_EXT(s, pkt) + ECH_SAME_EXT(s, context, pkt) # endif if (!WPACKET_put_bytes_u16(pkt, TLSEXT_TYPE_signed_certificate_timestamp) @@ -651,7 +651,7 @@ EXT_RETURN tls_construct_ctos_ems(SSL_CONNECTION *s, WPACKET *pkt, if (s->options & SSL_OP_NO_EXTENDED_MASTER_SECRET) return EXT_RETURN_NOT_SENT; #ifndef OPENSSL_NO_ECH - ECH_SAME_EXT(s, pkt) + ECH_SAME_EXT(s, context, pkt) #endif if (!WPACKET_put_bytes_u16(pkt, TLSEXT_TYPE_extended_master_secret) @@ -682,7 +682,7 @@ EXT_RETURN tls_construct_ctos_supported_versions(SSL_CONNECTION *s, WPACKET *pkt if (max_version < TLS1_3_VERSION) return EXT_RETURN_NOT_SENT; #ifndef OPENSSL_NO_ECH - ECH_SAME_EXT(s, pkt) + ECH_SAME_EXT(s, context, pkt) #endif if (!WPACKET_put_bytes_u16(pkt, TLSEXT_TYPE_supported_versions) @@ -717,7 +717,7 @@ EXT_RETURN tls_construct_ctos_psk_kex_modes(SSL_CONNECTION *s, WPACKET *pkt, int nodhe = s->options & SSL_OP_ALLOW_NO_DHE_KEX; # ifndef OPENSSL_NO_ECH - ECH_SAME_EXT(s, pkt) + ECH_SAME_EXT(s, context, pkt) # endif if (!WPACKET_put_bytes_u16(pkt, TLSEXT_TYPE_psk_kex_modes) @@ -812,7 +812,7 @@ EXT_RETURN tls_construct_ctos_key_share(SSL_CONNECTION *s, WPACKET *pkt, size_t valid_keyshare = 0; # ifndef OPENSSL_NO_ECH - ECH_SAME_EXT(s, pkt) + ECH_SAME_EXT(s, context, pkt) # endif /* key_share extension */ @@ -909,7 +909,7 @@ EXT_RETURN tls_construct_ctos_cookie(SSL_CONNECTION *s, WPACKET *pkt, if (s->ext.tls13_cookie_len == 0) return EXT_RETURN_NOT_SENT; #ifndef OPENSSL_NO_ECH - ECH_SAME_EXT(s, pkt) + ECH_SAME_EXT(s, context, pkt) #endif if (!WPACKET_put_bytes_u16(pkt, TLSEXT_TYPE_cookie) @@ -1140,7 +1140,7 @@ EXT_RETURN tls_construct_ctos_padding(SSL_CONNECTION *s, WPACKET *pkt, if ((s->options & SSL_OP_TLSEXT_PADDING) == 0) return EXT_RETURN_NOT_SENT; #ifndef OPENSSL_NO_ECH - ECH_SAME_EXT(s, pkt); + ECH_SAME_EXT(s, context, pkt); #endif /* @@ -1514,7 +1514,7 @@ EXT_RETURN tls_construct_ctos_post_handshake_auth(SSL_CONNECTION *s, WPACKET *pk if (!s->pha_enabled) return EXT_RETURN_NOT_SENT; # ifndef OPENSSL_NO_ECH - ECH_SAME_EXT(s, pkt) + ECH_SAME_EXT(s, context, pkt) # endif /* construct extension - 0 length, no contents */ @@ -2411,7 +2411,7 @@ EXT_RETURN tls_construct_ctos_client_cert_type(SSL_CONNECTION *sc, WPACKET *pkt, if (sc->client_cert_type == NULL) return EXT_RETURN_NOT_SENT; #ifndef OPENSSL_NO_ECH - ECH_SAME_EXT(sc, pkt) + ECH_SAME_EXT(sc, context, pkt) #endif if (!WPACKET_put_bytes_u16(pkt, TLSEXT_TYPE_client_cert_type) @@ -2466,7 +2466,7 @@ EXT_RETURN tls_construct_ctos_server_cert_type(SSL_CONNECTION *sc, WPACKET *pkt, if (sc->server_cert_type == NULL) return EXT_RETURN_NOT_SENT; #ifndef OPENSSL_NO_ECH - ECH_SAME_EXT(sc, pkt) + ECH_SAME_EXT(sc, context, pkt) #endif if (!WPACKET_put_bytes_u16(pkt, TLSEXT_TYPE_server_cert_type) From f00ca8cd6b1dc4e2d7cdaf5864f41f414791b0d6 Mon Sep 17 00:00:00 2001 From: sftcd Date: Thu, 14 Aug 2025 19:17:07 +0100 Subject: [PATCH 16/24] s_client and s_server options for ECH Reviewed-by: Matt Caswell Reviewed-by: Tomas Mraz (Merged from https://github.com/openssl/openssl/pull/28270) --- apps/s_client.c | 211 ++++++++-- apps/s_server.c | 494 ++++++++++++++++++++++- doc/man1/openssl-s_client.pod.in | 67 +++ doc/man1/openssl-s_server.pod.in | 33 ++ ssl/ech/ech_internal.c | 13 +- ssl/ech/ech_ssl_apis.c | 2 + ssl/ech/ech_store.c | 4 +- ssl/statem/extensions.c | 2 +- ssl/statem/extensions_clnt.c | 6 +- test/ech_test.c | 71 +++- test/recipes/82-test_ech_client_server.t | 342 ++++++++++++++++ 11 files changed, 1197 insertions(+), 48 deletions(-) create mode 100644 test/recipes/82-test_ech_client_server.t diff --git a/apps/s_client.c b/apps/s_client.c index f9f11f642797a..9fcf533ed9d2c 100644 --- a/apps/s_client.c +++ b/apps/s_client.c @@ -108,7 +108,12 @@ static BIO *bio_c_out = NULL; static int c_quiet = 0; static char *sess_out = NULL; # ifndef OPENSSL_NO_ECH -static char *ech_config_list = NULL; +static char *ech_config_list = NULL, *ech_grease_suite = NULL; +static const char *sni_outer_name = NULL; +static int ech_grease = 0, ech_ignore_cid = 0; +static int ech_select = OSSL_ECHSTORE_ALL; +static int ech_grease_type = OSSL_ECH_CURRENT_VERSION; +static int ech_no_outer_sni = 0; # endif static SSL_SESSION *psksess = NULL; @@ -526,7 +531,10 @@ typedef enum OPTION_choice { OPT_SCTP_LABEL_BUG, OPT_KTLS, # ifndef OPENSSL_NO_ECH - OPT_ECHCONFIGLIST, + OPT_ECHCONFIGLIST, OPT_SNIOUTER, OPT_ALPN_OUTER, + OPT_ECH_SELECT, OPT_ECH_IGNORE_CONFIG_ID, + OPT_ECH_GREASE, OPT_ECH_GREASE_SUITE, OPT_ECH_GREASE_TYPE, + OPT_ECH_NO_OUTER_SNI, # endif OPT_R_ENUM, OPT_PROV_ENUM } OPTION_CHOICE; @@ -728,14 +736,31 @@ const OPTIONS s_client_options[] = { {"enable_pha", OPT_ENABLE_PHA, '-', "Enable post-handshake-authentication"}, {"enable_server_rpk", OPT_ENABLE_SERVER_RPK, '-', "Enable raw public keys (RFC7250) from the server"}, {"enable_client_rpk", OPT_ENABLE_CLIENT_RPK, '-', "Enable raw public keys (RFC7250) from the client"}, -# ifndef OPENSSL_NO_ECH - {"ech_config_list", OPT_ECHCONFIGLIST, 's', - "Set ECHConfigList, value is base 64 encoded ECHConfigList"}, -# endif #ifndef OPENSSL_NO_SRTP {"use_srtp", OPT_USE_SRTP, 's', "Offer SRTP key management with a colon-separated profile list"}, #endif + +# ifndef OPENSSL_NO_ECH + {"ech_config_list", OPT_ECHCONFIGLIST, 's', + "Set ECHConfigList, value is base64-encoded ECHConfigList"}, + {"ech_outer_alpn", OPT_ALPN_OUTER, 's', + "Specify outer ALPN value, when using ECH (comma-separated list)"}, + {"ech_outer_sni", OPT_SNIOUTER, 's', + "The name to put in the outer CH when overriding the server's choice"}, + {"ech_no_outer_sni", OPT_ECH_NO_OUTER_SNI, '-', + "Do not send the server name (SNI) extension in the outer ClientHello"}, + {"ech_select", OPT_ECH_SELECT, 'n', + "Select one ECHConfig from the set provided via -ech_config_list"}, + {"ech_grease", OPT_ECH_GREASE, '-', + "Send GREASE values when not really using ECH"}, + {"ech_grease_suite", OPT_ECH_GREASE_SUITE, 's', + "Use this HPKE suite for GREASE values when not really using ECH"}, + {"ech_grease_type", OPT_ECH_GREASE_TYPE, 'n', + "Use this TLS extension type for GREASE values when not really using ECH"}, + {"ech_ignore_cid", OPT_ECH_IGNORE_CONFIG_ID, '-', + "Ignore the server-chosen ECH config ID and send a random value"}, +# endif #ifndef OPENSSL_NO_SRP {"srpuser", OPT_SRPUSER, 's', "(deprecated) SRP authentication for 'user'"}, {"srppass", OPT_SRPPASS, 's', "(deprecated) Password for 'user'"}, @@ -934,6 +959,11 @@ int s_client_main(int argc, char **argv) char *sname_alloc = NULL; int noservername = 0; const char *alpn_in = NULL; +# ifndef OPENSSL_NO_ECH + const char *alpn_outer_in = NULL; + int rv = 0; + OSSL_ECHSTORE *es = NULL; +# endif tlsextctx tlsextcbp = { NULL, 0 }; const char *ssl_config = NULL; #define MAX_SI_TYPES 100 @@ -1541,6 +1571,30 @@ int s_client_main(int argc, char **argv) case OPT_ECHCONFIGLIST: ech_config_list = opt_arg(); break; + case OPT_ALPN_OUTER: + alpn_outer_in = opt_arg(); + break; + case OPT_SNIOUTER: + sni_outer_name = opt_arg(); + break; + case OPT_ECH_SELECT: + ech_select = atoi(opt_arg()); + break; + case OPT_ECH_GREASE: + ech_grease = 1; + break; + case OPT_ECH_GREASE_SUITE: + ech_grease_suite = opt_arg(); + break; + case OPT_ECH_GREASE_TYPE: + ech_grease_type = atoi(opt_arg()); + break; + case OPT_ECH_IGNORE_CONFIG_ID: + ech_ignore_cid = 1; + break; + case OPT_ECH_NO_OUTER_SNI: + ech_no_outer_sni = 1; + break; # endif case OPT_NOSERVERNAME: noservername = 1; @@ -1654,7 +1708,16 @@ int s_client_main(int argc, char **argv) goto opthelp; } } - +# ifndef OPENSSL_NO_ECH + if ((alpn_outer_in != NULL || sni_outer_name != NULL + || ech_no_outer_sni == 1) + && ech_config_list == NULL) { + BIO_printf(bio_err, "%s: Can't use -ech_outer_sni nor " + "-ech_outer_alpn nor -no_ech_outer_sni without " + "-ech_config_list\n", prog); + goto opthelp; + } +# endif #ifndef OPENSSL_NO_NEXTPROTONEG if (min_version == TLS1_3_VERSION && next_proto_neg_in != NULL) { BIO_printf(bio_err, "Cannot supply -nextprotoneg with TLSv1.3\n"); @@ -1885,6 +1948,13 @@ int s_client_main(int argc, char **argv) SSL_CTX_set_options(ctx, SSL_OP_ENABLE_KTLS); #endif +# ifndef OPENSSL_NO_ECH + if (ech_grease != 0) + SSL_CTX_set_options(ctx, SSL_OP_ECH_GREASE); + if (ech_ignore_cid != 0) + SSL_CTX_set_options(ctx, SSL_OP_ECH_IGNORE_CID); +# endif + if (vpmtouched && !SSL_CTX_set1_param(ctx, vpm)) { BIO_printf(bio_err, "Error setting verify params\n"); goto end; @@ -2090,6 +2160,26 @@ int s_client_main(int argc, char **argv) if (set_keylog_file(ctx, keylog_file)) goto end; +# ifndef OPENSSL_NO_ECH + if (alpn_outer_in != NULL) { + size_t alpn_outer_len; + unsigned char *alpn_outer = NULL; + + alpn_outer = next_protos_parse(&alpn_outer_len, alpn_outer_in); + if (alpn_outer == NULL) { + BIO_printf(bio_err, "Error parsing -ech_outer_alpn argument\n"); + goto end; + } + if (SSL_CTX_ech_set1_outer_alpn_protos(ctx, alpn_outer, + alpn_outer_len) != 1) { + BIO_printf(bio_err, "Error setting ALPN-OUTER\n"); + OPENSSL_free(alpn_outer); + goto end; + } + OPENSSL_free(alpn_outer); + } +# endif + con = SSL_new(ctx); if (con == NULL) goto end; @@ -2109,6 +2199,25 @@ int s_client_main(int argc, char **argv) } } +# ifndef OPENSSL_NO_ECH + if (ech_grease_suite != NULL) { + if (SSL_ech_set1_grease_suite(con, ech_grease_suite) != 1) { + ERR_print_errors(bio_err); + goto end; + } + } + /* no point in setting to our default */ + if (ech_grease_type != OSSL_ECH_CURRENT_VERSION) { + BIO_printf(bio_err, "Setting GREASE ECH type 0x%4x\n", ech_grease_type); + if (SSL_ech_set_grease_type(con, ech_grease_type) != 1) { + BIO_printf(bio_err, "Can't set GREASE ECH type 0x%4x\n", + ech_grease_type); + ERR_print_errors(bio_err); + goto end; + } + } +# endif + if (sess_in != NULL) { SSL_SESSION *sess; BIO *stmp = BIO_new_file(sess_in, "r"); @@ -2123,6 +2232,7 @@ int s_client_main(int argc, char **argv) goto end; } if (!SSL_set_session(con, sess)) { + SSL_SESSION_free(sess); BIO_printf(bio_err, "Can't set session\n"); goto end; } @@ -2145,10 +2255,47 @@ int s_client_main(int argc, char **argv) } # ifndef OPENSSL_NO_ECH - if (ech_config_list != NULL - && SSL_set1_ech_config_list(con, (unsigned char *)ech_config_list, - strlen(ech_config_list)) != 1) - goto end; + if (ech_config_list != NULL) { + if (SSL_set1_ech_config_list(con, (unsigned char *)ech_config_list, + strlen(ech_config_list)) != 1) { + BIO_printf(bio_err, "%s: error setting ECHConfigList.\n", prog); + goto end; + } + if (ech_no_outer_sni == 1) { + if (sni_outer_name != NULL) { + BIO_printf(bio_err, "%s: can't set -ech_no_outer_sni and " + "-ech_outer_sni together.\n", prog); + goto end; + } + if (SSL_ech_set1_outer_server_name(con, NULL, 1) != 1) { + BIO_printf(bio_err, "%s: setting no ECH outer name failed.\n", + prog); + ERR_print_errors(bio_err); + goto end; + } + } + if (sni_outer_name != NULL) { + rv = SSL_ech_set1_outer_server_name(con, sni_outer_name, 0); + if (rv != 1) { + BIO_printf(bio_err, "%s: setting ECH outer name to %s failed.\n", + prog, sni_outer_name); + ERR_print_errors(bio_err); + goto end; + } + } + } + if (ech_select != OSSL_ECHSTORE_ALL) { + if ((es = SSL_get1_echstore(con)) == NULL + || OSSL_ECHSTORE_downselect(es, ech_select) != 1 + || SSL_set1_echstore(con, es) != 1) { + BIO_printf(bio_err, "%s: ECH downselect to (%d) failed.\n", + prog, ech_select); + ERR_print_errors(bio_err); + goto end; + } + OSSL_ECHSTORE_free(es); + es = NULL; + } # endif if (dane_tlsa_domain != NULL) { @@ -3371,6 +3518,9 @@ int s_client_main(int argc, char **argv) bio_c_out = NULL; BIO_free(bio_c_msg); bio_c_msg = NULL; +# ifndef OPENSSL_NO_ECH + OSSL_ECHSTORE_free(es); +# endif return ret; } @@ -3446,11 +3596,12 @@ static void print_ech_retry_configs(BIO *bio, SSL *s) for (ind = 0; ind != cnt; ind++) { if (OSSL_ECHSTORE_get1_info(es, ind, &secs, &pn, &ec, &has_priv, &for_retry) != 1) { - BIO_printf(bio, "ECH: Error getting retry-config %d\n", ind); + BIO_printf(bio, "ECH: Error getting retry-config %d.\n", ind); goto end; } - BIO_printf(bio, "ECH: entry: %d public_name: %s age: %d%s\n", - ind, pn, (int)secs, has_priv ? " (has private key)" : ""); + BIO_printf(bio, "ECH: entry: %d public_name: %s age: %lld%s\n", + ind, pn, (long long)secs, + has_priv ? " (has private key)" : ""); BIO_printf(bio, "ECH: \t%s\n", ec); OPENSSL_free(pn); pn = NULL; @@ -3466,6 +3617,7 @@ static void print_ech_retry_configs(BIO *bio, SSL *s) return; } +/* outcomes marked as "odd" shouldn't happen in s_client */ static void print_ech_status(BIO *bio, SSL *s, int estat) { switch (estat) { @@ -3482,7 +3634,7 @@ static void print_ech_status(BIO *bio, SSL *s, int estat) BIO_printf(bio, "ECH: success: %d\n", estat); break; case SSL_ECH_STATUS_GREASE_ECH: - BIO_printf(bio, "ECH: GREASE+retry-configs%d\n", estat); + BIO_printf(bio, "ECH: GREASE+retry-configs: %d\n", estat); break; case SSL_ECH_STATUS_BACKEND: BIO_printf(bio, "ECH: BACKEND: %d\n", estat); @@ -3523,6 +3675,10 @@ static void print_stuff(BIO *bio, SSL *s, int full) #ifndef OPENSSL_NO_CT const SSL_CTX *ctx = SSL_get_SSL_CTX(s); #endif +# ifndef OPENSSL_NO_ECH + char *inner = NULL, *outer = NULL; + int estat = 0; +# endif if (full) { int got_a_chain = 0; @@ -3753,22 +3909,17 @@ static void print_stuff(BIO *bio, SSL *s, int full) } BIO_printf(bio, "---\n"); # ifndef OPENSSL_NO_ECH - { - char *inner = NULL, *outer = NULL; - int estat = 0; - - estat = SSL_ech_get1_status(s, &inner, &outer); - print_ech_status(bio, s, estat); - if (estat == SSL_ECH_STATUS_SUCCESS) { - BIO_printf(bio, "ECH: inner: %s\n", inner); - BIO_printf(bio, "ECH: outer: %s\n", outer); - } - if (estat == SSL_ECH_STATUS_FAILED_ECH - || estat == SSL_ECH_STATUS_FAILED_ECH_BAD_NAME) - print_ech_retry_configs(bio, s); - OPENSSL_free(inner); - OPENSSL_free(outer); + estat = SSL_ech_get1_status(s, &inner, &outer); + print_ech_status(bio, s, estat); + if (estat == SSL_ECH_STATUS_SUCCESS) { + BIO_printf(bio, "ECH: inner: %s\n", inner); + BIO_printf(bio, "ECH: outer: %s\n", outer); } + if (estat == SSL_ECH_STATUS_FAILED_ECH + || estat == SSL_ECH_STATUS_FAILED_ECH_BAD_NAME) + print_ech_retry_configs(bio, s); + OPENSSL_free(inner); + OPENSSL_free(outer); BIO_printf(bio, "---\n"); # endif diff --git a/apps/s_server.c b/apps/s_server.c index 82590f9adbbd8..572787edde14c 100644 --- a/apps/s_server.c +++ b/apps/s_server.c @@ -18,6 +18,9 @@ #if defined(_WIN32) /* Included before async.h to avoid some warnings */ # include +# if !defined(OPENSSL_NO_ECH) && !defined(PATH_MAX) +# define PATH_MAX 4096 +# endif #endif #include @@ -26,6 +29,27 @@ #include #include "internal/sockets.h" /* for openssl_fdset() */ +#ifndef OPENSSL_NO_ECH +/* to use tracing, if configured and requested */ +# ifndef OPENSSL_NO_SSL_TRACE +# include +# endif +/* sockaddr stuff */ +# if defined(_WIN32) +# include +# include +# include +# else +# include +# include +# include +# include +# endif +/* for timing in some TRACE statements */ +# include +# include "internal/o_dir.h" /* for OPENSSL_DIR_read */ +#endif + #ifndef OPENSSL_NO_SOCK /* @@ -59,6 +83,11 @@ typedef unsigned int u_int; #include "internal/sockets.h" #include "internal/statem.h" +# ifndef OPENSSL_NO_ECH +/* needed for X509_check_host in some CI builds "no-http" */ +# include +# endif + static int not_resumable_sess_cb(SSL *s, int is_forward_secure); static int sv_body(int s, int stype, int prot, unsigned char *context); static int www_body(int s, int stype, int prot, unsigned char *context); @@ -72,6 +101,10 @@ static void init_session_cache_ctx(SSL_CTX *sctx); static void free_sessions(void); static void print_connection_info(SSL *con); +# ifndef OPENSSL_NO_ECH +static unsigned int ech_print_cb(SSL *s, const char *str); +# endif + static const int bufsize = 16 * 1024; static int accept_socket = -1; @@ -420,8 +453,194 @@ typedef struct tlsextctx_st { char *servername; BIO *biodebug; int extension_error; + X509 *scert; /* ECH needs 2nd cert for testing */ } tlsextctx; +# ifndef OPENSSL_NO_ECH +static unsigned int ech_print_cb(SSL *s, const char *str) +{ + if (str != NULL) + BIO_printf(bio_s_out, "ECH Server callback printing: \n%s\n", str); + return 1; +} + +/* + * The server has possibly 2 TLS server names basically in ctx and ctx2. So we + * need to check if any client-supplied SNI in the inner/outer matches either + * and serve whichever is appropriate. X509_check_host is the way to do that, + * given an X509* pointer. + * + * We default to the "main" ctx if the client-supplied SNI does not match the + * ctx2 certificate. We don't fail if the client-supplied SNI matches neither, + * but just continue with the "main" ctx. If the client-supplied SNI matches + * both ctx and ctx2, then we'll switch to ctx2 anyway - we don't try for a + * "best" match in that case. + * + * Note that since we attempt ECH decryption whenever configured to do that, + * the only way to get the "outer" SNI is via SSL_ech_get1_status. + */ + +/* apparently 26 is all we need, but round it up to 32 to be on the safe side */ +# define ECH_TIME_STR_LEN 32 + +static int ssl_ech_servername_cb(SSL *s, int *ad, void *arg) +{ + tlsextctx *p = (tlsextctx *) arg; + time_t now = time(0); /* For a bit of basic logging */ + int sockfd = 0, res = 0, echrv = 0; + size_t srv = 0; + struct sockaddr_storage ss; + socklen_t salen = sizeof(ss); + struct sockaddr *sa; + char clientip[INET6_ADDRSTRLEN], lstr[ECH_TIME_STR_LEN]; + const char *servername = NULL; + char *inner_sni = NULL, *outer_sni = NULL; + struct tm local; +# if !defined(OPENSSL_SYS_WINDOWS) + struct tm *local_p = NULL; +# else + errno_t grv; +# endif + +# if !defined(OPENSSL_SYS_WINDOWS) + local_p = gmtime_r(&now, &local); + if (local_p != &local) { + strcpy(lstr, "sometime"); + } else { + srv = strftime(lstr, ECH_TIME_STR_LEN, "%c", &local); + if (srv == 0) + strcpy(lstr, "sometime"); + } +# else + grv = gmtime_s(&local, &now); + if (grv != 0) { + strcpy(lstr, "sometime"); + } else { + srv = strftime(lstr, ECH_TIME_STR_LEN, "%c", &local); + if (srv == 0) + strcpy(lstr, "sometime"); + } +# endif + memset(clientip, 0, INET6_ADDRSTRLEN); + strncpy(clientip, "unknown", INET6_ADDRSTRLEN); + memset(&ss, 0, salen); + sa = (struct sockaddr *)&ss; + res = BIO_get_fd(SSL_get_wbio(s), &sockfd); + if (res != -1) { +# if !defined(_WIN32) + res = getpeername(sockfd, sa, &salen); +# else + res = getpeername(sockfd, sa, (int *)&salen); +# endif + if (res == 0) + res = getnameinfo(sa, salen, clientip, INET6_ADDRSTRLEN, + 0, 0, NI_NUMERICHOST); + } + /* Name that matches "main" ctx */ + servername = SSL_get_servername(s, TLSEXT_NAMETYPE_host_name); + echrv = SSL_ech_get1_status(s, &inner_sni, &outer_sni); + if (p->biodebug != NULL) { + /* spit out basic logging */ + BIO_printf(p->biodebug, + "ssl_ech_servername_cb: connection from %s at %s\n", + clientip, lstr); + /* Client supplied SNI from inner and outer */ + switch (echrv) { + case SSL_ECH_STATUS_BACKEND: + BIO_printf(p->biodebug, + "ssl_ech_servername_cb: ECH backend got inner ECH\n"); + break; + case SSL_ECH_STATUS_NOT_CONFIGURED: + BIO_printf(p->biodebug, + "ssl_ech_servername_cb: ECH not configured\n"); + break; + case SSL_ECH_STATUS_GREASE: + BIO_printf(p->biodebug, + "ssl_ech_servername_cb: attempt we think is GREASE\n"); + break; + case SSL_ECH_STATUS_NOT_TRIED: + BIO_printf(p->biodebug, + "ssl_ech_servername_cb: not attempted\n"); + break; + case SSL_ECH_STATUS_FAILED: + BIO_printf(p->biodebug, + "ssl_ech_servername_cb: tried but failed\n"); + break; + case SSL_ECH_STATUS_BAD_CALL: + BIO_printf(p->biodebug, + "ssl_ech_servername_cb: bad input to API\n"); + break; + case SSL_ECH_STATUS_BAD_NAME: + BIO_printf(p->biodebug, + "ssl_ech_servername_cb: worked but bad name\n"); + break; + case SSL_ECH_STATUS_SUCCESS: + BIO_printf(p->biodebug, + "ssl_ech_servername_cb: success: outer %s, inner: %s\n", + (outer_sni == NULL ? "none" : outer_sni), + (inner_sni == NULL ? "none" : inner_sni)); + break; + default: + BIO_printf(p->biodebug, + "ssl_ech_servername_cb: Error getting ECH status\n"); + break; + } + } + OPENSSL_free(inner_sni); + OPENSSL_free(outer_sni); + if (servername != NULL && p->biodebug != NULL) { + const char *cp = servername; + unsigned char uc; + + BIO_printf(p->biodebug, + "ssl_ech_servername_cb: Hostname in TLS extension: \""); + while ((uc = *cp++) != 0) + BIO_printf(p->biodebug, + isascii(uc) && isprint(uc) ? "%c" : "\\x%02x", uc); + BIO_printf(p->biodebug, "\"\n"); + if (p->servername != NULL) + BIO_printf(p->biodebug, + "ssl_ech_servername_cb: ctx servername: %s\n", + p->servername); + else + BIO_printf(p->biodebug, + "ssl_ech_servername_cb: ctx servername is NULL\n"); + if (p->scert == NULL) + BIO_printf(p->biodebug, + "ssl_ech_servername_cb: No 2nd cert! That's bad.\n"); + } + if (p->servername == NULL) + return SSL_TLSEXT_ERR_NOACK; + if (p->scert == NULL) + return SSL_TLSEXT_ERR_NOACK; + if (echrv == SSL_ECH_STATUS_SUCCESS && servername != NULL) { + if (ctx2 != NULL) { + int check_host = X509_check_host(p->scert, servername, 0, 0, NULL); + + if (check_host == 1) { + if (p->biodebug != NULL) + BIO_printf(p->biodebug, + "ssl_ech_servername_cb: Switching context.\n"); + SSL_set_SSL_CTX(s, ctx2); + } else { + if (p->biodebug != NULL) + BIO_printf(p->biodebug, + "ssl_ech_servername_cb: Not switching context " + "- no name match (%d).\n", check_host); + } + } + } else { + if (p->biodebug != NULL) + BIO_printf(p->biodebug, + "ssl_ech_servername_cb: Not switching context " + "- no ECH SUCCESS\n"); + } + return SSL_TLSEXT_ERR_OK; +} +/* Below is the "original" ssl_servername_cb, before ECH */ + +# else + static int ssl_servername_cb(SSL *s, int *ad, void *arg) { tlsextctx *p = (tlsextctx *) arg; @@ -452,6 +671,8 @@ static int ssl_servername_cb(SSL *s, int *ad, void *arg) return SSL_TLSEXT_ERR_OK; } +# endif + /* Structure passed to cert status callback */ typedef struct tlsextstatusctx_st { int timeout; @@ -960,6 +1181,10 @@ typedef enum OPTION_choice { OPT_TFO, OPT_CERT_COMP, OPT_ENABLE_SERVER_RPK, OPT_ENABLE_CLIENT_RPK, +# ifndef OPENSSL_NO_ECH + OPT_ECH_PEM, OPT_ECH_DIR, OPT_ECH_NORETRY, + OPT_ECH_TRIALDECRYPT, OPT_ECH_GREASE_RT, +# endif OPT_R_ENUM, OPT_S_ENUM, OPT_V_ENUM, @@ -1209,6 +1434,19 @@ const OPTIONS s_server_options[] = { #endif {"alpn", OPT_ALPN, 's', "Set the advertised protocols for the ALPN extension (comma-separated list)"}, + +# ifndef OPENSSL_NO_ECH + {"ech_key", OPT_ECH_PEM, 's', "Load ECH PEM-formatted key pair"}, + {"ech_dir", OPT_ECH_DIR, 's', "Load ECH key pairs (for retries) " \ + "from the specified directory"}, + {"ech_noretry_dir", OPT_ECH_NORETRY, 's', "Load ECH key pairs (not " \ + "for retry) from the specified directory"}, + {"ech_trialdecrypt", OPT_ECH_TRIALDECRYPT, '-', + "Do trial decryption even if ECH record_digest matching fails"}, + {"ech_greaseretries", OPT_ECH_GREASE_RT, '-', + "Set server to GREASE retry_config values"}, +# endif + #ifndef OPENSSL_NO_KTLS {"ktls", OPT_KTLS, '-', "Enable Kernel TLS for sending and receiving"}, {"sendfile", OPT_SENDFILE, '-', "Use sendfile to response file with -WWW"}, @@ -1224,6 +1462,64 @@ const OPTIONS s_server_options[] = { {NULL} }; +# ifndef OPENSSL_NO_ECH +static int ech_load_dir(SSL_CTX *lctx, const char *thedir, + int for_retry, int *nloaded) +{ + size_t elen = strlen(thedir); + OPENSSL_DIR_CTX *d = NULL; + const char *thisfile = NULL; + OSSL_ECHSTORE *es = NULL; + BIO *in = NULL; + int loaded = 0; + + if ((elen + 7) >= PATH_MAX) { /* too long, go away */ + BIO_printf(bio_err, "'%s' too long - exiting\n", thedir); + return 0; + } + if (app_isdir(thedir) <= 0) { /* if not a directory, ignore it */ + BIO_printf(bio_err, "'%s' not a directory - exiting\n", thedir); + return 0; + } + if ((es = SSL_CTX_get1_echstore(lctx)) == NULL + && (es = OSSL_ECHSTORE_new(app_get0_libctx(), + app_get0_propq())) == NULL) { + BIO_printf(bio_err, "internal error\n"); + return 0; + } + while ((thisfile = OPENSSL_DIR_read(&d, thedir))) { + char filepath[PATH_MAX]; + int r; + +# ifdef OPENSSL_SYS_VMS + r = BIO_snprintf(filepath, sizeof(filepath), "%s%s", thedir, thisfile); +# else + r = BIO_snprintf(filepath, sizeof(filepath), "%s/%s", thedir, thisfile); +# endif + if (r < 0 + || app_isdir(filepath) > 0 + || (in = BIO_new_file(filepath, "r")) == NULL + || OSSL_ECHSTORE_read_pem(es, in, for_retry) != 1) { + BIO_printf(bio_err, "Failed reading from: %s\n", thisfile); + continue; + } + BIO_free_all(in); + if (bio_s_out != NULL) + BIO_printf(bio_s_out, "Added ECH key pair from: %s\n", thisfile); + loaded++; + } + if (SSL_CTX_set1_echstore(lctx, es) != 1) { + BIO_printf(bio_err, "internal error\n"); + return 0; + } + if (bio_s_out != NULL) + BIO_printf(bio_s_out, "Added %d ECH key pairs from: %s\n", + loaded, thedir); + *nloaded = loaded; + return 1; +} +# endif + #define IS_PROT_FLAG(o) \ (o == OPT_SSL3 || o == OPT_TLS1 || o == OPT_TLS1_1 || o == OPT_TLS1_2 \ || o == OPT_TLS1_3 || o == OPT_DTLS || o == OPT_DTLS1 || o == OPT_DTLS1_2) @@ -1266,7 +1562,7 @@ int s_server_main(int argc, char *argv[]) OPTION_CHOICE o; EVP_PKEY *s_key2 = NULL; X509 *s_cert2 = NULL; - tlsextctx tlsextcbp = { NULL, NULL, SSL_TLSEXT_ERR_ALERT_WARNING }; + tlsextctx tlsextcbp = { NULL, NULL, SSL_TLSEXT_ERR_ALERT_WARNING, NULL }; const char *ssl_config = NULL; int read_buf_len = 0; #ifndef OPENSSL_NO_NEXTPROTONEG @@ -1304,6 +1600,14 @@ int s_server_main(int argc, char *argv[]) int max_early_data = -1, recv_max_early_data = -1; char *psksessf = NULL; int no_ca_names = 0; +# ifndef OPENSSL_NO_ECH + char *echkeyfile = NULL; + char *echkeydir = NULL; + char *echnoretrydir = NULL; + int ech_files_loaded = 0; + int echtrialdecrypt = 0; /* trial decryption off by default */ + int echgrease_rc = 0; /* retry_config GREASEing off by default */ +# endif #ifndef OPENSSL_NO_SCTP int sctp_label_bug = 0; #endif @@ -1904,6 +2208,23 @@ int s_server_main(int argc, char *argv[]) case OPT_HTTP_SERVER_BINMODE: http_server_binmode = 1; break; +# ifndef OPENSSL_NO_ECH + case OPT_ECH_PEM: + echkeyfile = opt_arg(); + break; + case OPT_ECH_DIR: + echkeydir = opt_arg(); + break; + case OPT_ECH_NORETRY: + echnoretrydir = opt_arg(); + break; + case OPT_ECH_TRIALDECRYPT: + echtrialdecrypt = 1; + break; + case OPT_ECH_GREASE_RT: + echgrease_rc = 1; + break; +# endif case OPT_NOCANAMES: no_ca_names = 1; break; @@ -2063,6 +2384,9 @@ int s_server_main(int argc, char *argv[]) if (s_cert2 == NULL) goto end; +# ifndef OPENSSL_NO_ECH + tlsextcbp.scert = s_cert2; +# endif } } #if !defined(OPENSSL_NO_NEXTPROTONEG) @@ -2275,12 +2599,69 @@ int s_server_main(int argc, char *argv[]) goto end; } +# ifndef OPENSSL_NO_ECH + if (echtrialdecrypt != 0) + SSL_CTX_set_options(ctx, SSL_OP_ECH_TRIALDECRYPT); + if (echgrease_rc != 0) + SSL_CTX_set_options(ctx, SSL_OP_ECH_GREASE_RETRY_CONFIG); + if (echkeyfile != NULL) { + OSSL_ECHSTORE *es = NULL; + BIO *in = NULL; + + if ((in = BIO_new_file(echkeyfile, "r")) == NULL + || (es = OSSL_ECHSTORE_new(app_get0_libctx(), + app_get0_propq())) == 0 + || OSSL_ECHSTORE_read_pem(es, in, OSSL_ECH_FOR_RETRY) != 1 + || SSL_CTX_set1_echstore(ctx, es) != 1) { + BIO_printf(bio_err, "Failed reading: %s\n", echkeyfile); + OSSL_ECHSTORE_free(es); + BIO_free_all(in); + goto end; + } + OSSL_ECHSTORE_free(es); + BIO_free_all(in); + if (bio_s_out != NULL) + BIO_printf(bio_s_out, "Added ECH key pair from: %s\n", echkeyfile); + ech_files_loaded++; + } + if (echkeydir != NULL) { + int nloaded = 0; + + if (ech_load_dir(ctx, echkeydir, OSSL_ECH_FOR_RETRY, &nloaded) != 1) { + BIO_printf(bio_err, "error loading from %s\n", echkeydir); + goto end; + } + ech_files_loaded += nloaded; + } + if (echnoretrydir != NULL) { + int nloaded = 0; + + if (ech_load_dir(ctx, echnoretrydir, OSSL_ECH_NO_RETRY, + &nloaded) != 1) { + BIO_printf(bio_err, "error loading from %s\n", echnoretrydir); + goto end; + } + ech_files_loaded += nloaded; + } + if ((echkeyfile != NULL || echkeydir != NULL || echnoretrydir != NULL) + && bio_s_out != NULL) { + BIO_printf(bio_s_out, "Loaded %d ECH key pairs in total\n", + ech_files_loaded); + } +# endif + if (s_cert2) { ctx2 = SSL_CTX_new_ex(app_get0_libctx(), app_get0_propq(), meth); if (ctx2 == NULL) { ERR_print_errors(bio_err); goto end; } +# ifndef OPENSSL_NO_ECH + if (echtrialdecrypt != 0) + SSL_CTX_set_options(ctx2, SSL_OP_ECH_TRIALDECRYPT); + if (echgrease_rc != 0) + SSL_CTX_set_options(ctx, SSL_OP_ECH_GREASE_RETRY_CONFIG); +# endif } if (ctx2 != NULL) { @@ -2339,6 +2720,13 @@ int s_server_main(int argc, char *argv[]) if (alpn_ctx.data) SSL_CTX_set_alpn_select_cb(ctx, alpn_cb, &alpn_ctx); + /* + * If we have a 2nd context to which we might switch, then set + * the same alpn callback for that too. + */ + if (s_cert2 != NULL && alpn_ctx.data != NULL) + SSL_CTX_set_alpn_select_cb(ctx2, alpn_cb, &alpn_ctx); + if (!no_dhe) { EVP_PKEY *dhpkey = NULL; @@ -2413,9 +2801,21 @@ int s_server_main(int argc, char *argv[]) goto end; } +# ifndef OPENSSL_NO_ECH + /* + * Giving the same chain to the 2nd key pair works for our tests. + * It would be better to supply s_chain_file2 as a new CLA in case + * the paths are very different but as that's not needed for tests, + * I didn't do it. + */ + if (ctx2 != NULL + && !set_cert_key_stuff(ctx2, s_cert2, s_key2, s_chain, build_chain)) + goto end; +# else if (ctx2 != NULL && !set_cert_key_stuff(ctx2, s_cert2, s_key2, NULL, build_chain)) goto end; +# endif if (s_dcert != NULL) { if (!set_cert_key_stuff(ctx, s_dcert, s_dkey, s_dchain, build_chain)) @@ -2497,10 +2897,19 @@ int s_server_main(int argc, char *argv[]) goto end; } tlsextcbp.biodebug = bio_s_out; +# ifndef OPENSSL_NO_ECH + SSL_CTX_set_tlsext_servername_callback(ctx2, ssl_ech_servername_cb); + SSL_CTX_set_tlsext_servername_arg(ctx2, &tlsextcbp); + SSL_CTX_set_tlsext_servername_callback(ctx, ssl_ech_servername_cb); + SSL_CTX_set_tlsext_servername_arg(ctx, &tlsextcbp); + SSL_CTX_ech_set_callback(ctx2, ech_print_cb); + SSL_CTX_ech_set_callback(ctx, ech_print_cb); +# else SSL_CTX_set_tlsext_servername_callback(ctx2, ssl_servername_cb); SSL_CTX_set_tlsext_servername_arg(ctx2, &tlsextcbp); SSL_CTX_set_tlsext_servername_callback(ctx, ssl_servername_cb); SSL_CTX_set_tlsext_servername_arg(ctx, &tlsextcbp); +# endif } #ifndef OPENSSL_NO_SRP @@ -2528,6 +2937,11 @@ int s_server_main(int argc, char *argv[]) #endif if (set_keylog_file(ctx, keylog_file)) goto end; +# ifndef OPENSSL_NO_ECH + /* not really an ECH issue but needed */ + if (ctx2 != NULL && set_keylog_file(ctx2, keylog_file)) + goto end; +# endif if (max_early_data >= 0) SSL_CTX_set_max_early_data(ctx, max_early_data); @@ -3542,6 +3956,10 @@ static int www_body(int s, int stype, int prot, unsigned char *context) X509 *peer = NULL; STACK_OF(SSL_CIPHER) *sk; static const char *space = " "; +# ifndef OPENSSL_NO_ECH + char *ech_inner = NULL, *ech_outer = NULL; + int echrv = 0; +# endif if (www == 1 && HAS_PREFIX(buf, "GET /reneg")) { if (HAS_PREFIX(buf, "GET /renegcert")) @@ -3605,6 +4023,80 @@ static int www_body(int s, int stype, int prot, unsigned char *context) } BIO_puts(io, "\n"); +# ifndef OPENSSL_NO_ECH + /* Customise output a bit to show ECH info at top */ + BIO_puts(io, "

OpenSSL with ECH

\n"); + BIO_puts(io, "

\n"); + echrv = SSL_ech_get1_status(con, &ech_inner, &ech_outer); + switch (echrv) { + case SSL_ECH_STATUS_NOT_TRIED: + BIO_puts(io, "ECH not attempted\n"); + break; + case SSL_ECH_STATUS_FAILED: + BIO_puts(io, "ECH tried but failed\n"); + break; + case SSL_ECH_STATUS_FAILED_ECH: + BIO_puts(io, "ECH tried but we got ECH which is weird\n"); + break; + case SSL_ECH_STATUS_BAD_NAME: + BIO_puts(io, "ECH worked but bad name\n"); + break; + case SSL_ECH_STATUS_BACKEND: + BIO_printf(io, "ECH acting as backend\n"); + break; + case SSL_ECH_STATUS_NOT_CONFIGURED: + BIO_printf(io, "ECH not configured\n"); + break; + case SSL_ECH_STATUS_GREASE: + BIO_printf(io, "ECH attempt we interpret as GREASE\n"); + break; + case SSL_ECH_STATUS_GREASE_ECH: + BIO_printf(io, "ECH attempt we interpret as GREASE, + ECH\n"); + break; + case SSL_ECH_STATUS_BAD_CALL: + BIO_printf(io, "ECH bad input to API\n"); + break; + case SSL_ECH_STATUS_SUCCESS: + BIO_printf(io, "ECH success: outer sni: %s, inner sni: %s\n", + (ech_outer == NULL ? "none" : ech_outer), + (ech_inner == NULL ? "none" : ech_inner)); + break; + default: + BIO_printf(io, " Error getting ECH status\n"); + break; + } + BIO_puts(io, "

\n"); + BIO_puts(io, "

TLS Session details

\n"); + BIO_puts(io, "
\n");
+            /*
+             * also dump session info to server stdout for debugging
+             */
+            SSL_SESSION_print(bio_s_out, SSL_get_session(con));
+            BIO_puts(io, "
\n");
+            BIO_puts(io, "\n");
+            for (i = 0; i < local_argc; i++) {
+                const char *myp;
+
+                for (myp = local_argv[i]; *myp; myp++)
+                    switch (*myp) {
+                    case '<':
+                        BIO_puts(io, "<");
+                        break;
+                    case '>':
+                        BIO_puts(io, ">");
+                        break;
+                    case '&':
+                        BIO_puts(io, "&");
+                        break;
+                    default:
+                        BIO_write(io, myp, 1);
+                        break;
+                    }
+                BIO_write(io, " ", 1);
+            }
+            BIO_puts(io, "\n");
+# endif
+
             ssl_print_secure_renegotiation_notes(io, con);
 
             /*
diff --git a/doc/man1/openssl-s_client.pod.in b/doc/man1/openssl-s_client.pod.in
index 5579a9c85f558..8de5efb7e18d1 100644
--- a/doc/man1/openssl-s_client.pod.in
+++ b/doc/man1/openssl-s_client.pod.in
@@ -125,6 +125,14 @@ B B
 [B<-enable_client_rpk>]
 [I:I]
 [B<-ech_config_list>]
+[B<-ech_outer_alpn> I]
+[B<-ech_grease>]
+[B<-ech_grease_suite> I]
+[B<-ech_grease_type> I]
+[B<-ech_ignore_cid>]
+[B<-ech_outer_sni> I]
+[B<-ech_no_outer_sni>]
+[B<-ech_select> I]
 
 =head1 DESCRIPTION
 
@@ -832,6 +840,63 @@ nor B<-connect> are provided, falls back to attempting to connect to
 I on port I<4433>.
 If the host string is an IPv6 address, it must be enclosed in C<[> and C<]>.
 
+=item B<-ech_outer_alpn> I
+
+When doing Encrypted Client Hello (ECH), this allows the caller to specify
+ALPN values to use in the outer ClientHello. (A "normal" ALPN value
+specified via -alpn will be used in the inner ClientHello.)
+
+=item B<-ech_grease>
+
+When not really doing Encrypted Client Hello (ECH), one can emit a so-called
+GREASE value, which is essentially a random value in order to try ensure that
+server code is less likely to ossify.
+
+=item B<-ech_grease_suite> I
+
+When B<-ech_grease> is specified, one can choose which ECH ciphersuite to use
+via this parameter.
+
+The comma-separated suite string names an HPKE suite in the form of
+I,I,I, e.g. "x25519,hkdf-sha256,aes256gcm" or can use
+the numeric values (in decimal or hexadecimal form) from the HPKE specification
+so "0x20,0x01,0x02" is the same as the previous example.
+
+KEM values supported: p256 or 0x10; p384 or 0x11, p521 or 0x12, x25519 or 0x20, x448 or 0x21
+
+KDF values supported: hkdf-sha256 or 0x01, hkdf-sha384 or 0x02, hkdf-sha512 or 0x03
+
+AEAD values supported: aes128gcm or 0x01, aes256gcm or 0x02, chachapoly1305 or 0x03
+
+=item B<-ech_grease_type> I
+
+Allows the client to set the TLS extension type for a GREASEd ECH value
+(currently equivalent to the ECH version number).  The current default is
+0xfe0d.
+
+=item B<-ech_ignore_cid>
+
+Encrypted Client Hello (ECH) extensions contain a configuration identifier
+(cid) taken from the ECHConfigList usually found in the domain name system
+(DNS). As those identifiers could be revealing, the client has the option to
+use a random value instead.
+
+=item B<-ech_outer_sni> I
+
+When doing Encrypted Client Hello (ECH), this allows the caller to specify a
+subject name indication (SNI) value to use in the outer ClientHello over-riding
+the public_name value from the relevant ECHConfigList.
+
+=item B<-ech_no_outer_sni>
+
+Setting this flag means no SNI will be emitted in the outer ClientHello.
+
+=item B<-ech_select> I
+
+If an ECHConfigList contains more than one ECHConfig then the client will by
+default use the first that works. This allows the caller to specify which
+ECHConfig to use (using a zero-based index).
+
 =back
 
 =head1 CONNECTED COMMANDS (BASIC)
@@ -1058,6 +1123,8 @@ The
 and B<-ocsp_check_all>
 options were added in OpenSSL 3.6.
 
+The B options were added in OpenSSL 4.0.
+
 =head1 COPYRIGHT
 
 Copyright 2000-2025 The OpenSSL Project Authors. All Rights Reserved.
diff --git a/doc/man1/openssl-s_server.pod.in b/doc/man1/openssl-s_server.pod.in
index 4c30c9c628330..c3bb6d3bf3982 100644
--- a/doc/man1/openssl-s_server.pod.in
+++ b/doc/man1/openssl-s_server.pod.in
@@ -135,6 +135,11 @@ B B
 {- $OpenSSL::safe::opt_engine_synopsis -}{- $OpenSSL::safe::opt_provider_synopsis -}
 [B<-enable_server_rpk>]
 [B<-enable_client_rpk>]
+[B<-ech_key> I]
+[B<-ech_dir> I]
+[B<-ech_noretry_dir> I]
+[B<-ech_trialdecrypt>]
+[B<-ech_greaseretries>]
 
 =head1 DESCRIPTION
 
@@ -824,6 +829,32 @@ certificates can still elect to send X.509 certificates as usual.
 
 Raw public keys are extracted from the configured certificate/private key.
 
+=item B<-ech_key> I
+
+Load one Encrypted Client Hello (ECH) key pair.
+
+=item B<-ech_dir> I
+
+Attempt to load an ECH key pair from every file in the named directory.
+Any keys successfully loaded will be returned in 'retry_configs'.
+
+=item B<-ech_noretry_dir> I
+
+Attempt to load an ECH key pair from every file in the named directory.
+Keys loaded will not be returned in 'retry_configs'.
+
+=item B<-ech_trialdecrypt>
+
+When an Encrypted Client Hello (ECH) extension is seen in a ClientHello,
+attempt to decrypt with all known ECH private keys if necessary. Without
+this, the ECH "config_id" is used to match against the loaded ECH private
+keys and decryption is only attempted when there's a match.
+
+=item B<-ech_greaseretries>
+
+If set, servers will add GREASEy ECHConfig values to those sent
+in retry_configs.
+
 =back
 
 =head1 CONNECTED COMMANDS
@@ -938,6 +969,8 @@ options were added in OpenSSL 3.2.
 
 The B<-status_all> option was added in OpenSSL 3.6.
 
+The B options were added in OpenSSL 4.0.
+
 =head1 COPYRIGHT
 
 Copyright 2000-2025 The OpenSSL Project Authors. All Rights Reserved.
diff --git a/ssl/ech/ech_internal.c b/ssl/ech/ech_internal.c
index 89fc63d19b083..233536e59ab88 100644
--- a/ssl/ech/ech_internal.c
+++ b/ssl/ech/ech_internal.c
@@ -388,7 +388,7 @@ int ossl_ech_pick_matching_cfg(SSL_CONNECTION *s, OSSL_ECHSTORE_ENTRY **ee,
     num = sk_OSSL_ECHSTORE_ENTRY_num(es->entries);
     /* allow API-set pref to override */
     hn = s->ext.ech.outer_hostname;
-    hnlen = (hn == NULL ? 0 : strlen(hn));
+    hnlen = (hn == NULL ? 0 : (unsigned int)strlen(hn));
     if (hnlen != 0)
         nameoverride = 1;
     if (s->ext.ech.no_outer == 1) {
@@ -513,7 +513,7 @@ int ossl_ech_encode_inner(SSL_CONNECTION *s, unsigned char **encoded,
     }
     /* now copy the rest, as "proper" exts, into encoded inner */
     for (ind = 0; ind < TLSEXT_IDX_num_builtins; ind++) {
-        if (raws[ind].present == 0 || ossl_ech_2bcompressed(ind) == 1)
+        if (raws[ind].present == 0 || ossl_ech_2bcompressed((int)ind) == 1)
             continue;
         if (!WPACKET_put_bytes_u16(&inner, raws[ind].type)
             || !WPACKET_sub_memcpy_u16(&inner, PACKET_data(&raws[ind].data),
@@ -632,15 +632,16 @@ size_t ossl_ech_calc_padding(SSL_CONNECTION *s, OSSL_ECHSTORE_ENTRY *ee,
         /* do weirder padding if SNI present in inner */
         if (s->ext.hostname != NULL) {
             isnilen = strlen(s->ext.hostname) + 9;
-            innersnipadding = (mnl > isnilen) ? mnl - isnilen : 0;
+            innersnipadding = (mnl > isnilen) ? (int)(mnl - isnilen) : 0;
         } else {
-            innersnipadding = mnl + 9;
+            innersnipadding = (int)mnl + 9;
         }
     }
     /* padding is after the inner client hello has been encoded */
-    length_with_snipadding = innersnipadding + encoded_len;
+    length_with_snipadding = innersnipadding + (int)encoded_len;
     length_of_padding = 31 - ((length_with_snipadding - 1) % 32);
-    length_with_padding = encoded_len + length_of_padding + innersnipadding;
+    length_with_padding = (int)encoded_len + length_of_padding
+        + innersnipadding;
     /*
      * Finally - make sure final result is longer than padding target
      * and a multiple of our padding increment.
diff --git a/ssl/ech/ech_ssl_apis.c b/ssl/ech/ech_ssl_apis.c
index 45d04c616e964..45e1ead616454 100644
--- a/ssl/ech/ech_ssl_apis.c
+++ b/ssl/ech/ech_ssl_apis.c
@@ -260,6 +260,7 @@ int SSL_ech_set1_grease_suite(SSL *ssl, const char *suite)
     if (s->ext.ech.grease_suite == NULL)
         return 0;
     s->ext.ech.attempted = 1;
+    s->ext.ech.grease = OSSL_ECH_IS_GREASE;
     return 1;
 }
 
@@ -272,6 +273,7 @@ int SSL_ech_set_grease_type(SSL *ssl, uint16_t type)
         return 0;
     s->ext.ech.attempted_type = type;
     s->ext.ech.attempted = 1;
+    s->ext.ech.grease = OSSL_ECH_IS_GREASE;
     return 1;
 }
 
diff --git a/ssl/ech/ech_store.c b/ssl/ech/ech_store.c
index ee9fbd76feca0..a9a1b32561f1d 100644
--- a/ssl/ech/ech_store.c
+++ b/ssl/ech/ech_store.c
@@ -271,7 +271,7 @@ static int ech_final_config_checks(OSSL_ECHSTORE_ENTRY *ee)
     /* check no mandatory exts (with high bit set in type) */
     num = (ee->exts == NULL ? 0 : sk_OSSL_ECHEXT_num(ee->exts));
     for (ind = 0; ind != num; ind++) {
-        OSSL_ECHEXT *oe = sk_OSSL_ECHEXT_value(ee->exts, ind);
+        OSSL_ECHEXT *oe = sk_OSSL_ECHEXT_value(ee->exts, (int)ind);
 
         if (oe->type & 0x8000) {
             ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_INVALID_ARGUMENT);
@@ -331,7 +331,7 @@ static int ech_decode_one_entry(OSSL_ECHSTORE_ENTRY **rent, PACKET *pkt,
         ERR_raise(ERR_LIB_SSL, SSL_R_ECH_DECODE_ERROR);
         goto err;
     }
-    ech_content_length = PACKET_remaining(&ver_pkt);
+    ech_content_length = (unsigned int)PACKET_remaining(&ver_pkt);
     switch (ee->version) {
     case OSSL_ECH_RFCXXXX_VERSION:
         break;
diff --git a/ssl/statem/extensions.c b/ssl/statem/extensions.c
index ba78bcedeabc7..deed81e4b7935 100644
--- a/ssl/statem/extensions.c
+++ b/ssl/statem/extensions.c
@@ -1144,7 +1144,7 @@ int tls_construct_extensions(SSL_CONNECTION *s, WPACKET *pkt,
 
 #ifndef OPENSSL_NO_ECH
             /* do compressed in pass 0, non-compressed in pass 1 */
-            if (ossl_ech_2bcompressed(i) == pass)
+            if (ossl_ech_2bcompressed((int)i) == pass)
                 continue;
             /* stash index - needed for COMPRESS ECH handling */
             s->ext.ech.ext_ind = (int)i;
diff --git a/ssl/statem/extensions_clnt.c b/ssl/statem/extensions_clnt.c
index a77943ace01ac..b1f6c70be72a9 100644
--- a/ssl/statem/extensions_clnt.c
+++ b/ssl/statem/extensions_clnt.c
@@ -2526,14 +2526,14 @@ EXT_RETURN tls_construct_ctos_ech(SSL_CONNECTION *s, WPACKET *pkt,
     size_t cipherlen = 0, aad_len = 0, lenclen = 0, mypub_len = 0;
     size_t info_len = OSSL_ECH_MAX_INFO_LEN, clear_len = 0, encoded_len = 0;
     /* whether or not we've been asked to GREASE, one way or another */
-    int grease_opt_set = (s->ext.ech.grease == OSSL_ECH_IS_GREASE
+    int grease_opt_set = ((s->ext.ech.grease == OSSL_ECH_IS_GREASE)
                           || ((s->options & SSL_OP_ECH_GREASE) != 0));
 
     /* if we're not doing real ECH and not GREASEing then exit */
     if (s->ext.ech.attempted_type != TLSEXT_TYPE_ech && grease_opt_set == 0)
         return EXT_RETURN_NOT_SENT;
     /* send grease if not really attempting ECH */
-    if (s->ext.ech.attempted == 0 && grease_opt_set == 1) {
+    if (grease_opt_set == 1) {
         if (s->hello_retry_request == SSL_HRR_PENDING
             && s->ext.ech.sent != NULL) {
             /* re-tx already sent GREASEy ECH */
@@ -2732,7 +2732,7 @@ int tls_parse_stoc_ech(SSL_CONNECTION *s, PACKET *pkt, unsigned int context,
         return 0;
     }
     rval = PACKET_data(&rcfgs_pkt);
-    rlen = PACKET_remaining(&rcfgs_pkt);
+    rlen = (unsigned int)PACKET_remaining(&rcfgs_pkt);
     OPENSSL_free(s->ext.ech.returned);
     s->ext.ech.returned = NULL;
     srval = OPENSSL_malloc(rlen + 2);
diff --git a/test/ech_test.c b/test/ech_test.c
index 07fd9bddf495b..fcd4795aa58a3 100644
--- a/test/ech_test.c
+++ b/test/ech_test.c
@@ -11,6 +11,7 @@
 #include 
 #include "testutil.h"
 #include "helpers/ssltestlib.h"
+#include "internal/packet.h"
 
 #ifndef OPENSSL_NO_ECH
 
@@ -23,15 +24,58 @@ static char *certsdir = NULL;
 static char *cert = NULL;
 static char *privkey = NULL;
 static char *rootcert = NULL;
+static int ch_test_cb_ok = 0;
 
 /* TODO(ECH): add some testing of SSL_OP_ECH_IGNORE_CID */
 
-/* callback */
-static unsigned int test_cb(SSL *s, const char *str)
+/* ECH callback */
+static unsigned int ech_test_cb(SSL *s, const char *str)
 {
+    if (verbose)
+        TEST_info("ech_test_cb called");
     return 1;
 }
 
+/* ClientHello callback */
+static int ch_test_cb(SSL *ssl, int *al, void *arg)
+{
+    char *servername = NULL;
+    const unsigned char *pos;
+    size_t remaining;
+    unsigned int servname_type;
+    PACKET pkt, sni, hostname;
+
+    if (verbose) {
+        TEST_info("ch_test_cb called");
+        if (SSL_client_hello_get0_ext(ssl, TLSEXT_TYPE_ech, &pos, &remaining)) {
+            TEST_info("there is an ECH extension");
+        } else {
+            TEST_info("there is NO ECH extension");
+        }
+    }
+    if (!SSL_client_hello_get0_ext(ssl, TLSEXT_TYPE_server_name, &pos,
+                                   &remaining)
+            || remaining <= 2)
+        goto give_up;
+    if (!PACKET_buf_init(&pkt, pos, remaining)
+        || !PACKET_as_length_prefixed_2(&pkt, &sni)
+        || !PACKET_get_1(&sni, &servname_type)
+        || servname_type != TLSEXT_NAMETYPE_host_name
+        || !PACKET_as_length_prefixed_2(&sni, &hostname)
+        || (PACKET_remaining(&hostname) > TLSEXT_MAXLEN_host_name)
+        || PACKET_contains_zero_byte(&hostname)
+        || !PACKET_strndup(&hostname, &servername))
+        goto give_up;
+    if (verbose)
+        TEST_info("servername: %s", servername);
+    OPENSSL_free(servername);
+    /* signal to caller all is good */
+    ch_test_cb_ok = 1;
+    return 1;
+give_up:
+    return 0;
+}
+
 /*
  * The define/vars below and the 3 callback functions are modified
  * from test/sslapitest.c
@@ -1095,8 +1139,8 @@ static int ech_api_basic_calls(void)
         || !TEST_false(rclen)
         || !TEST_ptr_eq(rc, NULL))
         goto end;
-    SSL_CTX_ech_set_callback(ctx, test_cb);
-    SSL_ech_set_callback(s, test_cb);
+    SSL_CTX_ech_set_callback(ctx, ech_test_cb);
+    SSL_ech_set_callback(s, ech_test_cb);
 
     /* all good */
     rv = 1;
@@ -1145,6 +1189,7 @@ static int ech_boring_compat(void)
 # define OSSL_ECH_TEST_EARLY    2
 # define OSSL_ECH_TEST_CUSTOM   3
 # define OSSL_ECH_TEST_ENOE     4 /* early + no-ech */
+# define OSSL_ECH_TEST_CBS      5 /* test callbacks */
 /* note: early-data is prohibited after HRR so no tests for that */
 
 /*
@@ -1224,6 +1269,10 @@ static int test_ech_roundtrip_helper(int idx, int combo)
                                                  &server, NULL, &server)))
             goto end;
     }
+    if (combo == OSSL_ECH_TEST_CBS) {
+        SSL_CTX_ech_set_callback(sctx, ech_test_cb);
+        SSL_CTX_set_client_hello_cb(sctx, ch_test_cb, NULL);
+    }
     if (combo != OSSL_ECH_TEST_ENOE
         && !TEST_true(SSL_CTX_set1_echstore(cctx, es)))
         goto end;
@@ -1259,9 +1308,11 @@ static int test_ech_roundtrip_helper(int idx, int combo)
     if (combo == OSSL_ECH_TEST_ENOE
         && !TEST_int_eq(clientstatus, SSL_ECH_STATUS_NOT_CONFIGURED))
         goto end;
+    if (combo == OSSL_ECH_TEST_CBS && !TEST_int_eq(ch_test_cb_ok, 1))
+        goto end;
     /* all good */
     if (combo == OSSL_ECH_TEST_BASIC || combo == OSSL_ECH_TEST_HRR
-        || combo == OSSL_ECH_TEST_CUSTOM) {
+        || combo == OSSL_ECH_TEST_CUSTOM || combo == OSSL_ECH_TEST_CBS) {
         res = 1;
         goto end;
     }
@@ -1334,6 +1385,7 @@ static int test_ech_roundtrip_helper(int idx, int combo)
     SSL_free(serverssl);
     SSL_CTX_free(cctx);
     SSL_CTX_free(sctx);
+    ch_test_cb_ok = 0;
     return res;
 }
 
@@ -1377,6 +1429,14 @@ static int ech_enoe_test(int idx)
     return test_ech_roundtrip_helper(idx, OSSL_ECH_TEST_ENOE);
 }
 
+/* Test a roundtrip with ECH, and callbacks */
+static int ech_cb_test(int idx)
+{
+    if (verbose)
+        TEST_info("Doing: ech + callbacks test ");
+    return test_ech_roundtrip_helper(idx, OSSL_ECH_TEST_CBS);
+}
+
 #endif
 
 int setup_tests(void)
@@ -1420,6 +1480,7 @@ int setup_tests(void)
     ADD_ALL_TESTS(test_ech_early, suite_combos);
     ADD_ALL_TESTS(ech_custom_test, suite_combos);
     ADD_ALL_TESTS(ech_enoe_test, suite_combos);
+    ADD_ALL_TESTS(ech_cb_test, suite_combos);
     /* TODO(ECH): add more test code as other PRs done */
     return 1;
 err:
diff --git a/test/recipes/82-test_ech_client_server.t b/test/recipes/82-test_ech_client_server.t
new file mode 100644
index 0000000000000..9197c52f651b3
--- /dev/null
+++ b/test/recipes/82-test_ech_client_server.t
@@ -0,0 +1,342 @@
+#! /usr/bin/env perl
+# Copyright 2023-2025 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
+
+use strict;
+use warnings;
+
+use IPC::Open3;
+use OpenSSL::Test qw/:DEFAULT srctop_file bldtop_file/;
+use OpenSSL::Test::Utils;
+use Symbol 'gensym';
+
+# servers randomly pick a port, then set this for clients to use
+# we also record the pid so we can kill it later if needed
+my $s_server_port = 0;
+my $s_server_pid = 0;
+my $s_client_match = 0;
+
+my $test_name = "test_ech_client_server";
+setup($test_name);
+
+plan skip_all => "$test_name requires EC cryptography"
+    if disabled("ec") || disabled("ecx");
+plan skip_all => "$test_name requires sock enabled"
+    if disabled("sock");
+plan skip_all => "$test_name requires TLSv1.3 enabled"
+    if disabled("tls1_3");
+plan skip_all => "$test_name is not available Windows or VMS"
+    if $^O =~ /^(VMS|MSWin32|msys)$/;
+
+plan tests => 18;
+
+my $shlib_wrap   = bldtop_file("util", "shlib_wrap.sh");
+my $apps_openssl = bldtop_file("apps", "openssl");
+
+my $echconfig_pem         = srctop_file("test", "certs", "ech-eg.pem");
+my $badconfig_pem         = srctop_file("test", "certs", "ech-mid.pem");
+my $server_pem            = srctop_file("test", "certs", "echserver.pem");
+my $server_key            = srctop_file("test", "certs", "echserver.key");
+my $root_pem              = srctop_file("test", "certs", "rootcert.pem");
+
+sub extract_ecl()
+{
+    # extract b64 encoded ECHConfigList from pem file
+    my $lb64 = "";
+    my $inwanted = 0;
+    open( my $fh, '<', $echconfig_pem ) or die "Can't open $echconfig_pem $!";
+    while( my $line = <$fh>) {
+        chomp $line;
+        if ( $line =~ /^-----BEGIN ECHCONFIG/) {
+            $inwanted = 1;
+        } elsif ( $line =~ /^-----END ECHCONFIG/) {
+            $inwanted = 0;
+        } elsif ($inwanted == 1) {
+            $lb64 .= $line;
+        }
+    }
+    print("base64 ECHConfigList: $lb64\n");
+    return($lb64);
+}
+
+my $good_b64 = extract_ecl();
+
+sub start_ech_client_server
+{
+    my ( $test_type, $winpattern ) = @_;
+
+    # start an s_server listening on some random port, with ECH enabled
+    # and willing to accept one request
+
+    # openssl s_server -accept 0 -naccept 1
+    #                  -key $server_key -cert $server_cert
+    #                  -key2 $server_key -cert2 $server_cert
+    #                  -ech_key $echconfig_pem
+    #                  -servername example.com
+    #                  -tls1_3
+    my @s_server_cmd;
+    if ($test_type eq "cid-free" ) {
+        # turn on trial-decrypt, so client can use random CID
+        @s_server_cmd = ("s_server", "-accept", "0", "-naccept", "1",
+                         "-cert", $server_pem, "-key", $server_key,
+                         "-cert2", $server_pem, "-key2", $server_key,
+                         "-ech_key", $echconfig_pem,
+                         "-servername", "example.com",
+                         "-ech_trialdecrypt",
+                         "-tls1_3");
+    } else {
+        # default for all other tests (for now)
+        @s_server_cmd = ("s_server", "-accept", "0", "-naccept", "1",
+                         "-cert", $server_pem, "-key", $server_key,
+                         "-cert2", $server_pem, "-key2", $server_key,
+                         "-ech_key", $echconfig_pem,
+                         "-servername", "example.com",
+                         "-ech_greaseretries",
+                         "-tls1_3");
+    }
+    print("@s_server_cmd\n");
+    $s_server_pid = open3(my $s_server_i, my $s_server_o,
+                             my $s_server_e = gensym,
+                             $shlib_wrap, $apps_openssl, @s_server_cmd);
+    # we're looking for...
+    # ACCEPT 0.0.0.0:45921
+    # ACCEPT [::]:45921
+    $s_server_port = "0";
+    while (<$s_server_o>) {
+        print($_);
+        chomp;
+        if (/^ACCEPT 0.0.0.0:(\d+)/) {
+            $s_server_port = $1;
+            last;
+        } elsif (/^ACCEPT \[::\]:(\d+)/) {
+            $s_server_port = $1;
+            last;
+        } elsif (/^Using default/) {
+            ;
+        } elsif (/^Added ECH key pair/) {
+            ;
+        } elsif (/^Loaded/) {
+            ;
+        } elsif (/^Setting secondary/) {
+            ;
+        } else {
+            last;
+        }
+    }
+    # openssl s_client -connect localhost:NNNNN
+    #                  -servername server.example
+    #                  -CAfile test/certs/rootcert.pem
+    #                  -ech_config_list "ADn+...AA="
+    #                  -prexit
+    my @s_client_cmd;
+    if ($test_type eq "GREASE-suite" ) {
+        # GREASE
+        @s_client_cmd = ("s_client",
+                         "-connect", "localhost:$s_server_port",
+                         "-servername", "server.example",
+                         "-CAfile", $root_pem,
+                         "-ech_grease_suite", "0x21,2,3",
+                         "-prexit");
+    } elsif ($test_type eq "lots-of-options" ) {
+        # real ECH with lots of options
+        @s_client_cmd = ("s_client",
+                         "-connect", "localhost:$s_server_port",
+                         "-servername", "server.example",
+                         "-CAfile", $root_pem,
+                         "-ech_config_list", $good_b64,
+                         "-ech_outer_sni", "foodle.doodle",
+                         "-ech_select", "0",
+                         "-alpn", "http/1.1",
+                         "-ech_outer_alpn", "http451",
+                         "-prexit");
+    } elsif ($test_type eq "GREASE-type" ) {
+        # GREASE with suite
+        @s_client_cmd = ("s_client",
+                         "-connect", "localhost:$s_server_port",
+                         "-servername", "server.example",
+                         "-CAfile", $root_pem,
+                         "-ech_grease_type", "12345",
+                         "-prexit");
+    } elsif ($test_type eq "GREASE" ) {
+        # GREASE with suite
+        @s_client_cmd = ("s_client",
+                         "-connect", "localhost:$s_server_port",
+                         "-servername", "server.example",
+                         "-CAfile", $root_pem,
+                         "-ech_grease",
+                         "-prexit");
+    } elsif ($test_type eq "no-outer" ) {
+        # Real ECH, no outer SNI
+        @s_client_cmd = ("s_client",
+                         "-connect", "localhost:$s_server_port",
+                         "-servername", "server.example",
+                         "-CAfile", $root_pem,
+                         "-ech_config_list", $good_b64,
+                         "-ech_no_outer_sni",
+                         "-prexit");
+    } elsif ($test_type eq "bad-ech" ) {
+        # bad ECH
+        @s_client_cmd = ("s_client",
+                         "-connect", "localhost:$s_server_port",
+                         "-servername", "server.example",
+                         "-CAfile", $root_pem,
+                         "-ech_config_list", "AEH+DQA91wAgACCBdNrnZxqNrUXSyimqqnfmNG4lHtVsbmaaIeRoUoFWFQAEAAEAAQAOc2VydmVyLmV4YW1wbGUAAA==",
+                         "-prexit");
+    } elsif ($test_type eq "cid-free" ) {
+        # Real ECH, ignore CID
+        @s_client_cmd = ("s_client",
+                         "-connect", "localhost:$s_server_port",
+                         "-servername", "server.example",
+                         "-CAfile", $root_pem,
+                         "-ech_config_list", $good_b64,
+                         "-ech_ignore_cid",
+                         "-prexit");
+    } elsif ($test_type eq "cid-wrong" ) {
+        # Real ECH, ignore CID, no trial decrypt
+        @s_client_cmd = ("s_client",
+                         "-connect", "localhost:$s_server_port",
+                         "-servername", "server.example",
+                         "-CAfile", $root_pem,
+                         "-ech_config_list", $good_b64,
+                         "-ech_ignore_cid",
+                         "-prexit");
+    } else {
+        # Real ECH, and default
+        @s_client_cmd = ("s_client",
+                         "-connect", "localhost:$s_server_port",
+                         "-servername", "server.example",
+                         "-CAfile", $root_pem,
+                         "-ech_config_list", $good_b64,
+                         "-prexit");
+    }
+    print("@s_client_cmd\n");
+    local (*sc_input);
+    my $s_client_pid = open3(*sc_input, my $s_client_o,
+                             my $s_client_e = gensym,
+                             $shlib_wrap, $apps_openssl, @s_client_cmd);
+    print sc_input "Q\n";
+    close(sc_input);
+    waitpid($s_client_pid, 0);
+    # the output from s_client that we want to check is written to its
+    # stdout, e.g: "^ECH: success, yay!"
+    $s_client_match = 0;
+    while (<$s_client_o>) {
+        print($_);
+        chomp;
+        if (/$winpattern/) {
+            $s_client_match = 1;
+            last;
+        }
+    }
+    my $stillthere = kill 0, $s_server_pid;
+    if ($stillthere) {
+       print("s_server process ($s_server_pid) is not dead yet.\n");
+       kill 'HUP', $s_server_pid;
+    }
+}
+
+sub basic_test {
+    print("\n\nBasic test.\n");
+    my $tt = "basic";
+    my $win = "^ECH: success";
+    start_ech_client_server($tt, $win);
+    ok($s_server_port ne "0", "s_server port check");
+    print("s_server ready, on port $s_server_port pid: $s_server_pid\n");
+    ok($s_client_match == 1, "s_client with ECH on command line");
+}
+
+sub wrong_test {
+    print("\n\nWrong ECHConfig test.\n");
+    # hardcoded 'cause we want a fail
+    my $tt="bad-ech",
+    my $win="^ECH: failed.retry-configs: -105";
+    start_ech_client_server($tt, $win);
+    ok($s_server_port ne "0", "s_server port check");
+    print("s_server ready, on port $s_server_port pid: $s_server_pid\n");
+    ok($s_client_match == 1, "s_client with bad ECH");
+}
+
+sub grease_test {
+    print("\n\nGREASE ECHConfig test.\n");
+    my $tt="GREASE";
+    my $win="^ECH: GREASE";
+    start_ech_client_server($tt, $win);
+    ok($s_server_port ne "0", "s_server port check");
+    print("s_server ready, on port $s_server_port pid: $s_server_pid\n");
+    ok($s_client_match == 1, "s_client with GREASE ECH");
+}
+
+sub grease_suite_test {
+    print("\n\nGREASE suite ECHConfig test.\n");
+    my $tt="GREASE-suite";
+    my $win="^ECH: GREASE";
+    start_ech_client_server($tt, $win);
+    ok($s_server_port ne "0", "s_server port check");
+    print("s_server ready, on port $s_server_port pid: $s_server_pid\n");
+    ok($s_client_match == 1, "s_client with GREASE-suite ECH");
+}
+
+sub grease_type_test {
+    print("\n\nGREASE type ECH test.\n");
+    my $tt="GREASE-type";
+    my $win="^ECH: GREASE";
+    start_ech_client_server($tt, $win);
+    ok($s_server_port ne "0", "s_server port check");
+    print("s_server ready, on port $s_server_port pid: $s_server_pid\n");
+    ok($s_client_match == 1, "s_client with GREASE-type ECH");
+}
+
+sub lots_of_options_test {
+    print("\n\nLots of options ECH test.\n");
+    my $tt="lots-of-options";
+    my $win="^ECH: success";
+    start_ech_client_server($tt, $win);
+    ok($s_server_port ne "0", "s_server port check");
+    print("s_server ready, on port $s_server_port pid: $s_server_pid\n");
+    ok($s_client_match == 1, "s_client with lots of ECH options");
+}
+
+sub no_outer_test {
+    print("\n\nNo outer SNI test.\n");
+    my $tt = "no-outer";
+    my $win = "^ECH: success";
+    start_ech_client_server($tt, $win);
+    ok($s_server_port ne "0", "s_server port check");
+    print("s_server ready, on port $s_server_port pid: $s_server_pid\n");
+    ok($s_client_match == 1, "s_client with no outer SNI ECH");
+}
+
+sub cid_free_test {
+    print("\n\nIgnore CIDs test.\n");
+    my $tt = "cid-free";
+    my $win = "^ECH: success";
+    start_ech_client_server($tt, $win);
+    ok($s_server_port ne "0", "s_server port check");
+    print("s_server ready, on port $s_server_port pid: $s_server_pid\n");
+    ok($s_client_match == 1, "s_client/s_server with no CID/trial decrypt");
+}
+
+sub cid_wrong_test {
+    print("\n\nIgnore CIDs test.\n");
+    my $tt = "cid-wrong";
+    my $win = "^ECH: failed";
+    start_ech_client_server($tt, $win);
+    ok($s_server_port ne "0", "s_server port check");
+    print("s_server ready, on port $s_server_port pid: $s_server_pid\n");
+    ok($s_client_match == 1, "s_client/s_server with no CID/no trial decrypt");
+}
+
+basic_test();
+wrong_test();
+grease_test();
+grease_suite_test();
+grease_type_test();
+lots_of_options_test();
+no_outer_test();
+cid_free_test();
+cid_wrong_test();
+

From daf47679a01c94b3de2a6faf6a983e9aca7bc27e Mon Sep 17 00:00:00 2001
From: Tomas Mraz 
Date: Tue, 18 Nov 2025 09:41:06 +0100
Subject: [PATCH 17/24] Fix typos in ECH implementation

Reviewed-by: Alicja Kario 
Reviewed-by: Dmitry Belyavskiy 
(Merged from https://github.com/openssl/openssl/pull/29164)
---
 doc/man1/openssl-ech.pod.in  | 2 +-
 ssl/ech/ech_internal.c       | 2 +-
 ssl/statem/extensions.c      | 8 ++++----
 ssl/statem/extensions_clnt.c | 4 ++--
 ssl/statem/extensions_srvr.c | 2 +-
 ssl/statem/statem_clnt.c     | 2 +-
 6 files changed, 10 insertions(+), 10 deletions(-)

diff --git a/doc/man1/openssl-ech.pod.in b/doc/man1/openssl-ech.pod.in
index c894feee1ff6c..5abe0ae5b0d80 100644
--- a/doc/man1/openssl-ech.pod.in
+++ b/doc/man1/openssl-ech.pod.in
@@ -58,7 +58,7 @@ values with public keys from the inputs, and no private key(s).
 
 =item B<-text>
 
-Provide human-readable text ouput.
+Provide human-readable text output.
 
 =item B<-public_name> I
 
diff --git a/ssl/ech/ech_internal.c b/ssl/ech/ech_internal.c
index 233536e59ab88..d0d00190e7b30 100644
--- a/ssl/ech/ech_internal.c
+++ b/ssl/ech/ech_internal.c
@@ -1674,7 +1674,7 @@ static int ech_decode_inner(SSL_CONNECTION *s, const unsigned char *ob,
     memset(outers, -1, sizeof(outers)); /* fill with known values for debug */
 # endif
 
-    /* 1. check for outers and make inital checks of those */
+    /* 1. check for outers and make initial checks of those */
     if (ech_find_outers(s, &ei, outers, &n_outers) != 1)
         goto err; /* SSLfatal called already */
 
diff --git a/ssl/statem/extensions.c b/ssl/statem/extensions.c
index deed81e4b7935..4bd3e677e0886 100644
--- a/ssl/statem/extensions.c
+++ b/ssl/statem/extensions.c
@@ -35,13 +35,13 @@
  * won't affect the outer CH size, due to padding, but might for some
  * larger extensions.
  *
- * Note there is a co-dependency with test/recipies/75-test_quicapi.t:
+ * Note there is a co-dependency with test/recipes/75-test_quicapi.t:
  * If you change an |ech_handling| value, that may well affect the order
  * of extensions in a ClientHello, which is reflected in the test data
- * in test/recipies/75-test_quicapi_data/\*.txt files. To fix, you need
+ * in test/recipes/75-test_quicapi_data/\*.txt files. To fix, you need
  * to look in test-runs/test_quicapi for the "new" files and then edit
  * (replacing actual octets with "?" in relevant places), and copy the
- * result back over to test/recipies/75-test_quicapi_data/. The reason
+ * result back over to test/recipes/75-test_quicapi_data/. The reason
  * this happens is the ECH COMPRESS'd extensions need to be contiguous
  * in the ClientHello, so changes to/from COMPRESS affect extension
  * order, in inner and outer CH. There doesn't seem to be an easy,
@@ -329,7 +329,7 @@ static const EXTENSION_DEFINITION ext_defs[] = {
          * If you want to demonstrate/exercise duplicate, then
          * this does that and has no effect on sizes, but it
          * will break the quicapi test (see above). Probably
-         * best done in local tests and not comitted to any
+         * best done in local tests and not committed to any
          * upstream.
          * OSSL_ECH_HANDLING_DUPLICATE,
          */
diff --git a/ssl/statem/extensions_clnt.c b/ssl/statem/extensions_clnt.c
index b1f6c70be72a9..457f138000849 100644
--- a/ssl/statem/extensions_clnt.c
+++ b/ssl/statem/extensions_clnt.c
@@ -2544,7 +2544,7 @@ EXT_RETURN tls_construct_ctos_ech(SSL_CONNECTION *s, WPACKET *pkt,
             }
             return EXT_RETURN_SENT;
         }
-        /* if nobody set a type, use the defaulf */
+        /* if nobody set a type, use the default */
         if (s->ext.ech.attempted_type == OSSL_ECH_type_unknown)
             s->ext.ech.attempted_type = TLSEXT_TYPE_ech;
         if (ossl_ech_send_grease(s, pkt) != 1) {
@@ -2726,7 +2726,7 @@ int tls_parse_stoc_ech(SSL_CONNECTION *s, PACKET *pkt, unsigned int context,
                OSSL_ECH_SIGNAL_LEN);
         return 1;
     }
-    /* othewise we expect retry-configs */
+    /* otherwise we expect retry-configs */
     if (!PACKET_get_length_prefixed_2(pkt, &rcfgs_pkt)) {
         SSLfatal(s, SSL_AD_DECODE_ERROR, SSL_R_LENGTH_MISMATCH);
         return 0;
diff --git a/ssl/statem/extensions_srvr.c b/ssl/statem/extensions_srvr.c
index fd1ddde2f67c3..7a5514a556efc 100644
--- a/ssl/statem/extensions_srvr.c
+++ b/ssl/statem/extensions_srvr.c
@@ -2479,7 +2479,7 @@ int tls_parse_ctos_ech(SSL_CONNECTION *s, PACKET *pkt, unsigned int context,
     }
     /* yay - we're ok with this */
     OSSL_TRACE_BEGIN(TLS) {
-        BIO_printf(trc_out, "ECH seen in inner as exptected.\n");
+        BIO_printf(trc_out, "ECH seen in inner as expected.\n");
     } OSSL_TRACE_END(TLS);
     return 1;
 }
diff --git a/ssl/statem/statem_clnt.c b/ssl/statem/statem_clnt.c
index 9b21027062937..cc69dc4356fea 100644
--- a/ssl/statem/statem_clnt.c
+++ b/ssl/statem/statem_clnt.c
@@ -1858,7 +1858,7 @@ MSG_PROCESS_RETURN tls_process_server_hello(SSL_CONNECTION *s, PACKET *pkt)
         /* check the ECH accept signal */
         if (ossl_ech_calc_confirm(s, hrr, c_signal, shlen) != 1) {
             /* SSLfatal() already called */
-            OSSL_TRACE(TLS, "ECH calc confim failed\n");
+            OSSL_TRACE(TLS, "ECH calc confirm failed\n");
             goto err;
         }
         if (ossl_ech_find_confirm(s, hrr, s_signal) != 1

From ac3b44faf3bb51592e5d7904168801bc72ae3556 Mon Sep 17 00:00:00 2001
From: Tomas Mraz 
Date: Thu, 18 Dec 2025 11:43:41 +0100
Subject: [PATCH 18/24] ECH: Properly apply libctx and propq from SSL_CTX

And further minor refactoring.

Reviewed-by: Matt Caswell 
Reviewed-by: Dmitry Belyavskiy 
MergeDate: Thu Jan  8 09:59:56 2026
(Merged from https://github.com/openssl/openssl/pull/29439)
---
 ssl/ech/ech_internal.c       | 28 +++++++++++++---------------
 ssl/statem/extensions_clnt.c |  7 ++++---
 ssl/statem/extensions_srvr.c |  3 ++-
 ssl/statem/statem_clnt.c     |  3 ++-
 4 files changed, 21 insertions(+), 20 deletions(-)

diff --git a/ssl/ech/ech_internal.c b/ssl/ech/ech_internal.c
index d0d00190e7b30..1102d1e2669fd 100644
--- a/ssl/ech/ech_internal.c
+++ b/ssl/ech/ech_internal.c
@@ -9,12 +9,13 @@
 
 #include 
 #include 
-#include "../ssl_local.h"
-#include "ech_local.h"
 #include 
-#include "../statem/statem_local.h"
-#include "internal/ech_helpers.h"
 #include 
+#include "internal/ech_helpers.h"
+#include "internal/ssl_unwrap.h"
+#include "../ssl_local.h"
+#include "../statem/statem_local.h"
+#include "ech_local.h"
 
 #ifndef OPENSSL_NO_ECH
 
@@ -294,16 +295,11 @@ int ossl_ech_send_grease(SSL_CONNECTION *s, WPACKET *pkt)
     size_t cipher_len = 0, cipher_len_jitter = 0;
     unsigned char cid, senderpub[OSSL_ECH_MAX_GREASE_PUB];
     unsigned char cipher[OSSL_ECH_MAX_GREASE_CT];
+    SSL_CTX *sctx = SSL_CONNECTION_GET_CTX(s);
 
-    if (s == NULL)
-        return 0;
-    if (s->ssl.ctx == NULL) {
-        SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
-        return 0;
-    }
     WPACKET_get_total_written(pkt, &pp_at_start);
     /* randomly select cipher_len to be one of 144, 176, 208, 244 */
-    if (RAND_bytes_ex(s->ssl.ctx->libctx, &cid, 1, 0) <= 0) {
+    if (RAND_bytes_ex(sctx->libctx, &cid, 1, 0) <= 0) {
         SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
         return 0;
     }
@@ -311,7 +307,7 @@ int ossl_ech_send_grease(SSL_CONNECTION *s, WPACKET *pkt)
     cipher_len = 144;
     cipher_len += 32 * cipher_len_jitter;
     /* generate a random (1 octet) client id */
-    if (RAND_bytes_ex(s->ssl.ctx->libctx, &cid, 1, 0) <= 0) {
+    if (RAND_bytes_ex(sctx->libctx, &cid, 1, 0) <= 0) {
         SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
         return 0;
     }
@@ -327,7 +323,7 @@ int ossl_ech_send_grease(SSL_CONNECTION *s, WPACKET *pkt)
     if (OSSL_HPKE_get_grease_value(hpke_suite_in_p, &hpke_suite,
                                    senderpub, &senderpub_len,
                                    cipher, cipher_len,
-                                   s->ssl.ctx->libctx, NULL) != 1) {
+                                   sctx->libctx, sctx->propq) != 1) {
         SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
         return 0;
     }
@@ -1019,6 +1015,7 @@ int ossl_ech_calc_confirm(SSL_CONNECTION *s, int for_hrr,
         + SSL3_RANDOM_SIZE - OSSL_ECH_SIGNAL_LEN;
     unsigned int hashlen = 0;
     unsigned char hashval[EVP_MAX_MD_SIZE];
+    SSL_CTX *sctx = SSL_CONNECTION_GET_CTX(s);
 
     if ((md = (EVP_MD *)ssl_handshake_md(s)) == NULL) {
         SSLfatal(s, SSL_AD_INTERNAL_ERROR, SSL_R_ECH_REQUIRED);
@@ -1046,7 +1043,7 @@ int ossl_ech_calc_confirm(SSL_CONNECTION *s, int for_hrr,
         } else {
             if (s->ext.ech.hrrsignal_p == NULL) {
                 /* No ECH found so we'll exit, but set random output */
-                if (RAND_bytes_ex(s->ssl.ctx->libctx, acbuf,
+                if (RAND_bytes_ex(sctx->libctx, acbuf,
                                   OSSL_ECH_SIGNAL_LEN, 0) <= 0) {
                     SSLfatal(s, SSL_AD_INTERNAL_ERROR, SSL_R_ECH_REQUIRED);
                     goto end;
@@ -1733,6 +1730,7 @@ static unsigned char *hpke_decrypt_encch(SSL_CONNECTION *s,
     size_t info_len = OSSL_ECH_MAX_INFO_LEN;
     int rv = 0;
     OSSL_HPKE_CTX *hctx = NULL;
+    SSL_CTX *sctx = SSL_CONNECTION_GET_CTX(s);
 # ifdef OSSL_ECH_SUPERVERBOSE
     size_t publen = 0;
     unsigned char *pub = NULL;
@@ -1783,7 +1781,7 @@ static unsigned char *hpke_decrypt_encch(SSL_CONNECTION *s,
     ERR_set_mark();
     /* Use OSSL_HPKE_* APIs */
     hctx = OSSL_HPKE_CTX_new(hpke_mode, hpke_suite, OSSL_HPKE_ROLE_RECEIVER,
-                             NULL, NULL);
+                             sctx->libctx, sctx->propq);
     if (hctx == NULL)
         goto clearerrs;
     rv = OSSL_HPKE_decap(hctx, senderpub, senderpublen, ee->keyshare,
diff --git a/ssl/statem/extensions_clnt.c b/ssl/statem/extensions_clnt.c
index 457f138000849..18c4ed548d464 100644
--- a/ssl/statem/extensions_clnt.c
+++ b/ssl/statem/extensions_clnt.c
@@ -1401,7 +1401,7 @@ EXT_RETURN tls_construct_ctos_psk(SSL_CONNECTION *s, WPACKET *pkt,
             return EXT_RETURN_FAIL;
         }
         /* for outer CH allocate a similar sized random value */
-        if (RAND_bytes_ex(s->ssl.ctx->libctx, rndbuf, totalrndsize, 0) <= 0) {
+        if (RAND_bytes_ex(sctx->libctx, rndbuf, totalrndsize, 0) <= 0) {
             OPENSSL_free(rndbuf);
             SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
             return EXT_RETURN_FAIL;
@@ -2519,6 +2519,7 @@ EXT_RETURN tls_construct_ctos_ech(SSL_CONNECTION *s, WPACKET *pkt,
                                   size_t chainidx)
 {
     int rv = 0, hpke_mode = OSSL_HPKE_MODE_BASE;
+    SSL_CTX *sctx = SSL_CONNECTION_GET_CTX(s);
     OSSL_ECHSTORE_ENTRY *ee = NULL;
     OSSL_HPKE_SUITE hpke_suite = OSSL_HPKE_SUITE_DEFAULT;
     unsigned char config_id_to_use = 0x00, info[OSSL_ECH_MAX_INFO_LEN];
@@ -2597,7 +2598,7 @@ EXT_RETURN tls_construct_ctos_ech(SSL_CONNECTION *s, WPACKET *pkt,
     } OSSL_TRACE_END(TLS);
     config_id_to_use = ee->config_id; /* if requested, use a random config_id instead */
     if ((s->options & SSL_OP_ECH_IGNORE_CID) != 0) {
-        if (RAND_bytes_ex(s->ssl.ctx->libctx, &config_id_to_use, 1, 0) <= 0) {
+        if (RAND_bytes_ex(sctx->libctx, &config_id_to_use, 1, 0) <= 0) {
             SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
             return 0;
         }
@@ -2637,7 +2638,7 @@ EXT_RETURN tls_construct_ctos_ech(SSL_CONNECTION *s, WPACKET *pkt,
 # endif
         s->ext.ech.hpke_ctx = OSSL_HPKE_CTX_new(hpke_mode, hpke_suite,
                                                 OSSL_HPKE_ROLE_SENDER,
-                                                NULL, NULL);
+                                                sctx->libctx, sctx->propq);
         if (s->ext.ech.hpke_ctx == NULL) {
             SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
             goto err;
diff --git a/ssl/statem/extensions_srvr.c b/ssl/statem/extensions_srvr.c
index 7a5514a556efc..6a65b8b0b6de6 100644
--- a/ssl/statem/extensions_srvr.c
+++ b/ssl/statem/extensions_srvr.c
@@ -2500,6 +2500,7 @@ EXT_RETURN tls_construct_stoc_ech(SSL_CONNECTION *s, WPACKET *pkt,
 {
     unsigned char *rcfgs = NULL;
     size_t rcfgslen = 0;
+    SSL_CTX *sctx = SSL_CONNECTION_GET_CTX(s);
 
     if (context == SSL_EXT_TLS1_3_HELLO_RETRY_REQUEST
         && (s->ext.ech.success == 1 || s->ext.ech.backend == 1)
@@ -2522,7 +2523,7 @@ EXT_RETURN tls_construct_stoc_ech(SSL_CONNECTION *s, WPACKET *pkt,
         && s->ext.ech.attempted == 1) {
         unsigned char randomconf[8];
 
-        if (RAND_bytes_ex(s->ssl.ctx->libctx, randomconf, 8,
+        if (RAND_bytes_ex(sctx->libctx, randomconf, 8,
                           RAND_DRBG_STRENGTH) <= 0) {
             SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
             return 0;
diff --git a/ssl/statem/statem_clnt.c b/ssl/statem/statem_clnt.c
index cc69dc4356fea..b6c68194c29e2 100644
--- a/ssl/statem/statem_clnt.c
+++ b/ssl/statem/statem_clnt.c
@@ -1220,6 +1220,7 @@ __owur CON_FUNC_RETURN tls_construct_client_hello(SSL_CONNECTION *s,
     BUF_MEM *inner_mem = NULL;
     PACKET rpkt; /* we'll decode back the inner ch to help make the outer */
     SSL_SESSION *sess = NULL;
+    SSL_CTX *sctx = SSL_CONNECTION_GET_CTX(s);
     size_t sess_id_len = 0, innerlen = 0;
     int mt = SSL3_MT_CLIENT_HELLO, rv = 0;
     OSSL_HPKE_SUITE suite;
@@ -1277,7 +1278,7 @@ __owur CON_FUNC_RETURN tls_construct_client_hello(SSL_CONNECTION *s,
             sess_id_len = sizeof(s->tmp_session_id);
             s->tmp_session_id_len = sess_id_len;
             if (s->hello_retry_request == SSL_HRR_NONE
-                && RAND_bytes_ex(s->ssl.ctx->libctx, s->tmp_session_id,
+                && RAND_bytes_ex(sctx->libctx, s->tmp_session_id,
                                  sess_id_len, 0) <= 0) {
                 SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
                 return 0;

From 2ab36b206c87e966984e6af2715c5163f634dbef Mon Sep 17 00:00:00 2001
From: sftcd 
Date: Tue, 25 Nov 2025 22:41:23 +0000
Subject: [PATCH 19/24] ech_read_priv_echconfiglist(): Pass encodedlen to
 BIO_new_mem_buf()

Fixes DEF-02-001

Reviewed-by: Paul Dale 
Reviewed-by: Matt Caswell 
MergeDate: Wed Feb 11 17:19:08 2026
(Merged from https://github.com/openssl/openssl/pull/29593)
---
 ssl/ech/ech_store.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/ssl/ech/ech_store.c b/ssl/ech/ech_store.c
index a9a1b32561f1d..15c559e144f96 100644
--- a/ssl/ech/ech_store.c
+++ b/ssl/ech/ech_store.c
@@ -559,7 +559,7 @@ static int ech_read_priv_echconfiglist(OSSL_ECHSTORE *es, BIO *in,
         binlen = encodedlen;
     }
     if (detfmt == OSSL_ECH_FMT_B64TXT) {
-        btmp = BIO_new_mem_buf(encodedval, -1);
+        btmp = BIO_new_mem_buf(encodedval, encodedlen);
         if (btmp == NULL) {
             ERR_raise(ERR_LIB_SSL, ERR_R_INTERNAL_ERROR);
             goto err;

From 183f8ff8e544207d3419f71cfa5cde1a3bcbf29f Mon Sep 17 00:00:00 2001
From: sftcd 
Date: Tue, 25 Nov 2025 23:39:33 +0000
Subject: [PATCH 20/24] Document that SSL_OP_ECH_TRIALDECRYPT can cause DoS in
 some circumstances

Fixes DEF-02-002

Reviewed-by: Paul Dale 
Reviewed-by: Matt Caswell 
MergeDate: Wed Feb 11 17:19:10 2026
(Merged from https://github.com/openssl/openssl/pull/29593)
---
 doc/man3/SSL_CTX_set_options.pod | 13 +++++++++++++
 1 file changed, 13 insertions(+)

diff --git a/doc/man3/SSL_CTX_set_options.pod b/doc/man3/SSL_CTX_set_options.pod
index 21c33e22345e2..cc7cb932d43a7 100644
--- a/doc/man3/SSL_CTX_set_options.pod
+++ b/doc/man3/SSL_CTX_set_options.pod
@@ -383,6 +383,19 @@ ECH key pairs. By default, servers will only attempt decryption using
 an ECH key pair that matches the config_id in the ECH extension value
 received.
 
+Note that a server that has loaded many ECH configurations and that enables ECH
+trial decryption will attempt decryption with every ECH key when presented with
+a GREASEd ECH, and with possibly that many even when presented with a real ECH.
+That could easily become an accidental denial of service.
+
+Note also that the ECH specification recommends that servers that enable this
+option consider implementing some form of rate limiting mechanism to limit the
+potential damage caused in such scenarios.
+
+If trial decryption is enabled then decryption will be attempted with the ECH
+configurations in the order they were loaded. So, were it possible to load the
+configuration most likely to be used first, that would improve efficiency.
+
 =item SSL_OP_ECH_GREASE_RETRY_CONFIG
 
 If set, servers will add GREASEy ECHConfig values to those sent to the

From 6ab100fdd5e97651acec78a2f0945271195c4cf6 Mon Sep 17 00:00:00 2001
From: sftcd 
Date: Thu, 18 Dec 2025 02:10:38 +0000
Subject: [PATCH 21/24] ssl_choose_server_version(): With ECH check if
 connection is TLSv1.3

Fixes DEF-02-005

Reviewed-by: Paul Dale 
Reviewed-by: Matt Caswell 
MergeDate: Wed Feb 11 17:19:11 2026
(Merged from https://github.com/openssl/openssl/pull/29593)
---
 ssl/statem/statem_lib.c | 17 +++++++++++++++++
 1 file changed, 17 insertions(+)

diff --git a/ssl/statem/statem_lib.c b/ssl/statem/statem_lib.c
index 65530df7a433c..82582af54cce8 100644
--- a/ssl/statem/statem_lib.c
+++ b/ssl/statem/statem_lib.c
@@ -2205,6 +2205,18 @@ int ssl_choose_server_version(SSL_CONNECTION *s, CLIENTHELLO_MSG *hello,
 
     suppversions = &hello->pre_proc_exts[TLSEXT_IDX_supported_versions];
 
+#ifndef OPENSSL_NO_ECH
+    /*
+     * Check we're dealing with a TLSv1.3 connection when ECH has
+     * succeeded, and not with a smuggled earlier version ClientHello
+     * (which could be a form of attack).
+     * This bit checks there is a supported version present, a little
+     * bit further below, we check that that version is TLSv1.3
+     */
+    if (!suppversions->present && s->ext.ech.success == 1)
+        return SSL_R_UNSUPPORTED_PROTOCOL;
+#endif
+
     /* If we did an HRR then supported versions is mandatory */
     if (!suppversions->present && s->hello_retry_request != SSL_HRR_NONE)
         return SSL_R_UNSUPPORTED_PROTOCOL;
@@ -2246,6 +2258,11 @@ int ssl_choose_server_version(SSL_CONNECTION *s, CLIENTHELLO_MSG *hello,
         }
 
         if (best_vers > 0) {
+#ifndef OPENSSL_NO_ECH
+            /* ECH needs TLSV1.3 also */
+            if (s->ext.ech.success == 1 && best_vers != TLS1_3_VERSION)
+                return SSL_R_UNSUPPORTED_PROTOCOL;
+#endif
             if (s->hello_retry_request != SSL_HRR_NONE) {
                 /*
                  * This is after a HelloRetryRequest so we better check that we

From 998e4acb646472f76a3eb6c5d9a9fe757a4abcdb Mon Sep 17 00:00:00 2001
From: sftcd 
Date: Thu, 18 Dec 2025 13:48:28 +0000
Subject: [PATCH 22/24] ech_test.c: Add test for trying ECH with TLSv1.2

Fixes DEF-02-006

Reviewed-by: Paul Dale 
Reviewed-by: Matt Caswell 
MergeDate: Wed Feb 11 17:19:13 2026
(Merged from https://github.com/openssl/openssl/pull/29593)
---
 doc/man3/SSL_CTX_set_options.pod |  9 +++++----
 test/ech_test.c                  | 29 +++++++++++++++++++++++++++--
 2 files changed, 32 insertions(+), 6 deletions(-)

diff --git a/doc/man3/SSL_CTX_set_options.pod b/doc/man3/SSL_CTX_set_options.pod
index cc7cb932d43a7..97bb373c18675 100644
--- a/doc/man3/SSL_CTX_set_options.pod
+++ b/doc/man3/SSL_CTX_set_options.pod
@@ -385,16 +385,17 @@ received.
 
 Note that a server that has loaded many ECH configurations and that enables ECH
 trial decryption will attempt decryption with every ECH key when presented with
-a GREASEd ECH, and with possibly that many even when presented with a real ECH.
-That could easily become an accidental denial of service.
+a GREASEd ECH, (see B) and with possibly that many even
+when presented with a real ECH.  That could easily become an accidental denial
+of service.
 
 Note also that the ECH specification recommends that servers that enable this
 option consider implementing some form of rate limiting mechanism to limit the
 potential damage caused in such scenarios.
 
 If trial decryption is enabled then decryption will be attempted with the ECH
-configurations in the order they were loaded. So, were it possible to load the
-configuration most likely to be used first, that would improve efficiency.
+configurations in the order they were loaded. So, where it is possible to load
+the configuration most likely to be used first, that would improve efficiency.
 
 =item SSL_OP_ECH_GREASE_RETRY_CONFIG
 
diff --git a/test/ech_test.c b/test/ech_test.c
index fcd4795aa58a3..32182125a98d0 100644
--- a/test/ech_test.c
+++ b/test/ech_test.c
@@ -1190,6 +1190,7 @@ static int ech_boring_compat(void)
 # define OSSL_ECH_TEST_CUSTOM   3
 # define OSSL_ECH_TEST_ENOE     4 /* early + no-ech */
 # define OSSL_ECH_TEST_CBS      5 /* test callbacks */
+# define OSSL_ECH_TEST_V12      6 /* test TLSv1.2 */
 /* note: early-data is prohibited after HRR so no tests for that */
 
 /*
@@ -1245,6 +1246,13 @@ static int test_ech_roundtrip_helper(int idx, int combo)
                                           TLS1_3_VERSION, TLS1_3_VERSION,
                                           &sctx, &cctx, cert, privkey)))
         goto end;
+    if (combo == OSSL_ECH_TEST_V12) {
+        /* force client to TLSv1.2 and later fail as expected */
+        if (!TEST_true(SSL_CTX_set_max_proto_version(cctx, TLS1_2_VERSION)))
+            goto end;
+        if (!TEST_true(SSL_CTX_set_min_proto_version(cctx, TLS1_2_VERSION)))
+            goto end;
+    }
     if (combo == OSSL_ECH_TEST_EARLY || combo == OSSL_ECH_TEST_ENOE) {
         if (!TEST_true(SSL_CTX_set_options(sctx, SSL_OP_NO_ANTI_REPLAY))
             || !TEST_true(SSL_CTX_set_max_early_data(sctx,
@@ -1285,9 +1293,17 @@ static int test_ech_roundtrip_helper(int idx, int combo)
         goto end;
     if (!TEST_true(SSL_set_tlsext_host_name(clientssl, "server.example")))
         goto end;
-    if (!TEST_true(create_ssl_connection(serverssl, clientssl,
-                                         SSL_ERROR_NONE)))
+    if (combo == OSSL_ECH_TEST_V12) {
+        if (!TEST_false(create_ssl_connection(serverssl, clientssl,
+                                              SSL_ERROR_NONE)))
+            goto end;
+        res = 1;
         goto end;
+    } else {
+        if (!TEST_true(create_ssl_connection(serverssl, clientssl,
+                                             SSL_ERROR_NONE)))
+            goto end;
+    }
     /* override cert verification */
     SSL_set_verify_result(clientssl, X509_V_OK);
     clientstatus = SSL_ech_get1_status(clientssl, &cinner, &couter);
@@ -1437,6 +1453,14 @@ static int ech_cb_test(int idx)
     return test_ech_roundtrip_helper(idx, OSSL_ECH_TEST_CBS);
 }
 
+/* Test a roundtrip (fails) with ECH but a TLSv1.2 SSL_CTX */
+static int ech_v12_test(int idx)
+{
+    if (verbose)
+        TEST_info("Doing: ech TLSv1.2 test ");
+    return test_ech_roundtrip_helper(idx, OSSL_ECH_TEST_V12);
+}
+
 #endif
 
 int setup_tests(void)
@@ -1481,6 +1505,7 @@ int setup_tests(void)
     ADD_ALL_TESTS(ech_custom_test, suite_combos);
     ADD_ALL_TESTS(ech_enoe_test, suite_combos);
     ADD_ALL_TESTS(ech_cb_test, suite_combos);
+    ADD_ALL_TESTS(ech_v12_test, suite_combos);
     /* TODO(ECH): add more test code as other PRs done */
     return 1;
 err:

From dfbb985f354fe65a5227f9454e6ed4c0f9281876 Mon Sep 17 00:00:00 2001
From: sftcd 
Date: Thu, 18 Dec 2025 14:16:10 +0000
Subject: [PATCH 23/24] tls_process_server_hello(): With retry config validate
 the outer hostname

Call SSL_set1_host() to apply the outer hostname to the certificate
validation.

Fixes DEF-02-009

Reviewed-by: Paul Dale 
Reviewed-by: Matt Caswell 
MergeDate: Wed Feb 11 17:19:14 2026
(Merged from https://github.com/openssl/openssl/pull/29593)
---
 ssl/statem/statem_clnt.c | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/ssl/statem/statem_clnt.c b/ssl/statem/statem_clnt.c
index b6c68194c29e2..18f4673d1ec09 100644
--- a/ssl/statem/statem_clnt.c
+++ b/ssl/statem/statem_clnt.c
@@ -1901,6 +1901,10 @@ MSG_PROCESS_RETURN tls_process_server_hello(SSL_CONNECTION *s, PACKET *pkt)
                     SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
                     goto err;
                 }
+                if (SSL_set1_host(ssl, s->ext.ech.outer_hostname) != 1) {
+                    SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR);
+                    goto err;
+                }
             }
         }
     }

From 97068e36a2c3ef68b640196b39f16073563f220e Mon Sep 17 00:00:00 2001
From: sftcd 
Date: Thu, 18 Dec 2025 14:39:10 +0000
Subject: [PATCH 24/24] ossl_ech_get_retry_configs(): Check for integer
 overflow

Fixes DEF-02-010

Reviewed-by: Paul Dale 
Reviewed-by: Matt Caswell 
MergeDate: Wed Feb 11 17:19:16 2026
(Merged from https://github.com/openssl/openssl/pull/29593)
---
 ssl/ech/ech_internal.c | 2 ++
 1 file changed, 2 insertions(+)

diff --git a/ssl/ech/ech_internal.c b/ssl/ech/ech_internal.c
index 1102d1e2669fd..2c96ded096a3b 100644
--- a/ssl/ech/ech_internal.c
+++ b/ssl/ech/ech_internal.c
@@ -251,6 +251,8 @@ int ossl_ech_get_retry_configs(SSL_CONNECTION *s, unsigned char **rcfgs,
     for (i = 0; i != num; i++) {
         ee = sk_OSSL_ECHSTORE_ENTRY_value(es->entries, i);
         if (ee != NULL && ee->for_retry == OSSL_ECH_FOR_RETRY) {
+            if (ee->encoded_len > SIZE_MAX - retslen)
+                return 0;
             tmp = (unsigned char *)OPENSSL_realloc(rets,
                                                    retslen + ee->encoded_len);
             if (tmp == NULL)