diff --git a/crypto/err/ssl.errordata b/crypto/err/ssl.errordata index 04c7d53ee..a279ca6b1 100644 --- a/crypto/err/ssl.errordata +++ b/crypto/err/ssl.errordata @@ -43,6 +43,7 @@ SSL,131,CLIENTHELLO_PARSE_FAILED SSL,132,CLIENTHELLO_TLSEXT SSL,133,CONNECTION_REJECTED SSL,134,CONNECTION_TYPE_NOT_SET +SSL,316,COULD_NOT_PARSE_HINTS SSL,135,CUSTOM_EXTENSION_ERROR SSL,136,DATA_LENGTH_TOO_LONG SSL,137,DECODE_ERROR diff --git a/include/openssl/ssl.h b/include/openssl/ssl.h index 8d8e09162..aef6b35e7 100644 --- a/include/openssl/ssl.h +++ b/include/openssl/ssl.h @@ -567,6 +567,11 @@ OPENSSL_EXPORT int SSL_get_error(const SSL *ssl, int ret_code); // See also |ssl_renegotiate_explicit|. #define SSL_ERROR_WANT_RENEGOTIATE 19 +// SSL_ERROR_HANDSHAKE_HINTS_READY indicates the handshake has progressed enough +// for |SSL_serialize_handshake_hints| to be called. See also +// |SSL_request_handshake_hints|. +#define SSL_ERROR_HANDSHAKE_HINTS_READY 20 + // SSL_error_description returns a string representation of |err|, where |err| // is one of the |SSL_ERROR_*| constants returned by |SSL_get_error|, or NULL // if the value is unrecognized. @@ -3793,6 +3798,101 @@ OPENSSL_EXPORT uint64_t SSL_get_read_sequence(const SSL *ssl); OPENSSL_EXPORT uint64_t SSL_get_write_sequence(const SSL *ssl); +// Handshake hints. +// +// *** EXPERIMENTAL — DO NOT USE WITHOUT CHECKING *** +// +// Some server deployments make asynchronous RPC calls in both ClientHello +// dispatch and private key operations. In TLS handshakes where the private key +// operation occurs in the first round-trip, this results in two consecutive RPC +// round-trips. Handshake hints allow the RPC service to predicte a signature. +// If correctly predicted, this can skip the second RPC call. +// +// First, the server installs a certificate selection callback (see +// |SSL_CTX_set_select_certificate_cb|). When that is called, it performs the +// RPC as before, but includes the ClientHello and a capabilities string from +// |SSL_serialize_capabilities|. +// +// Next, the RPC service creates its own |SSL| object, applies the results of +// certificate selection, calls |SSL_request_handshake_hints|, and runs the +// handshake. If this successfully computes handshake hints (see +// |SSL_serialize_handshake_hints|), the RPC server should send the hints +// alongside any certificate selection results. +// +// Finally, the server calls |SSL_set_handshake_hints| and applies any +// configuration from the RPC server. It then completes the handshake as before. +// If the hints apply, BoringSSL will use the predicted signature and skip the +// private key callbacks. Otherwise, BoringSSL will call private key callbacks +// to generate a signature as before. +// +// Callers should synchronize configuration across the two services. +// Configuration mismatches and some cases of version skew are not fatal, but +// may result in the hints not applying. Additionally, some handshake flows use +// the private key in later round-trips, such as TLS 1.3 HelloRetryRequest. In +// those cases, BoringSSL will not predict a signature as there is no benefit. +// Callers must allow for handshakes to complete without a predicted signature. +// +// For now, only TLS 1.3 is hinted. TLS 1.2 will work, but the hints will be +// empty. + +// SSL_serialize_capabilities writes an opaque byte string to |out| describing +// some of |ssl|'s capabilities. It returns one on success and zero on error. +// +// This string is used by BoringSSL internally to reduce the impact of version +// skew. +OPENSSL_EXPORT int SSL_serialize_capabilities(const SSL *ssl, CBB *out); + +// SSL_request_handshake_hints configures |ssl| to generate a handshake hint for +// |client_hello|. It returns one on success and zero on error. |client_hello| +// should contain a serialized ClientHello structure, from the |client_hello| +// and |client_hello_len| fields of the |SSL_CLIENT_HELLO| structure. +// |capabilities| should contain the output of |SSL_serialize_capabilities|. +// +// When configured, |ssl| will perform no I/O (so there is no need to configure +// |BIO|s). For QUIC, the caller should still configure an |SSL_QUIC_METHOD|, +// but the callbacks themselves will never be called and may be left NULL or +// report failure. |SSL_provide_quic_data| also should not be called. +// +// If hint generation is successful, |SSL_do_handshake| will stop the handshake +// early with |SSL_get_error| returning |SSL_ERROR_HANDSHAKE_HINTS_READY|. At +// this point, the caller should run |SSL_serialize_handshake_hints| to extract +// the resulting hints. +// +// Hint generation may fail if, e.g., |ssl| was unable to process the +// ClientHello. Callers should then complete the certificate selection RPC and +// continue the original handshake with no hint. It will likely fail, but this +// reports the correct alert to the client and is more robust in case of +// mismatch. +OPENSSL_EXPORT int SSL_request_handshake_hints(SSL *ssl, + const uint8_t *client_hello, + size_t client_hello_len, + const uint8_t *capabilities, + size_t capabilities_len); + +// SSL_serialize_handshake_hints writes an opaque byte string to |out| +// containing the handshake hints computed by |out|. It returns one on success +// and zero on error. This function should only be called if +// |SSL_request_handshake_hints| was configured and the handshake terminated +// with |SSL_ERROR_HANDSHAKE_HINTS_READY|. +// +// This string may be passed to |SSL_set_handshake_hints| on another |SSL| to +// avoid an extra signature call. +OPENSSL_EXPORT int SSL_serialize_handshake_hints(const SSL *ssl, CBB *out); + +// SSL_set_handshake_hints configures |ssl| to use |hints| as handshake hints. +// It returns one on success and zero on error. The handshake will then continue +// as before, but apply predicted values from |hints| where applicable. +// +// Hints may contain connection and session secrets, so they must not leak and +// must come from a source trusted to terminate the connection. However, they +// will not change |ssl|'s configuration. The caller is responsible for +// serializing and applying options from the RPC server as needed. This ensures +// |ssl|'s behavior is self-consistent and consistent with the caller's local +// decisions. +OPENSSL_EXPORT int SSL_set_handshake_hints(SSL *ssl, const uint8_t *hints, + size_t hints_len); + + // Obscure functions. // SSL_CTX_set_msg_callback installs |cb| as the message callback for |ctx|. @@ -5148,6 +5248,7 @@ OPENSSL_EXPORT bool SSL_get_traffic_secrets( const SSL *ssl, Span *out_read_traffic_secret, Span *out_write_traffic_secret); + BSSL_NAMESPACE_END } // extern C++ @@ -5371,6 +5472,7 @@ BSSL_NAMESPACE_END #define SSL_R_ECH_SERVER_WOULD_HAVE_NO_RETRY_CONFIGS 313 #define SSL_R_INVALID_CLIENT_HELLO_INNER 314 #define SSL_R_INVALID_ALPN_PROTOCOL_LIST 315 +#define SSL_R_COULD_NOT_PARSE_HINTS 316 #define SSL_R_SSLV3_ALERT_CLOSE_NOTIFY 1000 #define SSL_R_SSLV3_ALERT_UNEXPECTED_MESSAGE 1010 #define SSL_R_SSLV3_ALERT_BAD_RECORD_MAC 1020 diff --git a/ssl/handoff.cc b/ssl/handoff.cc index 6cf0a444a..aededdb88 100644 --- a/ssl/handoff.cc +++ b/ssl/handoff.cc @@ -15,6 +15,7 @@ #include #include +#include #include "internal.h" @@ -708,3 +709,245 @@ bool SSL_apply_handback(SSL *ssl, Span handback) { } BSSL_NAMESPACE_END + +using namespace bssl; + +int SSL_serialize_capabilities(const SSL *ssl, CBB *out) { + CBB seq; + if (!CBB_add_asn1(out, &seq, CBS_ASN1_SEQUENCE) || + !serialize_features(&seq) || // + !CBB_flush(out)) { + return 0; + } + + return 1; +} + +int SSL_request_handshake_hints(SSL *ssl, const uint8_t *client_hello, + size_t client_hello_len, + const uint8_t *capabilities, + size_t capabilities_len) { + if (SSL_is_dtls(ssl)) { + OPENSSL_PUT_ERROR(SSL, ERR_R_SHOULD_NOT_HAVE_BEEN_CALLED); + return 0; + } + + CBS cbs, seq; + CBS_init(&cbs, capabilities, capabilities_len); + UniquePtr hints = MakeUnique(); + if (hints == nullptr || + !CBS_get_asn1(&cbs, &seq, CBS_ASN1_SEQUENCE) || + !apply_remote_features(ssl, &seq)) { + return 0; + } + + SSL3_STATE *const s3 = ssl->s3; + s3->v2_hello_done = true; + s3->has_message = true; + + Array client_hello_msg; + ScopedCBB client_hello_cbb; + CBB client_hello_body; + if (!ssl->method->init_message(ssl, client_hello_cbb.get(), + &client_hello_body, SSL3_MT_CLIENT_HELLO) || + !CBB_add_bytes(&client_hello_body, client_hello, client_hello_len) || + !ssl->method->finish_message(ssl, client_hello_cbb.get(), + &client_hello_msg)) { + return 0; + } + + s3->hs_buf.reset(BUF_MEM_new()); + if (!s3->hs_buf || !BUF_MEM_append(s3->hs_buf.get(), client_hello_msg.data(), + client_hello_msg.size())) { + return 0; + } + + s3->hs->hints_requested = true; + s3->hs->hints = std::move(hints); + return 1; +} + +// |SSL_HANDSHAKE_HINTS| is serialized as the following ASN.1 structure. We use +// implicit tagging to make it a little more compact. +// +// HandshakeHints ::= SEQUENCE { +// serverRandom [0] IMPLICIT OCTET STRING OPTIONAL, +// keyShareHint [1] IMPLICIT KeyShareHint OPTIONAL, +// signatureHint [2] IMPLICIT SignatureHint OPTIONAL, +// -- At most one of decryptedPSKHint or ignorePSKHint may be present. It +// -- corresponds to the first entry in pre_shared_keys. TLS 1.2 session +// -- tickets will use a separate hint, to ensure the caller does not mix +// -- them up. +// decryptedPSKHint [3] IMPLICIT OCTET STRING OPTIONAL, +// ignorePSKHint [4] IMPLICIT NULL OPTIONAL, +// } +// +// KeyShareHint ::= SEQUENCE { +// groupId INTEGER, +// publicKey OCTET STRING, +// secret OCTET STRING, +// } +// +// SignatureHint ::= SEQUENCE { +// algorithm INTEGER, +// input OCTET STRING, +// subjectPublicKeyInfo OCTET STRING, +// signature OCTET STRING, +// } + +// HandshakeHints tags. +static const unsigned kServerRandomTag = CBS_ASN1_CONTEXT_SPECIFIC | 0; +static const unsigned kKeyShareHintTag = + CBS_ASN1_CONSTRUCTED | CBS_ASN1_CONTEXT_SPECIFIC | 1; +static const unsigned kSignatureHintTag = + CBS_ASN1_CONSTRUCTED | CBS_ASN1_CONTEXT_SPECIFIC | 2; +static const unsigned kDecryptedPSKTag = CBS_ASN1_CONTEXT_SPECIFIC | 3; +static const unsigned kIgnorePSKTag = CBS_ASN1_CONTEXT_SPECIFIC | 4; + +int SSL_serialize_handshake_hints(const SSL *ssl, CBB *out) { + const SSL_HANDSHAKE *hs = ssl->s3->hs.get(); + if (!ssl->server || !hs->hints_requested) { + OPENSSL_PUT_ERROR(SSL, ERR_R_SHOULD_NOT_HAVE_BEEN_CALLED); + return 0; + } + + const SSL_HANDSHAKE_HINTS *hints = hs->hints.get(); + CBB seq, server_random, key_share_hint, signature_hint, decrypted_psk, + ignore_psk; + if (!CBB_add_asn1(out, &seq, CBS_ASN1_SEQUENCE)) { + return 0; + } + + if (!hints->server_random.empty()) { + if (!CBB_add_asn1(&seq, &server_random, kServerRandomTag) || + !CBB_add_bytes(&server_random, hints->server_random.data(), + hints->server_random.size())) { + return 0; + } + } + + if (hints->key_share_group_id != 0 && !hints->key_share_public_key.empty() && + !hints->key_share_secret.empty()) { + if (!CBB_add_asn1(&seq, &key_share_hint, kKeyShareHintTag) || + !CBB_add_asn1_uint64(&key_share_hint, hints->key_share_group_id) || + !CBB_add_asn1_octet_string(&key_share_hint, + hints->key_share_public_key.data(), + hints->key_share_public_key.size()) || + !CBB_add_asn1_octet_string(&key_share_hint, + hints->key_share_secret.data(), + hints->key_share_secret.size())) { + return 0; + } + } + + if (hints->signature_algorithm != 0 && !hints->signature_input.empty() && + !hints->signature.empty()) { + if (!CBB_add_asn1(&seq, &signature_hint, kSignatureHintTag) || + !CBB_add_asn1_uint64(&signature_hint, hints->signature_algorithm) || + !CBB_add_asn1_octet_string(&signature_hint, + hints->signature_input.data(), + hints->signature_input.size()) || + !CBB_add_asn1_octet_string(&signature_hint, + hints->signature_spki.data(), + hints->signature_spki.size()) || + !CBB_add_asn1_octet_string(&signature_hint, hints->signature.data(), + hints->signature.size())) { + return 0; + } + } + + if (!hints->decrypted_psk.empty()) { + if (!CBB_add_asn1(&seq, &decrypted_psk, kDecryptedPSKTag) || + !CBB_add_bytes(&decrypted_psk, hints->decrypted_psk.data(), + hints->decrypted_psk.size())) { + return 0; + } + } + + if (hints->ignore_psk && // + !CBB_add_asn1(&seq, &ignore_psk, kIgnorePSKTag)) { + return 0; + } + + return CBB_flush(out); +} + +int SSL_set_handshake_hints(SSL *ssl, const uint8_t *hints, size_t hints_len) { + if (SSL_is_dtls(ssl)) { + OPENSSL_PUT_ERROR(SSL, ERR_R_SHOULD_NOT_HAVE_BEEN_CALLED); + return 0; + } + + UniquePtr hints_obj = MakeUnique(); + if (hints_obj == nullptr) { + return 0; + } + + CBS cbs, seq, server_random, key_share, signature_hint, ticket, ignore_psk; + int has_server_random, has_key_share, has_signature_hint, has_ticket, + has_ignore_psk; + CBS_init(&cbs, hints, hints_len); + if (!CBS_get_asn1(&cbs, &seq, CBS_ASN1_SEQUENCE) || + !CBS_get_optional_asn1(&seq, &server_random, &has_server_random, + kServerRandomTag) || + !CBS_get_optional_asn1(&seq, &key_share, &has_key_share, + kKeyShareHintTag) || + !CBS_get_optional_asn1(&seq, &signature_hint, &has_signature_hint, + kSignatureHintTag) || + !CBS_get_optional_asn1(&seq, &ticket, &has_ticket, kDecryptedPSKTag) || + !CBS_get_optional_asn1(&seq, &ignore_psk, &has_ignore_psk, + kIgnorePSKTag)) { + OPENSSL_PUT_ERROR(SSL, SSL_R_COULD_NOT_PARSE_HINTS); + return 0; + } + + if (has_server_random && !hints_obj->server_random.CopyFrom(server_random)) { + return 0; + } + + if (has_key_share) { + uint64_t group_id; + CBS public_key, secret; + if (!CBS_get_asn1_uint64(&key_share, &group_id) || // + group_id == 0 || group_id > 0xffff || + !CBS_get_asn1(&key_share, &public_key, CBS_ASN1_OCTETSTRING) || + !hints_obj->key_share_public_key.CopyFrom(public_key) || + !CBS_get_asn1(&key_share, &secret, CBS_ASN1_OCTETSTRING) || + !hints_obj->key_share_secret.CopyFrom(secret)) { + OPENSSL_PUT_ERROR(SSL, SSL_R_COULD_NOT_PARSE_HINTS); + return 0; + } + hints_obj->key_share_group_id = static_cast(group_id); + } + + if (has_signature_hint) { + uint64_t sig_alg; + CBS input, spki, signature; + if (!CBS_get_asn1_uint64(&signature_hint, &sig_alg) || // + sig_alg == 0 || sig_alg > 0xffff || + !CBS_get_asn1(&signature_hint, &input, CBS_ASN1_OCTETSTRING) || + !hints_obj->signature_input.CopyFrom(input) || + !CBS_get_asn1(&signature_hint, &spki, CBS_ASN1_OCTETSTRING) || + !hints_obj->signature_spki.CopyFrom(spki) || + !CBS_get_asn1(&signature_hint, &signature, CBS_ASN1_OCTETSTRING) || + !hints_obj->signature.CopyFrom(signature)) { + OPENSSL_PUT_ERROR(SSL, SSL_R_COULD_NOT_PARSE_HINTS); + return 0; + } + hints_obj->signature_algorithm = static_cast(sig_alg); + } + + if (has_ticket && !hints_obj->decrypted_psk.CopyFrom(ticket)) { + return 0; + } + + if (has_ignore_psk) { + if (CBS_len(&ignore_psk) != 0) { + return 0; + } + hints_obj->ignore_psk = true; + } + + ssl->s3->hs->hints = std::move(hints_obj); + return 1; +} diff --git a/ssl/handshake.cc b/ssl/handshake.cc index f5b6ca067..289c612a5 100644 --- a/ssl/handshake.cc +++ b/ssl/handshake.cc @@ -149,6 +149,7 @@ SSL_HANDSHAKE::SSL_HANDSHAKE(SSL *ssl_arg) pending_private_key_op(false), grease_seeded(false), handback(false), + hints_requested(false), cert_compression_negotiated(false), apply_jdk11_workaround(false) { assert(ssl); @@ -713,6 +714,10 @@ int ssl_run_handshake(SSL_HANDSHAKE *hs, bool *out_early_return) { hs->wait = ssl_hs_ok; return 1; + case ssl_hs_hints_ready: + ssl->s3->rwstate = SSL_ERROR_HANDSHAKE_HINTS_READY; + return -1; + case ssl_hs_ok: break; } diff --git a/ssl/handshake_server.cc b/ssl/handshake_server.cc index 1c5f0cf17..2eaec01ac 100644 --- a/ssl/handshake_server.cc +++ b/ssl/handshake_server.cc @@ -830,6 +830,12 @@ static enum ssl_hs_wait_t do_select_certificate(SSL_HANDSHAKE *hs) { // or below. assert(!hs->ech_accept); + // TODO(davidben): Also compute hints for TLS 1.2. When doing so, update the + // check in bssl_shim.cc to test this. + if (hs->hints_requested) { + return ssl_hs_hints_ready; + } + ssl->s3->early_data_reason = ssl_early_data_protocol_version; SSLMessage msg_unused; diff --git a/ssl/internal.h b/ssl/internal.h index 6edd26cf7..16e100b6d 100644 --- a/ssl/internal.h +++ b/ssl/internal.h @@ -1577,6 +1577,7 @@ enum ssl_hs_wait_t { ssl_hs_read_end_of_early_data, ssl_hs_read_change_cipher_spec, ssl_hs_certificate_verify, + ssl_hs_hints_ready, }; enum ssl_grease_index_t { @@ -1644,6 +1645,26 @@ enum handback_t { handback_max_value = handback_tls13, }; +// SSL_HANDSHAKE_HINTS contains handshake hints for a connection. See +// |SSL_request_handshake_hints| and related functions. +struct SSL_HANDSHAKE_HINTS { + static constexpr bool kAllowUniquePtr = true; + + Array server_random; + + uint16_t key_share_group_id = 0; + Array key_share_public_key; + Array key_share_secret; + + uint16_t signature_algorithm = 0; + Array signature_input; + Array signature_spki; + Array signature; + + Array decrypted_psk; + bool ignore_psk = false; +}; + struct SSL_HANDSHAKE { explicit SSL_HANDSHAKE(SSL *ssl); ~SSL_HANDSHAKE(); @@ -1842,6 +1863,13 @@ struct SSL_HANDSHAKE { // key_block is the record-layer key block for TLS 1.2 and earlier. Array key_block; + // hints contains the handshake hints for this connection. If + // |hints_requested| is true, this field is non-null and contains the pending + // hints to filled as the predicted handshake progresses. Otherwise, this + // field, if non-null, contains hints configured by the caller and will + // influence the handshake on match. + UniquePtr hints; + // ech_accept, on the server, indicates whether the server should overwrite // part of ServerHello.random with the ECH accept_confirmation value. bool ech_accept : 1; @@ -1931,6 +1959,11 @@ struct SSL_HANDSHAKE { // |SSL_apply_handoff|. bool handback : 1; + // hints_requested indicates the caller has requested handshake hints. Only + // the first round-trip of the handshake will complete, after which the + // |hints| structure can be serialized. + bool hints_requested : 1; + // cert_compression_negotiated is true iff |cert_compression_alg_id| is valid. bool cert_compression_negotiated : 1; diff --git a/ssl/s3_both.cc b/ssl/s3_both.cc index 92d3a247b..7ad821041 100644 --- a/ssl/s3_both.cc +++ b/ssl/s3_both.cc @@ -251,7 +251,8 @@ bool tls_flush_pending_hs_data(SSL *ssl) { MakeConstSpan(reinterpret_cast(pending_hs_data->data), pending_hs_data->length); if (ssl->quic_method) { - if (!ssl->quic_method->add_handshake_data(ssl, ssl->s3->write_level, + if ((ssl->s3->hs == nullptr || !ssl->s3->hs->hints_requested) && + !ssl->quic_method->add_handshake_data(ssl, ssl->s3->write_level, data.data(), data.size())) { OPENSSL_PUT_ERROR(SSL, SSL_R_QUIC_INTERNAL_ERROR); return false; diff --git a/ssl/ssl_lib.cc b/ssl/ssl_lib.cc index 522c09e52..260d3cd78 100644 --- a/ssl/ssl_lib.cc +++ b/ssl/ssl_lib.cc @@ -1378,6 +1378,7 @@ int SSL_get_error(const SSL *ssl, int ret_code) { case SSL_ERROR_EARLY_DATA_REJECTED: case SSL_ERROR_WANT_CERTIFICATE_VERIFY: case SSL_ERROR_WANT_RENEGOTIATE: + case SSL_ERROR_HANDSHAKE_HINTS_READY: return ssl->s3->rwstate; case SSL_ERROR_WANT_READ: { @@ -1463,6 +1464,8 @@ const char *SSL_error_description(int err) { return "HANDOFF"; case SSL_ERROR_HANDBACK: return "HANDBACK"; + case SSL_ERROR_HANDSHAKE_HINTS_READY: + return "HANDSHAKE_HINTS_READY"; default: return nullptr; } diff --git a/ssl/t1_lib.cc b/ssl/t1_lib.cc index 3a88cd15b..e8d5f2edc 100644 --- a/ssl/t1_lib.cc +++ b/ssl/t1_lib.cc @@ -4059,6 +4059,7 @@ enum ssl_ticket_aead_result_t ssl_process_ticket( SSL_HANDSHAKE *hs, UniquePtr *out_session, bool *out_renew_ticket, Span ticket, Span session_id) { + SSL *const ssl = hs->ssl; *out_renew_ticket = false; out_session->reset(); @@ -4067,9 +4068,21 @@ enum ssl_ticket_aead_result_t ssl_process_ticket( return ssl_ticket_aead_ignore_ticket; } + // Tickets in TLS 1.3 are tied into pre-shared keys (PSKs), unlike in TLS 1.2 + // where that concept doesn't exist. The |decrypted_psk| and |ignore_psk| + // hints only apply to PSKs. We check the version to determine which this is. + const bool is_psk = ssl_protocol_version(ssl) >= TLS1_3_VERSION; + Array plaintext; enum ssl_ticket_aead_result_t result; - if (hs->ssl->session_ctx->ticket_aead_method != NULL) { + SSL_HANDSHAKE_HINTS *const hints = hs->hints.get(); + if (is_psk && hints && !hs->hints_requested && + !hints->decrypted_psk.empty()) { + result = plaintext.CopyFrom(hints->decrypted_psk) ? ssl_ticket_aead_success + : ssl_ticket_aead_error; + } else if (is_psk && hints && !hs->hints_requested && hints->ignore_psk) { + result = ssl_ticket_aead_ignore_ticket; + } else if (ssl->session_ctx->ticket_aead_method != NULL) { result = ssl_decrypt_ticket_with_method(hs, &plaintext, out_renew_ticket, ticket); } else { @@ -4078,9 +4091,8 @@ enum ssl_ticket_aead_result_t ssl_process_ticket( // length should be well under the minimum size for the session material and // HMAC. if (ticket.size() < SSL_TICKET_KEY_NAME_LEN + EVP_MAX_IV_LENGTH) { - return ssl_ticket_aead_ignore_ticket; - } - if (hs->ssl->session_ctx->ticket_key_cb != NULL) { + result = ssl_ticket_aead_ignore_ticket; + } else if (ssl->session_ctx->ticket_key_cb != NULL) { result = ssl_decrypt_ticket_with_cb(hs, &plaintext, out_renew_ticket, ticket); } else { @@ -4088,13 +4100,22 @@ enum ssl_ticket_aead_result_t ssl_process_ticket( } } + if (is_psk && hints && hs->hints_requested) { + if (result == ssl_ticket_aead_ignore_ticket) { + hints->ignore_psk = true; + } else if (result == ssl_ticket_aead_success && + !hints->decrypted_psk.CopyFrom(plaintext)) { + return ssl_ticket_aead_error; + } + } + if (result != ssl_ticket_aead_success) { return result; } // Decode the session. UniquePtr session(SSL_SESSION_from_bytes( - plaintext.data(), plaintext.size(), hs->ssl->ctx.get())); + plaintext.data(), plaintext.size(), ssl->ctx.get())); if (!session) { ERR_clear_error(); // Don't leave an error on the queue. return ssl_ticket_aead_ignore_ticket; diff --git a/ssl/test/bssl_shim.cc b/ssl/test/bssl_shim.cc index 6a5fca389..89313493c 100644 --- a/ssl/test/bssl_shim.cc +++ b/ssl/test/bssl_shim.cc @@ -671,6 +671,27 @@ static bool CheckHandshakeProperties(SSL *ssl, bool is_resume, SSL_used_hello_retry_request(ssl) ? "" : "no "); return false; } + + // Test that handshake hints correctly skipped the expected operations. + // + // TODO(davidben): Add support for TLS 1.2 hints and remove the version check. + // Also add a check for the session cache lookup. + if (config->handshake_hints && !config->allow_hint_mismatch && + SSL_version(ssl) == TLS1_3_VERSION) { + const TestState *state = GetTestState(ssl); + if (!SSL_used_hello_retry_request(ssl) && state->used_private_key) { + fprintf( + stderr, + "Performed private key operation, but hint should have skipped it\n"); + return false; + } + + if (state->ticket_decrypt_done) { + fprintf(stderr, + "Performed ticket decryption, but hint should have skipped it\n"); + return false; + } + } return true; } @@ -697,6 +718,17 @@ static bool DoConnection(bssl::UniquePtr *out_session, } else { SSL_set_connect_state(ssl.get()); } + if (config->handshake_hints) { +#if defined(HANDSHAKER_SUPPORTED) + GetTestState(ssl.get())->get_handshake_hints_cb = + [&](const SSL_CLIENT_HELLO *client_hello) { + return GetHandshakeHint(ssl.get(), writer, is_resume, client_hello); + }; +#else + fprintf(stderr, "The external handshaker can only be used on Linux\n"); + return false; +#endif + } int sock = Connect(config->port); if (sock == -1) { @@ -1180,8 +1212,8 @@ int main(int argc, char **argv) { CRYPTO_library_init(); TestConfig initial_config, resume_config, retry_config; - if (!ParseConfig(argc - 1, argv + 1, &initial_config, &resume_config, - &retry_config)) { + if (!ParseConfig(argc - 1, argv + 1, /*is_shim=*/true, &initial_config, + &resume_config, &retry_config)) { return Usage(argv[0]); } diff --git a/ssl/test/fuzzer.h b/ssl/test/fuzzer.h index eacbdc044..8f7a35508 100644 --- a/ssl/test/fuzzer.h +++ b/ssl/test/fuzzer.h @@ -515,6 +515,15 @@ class TLSFuzzer { break; } + case kHintsTag: { + CBS hints; + if (!CBS_get_u24_length_prefixed(cbs, &hints)) { + return nullptr; + } + SSL_set_handshake_hints(ssl.get(), CBS_data(&hints), CBS_len(&hints)); + break; + } + default: return nullptr; } diff --git a/ssl/test/fuzzer_tags.h b/ssl/test/fuzzer_tags.h index eb9991d30..3946df797 100644 --- a/ssl/test/fuzzer_tags.h +++ b/ssl/test/fuzzer_tags.h @@ -45,4 +45,7 @@ static const uint16_t kHandoffTag = 3; // kHandbackTag is followed by te output of |SSL_serialize_handback|. static const uint16_t kHandbackTag = 4; +// kHintsTag is followed by the output of |SSL_serialize_handshake_hints|. +static const uint16_t kHintsTag = 5; + #endif // HEADER_SSL_TEST_FUZZER_TAGS diff --git a/ssl/test/handshake_util.cc b/ssl/test/handshake_util.cc index 7fa0fb5a9..0fdf47f10 100644 --- a/ssl/test/handshake_util.cc +++ b/ssl/test/handshake_util.cc @@ -34,6 +34,7 @@ #include "test_config.h" #include "test_state.h" +#include #include using namespace bssl; @@ -232,7 +233,7 @@ static bool Proxy(BIO *socket, bool async, int control, int rfd, int wfd) { return false; } switch (msg) { - case kControlMsgHandback: + case kControlMsgDone: return true; case kControlMsgError: return false; @@ -304,22 +305,68 @@ static bool Proxy(BIO *socket, bool async, int control, int rfd, int wfd) { class ScopedFD { public: - explicit ScopedFD(int fd): fd_(fd) {} - ~ScopedFD() { Close(); } - ScopedFD(const ScopedFD &) = delete; - ScopedFD &operator=(const ScopedFD &) = delete; + ScopedFD() : fd_(-1) {} + explicit ScopedFD(int fd) : fd_(fd) {} + ~ScopedFD() { Reset(); } - void Close() { + ScopedFD(ScopedFD &&other) { *this = std::move(other); } + ScopedFD &operator=(ScopedFD &&other) { + Reset(other.fd_); + other.fd_ = -1; + return *this; + } + + int fd() const { return fd_; } + + void Reset(int fd = -1) { if (fd_ >= 0) { close(fd_); } - fd_ = -1; + fd_ = fd; } private: int fd_; }; +class ScopedProcess { + public: + ScopedProcess() : pid_(-1) {} + ~ScopedProcess() { Reset(); } + + ScopedProcess(ScopedProcess &&other) { *this = std::move(other); } + ScopedProcess &operator=(ScopedProcess &&other) { + Reset(other.pid_); + other.pid_ = -1; + return *this; + } + + pid_t pid() const { return pid_; } + + void Reset(pid_t pid = -1) { + if (pid_ >= 0) { + kill(pid_, SIGTERM); + int unused; + Wait(&unused); + } + pid_ = pid; + } + + bool Wait(int *out_status) { + if (pid_ < 0) { + return false; + } + if (waitpid_eintr(pid_, out_status, 0) != pid_) { + return false; + } + pid_ = -1; + return true; + } + + private: + pid_t pid_; +}; + class FileActionsDestroyer { public: explicit FileActionsDestroyer(posix_spawn_file_actions_t *actions) @@ -332,11 +379,9 @@ class FileActionsDestroyer { posix_spawn_file_actions_t *actions_; }; -// RunHandshaker forks and execs the handshaker binary, handing off |input|, -// and, after proxying some amount of handshake traffic, handing back |out|. -static bool RunHandshaker(BIO *bio, const TestConfig *config, bool is_resume, - Span input, - std::vector *out) { +static bool StartHandshaker(ScopedProcess *out, ScopedFD *out_control, + const TestConfig *config, bool is_resume, + posix_spawn_file_actions_t *actions) { if (config->handshaker_path.empty()) { fprintf(stderr, "no -handshaker-path specified\n"); return false; @@ -347,13 +392,62 @@ static bool RunHandshaker(BIO *bio, const TestConfig *config, bool is_resume, return false; } + std::vector args; + args.push_back(config->handshaker_path.c_str()); + static const char kResumeFlag[] = "-handshaker-resume"; + if (is_resume) { + args.push_back(kResumeFlag); + } + // config->argv omits argv[0]. + for (int j = 0; j < config->argc; ++j) { + args.push_back(config->argv[j]); + } + args.push_back(nullptr); + // A datagram socket guarantees that writes are all-or-nothing. int control[2]; if (socketpair(AF_LOCAL, SOCK_DGRAM, 0, control) != 0) { perror("socketpair"); return false; } - ScopedFD control0_closer(control[0]), control1_closer(control[1]); + ScopedFD scoped_control0(control[0]), scoped_control1(control[1]); + + assert(control[1] != kFdHandshakerToProxy); + assert(control[1] != kFdProxyToHandshaker); + if (posix_spawn_file_actions_addclose(actions, control[0]) != 0) { + return false; + } + if (control[1] != kFdControl && + posix_spawn_file_actions_adddup2(actions, control[1], kFdControl) != 0) { + return false; + } + + fflush(stdout); + fflush(stderr); + + // MSan doesn't know that |posix_spawn| initializes its output, so initialize + // it to -1. + pid_t pid = -1; + if (posix_spawn(&pid, args[0], actions, nullptr, + const_cast(args.data()), environ) != 0) { + return false; + } + + out->Reset(pid); + *out_control = std::move(scoped_control0); + return true; +} + +// RunHandshaker forks and execs the handshaker binary, handing off |input|, +// and, after proxying some amount of handshake traffic, handing back |out|. +static bool RunHandshaker(BIO *bio, const TestConfig *config, bool is_resume, + Span input, + std::vector *out) { + posix_spawn_file_actions_t actions; + if (posix_spawn_file_actions_init(&actions) != 0) { + return false; + } + FileActionsDestroyer actions_destroyer(&actions); int rfd[2], wfd[2]; // We use pipes, rather than some other mechanism, for their buffers. During @@ -378,38 +472,15 @@ static bool RunHandshaker(BIO *bio, const TestConfig *config, bool is_resume, } ScopedFD wfd0_closer(wfd[0]), wfd1_closer(wfd[1]); - fflush(stdout); - fflush(stderr); - - std::vector args; - args.push_back(config->handshaker_path.c_str()); - static const char kResumeFlag[] = "-handshaker-resume"; - if (is_resume) { - args.push_back(kResumeFlag); - } - // config->argv omits argv[0]. - for (int j = 0; j < config->argc; ++j) { - args.push_back(config->argv[j]); - } - args.push_back(nullptr); - - posix_spawn_file_actions_t actions; - if (posix_spawn_file_actions_init(&actions) != 0) { - return false; - } - FileActionsDestroyer actions_destroyer(&actions); - if (posix_spawn_file_actions_addclose(&actions, control[0]) != 0 || - posix_spawn_file_actions_addclose(&actions, rfd[1]) != 0 || - posix_spawn_file_actions_addclose(&actions, wfd[0]) != 0) { - return false; - } assert(kFdControl != rfd[0]); assert(kFdControl != wfd[1]); - if (control[1] != kFdControl && - posix_spawn_file_actions_adddup2(&actions, control[1], kFdControl) != 0) { + assert(kFdHandshakerToProxy != rfd[0]); + assert(kFdProxyToHandshaker != wfd[1]); + + if (posix_spawn_file_actions_addclose(&actions, rfd[1]) != 0 || + posix_spawn_file_actions_addclose(&actions, wfd[0]) != 0) { return false; } - assert(kFdProxyToHandshaker != wfd[1]); if (rfd[0] != kFdProxyToHandshaker && posix_spawn_file_actions_adddup2(&actions, rfd[0], kFdProxyToHandshaker) != 0) { @@ -418,28 +489,25 @@ static bool RunHandshaker(BIO *bio, const TestConfig *config, bool is_resume, if (wfd[1] != kFdHandshakerToProxy && posix_spawn_file_actions_adddup2(&actions, wfd[1], kFdHandshakerToProxy) != 0) { - return false; + return false; } - // MSan doesn't know that |posix_spawn| initializes its output, so initialize - // it to -1. - pid_t handshaker_pid = -1; - if (posix_spawn(&handshaker_pid, args[0], &actions, nullptr, - const_cast(args.data()), environ) != 0) { + ScopedProcess handshaker; + ScopedFD control; + if (!StartHandshaker(&handshaker, &control, config, is_resume, &actions)) { return false; } - control1_closer.Close(); - rfd0_closer.Close(); - wfd1_closer.Close(); + rfd0_closer.Reset(); + wfd1_closer.Reset(); - if (write_eintr(control[0], input.data(), input.size()) == -1) { + if (write_eintr(control.fd(), input.data(), input.size()) == -1) { perror("write"); return false; } - bool ok = Proxy(bio, config->async, control[0], rfd[1], wfd[0]); + bool ok = Proxy(bio, config->async, control.fd(), rfd[1], wfd[0]); int wstatus; - if (waitpid_eintr(handshaker_pid, &wstatus, 0) != handshaker_pid) { + if (!handshaker.Wait(&wstatus)) { perror("waitpid"); return false; } @@ -453,7 +521,7 @@ static bool RunHandshaker(BIO *bio, const TestConfig *config, bool is_resume, constexpr size_t kBufSize = 1024 * 1024; std::vector buf(kBufSize); - ssize_t len = read_eintr(control[0], buf.data(), buf.size()); + ssize_t len = read_eintr(control.fd(), buf.data(), buf.size()); if (len == -1) { perror("read"); return false; @@ -463,6 +531,66 @@ static bool RunHandshaker(BIO *bio, const TestConfig *config, bool is_resume, return true; } +static bool RequestHandshakeHint(const TestConfig *config, bool is_resume, + Span input, bool *out_has_hints, + std::vector *out_hints) { + posix_spawn_file_actions_t actions; + if (posix_spawn_file_actions_init(&actions) != 0) { + return false; + } + FileActionsDestroyer actions_destroyer(&actions); + ScopedProcess handshaker; + ScopedFD control; + if (!StartHandshaker(&handshaker, &control, config, is_resume, &actions)) { + return false; + } + + if (write_eintr(control.fd(), input.data(), input.size()) == -1) { + perror("write"); + return false; + } + + char msg; + if (read_eintr(control.fd(), &msg, 1) != 1) { + perror("read"); + return false; + } + + switch (msg) { + case kControlMsgDone: { + constexpr size_t kBufSize = 1024 * 1024; + out_hints->resize(kBufSize); + ssize_t len = + read_eintr(control.fd(), out_hints->data(), out_hints->size()); + if (len == -1) { + perror("read"); + return false; + } + out_hints->resize(len); + *out_has_hints = true; + break; + } + case kControlMsgError: + *out_has_hints = false; + break; + default: + fprintf(stderr, "Unknown control message from handshaker: %c\n", msg); + return false; + } + + int wstatus; + if (!handshaker.Wait(&wstatus)) { + perror("waitpid"); + return false; + } + if (wstatus) { + fprintf(stderr, "handshaker exited irregularly\n"); + return false; + } + + return true; +} + // PrepareHandoff accepts the |ClientHello| from |ssl| and serializes state to // be passed to the handshaker. The serialized state includes both the SSL // handoff, as well test-related state. @@ -543,4 +671,35 @@ bool DoSplitHandshake(UniquePtr *ssl, SettingsWriter *writer, return true; } +bool GetHandshakeHint(SSL *ssl, SettingsWriter *writer, bool is_resume, + const SSL_CLIENT_HELLO *client_hello) { + ScopedCBB input; + CBB child; + if (!CBB_init(input.get(), client_hello->client_hello_len + 256) || + !CBB_add_u24_length_prefixed(input.get(), &child) || + !CBB_add_bytes(&child, client_hello->client_hello, + client_hello->client_hello_len) || + !CBB_add_u24_length_prefixed(input.get(), &child) || + !SSL_serialize_capabilities(ssl, &child) || // + !CBB_flush(input.get())) { + return false; + } + + bool has_hints; + std::vector hints; + if (!RequestHandshakeHint( + GetTestConfig(ssl), is_resume, + MakeConstSpan(CBB_data(input.get()), CBB_len(input.get())), + &has_hints, &hints)) { + return false; + } + if (has_hints && + (!writer->WriteHints(hints) || + !SSL_set_handshake_hints(ssl, hints.data(), hints.size()))) { + return false; + } + + return true; +} + #endif // defined(HANDSHAKER_SUPPORTED) diff --git a/ssl/test/handshake_util.h b/ssl/test/handshake_util.h index fa365a401..7fee20b44 100644 --- a/ssl/test/handshake_util.h +++ b/ssl/test/handshake_util.h @@ -44,17 +44,24 @@ int CheckIdempotentError(const char *name, SSL *ssl, std::function func); bool DoSplitHandshake(bssl::UniquePtr *ssl, SettingsWriter *writer, bool is_resume); +// GetHandshakeHint requests a handshake hint from the handshaker process and +// configures the result on |ssl|. It returns true on success and false on +// error. +bool GetHandshakeHint(SSL *ssl, SettingsWriter *writer, bool is_resume, + const SSL_CLIENT_HELLO *client_hello); + // The protocol between the proxy and the handshaker is defined by these -// single-character prefixes. +// single-character prefixes. |kControlMsgDone| uses 'H' for compatibility with +// older binaries. constexpr char kControlMsgWantRead = 'R'; // Handshaker wants data constexpr char kControlMsgWriteCompleted = 'W'; // Proxy has sent data -constexpr char kControlMsgHandback = 'H'; // Proxy should resume control +constexpr char kControlMsgDone = 'H'; // Proxy should resume control constexpr char kControlMsgError = 'E'; // Handshaker hit an error // The protocol between the proxy and handshaker uses these file descriptors. -constexpr int kFdControl = 3; // Bi-directional dgram socket. -constexpr int kFdProxyToHandshaker = 4; // Uni-directional pipe. -constexpr int kFdHandshakerToProxy = 5; // Uni-directional pipe. +constexpr int kFdControl = 20; // Bi-directional dgram socket. +constexpr int kFdProxyToHandshaker = 21; // Uni-directional pipe. +constexpr int kFdHandshakerToProxy = 22; // Uni-directional pipe. #endif // HANDSHAKER_SUPPORTED #endif // HEADER_TEST_HANDSHAKE diff --git a/ssl/test/handshaker.cc b/ssl/test/handshaker.cc index 812909182..ac890633d 100644 --- a/ssl/test/handshaker.cc +++ b/ssl/test/handshaker.cc @@ -12,11 +12,14 @@ * OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN * CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ +#include #include #include #include #include +#include + #include #include #include @@ -55,7 +58,11 @@ bool Handshaker(const TestConfig *config, int rfd, int wfd, if (!ctx) { return false; } - UniquePtr ssl = config->NewSSL(ctx.get(), nullptr, nullptr); + UniquePtr ssl = + config->NewSSL(ctx.get(), /*session=*/nullptr, /*test_state=*/nullptr); + if (!ssl) { + return false; + } // Set |O_NONBLOCK| in order to break out of the loop when we hit // |SSL_ERROR_WANT_READ|, so that we can send |kControlMsgWantRead| to the @@ -100,6 +107,8 @@ bool Handshaker(const TestConfig *config, int rfd, int wfd, } } if (!HandbackReady(ssl.get(), ret)) { + fprintf(stderr, "Handshaker: %s\n", + SSL_error_description(SSL_get_error(ssl.get(), ret))); ERR_print_errors_fp(stderr); return false; } @@ -115,7 +124,7 @@ bool Handshaker(const TestConfig *config, int rfd, int wfd, return false; } - char msg = kControlMsgHandback; + char msg = kControlMsgDone; if (write_eintr(control, &msg, 1) == -1 || write_eintr(control, CBB_data(output.get()), CBB_len(output.get())) == -1) { @@ -125,6 +134,84 @@ bool Handshaker(const TestConfig *config, int rfd, int wfd, return true; } +bool GenerateHandshakeHint(const TestConfig *config, + bssl::Span request, int control) { + // The handshake hint contains the ClientHello and the capabilities string. + CBS cbs = request; + CBS client_hello, capabilities; + if (!CBS_get_u24_length_prefixed(&cbs, &client_hello) || + !CBS_get_u24_length_prefixed(&cbs, &capabilities) || // + CBS_len(&cbs) != 0) { + fprintf(stderr, "Handshaker: Could not parse hint request\n"); + return false; + } + + UniquePtr ctx = config->SetupCtx(/*old_ctx=*/nullptr); + if (!ctx) { + return false; + } + + UniquePtr ssl = + config->NewSSL(ctx.get(), /*session=*/nullptr, + std::unique_ptr(new TestState)); + if (!ssl) { + return false; + } + + // TODO(davidben): When split handshakes is replaced, move this into |NewSSL|. + assert(config->is_server); + SSL_set_accept_state(ssl.get()); + + if (!SSL_request_handshake_hints( + ssl.get(), CBS_data(&client_hello), CBS_len(&client_hello), + CBS_data(&capabilities), CBS_len(&capabilities))) { + fprintf(stderr, "Handshaker: SSL_request_handshake_hints failed\n"); + return false; + } + + int ret = 0; + do { + ret = CheckIdempotentError("SSL_do_handshake", ssl.get(), + [&] { return SSL_do_handshake(ssl.get()); }); + } while (RetryAsync(ssl.get(), ret)); + + if (ret > 0) { + fprintf(stderr, "Handshaker: handshake unexpectedly succeeded.\n"); + return false; + } + + if (SSL_get_error(ssl.get(), ret) != SSL_ERROR_HANDSHAKE_HINTS_READY) { + // Errors here may be expected if the test is testing a failing case. The + // shim should continue executing without a hint, so we report an error + // "successfully". This allows the shim to distinguish this from the other + // unexpected error cases. + // + // We intentionally avoid printing the error in this case, to avoid mixing + // up test expectations with errors from the shim. + char msg = kControlMsgError; + if (write_eintr(control, &msg, 1) == -1) { + return false; + } + return true; + } + + bssl::ScopedCBB hints; + if (!CBB_init(hints.get(), 256) || + !SSL_serialize_handshake_hints(ssl.get(), hints.get())) { + fprintf(stderr, "Handshaker: failed to serialize handshake hints\n"); + return false; + } + + char msg = kControlMsgDone; + if (write_eintr(control, &msg, 1) == -1 || + write_eintr(control, CBB_data(hints.get()), CBB_len(hints.get())) == -1) { + perror("write"); + return false; + } + + return true; +} + int SignalError() { const char msg = kControlMsgError; if (write_eintr(kFdControl, &msg, 1) != 1) { @@ -137,12 +224,12 @@ int SignalError() { int main(int argc, char **argv) { TestConfig initial_config, resume_config, retry_config; - if (!ParseConfig(argc - 1, argv + 1, &initial_config, &resume_config, - &retry_config)) { + if (!ParseConfig(argc - 1, argv + 1, /*is_shim=*/false, &initial_config, + &resume_config, &retry_config)) { return SignalError(); } - const TestConfig *config = initial_config.handshaker_resume - ? &resume_config : &initial_config; + const TestConfig *config = + initial_config.handshaker_resume ? &resume_config : &initial_config; #if defined(BORINGSSL_UNSAFE_DETERMINISTIC_MODE) if (initial_config.handshaker_resume) { // If the PRNG returns exactly the same values when trying to resume then a @@ -157,15 +244,23 @@ int main(int argc, char **argv) { // read() will return the entire message in one go, because it's a datagram // socket. constexpr size_t kBufSize = 1024 * 1024; - std::vector handoff(kBufSize); - ssize_t len = read_eintr(kFdControl, handoff.data(), handoff.size()); + std::vector request(kBufSize); + ssize_t len = read_eintr(kFdControl, request.data(), request.size()); if (len == -1) { perror("read"); return 2; } - if (!Handshaker(config, kFdProxyToHandshaker, kFdHandshakerToProxy, handoff, - kFdControl)) { - return SignalError(); + request.resize(static_cast(len)); + + if (config->handshake_hints) { + if (!GenerateHandshakeHint(config, request, kFdControl)) { + return SignalError(); + } + } else { + if (!Handshaker(config, kFdProxyToHandshaker, kFdHandshakerToProxy, + request, kFdControl)) { + return SignalError(); + } } return 0; } diff --git a/ssl/test/runner/runner.go b/ssl/test/runner/runner.go index be934fedd..9e400f43e 100644 --- a/ssl/test/runner/runner.go +++ b/ssl/test/runner/runner.go @@ -63,6 +63,7 @@ var ( pipe = flag.Bool("pipe", false, "If true, print status output suitable for piping into another program.") testToRun = flag.String("test", "", "Semicolon-separated patterns of tests to run, or empty to run all tests") skipTest = flag.String("skip", "", "Semicolon-separated patterns of tests to skip") + allowHintMismatch = flag.String("allow-hint-mismatch", "", "Semicolon-separated patterns of tests where hints may mismatch") numWorkersFlag = flag.Int("num-workers", runtime.NumCPU(), "The number of workers to run in parallel.") shimPath = flag.String("shim-path", "../../../build/ssl/test/bssl_shim", "The location of the shim binary.") handshakerPath = flag.String("handshaker-path", "../../../build/ssl/test/handshaker", "The location of the handshaker binary.") @@ -698,6 +699,9 @@ type testCase struct { // should retry for early rejection. In a server test, this is whether the // test expects the shim to reject early data. expectEarlyDataRejected bool + // skipSplitHandshake, if true, will skip the generation of a split + // handshake copy of the test. + skipSplitHandshake bool } var testCases []testCase @@ -1835,12 +1839,12 @@ func bigFromHex(hex string) *big.Int { return ret } -func convertToSplitHandshakeTests(tests []testCase) (splitHandshakeTests []testCase) { +func convertToSplitHandshakeTests(tests []testCase) (splitHandshakeTests []testCase, err error) { var stdout bytes.Buffer shim := exec.Command(*shimPath, "-is-handshaker-supported") shim.Stdout = &stdout if err := shim.Run(); err != nil { - panic(err) + return nil, err } switch strings.TrimSpace(string(stdout.Bytes())) { @@ -1849,14 +1853,20 @@ func convertToSplitHandshakeTests(tests []testCase) (splitHandshakeTests []testC case "Yes": break default: - panic("Unknown output from shim: 0x" + hex.EncodeToString(stdout.Bytes())) + return nil, fmt.Errorf("unknown output from shim: %q", stdout.Bytes()) + } + + var allowHintMismatchPattern []string + if len(*allowHintMismatch) > 0 { + allowHintMismatchPattern = strings.Split(*allowHintMismatch, ";") } NextTest: for _, test := range tests { if test.protocol != tls || test.testType != serverTest || - strings.Contains(test.name, "DelegatedCredentials") { + strings.Contains(test.name, "DelegatedCredentials") || + test.skipSplitHandshake { continue } @@ -1868,14 +1878,40 @@ NextTest: shTest := test shTest.name += "-Split" - shTest.flags = make([]string, len(test.flags), len(test.flags)+1) + shTest.flags = make([]string, len(test.flags), len(test.flags)+3) copy(shTest.flags, test.flags) shTest.flags = append(shTest.flags, "-handoff", "-handshaker-path", *handshakerPath) splitHandshakeTests = append(splitHandshakeTests, shTest) } - return splitHandshakeTests + for _, test := range tests { + if test.protocol == dtls || + test.testType != serverTest { + continue + } + + var matched bool + if len(allowHintMismatchPattern) > 0 { + matched, err = match(allowHintMismatchPattern, nil, test.name) + if err != nil { + return nil, fmt.Errorf("error matching pattern: %s", err) + } + } + + shTest := test + shTest.name += "-Hints" + shTest.flags = make([]string, len(test.flags), len(test.flags)+3) + copy(shTest.flags, test.flags) + shTest.flags = append(shTest.flags, "-handshake-hints", "-handshaker-path", *handshakerPath) + if matched { + shTest.flags = append(shTest.flags, "-allow-hint-mismatch") + } + + splitHandshakeTests = append(splitHandshakeTests, shTest) + } + + return splitHandshakeTests, nil } func addBasicTests() { @@ -17275,6 +17311,229 @@ func addEncryptedClientHelloTests() { } } +func addHintMismatchTests() { + // Each of these tests skips split handshakes because split handshakes does + // not handle a mismatch between shim and handshaker. Handshake hints, + // however, are designed to tolerate the mismatch. + // + // Note also these tests do not specify -handshake-hints directly. Instead, + // we define normal tests, that run even without a handshaker, and rely on + // convertToSplitHandshakeTests to generate a handshaker hints variant. This + // avoids repeating the -is-handshaker-supported and -handshaker-path logic. + // (While not useful, the tests will still pass without a handshaker.) + for _, protocol := range []protocol{tls, quic} { + // If the signing payload is different, the handshake still completes + // successfully. Different ALPN preferences will trigger a mismatch. + testCases = append(testCases, testCase{ + name: protocol.String() + "-HintMismatch-SignatureInput", + testType: serverTest, + protocol: protocol, + skipSplitHandshake: true, + config: Config{ + MinVersion: VersionTLS13, + MaxVersion: VersionTLS13, + NextProtos: []string{"foo", "bar"}, + }, + flags: []string{ + "-allow-hint-mismatch", + "-on-shim-select-alpn", "foo", + "-on-handshaker-select-alpn", "bar", + }, + expectations: connectionExpectations{ + nextProto: "foo", + nextProtoType: alpn, + }, + }) + + // The shim and handshaker may have different curve preferences. + testCases = append(testCases, testCase{ + name: protocol.String() + "-HintMismatch-KeyShare", + testType: serverTest, + protocol: protocol, + skipSplitHandshake: true, + config: Config{ + MinVersion: VersionTLS13, + MaxVersion: VersionTLS13, + // Send both curves in the key share list, to avoid getting + // mixed up with HelloRetryRequest. + DefaultCurves: []CurveID{CurveX25519, CurveP256}, + }, + flags: []string{ + "-allow-hint-mismatch", + "-on-shim-curves", strconv.Itoa(int(CurveX25519)), + "-on-handshaker-curves", strconv.Itoa(int(CurveP256)), + }, + expectations: connectionExpectations{ + curveID: CurveX25519, + }, + }) + + // If the handshaker does HelloRetryRequest, it will omit most hints. + // The shim should still work. + testCases = append(testCases, testCase{ + name: protocol.String() + "-HintMismatch-HandshakerHelloRetryRequest", + testType: serverTest, + protocol: protocol, + skipSplitHandshake: true, + config: Config{ + MinVersion: VersionTLS13, + MaxVersion: VersionTLS13, + DefaultCurves: []CurveID{CurveX25519}, + }, + flags: []string{ + "-allow-hint-mismatch", + "-on-shim-curves", strconv.Itoa(int(CurveX25519)), + "-on-handshaker-curves", strconv.Itoa(int(CurveP256)), + }, + expectations: connectionExpectations{ + curveID: CurveX25519, + }, + }) + + // If the shim does HelloRetryRequest, the hints from the handshaker + // will be ignored. This is not reported as a mismatch because hints + // would not have helped the shim anyway. + testCases = append(testCases, testCase{ + name: protocol.String() + "-HintMismatch-ShimHelloRetryRequest", + testType: serverTest, + protocol: protocol, + skipSplitHandshake: true, + config: Config{ + MinVersion: VersionTLS13, + MaxVersion: VersionTLS13, + DefaultCurves: []CurveID{CurveX25519}, + }, + flags: []string{ + "-on-shim-curves", strconv.Itoa(int(CurveP256)), + "-on-handshaker-curves", strconv.Itoa(int(CurveX25519)), + }, + expectations: connectionExpectations{ + curveID: CurveP256, + }, + }) + + // The shim and handshaker may have different signature algorithm + // preferences. + testCases = append(testCases, testCase{ + name: protocol.String() + "-HintMismatch-SignatureAlgorithm", + testType: serverTest, + protocol: protocol, + skipSplitHandshake: true, + config: Config{ + MinVersion: VersionTLS13, + MaxVersion: VersionTLS13, + VerifySignatureAlgorithms: []signatureAlgorithm{ + signatureRSAPSSWithSHA256, + signatureRSAPSSWithSHA384, + }, + }, + flags: []string{ + "-allow-hint-mismatch", + "-cert-file", path.Join(*resourceDir, rsaCertificateFile), + "-key-file", path.Join(*resourceDir, rsaKeyFile), + "-on-shim-signing-prefs", strconv.Itoa(int(signatureRSAPSSWithSHA256)), + "-on-handshaker-signing-prefs", strconv.Itoa(int(signatureRSAPSSWithSHA384)), + }, + expectations: connectionExpectations{ + peerSignatureAlgorithm: signatureRSAPSSWithSHA256, + }, + }) + + // The shim and handshaker may disagree on whether resumption is allowed. + // We run the first connection with tickets enabled, so the client is + // issued a ticket, then disable tickets on the second connection. + testCases = append(testCases, testCase{ + name: protocol.String() + "-HintMismatch-NoTickets1", + testType: serverTest, + protocol: protocol, + skipSplitHandshake: true, + config: Config{ + MinVersion: VersionTLS13, + MaxVersion: VersionTLS13, + }, + flags: []string{ + "-on-resume-allow-hint-mismatch", + "-on-shim-on-resume-no-ticket", + }, + resumeSession: true, + expectResumeRejected: true, + }) + testCases = append(testCases, testCase{ + name: protocol.String() + "-HintMismatch-NoTickets2", + testType: serverTest, + protocol: protocol, + skipSplitHandshake: true, + config: Config{ + MinVersion: VersionTLS13, + MaxVersion: VersionTLS13, + }, + flags: []string{ + "-on-resume-allow-hint-mismatch", + "-on-handshaker-on-resume-no-ticket", + }, + resumeSession: true, + }) + + // The shim and handshaker may disagree on whether to request a client + // certificate. + testCases = append(testCases, testCase{ + name: protocol.String() + "-HintMismatch-CertificateRequest", + testType: serverTest, + protocol: protocol, + skipSplitHandshake: true, + config: Config{ + MinVersion: VersionTLS13, + MaxVersion: VersionTLS13, + Certificates: []Certificate{rsaCertificate}, + }, + flags: []string{ + "-allow-hint-mismatch", + "-on-shim-require-any-client-certificate", + }, + }) + + // The shim and handshaker may negotiate different versions altogether. + if protocol != quic { + testCases = append(testCases, testCase{ + name: protocol.String() + "-HintMismatch-Version1", + testType: serverTest, + protocol: protocol, + skipSplitHandshake: true, + config: Config{ + MinVersion: VersionTLS12, + MaxVersion: VersionTLS13, + }, + flags: []string{ + "-allow-hint-mismatch", + "-on-shim-max-version", strconv.Itoa(VersionTLS12), + "-on-handshaker-max-version", strconv.Itoa(VersionTLS13), + }, + expectations: connectionExpectations{ + version: VersionTLS12, + }, + }) + testCases = append(testCases, testCase{ + name: protocol.String() + "-HintMismatch-Version2", + testType: serverTest, + protocol: protocol, + skipSplitHandshake: true, + config: Config{ + MinVersion: VersionTLS12, + MaxVersion: VersionTLS13, + }, + flags: []string{ + "-allow-hint-mismatch", + "-on-shim-max-version", strconv.Itoa(VersionTLS13), + "-on-handshaker-max-version", strconv.Itoa(VersionTLS12), + }, + expectations: connectionExpectations{ + version: VersionTLS13, + }, + }) + } + } +} + func worker(statusChan chan statusMsg, c chan *testCase, shimPath string, wg *sync.WaitGroup) { defer wg.Done() @@ -17472,8 +17731,14 @@ func main() { addJDK11WorkaroundTests() addDelegatedCredentialTests() addEncryptedClientHelloTests() + addHintMismatchTests() - testCases = append(testCases, convertToSplitHandshakeTests(testCases)...) + toAppend, err := convertToSplitHandshakeTests(testCases) + if err != nil { + fmt.Fprintf(os.Stderr, "Error making split handshake tests: %s", err) + os.Exit(1) + } + testCases = append(testCases, toAppend...) var wg sync.WaitGroup diff --git a/ssl/test/settings_writer.cc b/ssl/test/settings_writer.cc index fe8d42e90..8605222fe 100644 --- a/ssl/test/settings_writer.cc +++ b/ssl/test/settings_writer.cc @@ -85,29 +85,26 @@ bool SettingsWriter::Commit() { } bool SettingsWriter::WriteHandoff(bssl::Span handoff) { - if (path_.empty()) { - return true; - } - - CBB child; - if (!CBB_add_u16(cbb_.get(), kHandoffTag) || - !CBB_add_u24_length_prefixed(cbb_.get(), &child) || - !CBB_add_bytes(&child, handoff.data(), handoff.size()) || - !CBB_flush(cbb_.get())) { - return false; - } - return true; + return WriteData(kHandoffTag, handoff); } bool SettingsWriter::WriteHandback(bssl::Span handback) { + return WriteData(kHandbackTag, handback); +} + +bool SettingsWriter::WriteHints(bssl::Span hints) { + return WriteData(kHintsTag, hints); +} + +bool SettingsWriter::WriteData(uint16_t tag, bssl::Span data) { if (path_.empty()) { return true; } CBB child; - if (!CBB_add_u16(cbb_.get(), kHandbackTag) || + if (!CBB_add_u16(cbb_.get(), tag) || !CBB_add_u24_length_prefixed(cbb_.get(), &child) || - !CBB_add_bytes(&child, handback.data(), handback.size()) || + !CBB_add_bytes(&child, data.data(), data.size()) || !CBB_flush(cbb_.get())) { return false; } diff --git a/ssl/test/settings_writer.h b/ssl/test/settings_writer.h index 179a5a40d..e1ffdc146 100644 --- a/ssl/test/settings_writer.h +++ b/ssl/test/settings_writer.h @@ -34,10 +34,12 @@ struct SettingsWriter { bool Commit(); bool WriteHandoff(bssl::Span handoff); - bool WriteHandback(bssl::Span handback); + bool WriteHints(bssl::Span hints); private: + bool WriteData(uint16_t tag, bssl::Span data); + std::string path_; bssl::ScopedCBB cbb_; }; diff --git a/ssl/test/test_config.cc b/ssl/test/test_config.cc index 7d66d23a6..fff536f3e 100644 --- a/ssl/test/test_config.cc +++ b/ssl/test/test_config.cc @@ -27,6 +27,8 @@ #include "../../crypto/internal.h" #include "../internal.h" +#include "handshake_util.h" +#include "mock_quic_transport.h" #include "test_state.h" namespace { @@ -134,6 +136,8 @@ const Flag kBoolFlags[] = { {"-allow-false-start-without-alpn", &TestConfig::allow_false_start_without_alpn}, {"-handoff", &TestConfig::handoff}, + {"-handshake-hints", &TestConfig::handshake_hints}, + {"-allow-hint-mismatch", &TestConfig::allow_hint_mismatch}, {"-use-ocsp-callback", &TestConfig::use_ocsp_callback}, {"-set-ocsp-in-callback", &TestConfig::set_ocsp_in_callback}, {"-decline-ocsp-callback", &TestConfig::decline_ocsp_callback}, @@ -268,7 +272,7 @@ bool DecodeBase64(std::string *out, const std::string &in) { return true; } -bool ParseFlag(char *flag, int argc, char **argv, int *i, +bool ParseFlag(const char *flag, int argc, char **argv, int *i, bool skip, TestConfig *out_config) { bool *bool_field = FindField(out_config, kBoolFlags, flag); if (bool_field != NULL) { @@ -398,13 +402,21 @@ bool ParseFlag(char *flag, int argc, char **argv, int *i, return false; } -const char kInit[] = "-on-initial"; -const char kResume[] = "-on-resume"; -const char kRetry[] = "-on-retry"; +// RemovePrefix checks if |*str| begins with |prefix| + "-". If so, it advances +// |*str| past |prefix| (but not past the "-") and returns true. Otherwise, it +// returns false and leaves |*str| unmodified. +bool RemovePrefix(const char **str, const char *prefix) { + size_t prefix_len = strlen(prefix); + if (strncmp(*str, prefix, strlen(prefix)) == 0 && (*str)[prefix_len] == '-') { + *str += strlen(prefix); + return true; + } + return false; +} } // namespace -bool ParseConfig(int argc, char **argv, +bool ParseConfig(int argc, char **argv, bool is_shim, TestConfig *out_initial, TestConfig *out_resume, TestConfig *out_retry) { @@ -412,21 +424,36 @@ bool ParseConfig(int argc, char **argv, out_initial->argv = out_resume->argv = out_retry->argv = argv; for (int i = 0; i < argc; i++) { bool skip = false; - char *flag = argv[i]; - if (strncmp(flag, kInit, strlen(kInit)) == 0) { - if (!ParseFlag(flag + strlen(kInit), argc, argv, &i, skip, out_initial)) { + const char *flag = argv[i]; + + // -on-shim and -on-handshaker prefixes enable flags only on the shim or + // handshaker. + if (RemovePrefix(&flag, "-on-shim")) { + if (!is_shim) { + skip = true; + } + } else if (RemovePrefix(&flag, "-on-handshaker")) { + if (is_shim) { + skip = true; + } + } + + // The following prefixes allow different configurations for each of the + // initial, resumption, and 0-RTT retry handshakes. + if (RemovePrefix(&flag, "-on-initial")) { + if (!ParseFlag(flag, argc, argv, &i, skip, out_initial)) { return false; } - } else if (strncmp(flag, kResume, strlen(kResume)) == 0) { - if (!ParseFlag(flag + strlen(kResume), argc, argv, &i, skip, - out_resume)) { + } else if (RemovePrefix(&flag, "-on-resume")) { + if (!ParseFlag(flag, argc, argv, &i, skip, out_resume)) { return false; } - } else if (strncmp(flag, kRetry, strlen(kRetry)) == 0) { - if (!ParseFlag(flag + strlen(kRetry), argc, argv, &i, skip, out_retry)) { + } else if (RemovePrefix(&flag, "-on-retry")) { + if (!ParseFlag(flag, argc, argv, &i, skip, out_retry)) { return false; } } else { + // Unprefixed flags apply to all three. int i_init = i; int i_resume = i; if (!ParseFlag(flag, argc, argv, &i_init, skip, out_initial) || @@ -1052,10 +1079,15 @@ static int ClientCertCallback(SSL *ssl, X509 **out_x509, EVP_PKEY **out_pkey) { return 1; } +static ssl_private_key_result_t AsyncPrivateKeyComplete(SSL *ssl, uint8_t *out, + size_t *out_len, + size_t max_out); + static ssl_private_key_result_t AsyncPrivateKeySign( SSL *ssl, uint8_t *out, size_t *out_len, size_t max_out, uint16_t signature_algorithm, const uint8_t *in, size_t in_len) { TestState *test_state = GetTestState(ssl); + test_state->used_private_key = true; if (!test_state->private_key_result.empty()) { fprintf(stderr, "AsyncPrivateKeySign called with operation pending.\n"); abort(); @@ -1096,8 +1128,7 @@ static ssl_private_key_result_t AsyncPrivateKeySign( } test_state->private_key_result.resize(len); - // The signature will be released asynchronously in |AsyncPrivateKeyComplete|. - return ssl_private_key_retry; + return AsyncPrivateKeyComplete(ssl, out, out_len, max_out); } static ssl_private_key_result_t AsyncPrivateKeyDecrypt(SSL *ssl, uint8_t *out, @@ -1106,6 +1137,7 @@ static ssl_private_key_result_t AsyncPrivateKeyDecrypt(SSL *ssl, uint8_t *out, const uint8_t *in, size_t in_len) { TestState *test_state = GetTestState(ssl); + test_state->used_private_key = true; if (!test_state->private_key_result.empty()) { fprintf(stderr, "AsyncPrivateKeyDecrypt called with operation pending.\n"); abort(); @@ -1124,8 +1156,7 @@ static ssl_private_key_result_t AsyncPrivateKeyDecrypt(SSL *ssl, uint8_t *out, test_state->private_key_result.resize(*out_len); - // The decryption will be released asynchronously in |AsyncPrivateComplete|. - return ssl_private_key_retry; + return AsyncPrivateKeyComplete(ssl, out, out_len, max_out); } static ssl_private_key_result_t AsyncPrivateKeyComplete(SSL *ssl, uint8_t *out, @@ -1138,9 +1169,9 @@ static ssl_private_key_result_t AsyncPrivateKeyComplete(SSL *ssl, uint8_t *out, abort(); } - if (test_state->private_key_retries < 2) { + if (GetTestConfig(ssl)->async && test_state->private_key_retries < 2) { // Only return the decryption on the second attempt, to test both incomplete - // |decrypt| and |decrypt_complete|. + // |sign|/|decrypt| and |complete|. return ssl_private_key_retry; } @@ -1174,7 +1205,10 @@ static bool InstallCertificate(SSL *ssl) { if (pkey) { TestState *test_state = GetTestState(ssl); const TestConfig *config = GetTestConfig(ssl); - if (config->async) { + if (config->async || config->handshake_hints) { + // Install a custom private key if testing asynchronous callbacks, or if + // testing handshake hints. In the handshake hints case, we wish to check + // that hints only mismatch when allowed. test_state->private_key = std::move(pkey); SSL_set_private_key_method(ssl, &g_async_private_key_method); } else if (!SSL_use_PrivateKey(ssl, pkey.get())) { @@ -1195,12 +1229,14 @@ static bool InstallCertificate(SSL *ssl) { static enum ssl_select_cert_result_t SelectCertificateCallback( const SSL_CLIENT_HELLO *client_hello) { - const TestConfig *config = GetTestConfig(client_hello->ssl); - GetTestState(client_hello->ssl)->early_callback_called = true; + SSL *ssl = client_hello->ssl; + const TestConfig *config = GetTestConfig(ssl); + TestState *test_state = GetTestState(ssl); + test_state->early_callback_called = true; if (!config->expect_server_name.empty()) { const char *server_name = - SSL_get_servername(client_hello->ssl, TLSEXT_NAMETYPE_host_name); + SSL_get_servername(ssl, TLSEXT_NAMETYPE_host_name); if (server_name == nullptr || std::string(server_name) != config->expect_server_name) { fprintf(stderr, @@ -1214,48 +1250,73 @@ static enum ssl_select_cert_result_t SelectCertificateCallback( return ssl_select_cert_error; } - // Install the certificate in the early callback. - if (config->use_early_callback) { - bool early_callback_ready = - GetTestState(client_hello->ssl)->early_callback_ready; - if (config->async && !early_callback_ready) { - // Install the certificate asynchronously. - return ssl_select_cert_retry; - } - if (!InstallCertificate(client_hello->ssl)) { - return ssl_select_cert_error; - } + // Simulate some asynchronous work in the early callback. + if ((config->use_early_callback || test_state->get_handshake_hints_cb) && + config->async && !test_state->early_callback_ready) { + return ssl_select_cert_retry; } + + if (test_state->get_handshake_hints_cb && + !test_state->get_handshake_hints_cb(client_hello)) { + return ssl_select_cert_error; + } + + if (config->use_early_callback && !InstallCertificate(ssl)) { + return ssl_select_cert_error; + } + return ssl_select_cert_success; } static int SetQuicReadSecret(SSL *ssl, enum ssl_encryption_level_t level, const SSL_CIPHER *cipher, const uint8_t *secret, size_t secret_len) { - return GetTestState(ssl)->quic_transport->SetReadSecret(level, cipher, secret, - secret_len); + MockQuicTransport *quic_transport = GetTestState(ssl)->quic_transport.get(); + if (quic_transport == nullptr) { + fprintf(stderr, "No QUIC transport.\n"); + return 0; + } + return quic_transport->SetReadSecret(level, cipher, secret, secret_len); } static int SetQuicWriteSecret(SSL *ssl, enum ssl_encryption_level_t level, const SSL_CIPHER *cipher, const uint8_t *secret, size_t secret_len) { - return GetTestState(ssl)->quic_transport->SetWriteSecret(level, cipher, - secret, secret_len); + MockQuicTransport *quic_transport = GetTestState(ssl)->quic_transport.get(); + if (quic_transport == nullptr) { + fprintf(stderr, "No QUIC transport.\n"); + return 0; + } + return quic_transport->SetWriteSecret(level, cipher, secret, secret_len); } static int AddQuicHandshakeData(SSL *ssl, enum ssl_encryption_level_t level, const uint8_t *data, size_t len) { - return GetTestState(ssl)->quic_transport->WriteHandshakeData(level, data, - len); + MockQuicTransport *quic_transport = GetTestState(ssl)->quic_transport.get(); + if (quic_transport == nullptr) { + fprintf(stderr, "No QUIC transport.\n"); + return 0; + } + return quic_transport->WriteHandshakeData(level, data, len); } static int FlushQuicFlight(SSL *ssl) { - return GetTestState(ssl)->quic_transport->Flush(); + MockQuicTransport *quic_transport = GetTestState(ssl)->quic_transport.get(); + if (quic_transport == nullptr) { + fprintf(stderr, "No QUIC transport.\n"); + return 0; + } + return quic_transport->Flush(); } static int SendQuicAlert(SSL *ssl, enum ssl_encryption_level_t level, uint8_t alert) { - return GetTestState(ssl)->quic_transport->SendAlert(level, alert); + MockQuicTransport *quic_transport = GetTestState(ssl)->quic_transport.get(); + if (quic_transport == nullptr) { + fprintf(stderr, "No QUIC transport.\n"); + return 0; + } + return quic_transport->SendAlert(level, alert); } static const SSL_QUIC_METHOD g_quic_method = { @@ -1320,7 +1381,10 @@ bssl::UniquePtr TestConfig::SetupCtx(SSL_CTX *old_ctx) const { SSL_CTX_set_info_callback(ssl_ctx.get(), InfoCallback); SSL_CTX_sess_set_new_cb(ssl_ctx.get(), NewSessionCallback); - if (use_ticket_callback) { + if (use_ticket_callback || handshake_hints) { + // If using handshake hints, always enable the ticket callback, so we can + // check that hints only mismatch when allowed. The ticket callback also + // uses a constant key, which simplifies the test. SSL_CTX_set_tlsext_ticket_key_cb(ssl_ctx.get(), TicketKeyCallback); } diff --git a/ssl/test/test_config.h b/ssl/test/test_config.h index 4946bc7ad..f4e3f61a5 100644 --- a/ssl/test/test_config.h +++ b/ssl/test/test_config.h @@ -167,6 +167,8 @@ struct TestConfig { std::string expect_msg_callback; bool allow_false_start_without_alpn = false; bool handoff = false; + bool handshake_hints = false; + bool allow_hint_mismatch = false; bool use_ocsp_callback = false; bool set_ocsp_in_callback = false; bool decline_ocsp_callback = false; @@ -198,7 +200,7 @@ struct TestConfig { std::unique_ptr test_state) const; }; -bool ParseConfig(int argc, char **argv, TestConfig *out_initial, +bool ParseConfig(int argc, char **argv, bool is_shim, TestConfig *out_initial, TestConfig *out_resume, TestConfig *out_retry); bool SetTestConfig(SSL *ssl, const TestConfig *config); diff --git a/ssl/test/test_state.h b/ssl/test/test_state.h index e5c96e5e0..2c558a425 100644 --- a/ssl/test/test_state.h +++ b/ssl/test/test_state.h @@ -17,6 +17,7 @@ #include +#include #include #include #include @@ -48,6 +49,8 @@ struct TestState { bool handshake_done = false; // private_key is the underlying private key used when testing custom keys. bssl::UniquePtr private_key; + // When private key methods are used, whether the private key was used. + bool used_private_key = false; std::vector private_key_result; // private_key_retries is the number of times an asynchronous private key // operation has been retried. @@ -64,6 +67,7 @@ struct TestState { // completion. This tests that the callback is not called again after this. bool cert_verified = false; int explicit_renegotiates = 0; + std::function get_handshake_hints_cb; }; bool SetTestState(SSL *ssl, std::unique_ptr state); diff --git a/ssl/tls13_both.cc b/ssl/tls13_both.cc index c6bc2b19a..5837187e3 100644 --- a/ssl/tls13_both.cc +++ b/ssl/tls13_both.cc @@ -580,10 +580,40 @@ enum ssl_private_key_result_t tls13_add_certificate_verify(SSL_HANDSHAKE *hs) { return ssl_private_key_failure; } - enum ssl_private_key_result_t sign_result = ssl_private_key_sign( - hs, sig, &sig_len, max_sig_len, signature_algorithm, msg); - if (sign_result != ssl_private_key_success) { - return sign_result; + SSL_HANDSHAKE_HINTS *const hints = hs->hints.get(); + Array spki; + if (hints) { + ScopedCBB spki_cbb; + if (!CBB_init(spki_cbb.get(), 64) || + !EVP_marshal_public_key(spki_cbb.get(), hs->local_pubkey.get()) || + !CBBFinishArray(spki_cbb.get(), &spki)) { + ssl_send_alert(ssl, SSL3_AL_FATAL, SSL_AD_INTERNAL_ERROR); + return ssl_private_key_failure; + } + } + + if (hints && !hs->hints_requested && + signature_algorithm == hints->signature_algorithm && + MakeConstSpan(msg) == hints->signature_input && + MakeConstSpan(spki) == hints->signature_spki && + !hints->signature.empty() && hints->signature.size() <= max_sig_len) { + // Signature algorithm and input both match. Reuse the signature from hints. + sig_len = hints->signature.size(); + OPENSSL_memcpy(sig, hints->signature.data(), sig_len); + } else { + enum ssl_private_key_result_t sign_result = ssl_private_key_sign( + hs, sig, &sig_len, max_sig_len, signature_algorithm, msg); + if (sign_result != ssl_private_key_success) { + return sign_result; + } + if (hints && hs->hints_requested) { + hints->signature_algorithm = signature_algorithm; + hints->signature_input = std::move(msg); + hints->signature_spki = std::move(spki); + if (!hints->signature.CopyFrom(MakeSpan(sig, sig_len))) { + return ssl_private_key_failure; + } + } } if (!CBB_did_write(&child, sig_len) || diff --git a/ssl/tls13_server.cc b/ssl/tls13_server.cc index 21f36610e..c9624c830 100644 --- a/ssl/tls13_server.cc +++ b/ssl/tls13_server.cc @@ -63,14 +63,33 @@ static bool resolve_ecdhe_secret(SSL_HANDSHAKE *hs, } Array secret; - ScopedCBB public_key; - UniquePtr key_share = SSLKeyShare::Create(group_id); - if (!key_share || - !CBB_init(public_key.get(), 32) || - !key_share->Accept(public_key.get(), &secret, &alert, peer_key) || - !CBBFinishArray(public_key.get(), &hs->ecdh_public_key)) { - ssl_send_alert(ssl, SSL3_AL_FATAL, alert); - return false; + SSL_HANDSHAKE_HINTS *const hints = hs->hints.get(); + if (hints && !hs->hints_requested && hints->key_share_group_id == group_id && + !hints->key_share_secret.empty()) { + // Copy DH secret from hints. + if (!hs->ecdh_public_key.CopyFrom(hints->key_share_public_key) || + !secret.CopyFrom(hints->key_share_secret)) { + ssl_send_alert(ssl, SSL3_AL_FATAL, SSL_AD_INTERNAL_ERROR); + return false; + } + } else { + ScopedCBB public_key; + UniquePtr key_share = SSLKeyShare::Create(group_id); + if (!key_share || // + !CBB_init(public_key.get(), 32) || + !key_share->Accept(public_key.get(), &secret, &alert, peer_key) || + !CBBFinishArray(public_key.get(), &hs->ecdh_public_key)) { + ssl_send_alert(ssl, SSL3_AL_FATAL, alert); + return false; + } + if (hints && hs->hints_requested) { + hints->key_share_group_id = group_id; + if (!hints->key_share_public_key.CopyFrom(hs->ecdh_public_key) || + !hints->key_share_secret.CopyFrom(secret)) { + ssl_send_alert(ssl, SSL3_AL_FATAL, SSL_AD_INTERNAL_ERROR); + return false; + } + } } return tls13_advance_key_schedule(hs, secret); @@ -538,6 +557,9 @@ static enum ssl_hs_wait_t do_select_session(SSL_HANDSHAKE *hs) { static enum ssl_hs_wait_t do_send_hello_retry_request(SSL_HANDSHAKE *hs) { SSL *const ssl = hs->ssl; + if (hs->hints_requested) { + return ssl_hs_hints_ready; + } ScopedCBB cbb; CBB body, session_id, extensions; @@ -734,7 +756,18 @@ static enum ssl_hs_wait_t do_send_server_hello(SSL_HANDSHAKE *hs) { SSL *const ssl = hs->ssl; Span random(ssl->s3->server_random); - RAND_bytes(random.data(), random.size()); + + SSL_HANDSHAKE_HINTS *const hints = hs->hints.get(); + if (hints && !hs->hints_requested && + hints->server_random.size() == random.size()) { + OPENSSL_memcpy(random.data(), hints->server_random.data(), random.size()); + } else { + RAND_bytes(random.data(), random.size()); + if (hints && hs->hints_requested && + !hints->server_random.CopyFrom(random)) { + return ssl_hs_error; + } + } assert(!hs->ech_accept || hs->ech_is_inner_present); @@ -864,6 +897,10 @@ static enum ssl_hs_wait_t do_send_server_certificate_verify(SSL_HANDSHAKE *hs) { static enum ssl_hs_wait_t do_send_server_finished(SSL_HANDSHAKE *hs) { SSL *const ssl = hs->ssl; + if (hs->hints_requested) { + return ssl_hs_hints_ready; + } + if (!tls13_add_finished(hs) || // Update the secret to the master secret and derive traffic keys. !tls13_advance_key_schedule( diff --git a/ssl/tls_method.cc b/ssl/tls_method.cc index 8165d1c92..326cbe753 100644 --- a/ssl/tls_method.cc +++ b/ssl/tls_method.cc @@ -93,7 +93,8 @@ static bool tls_set_read_state(SSL *ssl, ssl_encryption_level_t level, } if (ssl->quic_method != nullptr) { - if (!ssl->quic_method->set_read_secret(ssl, level, aead_ctx->cipher(), + if ((ssl->s3->hs == nullptr || !ssl->s3->hs->hints_requested) && + !ssl->quic_method->set_read_secret(ssl, level, aead_ctx->cipher(), secret_for_quic.data(), secret_for_quic.size())) { return false; @@ -121,7 +122,8 @@ static bool tls_set_write_state(SSL *ssl, ssl_encryption_level_t level, } if (ssl->quic_method != nullptr) { - if (!ssl->quic_method->set_write_secret(ssl, level, aead_ctx->cipher(), + if ((ssl->s3->hs == nullptr || !ssl->s3->hs->hints_requested) && + !ssl->quic_method->set_write_secret(ssl, level, aead_ctx->cipher(), secret_for_quic.data(), secret_for_quic.size())) { return false;