diff --git a/doc/man3/SSL_ech_set1_echconfig.pod b/doc/man3/SSL_ech_set1_echconfig.pod index d52891f1d297dc..7132c449370338 100644 --- a/doc/man3/SSL_ech_set1_echconfig.pod +++ b/doc/man3/SSL_ech_set1_echconfig.pod @@ -13,7 +13,8 @@ SSL_CTX_ech_server_flush_keys, SSL_CTX_ech_server_enable_file, SSL_CTX_ech_server_enable_buffer, SSL_CTX_ech_server_enable_dir, SSL_CTX_ech_raw_decrypt, SSL_CTX_ech_set_callback, SSL_CTX_ech_set_outer_alpn_protos, SSL_ech_set_outer_alpn_protos, -OSSL_ech_make_echconfig, OSSL_ech_find_echconfigs +OSSL_ech_make_echconfig, OSSL_ech_find_echconfigs, +SSL_CTX_ech_set_pad_sizes,SSL_ech_set_pad_sizes - Encrypted ClientHello (ECH) functions =head1 SYNOPSIS @@ -69,6 +70,8 @@ OSSL_ech_make_echconfig, OSSL_ech_find_echconfigs int OSSL_ech_find_echconfigs(int *num_echs, unsigned char ***echconfigs, size_t **echlens, const unsigned char *val, size_t len); + int SSL_ech_set_pad_sizes(SSL *s, OSSL_ECH_PAD_SIZES *sizes); + int SSL_CTX_ech_set_pad_sizes(SSL_CTX *ctx, OSSL_ECH_PAD_SIZES *sizes); =head1 DESCRIPTION @@ -246,7 +249,6 @@ make use of ECH extensions. OSSL_ech_make_echconfig() is intended for use by applications that generate ECH configuration data, such as L. - =head2 Enabling ECH for servers In order to enable ECH for servers it is necessary to load one or more ECH @@ -320,6 +322,52 @@ succeeds, the I and I values will be populated based on the values seen. (The caller must free those strings using OPENSSL_free() when done.) +=head2 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 +``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 | 304 | 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. + + /* + * 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; + =head2 Accessing ECH information Given multiple ECH public values can be associated with a single I diff --git a/include/openssl/ech.h b/include/openssl/ech.h index cf9e51d54044f2..1855ab53442586 100644 --- a/include/openssl/ech.h +++ b/include/openssl/ech.h @@ -101,7 +101,7 @@ typedef struct ossl_ech_info_st { * SSL_CTX_set_options(ctx, SSL_OP_ECH_SPECIFIC_PADDING); */ # define OSSL_ECH_CERTPAD_MIN 1792 -# define OSSL_ECH_CERTVERPAD_MIN 480 +# define OSSL_ECH_CERTVERPAD_MIN 304 # define OSSL_ECH_ENCEXTPAD_MIN 32 # define OSSL_ECH_CERTPAD_UNIT 128 # define OSSL_ECH_CERTVERPAD_UNIT 16 diff --git a/ssl/ech.c b/ssl/ech.c index 7eade78918e81c..6ceace92fa1136 100644 --- a/ssl/ech.c +++ b/ssl/ech.c @@ -4700,8 +4700,16 @@ int OSSL_ECH_INFO_print(BIO *out, OSSL_ECH_INFO *se, int count) int SSL_CTX_ech_set_pad_sizes(SSL_CTX *ctx, OSSL_ECH_PAD_SIZES *sizes) { - if (ctx == NULL || sizes == NULL) + if (ctx == NULL || sizes == NULL) { + ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_NULL_PARAMETER); return 0; + } + if (sizes->cert_min == 0 || sizes->certver_min == 0 || sizes->ee_min == 0 + || sizes->cert_unit == 0 || sizes->certver_unit == 0 + || sizes->ee_unit == 0) { + ERR_raise(ERR_LIB_SSL, ERR_R_PASSED_INVALID_ARGUMENT); + return 0; + } ctx->ext.pad_sizes = *sizes; return 1; } @@ -4710,8 +4718,16 @@ int SSL_ech_set_pad_sizes(SSL *ssl, OSSL_ECH_PAD_SIZES *sizes) { SSL_CONNECTION *s = SSL_CONNECTION_FROM_SSL(ssl); - if (s == NULL || sizes == NULL) + if (s == NULL || sizes == NULL) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, ERR_R_PASSED_NULL_PARAMETER); + return 0; + } + if (sizes->cert_min == 0 || sizes->certver_min == 0 || sizes->ee_min == 0 + || sizes->cert_unit == 0 || sizes->certver_unit == 0 + || sizes->ee_unit == 0) { + SSLfatal(s, SSL_AD_INTERNAL_ERROR, SSL_R_BAD_LENGTH); return 0; + } s->ext.ech.pad_sizes = *sizes; return 1; } diff --git a/ssl/record/methods/tls13_meth.c b/ssl/record/methods/tls13_meth.c index afae14ad22b201..a611bbb9661dd7 100644 --- a/ssl/record/methods/tls13_meth.c +++ b/ssl/record/methods/tls13_meth.c @@ -329,6 +329,59 @@ static int tls13_add_record_padding(OSSL_RECORD_LAYER *rl, } TLS_RL_RECORD_add_length(thiswr, 1); +#ifndef OPENSSL_NO_ECH + /* + * If we are doing ECH and we want ECH specific padding then + * now is the time to ensure that happens. + * A lack of padding can expose information intended to be hidden via ECH, + * e.g. if only two inner CH SNI values were in live use. In that case we + * pad the Certificate, CertificateVerify and EncryptedExtensions handshake + * messages from the server. These are the minimum lengths to which those + * will be padded in that case. + * We do this before calling any padding callback supplied by the + * application, so they get to see the final sizes. + */ + if ((rl->options & SSL_OP_ECH_SPECIFIC_PADDING) != 0) { + SSL_CONNECTION *s = rl->cbarg; + SSL *ssl = SSL_CONNECTION_GET_SSL(s); + int do_pad = 0, echpad = 0, state = SSL_get_state(ssl); + size_t len = 0, min = 0, unit = 0; + + if (!WPACKET_get_length(thispkt, &len)) { + RLAYERfatal(rl, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + return 0; + } + if (s != NULL && s->server == 1 && s->ext.ech.attempted == 1 + && s->ext.ech.success == 1) { + if (state == TLS_ST_SW_CERT) { + min = s->ext.ech.pad_sizes.cert_min; + unit = s->ext.ech.pad_sizes.cert_unit; + do_pad = 1; + } + if (state == TLS_ST_SW_CERT_VRFY) { + min = s->ext.ech.pad_sizes.certver_min; + unit = s->ext.ech.pad_sizes.certver_unit; + do_pad = 1; + } + if (state == TLS_ST_SW_ENCRYPTED_EXTENSIONS) { + min = s->ext.ech.pad_sizes.ee_min; + unit = s->ext.ech.pad_sizes.ee_unit; + do_pad = 1; + } + if (do_pad == 1) { + if (len < min) + echpad = min - len; + echpad += ((len + echpad) % unit ? unit - ((len + echpad) % unit) : 0); + if (echpad > 0 && !WPACKET_memset(thispkt, 0, echpad)) { + RLAYERfatal(rl, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); + return 0; + } + TLS_RL_RECORD_add_length(thiswr, echpad); + } + } + } +#endif + /* Add TLS1.3 padding */ rlen = TLS_RL_RECORD_get_length(thiswr); if (rlen < rl->max_frag_len) { diff --git a/ssl/record/methods/tls_common.c b/ssl/record/methods/tls_common.c index ea779060a3abb2..b09991cafb1d7c 100644 --- a/ssl/record/methods/tls_common.c +++ b/ssl/record/methods/tls_common.c @@ -1830,59 +1830,6 @@ int tls_write_records_default(OSSL_RECORD_LAYER *rl, goto err; } -#ifndef OPENSSL_NO_ECH - /* - * If we are doing ECH and we want ECH specific padding then - * now is the time to ensure that happens. - * A lack of padding can expose information intended to be hidden via ECH, - * e.g. if only two inner CH SNI values were in live use. In that case we - * pad the Certificate, CertificateVerify and EncryptedExtensions handshake - * messages from the server. These are the minimum lengths to which those - * will be padded in that case. - */ - if ((rl->options & SSL_OP_ECH_SPECIFIC_PADDING) != 0) - { - SSL_CONNECTION *s = rl->cbarg; - SSL *ssl = SSL_CONNECTION_GET_SSL(s); - int do_pad = 0, morepad = 0, state = SSL_get_state(ssl); - size_t len = 0, min = 0, unit = 0; - - if (!WPACKET_get_length(thispkt, &len)) { - RLAYERfatal(rl, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); - goto err; - } - - if (s != NULL && s->server == 1 && s->ext.ech.attempted == 1 - && s->ext.ech.success == 1) { - - if (state == TLS_ST_SW_CERT) { - min = s->ext.ech.pad_sizes.cert_min; - unit = s->ext.ech.pad_sizes.cert_unit; - do_pad = 1; - } - if (state == TLS_ST_SW_CERT_VRFY) { - min = s->ext.ech.pad_sizes.certver_min; - unit = s->ext.ech.pad_sizes.certver_min; - do_pad = 1; - } - if (state == TLS_ST_SW_ENCRYPTED_EXTENSIONS) { - min = s->ext.ech.pad_sizes.ee_min; - unit = s->ext.ech.pad_sizes.ee_min; - do_pad = 1; - } - if (do_pad == 1) { - if (len < min) - morepad = min - len; - morepad += ((len + morepad) % unit ? unit - ((len + morepad) % unit) : 0); - if (morepad > 0 && !WPACKET_memset(thispkt, 0, morepad)) { - RLAYERfatal(rl, SSL_AD_INTERNAL_ERROR, ERR_R_INTERNAL_ERROR); - goto err; - } - } - } - } -#endif - if (!rl->funcs->prepare_for_encryption(rl, mac_size, thispkt, thiswr)) { /* RLAYERfatal() already called */ goto err; diff --git a/test/ech_test.c b/test/ech_test.c index 3aabe21f58af2c..15680f979a14d3 100644 --- a/test/ech_test.c +++ b/test/ech_test.c @@ -911,6 +911,179 @@ static int extended_echconfig(int idx) return res; } +/* + * Test ECH-specific padding. + * + * Make one call per entry in pad_array and compare the + * resulting message sizes against the expected values. + * We check the actual values via a padding callback. + * + * pad_array[0] is a dummy that's written to in the 1st + * iteration so that we can calculate the expected sizes + * for subsequent iterations. + */ +static OSSL_ECH_PAD_SIZES pad_array[] = { + /* dummy for 1st iteration */ + { 0, 0, 0, 0, 0, 0}, + /* + * cert min is > actual + * certver min is set to 1, well lower than actual + * ee min is close to size + */ + { 1000, 16, 1, 16, 40, 16}, +}; + +/* callback code needs to tell test code if we see unexpected values */ +static int ech_pad_cb_fail = 0; + +/* + * This is passed to SSL_CTX_set_record_padding_callback + * and checks padding of the Certificate, CertificateVerify and + * EncryptedExtensions handshake messages vs. the expected + * size from the argument arg. + */ +static size_t ech_padding_cb(SSL *s, int type, size_t len, void *arg) +{ + OSSL_ECH_PAD_SIZES *pl = (OSSL_ECH_PAD_SIZES *)arg; + int state = SSL_get_state(s), do_check = 0; + size_t expected = 0, unpadded = 0, min = 0, unit = 0; + + /* for the first case, we remember the length in actual use + which depends on the pem files */ + if (state == TLS_ST_SW_CERT && pl->cert_min == 0) + pad_array[0].cert_min = len; + else if (state == TLS_ST_SW_CERT && pl->cert_min != 0) { + unpadded = pad_array[0].cert_min; + min = pl->cert_min; + unit = pl->cert_unit; + do_check = 1; + } + if (state == TLS_ST_SW_CERT_VRFY && pl->certver_min == 0) + pad_array[0].certver_min = len; + else if (state == TLS_ST_SW_CERT_VRFY && pl->certver_min != 0) { + unpadded = pad_array[0].certver_min; + min = pl->certver_min; + unit = pl->certver_unit; + do_check = 1; + } + if (state == TLS_ST_SW_ENCRYPTED_EXTENSIONS && pl->ee_min == 0) + pad_array[0].ee_min = len; + else if (state == TLS_ST_SW_ENCRYPTED_EXTENSIONS && pl->ee_min != 0) { + unpadded = pad_array[0].ee_min; + min = pl->ee_min; + unit = pl->ee_unit; + do_check = 1; + } + if (do_check == 1) { + /* check if we got what we expected */ + if (unpadded < min) + expected = min; + else + expected = unpadded; + expected += (expected % unit ? unit - (expected % unit) : 0); + if (len != expected) { + TEST_info("ech_pad_test: bad length %ld for type %d expected %ld)\n", + len, type, expected); + ech_pad_cb_fail = 1; + } + } + return 0; +} + +static int ech_pad_test(int idx) +{ + int res = 0; + char *echkeyfile = NULL; + char *echconfig = NULL; + size_t echconfiglen = 0; + SSL_CTX *cctx = NULL, *sctx = NULL; + SSL *clientssl = NULL, *serverssl = NULL; + int clientstatus, serverstatus; + char *cinner = NULL, *couter = NULL, *sinner = NULL, *souter = NULL; + + /* + * Ensure 1st iteration happens in all cases - needed so that + * we can calculate the expected values for subsequent iterations + * if e.g. someone uses "-iter 2" on the command line + * Doing this via recursion may be odd, but is ok:-) + */ + if (idx != 0 && pad_array[0].cert_min == 0) + ech_pad_test(0); + + /* read our pre-cooked ECH PEM file */ + echkeyfile = test_mk_file_path(certsdir, "echconfig.pem"); + if (!TEST_ptr(echkeyfile)) + goto end; + echconfig = echconfiglist_from_PEM(echkeyfile); + if (!TEST_ptr(echconfig)) + goto end; + echconfiglen = strlen(echconfig); + if (!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; + /* set padding callback */ + SSL_CTX_set_record_padding_callback_arg(sctx, (void *)&(pad_array[idx])); + SSL_CTX_set_record_padding_callback(sctx, ech_padding_cb); + /* if not in 1st test then use the API to set fine-grained sizes */ + if (idx == 0) { + /* check zeros cause a fail */ + if (!TEST_false(SSL_CTX_ech_set_pad_sizes(sctx, &pad_array[idx]))) + goto end; + /* don't pad 1st time. */ + } else { + /* turn on padding */ + SSL_CTX_set_options(sctx, SSL_OP_ECH_SPECIFIC_PADDING); + if (!TEST_true(SSL_CTX_ech_set_pad_sizes(sctx, &pad_array[idx]))) + goto end; + } + if (!TEST_true(SSL_CTX_ech_set1_echconfig(cctx, (unsigned char *)echconfig, + echconfiglen))) + goto end; + if (!TEST_true(SSL_CTX_ech_server_enable_file(sctx, echkeyfile, + SSL_ECH_USE_FOR_RETRY))) + goto end; + if (!TEST_true(create_ssl_objects(sctx, cctx, &serverssl, + &clientssl, NULL, NULL))) + 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))) + goto end; + serverstatus = SSL_ech_get_status(serverssl, &sinner, &souter); + if (verbose) + TEST_info("ech_pad_test: server status %d, %s, %s", + serverstatus, sinner, souter); + if (!TEST_int_eq(serverstatus, SSL_ECH_STATUS_SUCCESS)) + goto end; + /* override cert verification */ + SSL_set_verify_result(clientssl, X509_V_OK); + clientstatus = SSL_ech_get_status(clientssl, &cinner, &couter); + if (verbose) + TEST_info("ech_pad_test: client status %d, %s, %s", + clientstatus, cinner, couter); + if (!TEST_int_eq(clientstatus, SSL_ECH_STATUS_SUCCESS)) + goto end; + if (!TEST_int_eq(ech_pad_cb_fail,0)) + goto end; + /* all good */ + res = 1; +end: + OPENSSL_free(sinner); + OPENSSL_free(souter); + OPENSSL_free(cinner); + OPENSSL_free(couter); + OPENSSL_free(echkeyfile); + OPENSSL_free(echconfig); + SSL_free(clientssl); + SSL_free(serverssl); + SSL_CTX_free(cctx); + SSL_CTX_free(sctx); + return res; +} + /* Test a basic roundtrip with ECH, with a PEM file input */ static int ech_roundtrip_test(int idx) { @@ -998,12 +1171,10 @@ static int ech_tls12_with_ech_test(int idx) size_t badprivlen = sizeof(badpriv); uint16_t ech_version = OSSL_ECH_CURRENT_VERSION; OSSL_HPKE_SUITE hpke_suite = OSSL_HPKE_SUITE_DEFAULT; - // int err = 0, connrv = 0, err_reason = 0; char *good_public_name = "front.server.example"; char *public_name = good_public_name; X509_STORE *vfy = NULL; int cver; - // int exp_conn_err = SSL_R_ECH_REQUIRED; int client = 0; /* for these tests we want to chain to our root */ @@ -1078,7 +1249,6 @@ static int ech_tls12_with_ech_test(int idx) OPENSSL_free(souter); OPENSSL_free(cinner); OPENSSL_free(couter); - //OPENSSL_free(retryconfig); OPENSSL_free(echkeyfile); SSL_free(clientssl); SSL_free(serverssl); @@ -2376,6 +2546,7 @@ int setup_tests(void) ADD_ALL_TESTS(ech_wrong_pub_test, 3); ADD_ALL_TESTS(ech_tls12_with_ech_test, 1); ADD_ALL_TESTS(ech_sni_cb_test,1); + ADD_ALL_TESTS(ech_pad_test,OSSL_NELEM(pad_array)); return 1; err: return 0;