diff --git a/doc/migration_guide.rst b/doc/migration_guide.rst index 5302da222d6..dbdbc301c82 100644 --- a/doc/migration_guide.rst +++ b/doc/migration_guide.rst @@ -36,6 +36,21 @@ Functionality removed from the TLS implementation includes * AES-128 OCB ciphersuites * DHE_PSK ciphersuites +TLS 1.3 API adaptions +--------------------- + +Sessions +^^^^^^^^ + +Old (pre-Botan 3.0) sessions won't load in Botan 3.0 anymore and should be +discarded. + +``Session::session_id()`` is equal to the "session ticket" for TLS 1.3 sessions. +This ticket might be longer than a typical ID (up to 64kB). If your application +depends on a short ID for each session, it is safe to just hash the returned +buffer. + + Algorithms Removed ------------------- diff --git a/src/bogo_shim/bogo_shim.cpp b/src/bogo_shim/bogo_shim.cpp index 869df18b4cc..5d23aa8e71f 100644 --- a/src/bogo_shim/bogo_shim.cpp +++ b/src/bogo_shim/bogo_shim.cpp @@ -153,10 +153,13 @@ std::string map_to_bogo_error(const std::string& e) { "No shared DTLS version", ":UNSUPPORTED_PROTOCOL:" }, { "No shared TLS version", ":UNSUPPORTED_PROTOCOL:" }, { "OS2ECP: Unknown format type 251", ":BAD_ECPOINT:" }, + { "Peer sent signature algorithm that is not suitable for TLS 1.3", ":WRONG_SIGNATURE_TYPE:" }, { "Policy forbids all available DTLS version", ":NO_SUPPORTED_VERSIONS_ENABLED:" }, { "Policy forbids all available TLS version", ":NO_SUPPORTED_VERSIONS_ENABLED:" }, { "Policy refuses to accept signing with any hash supported by peer", ":NO_COMMON_SIGNATURE_ALGORITHMS:" }, { "Policy requires client send a certificate, but it did not", ":PEER_DID_NOT_RETURN_A_CERTIFICATE:" }, + { "PSK identity selected by server is out of bounds", ":PSK_IDENTITY_NOT_FOUND:" }, + { "PSK and ciphersuite selected by server are not compatible", ":OLD_SESSION_PRF_HASH_MISMATCH:" }, { "Received a record that exceeds maximum size", ":ENCRYPTED_LENGTH_TOO_LONG:" }, { "Received an encrypted record that exceeds maximum size", ":ENCRYPTED_LENGTH_TOO_LONG:" }, { "Received application data after connection closure", ":APPLICATION_DATA_ON_SHUTDOWN:" }, @@ -164,6 +167,7 @@ std::string map_to_bogo_error(const std::string& e) { "Received unexpected record version in initial record", ":WRONG_VERSION_NUMBER:" }, { "Received unexpected record version", ":WRONG_VERSION_NUMBER:" }, { "Rejecting ALPN request with alert", ":NO_APPLICATION_PROTOCOL:" }, + { "RSA signatures must use an RSASSA-PSS algorithm", ":WRONG_SIGNATURE_TYPE:" }, { "Server attempting to negotiate SSLv3 which is not supported", ":UNSUPPORTED_PROTOCOL:" }, { "Server certificate changed during renegotiation", ":SERVER_CERT_CHANGED:" }, { "Server changed its mind about extended master secret", ":RENEGOTIATION_EMS_MISMATCH:" }, @@ -197,6 +201,7 @@ std::string map_to_bogo_error(const std::string& e) { "Version downgrade received after Hello Retry", ":SECOND_SERVERHELLO_VERSION_MISMATCH:" }, { "protected change cipher spec received", ":UNEXPECTED_RECORD:" }, { "Server sent an unsupported extension", ":UNEXPECTED_EXTENSION:" }, + { "Unsupported extension found in Server Hello", ":UNEXPECTED_EXTENSION:" }, { "Unexpected extension received", ":UNEXPECTED_EXTENSION:" }, { "server hello must contain key exchange information", ":MISSING_KEY_SHARE:"}, { "Peer sent duplicated extensions", ":DUPLICATE_EXTENSION:" }, @@ -761,6 +766,8 @@ std::unique_ptr parse_options(char* argv[]) "max-version", "min-version", "mtu", + "on-initial-expect-curve-id", + "on-resume-expect-curve-id", "port", "read-size", "resume-count", diff --git a/src/bogo_shim/config.json b/src/bogo_shim/config.json index 7111d39c426..071a4a2dbdc 100644 --- a/src/bogo_shim/config.json +++ b/src/bogo_shim/config.json @@ -13,7 +13,8 @@ "HelloRetryRequest-DuplicateCurve-TLS13": "expects 'illegal parameter' but we want to stick with 'decode error'", "HelloRetryRequest-DuplicateCookie-TLS13": "expects 'illegal parameter' but we want to stick with 'decode error'", "EncryptedExtensionsWithKeyShare-TLS13": "expects 'unsupported extension' but RFC requires 'illegal parameter'", - "ClientSkipCertificateVerify-TLS13": "would require ambiguous error mapping" + "ClientSkipCertificateVerify-TLS13": "would require ambiguous error mapping", + "Resume-Client-Mismatch-TLS13-TLS12-TLS": "server requests a downgrade to TLS 1.2, echoing the random session ID during a TLS 1.3 resumption. => error mapping conflict" }, "DisabledTests": { @@ -41,33 +42,10 @@ "TooManyChangeCipherSpec-Client-TLS13": "Limits on the number of CCS are not implemented", "TooManyKeyUpdates": "Limits on the number of KeyUpdates are not implemented", - "CertificateVerificationSucceed-Client-TLS13-*" : "TLS 1.3 session resumption is NYI", - "Client-VerifyDefault-*-TLS13" : "TLS 1.3 session resumption is NYI", - "Client-Verify-*-TLS13" : "TLS 1.3 session resumption is NYI", - "ExportKeyingMaterial-TLS13" : "TLS 1.3 session resumption is NYI", - "InvalidPSKIdentity-TLS13" : "TLS 1.3 session resumption is NYI", - "NegotiatePSKResumption-TLS13" : "TLS 1.3 session resumption is NYI", - "Resume-Client-CipherMismatch-TLS13" : "TLS 1.3 session resumption is NYI", - "Resume-Client-Mismatch-TLS13-TLS12-TLS" : "TLS 1.3 session resumption is NYI", - "Resume-Client-TLS13-TLS13-TLS" : "TLS 1.3 session resumption is NYI", - "TLS-TLS13-AES_128_GCM_SHA256-client" : "TLS 1.3 session resumption is NYI", - "TLS-TLS13-AES_256_GCM_SHA384-client" : "TLS 1.3 session resumption is NYI", - "TLS-TLS13-CHACHA20_POLY1305_SHA256-client" : "TLS 1.3 session resumption is NYI", - "TLS12SessionID-TLS13" : "TLS 1.3 session resumption is NYI", - "TLS13-HonorServerSessionTicketLifetime" : "TLS 1.3 session resumption is NYI", - "TLS13-TestBadTicketAge-Client" : "TLS 1.3 session resumption is NYI", - "TLS13-TestValidTicketAge-Client" : "TLS 1.3 session resumption is NYI", - "TLS13SessionID-TLS13" : "TLS 1.3 session resumption is NYI", - "TolerateServerNameAck-TLS-TLS13" : "TLS 1.3 session resumption is NYI", - "OCSPStapling-Client-TLS13-*" : "TLS 1.3 session resumption is NYI", - "Resume-Client-NoResume-TLS12-TLS13-TLS": "TLS 1.3 session resumption is NYI", - "Resume-Client-Mismatch-TLS12-TLS13-TLS": "TLS 1.3 session resumption is NYI", - "Ticket-Forbidden-TLS13": "TLS 1.3 session resumption is NYI", - "Resume-Client-PRFMismatch-TLS13": "TLS 1.3 session resumption is NYI", - "TLS13-HelloRetryRequest-Client-*": "TLS 1.3 session resumption is NYI", - "TLS13-TicketAgeSkew-*": "TLS 1.3 session resumption is NYI", - "CurveID-Resume-Client-TLS13": "TLS 1.3 session resumption is NYI", - "ALPNClient-TLS-TLS13": "TLS 1.3 session resumption is NYI", + "TLS12SessionID-TLS13": "We don't offer TLS 1.3 when a TLS 1.2 session was found", + "Ticket-Forbidden-TLS13": "We don't offer TLS 1.3 when a TLS 1.2 session was found", + "Resume-Client-NoResume-TLS12-TLS13-TLS": "We don't offer TLS 1.3 when a TLS 1.2 session was found", + "Resume-Client-Mismatch-TLS12-TLS13-TLS": "We don't offer TLS 1.3 when a TLS 1.2 session was found", "KeyUpdate-FromServer": "No TLS 1.3 server, yet", "FragmentedClientVersion": "No TLS 1.3 server, yet", @@ -82,6 +60,7 @@ "ECDSACurveMismatch-Sign-TLS13": "No TLS 1.3 server, yet", "MinimumVersion-Server*-TLS13*": "No TLS 1.3 server, yet", "Resume-Server*TLS13*": "No TLS 1.3 server, yet", + "Resume-Server-*": "No TLS 1.3 server, yet", "Server-Sign-*-TLS13": "No TLS 1.3 server, yet", "Server-Verify-*-TLS13": "No TLS 1.3 server, yet", "Server-VerifyDefault-*-TLS13": "No TLS 1.3 server, yet", @@ -110,6 +89,7 @@ "UnexpectedClientEncryptedExtensions-TLS-TLS13": "No TLS 1.3 server, yet", "UnknownCipher-TLS13": "No TLS 1.3 server, yet", "VersionTolerance-TLS13": "No TLS 1.3 server, yet", + "TLS13-TicketAgeSkew-*": "No TLS 1.3 server, yet", "*EarlyData*": "No TLS 1.3 Early Data, yet", "TLS13-1RTT-Client-*": "No TLS 1.3 Early Data, yet", @@ -117,7 +97,6 @@ "SendNoClientCertificateExtensions-TLS13": "-signed-cert-timestamps currently not supported in the shim", "KeyUpdate-RequestACK-UnfinishedWrite": "-read-with-unfinished-write currently not supported in the shim", - "*Binder*": "No TLS 1.3", "NoExportEarlyKeyingMaterial*": "No TLS 1.3", "EarlyDataEnabled*": "No TLS 1.3", "TLS-ECH*": "No ECH support", diff --git a/src/lib/tls/msg_client_hello.cpp b/src/lib/tls/msg_client_hello.cpp index b87cfde5bf3..40342d38a49 100644 --- a/src/lib/tls/msg_client_hello.cpp +++ b/src/lib/tls/msg_client_hello.cpp @@ -23,6 +23,11 @@ #include #include +#ifdef BOTAN_HAS_TLS_13 + #include + #include +#endif + #include namespace Botan::TLS { @@ -485,7 +490,8 @@ Client_Hello_13::Client_Hello_13(const Policy& policy, Callbacks& cb, RandomNumberGenerator& rng, const std::string& hostname, - const std::vector& next_protocols) + const std::vector& next_protocols, + const std::optional& session) { // RFC 8446 4.1.2 // In TLS 1.3, the client indicates its version preferences in the @@ -522,6 +528,11 @@ Client_Hello_13::Client_Hello_13(const Policy& policy, m_extensions.add(new Signature_Algorithms(policy.acceptable_signature_schemes())); + // TODO: Support for PSK-only mode without a key exchange. + // This should be configurable in TLS::Policy and should allow no PSK + // support at all (e.g. to disable support for session resumption). + m_extensions.add(new PSK_Key_Exchange_Modes({PSK_Key_Exchange_Mode::PSK_DHE_KE})); + // TODO: Add a signature_algorithms_cert extension negotiating the acceptable // signature algorithms in a server certificate chain's certificates. @@ -552,10 +563,26 @@ Client_Hello_13::Client_Hello_13(const Policy& policy, m_extensions.add(new Supported_Point_Formats(policy.use_ecc_point_compression())); } + // TODO: Some extensions require a certain order or pose other assumptions. + // We should check those after the user was allowed to make changes to + // the extensions. cb.tls_modify_extensions(m_extensions, CLIENT); + + // RFC 8446 4.2.11 + // The "pre_shared_key" extension MUST be the last extension in the + // ClientHello (this facilitates implementation [...]). + // + // The PSK extension takes the partial transcript hash into account. Passing + // into Callbacks::tls_modify_extensions() does not make sense therefore. + if(session.has_value()) + { + m_extensions.add(new PSK(session.value(), cb)); + calculate_psk_binders({}); + } } void Client_Hello_13::retry(const Hello_Retry_Request& hrr, + const Transcript_Hash_State& transcript_hash_state, Callbacks& cb, RandomNumberGenerator& rng) { @@ -586,6 +613,44 @@ void Client_Hello_13::retry(const Hello_Retry_Request& hrr, // invocations to this callback due to the first Client_Hello or the // retried Client_Hello after receiving a Hello_Retry_Request. cb.tls_modify_extensions(m_extensions, CLIENT); + + auto psk = m_extensions.get(); + if(psk) + { + // Cipher suite should always be a known suite as this is checked upstream + const auto cipher = Ciphersuite::by_id(hrr.ciphersuite()); + BOTAN_ASSERT_NOMSG(cipher.has_value()); + + // RFC 8446 4.1.4 + // In [...] its updated ClientHello, the client SHOULD NOT offer + // any pre-shared keys associated with a hash other than that of the + // selected cipher suite. + psk->filter(cipher.value()); + + // RFC 8446 4.2.11.2 + // If the server responds with a HelloRetryRequest and the client + // then sends ClientHello2, its binder will be computed over: [...]. + calculate_psk_binders(transcript_hash_state.clone()); + } + } + +void Client_Hello_13::calculate_psk_binders(Transcript_Hash_State ths) + { + auto psk = m_extensions.get(); + if(!psk || psk->empty()) + return; + + // RFC 8446 4.2.11.2 + // Each entry in the binders list is computed as an HMAC over a + // transcript hash (see Section 4.4.1) containing a partial ClientHello + // [...]. + // + // Therefore we marshal the entire message prematurely to obtain the + // (truncated) transcript hash, calculate the PSK binders with it, update + // the Client Hello thus finalizing the message. Down the road, it will be + // re-marshalled with the correct binders and sent over the wire. + Handshake_Layer::prepare_message(*this, ths); + psk->calculate_binders(ths); } #endif // BOTAN_HAS_TLS_13 diff --git a/src/lib/tls/msg_server_hello.cpp b/src/lib/tls/msg_server_hello.cpp index dd9c6bb1bc9..43d6e087bb7 100644 --- a/src/lib/tls/msg_server_hello.cpp +++ b/src/lib/tls/msg_server_hello.cpp @@ -579,6 +579,7 @@ Server_Hello_13::Server_Hello_13(std::unique_ptr data, TLSEXT_KEY_SHARE, TLSEXT_PSK_KEY_EXCHANGE_MODES, TLSEXT_SUPPORTED_VERSIONS, + TLSEXT_PSK, }; // As the ServerHello shall only contain essential extensions, we don't give diff --git a/src/lib/tls/msg_session_ticket.cpp b/src/lib/tls/msg_session_ticket.cpp index c71f4e64cd7..d4f1052cdf7 100644 --- a/src/lib/tls/msg_session_ticket.cpp +++ b/src/lib/tls/msg_session_ticket.cpp @@ -11,6 +11,8 @@ #include #include +#include + namespace Botan::TLS { New_Session_Ticket_12::New_Session_Ticket_12(Handshake_IO& io, @@ -51,14 +53,45 @@ std::vector New_Session_Ticket_12::serialize() const #if defined (BOTAN_HAS_TLS_13) -New_Session_Ticket_13::New_Session_Ticket_13(const std::vector&) +New_Session_Ticket_13::New_Session_Ticket_13(const std::vector& buf, + Connection_Side from) + { + TLS_Data_Reader reader("New_Session_Ticket_13", buf); + + m_ticket_lifetime_hint = reader.get_uint32_t(); + + // RFC 8446 4.6.1 + // Servers MUST NOT use any value [of ticket_lifetime] greater than 604800 + // seconds (7 days). + if(m_ticket_lifetime_hint > 604800) + { + throw TLS_Exception(Alert::ILLEGAL_PARAMETER, + "Received a session ticket with lifetime longer than one week."); + } + + m_ticket_age_add = reader.get_uint32_t(); + m_ticket_nonce = reader.get_tls_length_value(1); + m_ticket = reader.get_tls_length_value(2); + + m_extensions.deserialize(reader, from, type()); + + reader.assert_done(); + } + +std::optional New_Session_Ticket_13::early_data_byte_limit() const { - // TODO: Implement + if(!m_extensions.has()) + return std::nullopt; + + const auto ext = m_extensions.get(); + BOTAN_ASSERT_NOMSG(ext->max_early_data_size().has_value()); + return ext->max_early_data_size().value(); } std::vector New_Session_Ticket_13::serialize() const { - return {}; + // TODO: might be needed once TLS 1.3 server is implemented + throw Not_Implemented("serializing New_Session_Ticket_13 is NYI"); } #endif diff --git a/src/lib/tls/tls13/tls_cipher_state.cpp b/src/lib/tls/tls13/tls_cipher_state.cpp index a73b0505e9e..164a17e32ea 100644 --- a/src/lib/tls/tls13/tls_cipher_state.cpp +++ b/src/lib/tls/tls13/tls_cipher_state.cpp @@ -16,6 +16,10 @@ * | * +-----> Derive-Secret(., "ext binder" | "res binder", "") * | = binder_key + * STATE PSK BINDER + * This state is reached by constructing the Cipher_State using init_with_psk(). + * The state can then be further advanced using advance_with_client_hello() once + * the initial Client Hello is fully generated. * | * +-----> Derive-Secret(., "c e traffic", ClientHello) * | = client_early_traffic_secret @@ -27,7 +31,8 @@ * | * * * STATE EARLY TRAFFIC - * This state is reached by constructing Cipher_State using init_with_psk() (not yet implemented). + * This state is reached by constructing Cipher_State using init_with_psk(). + * In this state the early data traffic secrets are available. TODO: implement early data. * The state can then be further advanced using advance_with_server_hello(). * * * | @@ -46,9 +51,9 @@ * | * * * STATE HANDSHAKE TRAFFIC - * This state is reached by constructing Cipher_State using init_with_server_hello(). - * In this state the handshake traffic secrets are available. The state can then be further - * advanced using advance_with_server_finished(). + * This state is reached by constructing Cipher_State using init_with_server_hello() or + * advance_with_server_hello(). In this state the handshake traffic secrets are available. + * The state can then be further advanced using advance_with_server_finished(). * * * | * v @@ -75,6 +80,8 @@ * ClientHello...client Finished) * = resumption_master_secret * STATE COMPLETED + * Once this state is reached the handshake is finished and no further cipher state advances + * are possible. */ #include @@ -113,12 +120,43 @@ std::unique_ptr Cipher_State::init_with_server_hello( const Ciphersuite& cipher, const Transcript_Hash& transcript_hash) { - auto cs = std::unique_ptr(new Cipher_State(side, cipher)); + auto cs = std::unique_ptr(new Cipher_State(side, cipher.prf_algo())); cs->advance_without_psk(); - cs->advance_with_server_hello(std::move(shared_secret), transcript_hash); + cs->advance_with_server_hello(cipher, std::move(shared_secret), transcript_hash); return cs; } +std::unique_ptr Cipher_State::init_with_psk( + const Connection_Side side, + const Cipher_State::PSK_Type type, + secure_vector&& psk, + const Ciphersuite& cipher) + { + auto cs = std::unique_ptr(new Cipher_State(side, cipher.prf_algo())); + cs->advance_with_psk(type, std::move(psk)); + return cs; + } + +void Cipher_State::advance_with_client_hello(const Transcript_Hash &transcript_hash) + { + BOTAN_ASSERT_NOMSG(m_state == State::PskBinder); + + zap(m_binder_key); + + // TODO: Currently 0-RTT is not yet implemented, hence we don't derive the + // early traffic secret for now. + // + // const auto client_early_traffic_secret = derive_secret(m_early_secret, "c e traffic", transcript_hash); + // derive_write_traffic_key(client_early_traffic_secret); + + m_exporter_master_secret = derive_secret(m_early_secret, "e exp master", transcript_hash); + + m_salt = derive_secret(m_early_secret, "derived", empty_hash()); + zap(m_early_secret); + + m_state = State::EarlyTraffic; + } + void Cipher_State::advance_with_server_finished(const Transcript_Hash& transcript_hash) { BOTAN_ASSERT_NOMSG(m_state == State::HandshakeTraffic); @@ -183,6 +221,8 @@ std::vector Cipher_State::current_nonce(const uint64_t seq_no, const se uint64_t Cipher_State::encrypt_record_fragment(const std::vector& header, secure_vector& fragment) { + BOTAN_ASSERT_NONNULL(m_encrypt); + m_encrypt->set_key(m_write_key); m_encrypt->set_associated_data_vec(header); m_encrypt->start(current_nonce(m_write_seq_no, m_write_iv)); @@ -194,6 +234,7 @@ uint64_t Cipher_State::encrypt_record_fragment(const std::vector& heade uint64_t Cipher_State::decrypt_record_fragment(const std::vector& header, secure_vector& encrypted_fragment) { + BOTAN_ASSERT_NONNULL(m_decrypt); BOTAN_ARG_CHECK(encrypted_fragment.size() >= m_decrypt->minimum_final_size(), "fragment too short to decrypt"); @@ -208,19 +249,52 @@ uint64_t Cipher_State::decrypt_record_fragment(const std::vector& heade size_t Cipher_State::encrypt_output_length(const size_t input_length) const { + BOTAN_ASSERT_NONNULL(m_encrypt); return m_encrypt->output_length(input_length); } size_t Cipher_State::decrypt_output_length(const size_t input_length) const { + BOTAN_ASSERT_NONNULL(m_decrypt); return m_decrypt->output_length(input_length); } size_t Cipher_State::minimum_decryption_input_length() const { + BOTAN_ASSERT_NONNULL(m_decrypt); return m_decrypt->minimum_final_size(); } +bool Cipher_State::is_compatible_with(const Ciphersuite& cipher) const + { + if(!cipher.usable_in_version(Protocol_Version::TLS_V13)) + return false; + + if(m_hash && m_hash->name() != cipher.prf_algo()) + return false; + + BOTAN_ASSERT_NOMSG((m_encrypt == nullptr) == (m_decrypt == nullptr)); + // TODO: Find a better way to check that the instantiated cipher algorithm + // is compatible with the one required by the cipher suite. + // AEAD_Mode::create() sets defaults the tag length to 16 which is then + // reported via AEAD_Mode::name() and hinders the trivial string comparison. + if(m_encrypt && m_encrypt->name() != cipher.cipher_algo() && + m_encrypt->name() != cipher.cipher_algo() + "(16)") + return false; + + return true; + } + +std::vector Cipher_State::psk_binder_mac(const Transcript_Hash& transcript_hash_with_truncated_client_hello) + { + BOTAN_ASSERT_NOMSG(m_state == State::PskBinder); + + auto hmac = HMAC(m_hash->new_object()); + hmac.set_key(m_binder_key); + hmac.update(transcript_hash_with_truncated_client_hello); + return hmac.final_stdvec(); + } + std::vector Cipher_State::finished_mac(const Transcript_Hash& transcript_hash) const { BOTAN_ASSERT_NOMSG(m_state == State::HandshakeTraffic); @@ -265,21 +339,19 @@ secure_vector Cipher_State::export_key(const std::string& label, namespace { -std::unique_ptr create_hmac(const Ciphersuite& cipher) +std::unique_ptr create_hmac(const std::string& hash) { - return std::make_unique(HashFunction::create_or_throw(cipher.prf_algo())); + return std::make_unique(HashFunction::create_or_throw(hash)); } } -Cipher_State::Cipher_State(Connection_Side whoami, const Ciphersuite& cipher) +Cipher_State::Cipher_State(Connection_Side whoami, const std::string& hash_function) : m_state(State::Uninitialized) , m_connection_side(whoami) - , m_encrypt(AEAD_Mode::create(cipher.cipher_algo(), ENCRYPTION)) - , m_decrypt(AEAD_Mode::create(cipher.cipher_algo(), DECRYPTION)) - , m_extract(std::make_unique(create_hmac(cipher))) - , m_expand(std::make_unique(create_hmac(cipher))) - , m_hash(HashFunction::create_or_throw(cipher.prf_algo())) + , m_extract(std::make_unique(create_hmac(hash_function))) + , m_expand(std::make_unique(create_hmac(hash_function))) + , m_hash(HashFunction::create_or_throw(hash_function)) , m_salt(m_hash->output_length(), 0x00) , m_write_seq_no(0) , m_read_seq_no(0) {} @@ -290,16 +362,49 @@ void Cipher_State::advance_without_psk() { BOTAN_ASSERT_NOMSG(m_state == State::Uninitialized); + // We are not using `m_early_secret` here because the secret won't be needed + // in any further state advancement methods. const auto early_secret = hkdf_extract(secure_vector(m_hash->output_length(), 0x00)); m_salt = derive_secret(early_secret, "derived", empty_hash()); + // Without PSK we skip the `PskBinder` state and go right to `EarlyTraffic`. m_state = State::EarlyTraffic; } -void Cipher_State::advance_with_server_hello(secure_vector&& shared_secret, +void Cipher_State::advance_with_psk(PSK_Type type, secure_vector&& psk) + { + BOTAN_ASSERT_NOMSG(m_state == State::Uninitialized); + + m_early_secret = hkdf_extract(std::move(psk)); + + const char* binder_label = + (type == PSK_Type::RESUMPTION) + ? "res binder" + : "ext binder"; + + // RFC 8446 4.2.11.2 + // The PskBinderEntry is computed in the same way as the Finished message + // [...] but with the BaseKey being the binder_key derived via the key + // schedule from the corresponding PSK which is being offered. + // + // Hence we are doing the binder key derivation and expansion in one go. + const auto binder_key = derive_secret(m_early_secret, binder_label, empty_hash()); + m_binder_key = hkdf_expand_label(binder_key, "finished", {}, m_hash->output_length()); + + m_state = State::PskBinder; + } + +void Cipher_State::advance_with_server_hello(const Ciphersuite& cipher, + secure_vector&& shared_secret, const Transcript_Hash& transcript_hash) { BOTAN_ASSERT_NOMSG(m_state == State::EarlyTraffic); + BOTAN_ASSERT_NOMSG(!m_encrypt); + BOTAN_ASSERT_NOMSG(!m_decrypt); + BOTAN_STATE_CHECK(is_compatible_with(cipher)); + + m_encrypt = AEAD_Mode::create_or_throw(cipher.cipher_algo(), ENCRYPTION); + m_decrypt = AEAD_Mode::create_or_throw(cipher.cipher_algo(), DECRYPTION); const auto handshake_secret = hkdf_extract(std::move(shared_secret)); @@ -325,6 +430,8 @@ void Cipher_State::advance_with_server_hello(secure_vector&& shared_sec void Cipher_State::derive_write_traffic_key(const secure_vector& traffic_secret, const bool handshake_traffic_secret) { + BOTAN_ASSERT_NONNULL(m_encrypt); + m_write_key = hkdf_expand_label(traffic_secret, "key", {}, m_encrypt->minimum_keylength()); m_write_iv = hkdf_expand_label(traffic_secret, "iv", {}, NONCE_LENGTH); m_write_seq_no = 0; @@ -340,6 +447,8 @@ void Cipher_State::derive_write_traffic_key(const secure_vector& traffi void Cipher_State::derive_read_traffic_key(const secure_vector& traffic_secret, const bool handshake_traffic_secret) { + BOTAN_ASSERT_NONNULL(m_encrypt); + m_read_key = hkdf_expand_label(traffic_secret, "key", {}, m_encrypt->minimum_keylength()); m_read_iv = hkdf_expand_label(traffic_secret, "iv", {}, NONCE_LENGTH); m_read_seq_no = 0; diff --git a/src/lib/tls/tls13/tls_cipher_state.h b/src/lib/tls/tls13/tls_cipher_state.h index 33f005be6a8..8706d86ccde 100644 --- a/src/lib/tls/tls13/tls_cipher_state.h +++ b/src/lib/tls/tls13/tls_cipher_state.h @@ -35,7 +35,8 @@ class Ciphersuite; * each facilitate certain cryptographic functionality: * * * init_with_psk() - * not yet implemented + * sets up the cipher state with a pre-shared key (out of band or via session + * ticket). will allow sending early data in the future * * * init_with_server_hello() / advance_with_server_hello() * allows encrypting and decrypting handshake traffic, as well as producing @@ -58,9 +59,25 @@ class Ciphersuite; */ class BOTAN_TEST_API Cipher_State { + public: + enum class PSK_Type + { + RESUMPTION, + EXTERNAL + }; + public: ~Cipher_State(); + /** + * Construct a Cipher_State from a Pre-Shared-Key. + */ + static std::unique_ptr init_with_psk( + const Connection_Side side, + const PSK_Type type, + secure_vector&& psk, + const Ciphersuite& cipher); + /** * Construct a Cipher_State after receiving a server hello message. */ @@ -70,6 +87,19 @@ class BOTAN_TEST_API Cipher_State const Ciphersuite& cipher, const Transcript_Hash& transcript_hash); + /** + * Transition internal secrets/keys for transporting early application data. + * Note that this state transition is legal only for handshakes using PSK. + */ + void advance_with_client_hello(const Transcript_Hash& transcript_hash); + + /** + * Transition internal secrets/keys for transporting handshake data. + */ + void advance_with_server_hello(const Ciphersuite& cipher, + secure_vector&& shared_secret, + const Transcript_Hash& transcript_hash); + /** * Transition internal secrets/keys for transporting application data. */ @@ -115,6 +145,13 @@ class BOTAN_TEST_API Cipher_State */ size_t minimum_decryption_input_length() const; + /** + * Calculates the MAC for a PSK binder value in Client Hellos. Note that + * the transcript hash passed into this method is computed from a partial + * Client Hello (RFC 8446 4.2.11.2) + */ + std::vector psk_binder_mac(const Transcript_Hash& transcript_hash_with_truncated_client_hello); + /** * Calculate the MAC for a TLS "Finished" handshake message (RFC 8446 4.4.4) */ @@ -156,7 +193,9 @@ class BOTAN_TEST_API Cipher_State */ bool can_export_keys() const { - return (m_state == State::ApplicationTraffic || m_state == State::Completed) && + return (m_state == State::EarlyTraffic || + m_state == State::ApplicationTraffic || + m_state == State::Completed) && !m_exporter_master_secret.empty(); } @@ -165,10 +204,21 @@ class BOTAN_TEST_API Cipher_State */ bool can_encrypt_application_traffic() const { + // TODO: when implementing early traffic (0-RTT) this will likely need + // to allow `State::EarlyTraffic`. return m_state != State::Uninitialized && m_state != State::HandshakeTraffic && !m_write_key.empty() && !m_write_iv.empty(); } + /** + * @returns true if the selected cipher primitives are compatible with + * the \p cipher suite. + * + * Note that cipher suites are considered "compatible" as long as the + * already selected cipher primitives in this cipher state are compatible. + */ + bool is_compatible_with(const Ciphersuite& cipher) const; + /** * Updates the key material used for decrypting data * This is triggered after we received a Key_Update from the peer. @@ -199,16 +249,14 @@ class BOTAN_TEST_API Cipher_State private: /** - * @param cipher the negotiated cipher suite - * @param whoami whether we play the SERVER or CLIENT + * @param hash_function the negotiated hash function to be used + * @param whoami whether we play the SERVER or CLIENT */ - Cipher_State(Connection_Side whoami, const Ciphersuite& cipher); + Cipher_State(Connection_Side whoami, const std::string& hash_function); + void advance_with_psk(PSK_Type type, secure_vector&& psk); void advance_without_psk(); - void advance_with_server_hello(secure_vector&& shared_secret, - const Transcript_Hash& transcript_hash); - std::vector current_nonce(const uint64_t seq_no, const secure_vector& iv) const; @@ -245,6 +293,7 @@ class BOTAN_TEST_API Cipher_State enum class State { Uninitialized, + PskBinder, EarlyTraffic, HandshakeTraffic, ApplicationTraffic, @@ -279,6 +328,9 @@ class BOTAN_TEST_API Cipher_State secure_vector m_peer_finished_key; secure_vector m_exporter_master_secret; secure_vector m_resumption_master_secret; + + secure_vector m_early_secret; + secure_vector m_binder_key; }; } diff --git a/src/lib/tls/tls13/tls_client_impl_13.cpp b/src/lib/tls/tls13/tls_client_impl_13.cpp index b2af75a2c9f..5774b4a6408 100644 --- a/src/lib/tls/tls13/tls_client_impl_13.cpp +++ b/src/lib/tls/tls13/tls_client_impl_13.cpp @@ -31,7 +31,8 @@ Client_Impl_13::Client_Impl_13(Callbacks& callbacks, const std::vector& next_protocols) : Channel_Impl_13(callbacks, session_manager, creds, rng, policy, false /* is_server */), m_info(info), - m_should_send_ccs(false) + m_should_send_ccs(false), + m_resumed_session(find_session_for_resumption()) { #if defined(BOTAN_HAS_TLS_12) if(policy.allow_tls12()) @@ -43,7 +44,8 @@ Client_Impl_13::Client_Impl_13(Callbacks& callbacks, callbacks, rng, m_info.hostname(), - next_protocols))); + next_protocols, + m_resumed_session))); if(expects_downgrade()) { preserve_client_hello(msg); } @@ -110,6 +112,29 @@ bool Client_Impl_13::handshake_finished() const return m_handshake_state.handshake_finished(); } +std::optional Client_Impl_13::find_session_for_resumption() + { + Session session; + if(!session_manager().load_from_server_info(m_info, session)) + return std::nullopt; + + // Ignore sessions that were not negotiated as TLS 1.3 + if(session.version().is_pre_tls_13()) + return std::nullopt; + + // RFC 8446 4.2.11.1 + // Clients MUST NOT attempt to use tickets which have ages greater than + // the "ticket_lifetime" value which was provided with the ticket. + const auto session_age = callbacks().tls_current_timestamp() - session.start_time(); + if(session_age > session.lifetime_hint()) + { + session_manager().remove_entry(session.session_id()); + return std::nullopt; + } + + return session; + } + void Client_Impl_13::handle(const Server_Hello_12& server_hello_msg) { if(m_handshake_state.has_hello_retry_request()) @@ -259,23 +284,52 @@ void Client_Impl_13::handle(const Server_Hello_13& sh) throw TLS_Exception(Alert::ILLEGAL_PARAMETER, "Server replied using a ciphersuite not allowed in version it offered"); } + // RFC 8446 4.2.11 + // Clients MUST verify that [...] a server "key_share" extension is present + // if required by the ClientHello "psk_key_exchange_modes" extension. If + // these values are not consistent, the client MUST abort the handshake + // with an "illegal_parameter" alert. + // + // Currently, we don't support PSK-only mode, hence a key share extension is + // considered mandatory. + // + // TODO: Implement PSK-only mode. if(!sh.extensions().has()) { - throw Not_Implemented("PSK mode (without key agreement) is NYI"); + throw TLS_Exception(Alert::ILLEGAL_PARAMETER, "Server Hello did not contain a key share extension"); } - // TODO: this is assuming a standard handshake without any PSK mode! - BOTAN_ASSERT_NOMSG(ch.extensions().has()); auto my_keyshare = ch.extensions().get(); auto shared_secret = my_keyshare->exchange(*sh.extensions().get(), policy(), callbacks(), rng()); my_keyshare->erase(); m_transcript_hash.set_algorithm(cipher.value().prf_algo()); - m_cipher_state = Cipher_State::init_with_server_hello(m_side, - std::move(shared_secret), - cipher.value(), - m_transcript_hash.current()); + if(sh.extensions().has()) + { + m_cipher_state = + ch.extensions().get()->select_cipher_state( + *sh.extensions().get(), cipher.value()); + + // TODO: When implementing pure PSK (negotiated out-of-band not as session + // tickets), we might mix resumption and external PSKs in a single + // handshake. In that case we might need to reset `m_resumed_session` + // if the server selected an out-of-band PSK over a resumption PSK! + + // TODO: When implementing early data, `advance_with_client_hello` must + // happen _before_ encrypting any early application data. + // Same when we want to support early key export. + m_cipher_state->advance_with_client_hello(m_transcript_hash.previous()); + m_cipher_state->advance_with_server_hello(cipher.value(), std::move(shared_secret), m_transcript_hash.current()); + } + else + { + m_resumed_session.reset(); // might have been set if we attempted a resumption + m_cipher_state = Cipher_State::init_with_server_hello(m_side, + std::move(shared_secret), + cipher.value(), + m_transcript_hash.current()); + } callbacks().tls_examine_extensions(sh.extensions(), SERVER); @@ -309,7 +363,7 @@ void Client_Impl_13::handle(const Hello_Retry_Request& hrr) m_transcript_hash = Transcript_Hash_State::recreate_after_hello_retry_request(cipher.value().prf_algo(), m_transcript_hash); - ch.retry(hrr, callbacks(), rng()); + ch.retry(hrr, m_transcript_hash, callbacks(), rng()); callbacks().tls_examine_extensions(hrr.extensions(), SERVER); @@ -353,9 +407,11 @@ void Client_Impl_13::handle(const Encrypted_Extensions& encrypted_extensions_msg callbacks().tls_examine_extensions(encrypted_extensions_msg.extensions(), SERVER); - bool psk_mode = false; // TODO - if(psk_mode) + if(m_handshake_state.server_hello().extensions().has()) { + // RFC 8446 2.2 + // As the server is authenticating via a PSK, it does not send a + // Certificate or a CertificateVerify message. m_transitions.set_expected_next(FINISHED); } else @@ -525,7 +581,9 @@ void Client_Impl_13::handle(const Finished_13& finished_msg) m_cipher_state->advance_with_server_finished(th_server_finished); m_cipher_state->advance_with_client_finished(m_transcript_hash.current()); - // TODO: save session and invoke tls_session_established callback + // TODO: Create a dummy session object and invoke tls_session_established. + // Alternatively, consider changing the expectations described in the + // callback's doc string. // no more handshake messages expected m_transitions.set_expected_next({}); @@ -533,10 +591,24 @@ void Client_Impl_13::handle(const Finished_13& finished_msg) callbacks().tls_session_activated(); } -void TLS::Client_Impl_13::handle(const New_Session_Ticket_13&) +void TLS::Client_Impl_13::handle(const New_Session_Ticket_13& new_session_ticket) { - // TODO: resumption is not yet implemented, hence we ignore session tickets - // received from the server. + Session session(new_session_ticket.ticket(), + m_cipher_state->psk(new_session_ticket.nonce()), + new_session_ticket.early_data_byte_limit(), + new_session_ticket.ticket_age_add(), + new_session_ticket.lifetime_hint(), + m_handshake_state.server_hello().selected_version(), + m_handshake_state.server_hello().ciphersuite(), + Connection_Side::CLIENT, + peer_cert_chain(), + m_info, + callbacks().tls_current_timestamp()); + + if(callbacks().tls_session_ticket_received(session)) + { + session_manager().save(session); + } } void TLS::Client_Impl_13::handle(const Key_Update& key_update) @@ -563,8 +635,20 @@ void TLS::Client_Impl_13::handle(const Key_Update& key_update) std::vector Client_Impl_13::peer_cert_chain() const { - throw Not_Implemented("peer cert chain is not implemented"); - return std::vector(); + std::vector result; + + if(m_handshake_state.has_server_certificate_chain()) + { + const auto& cert_chain = m_handshake_state.server_certificate().cert_chain(); + std::transform(cert_chain.cbegin(), cert_chain.cend(), std::back_inserter(result), + [](const auto& cert_entry) { return cert_entry.certificate; }); + } + else if(m_resumed_session.has_value()) + { + result = m_resumed_session->peer_certs(); + } + + return result; } bool Client_Impl_13::prepend_ccs() diff --git a/src/lib/tls/tls13/tls_client_impl_13.h b/src/lib/tls/tls13/tls_client_impl_13.h index b1e5adeb054..43061bc233f 100644 --- a/src/lib/tls/tls13/tls_client_impl_13.h +++ b/src/lib/tls/tls13/tls_client_impl_13.h @@ -83,6 +83,7 @@ class Client_Impl_13 : public Channel_Impl_13 void handle(const Key_Update& key_update); void send_client_authentication(Channel_Impl_13::AggregatedMessages& flight); + std::optional find_session_for_resumption(); private: const Server_Information m_info; @@ -90,7 +91,8 @@ class Client_Impl_13 : public Channel_Impl_13 Client_Handshake_State_13 m_handshake_state; Handshake_Transitions m_transitions; - bool m_should_send_ccs; + bool m_should_send_ccs; + std::optional m_resumed_session; }; } diff --git a/src/lib/tls/tls13/tls_extensions_key_share.cpp b/src/lib/tls/tls13/tls_extensions_key_share.cpp index 8431b3aa5bd..9e03981173b 100644 --- a/src/lib/tls/tls13/tls_extensions_key_share.cpp +++ b/src/lib/tls/tls13/tls_extensions_key_share.cpp @@ -454,14 +454,6 @@ bool Key_Share::empty() const return std::visit([](const auto& key_share) { return key_share.empty(); }, m_impl->key_share); } -namespace { -// This is a helper utility to emulate pattern matching with std::visit. -// See https://en.cppreference.com/w/cpp/utility/variant/visit for more info. -template struct overloaded : Ts... { using Ts::operator()...; }; -// explicit deduction guide (not needed as of C++20) -template overloaded(Ts...) -> overloaded; -} - secure_vector Key_Share::exchange(const Key_Share& peer_keyshare, const Policy& policy, Callbacks& cb, diff --git a/src/lib/tls/tls13/tls_extensions_psk.cpp b/src/lib/tls/tls13/tls_extensions_psk.cpp new file mode 100644 index 00000000000..1727a713049 --- /dev/null +++ b/src/lib/tls/tls13/tls_extensions_psk.cpp @@ -0,0 +1,279 @@ +/* +* TLS Extension Pre Shared Key +* (C) 2022 Jack Lloyd +* 2022 René Meusel, neXenio GmbH +* +* Botan is released under the Simplified BSD License (see license.txt) +*/ + +#include +#include +#include +#include +#include +#include +#include + +#include + +#if defined(BOTAN_HAS_TLS_13) + +namespace Botan::TLS { + +namespace { + +struct Client_PSK + { + std::vector identity; + std::vector binder; + uint32_t obfuscated_ticket_age; + + std::string hash_algorithm; + std::unique_ptr cipher_state; + }; + +struct Server_PSK + { + uint16_t selected_identity; + }; + +// RFC 8446 4.2.11.1 +// The "obfuscated_ticket_age" field of each PskIdentity contains an +// obfuscated version of the ticket age formed by taking the age in +// milliseconds and adding the "ticket_age_add" value that was included with +// the ticket, modulo 2^32. +uint32_t obfuscate_ticket_age(std::chrono::milliseconds ticket_age, uint32_t ticket_age_add) + { + const uint64_t age = ticket_age.count(); + const uint64_t add = ticket_age_add; + return static_cast(age + add); + } + +} // namespace + + +class PSK::PSK_Internal + { + public: + PSK_Internal(Server_PSK srv_psk) : psk(std::move(srv_psk)) {} + PSK_Internal(std::vector clt_psks) : psk(std::move(clt_psks)) {} + + std::variant, Server_PSK> psk; + }; + + +PSK::PSK(TLS_Data_Reader& reader, + uint16_t extension_size, + Handshake_Type message_type) + { + if(message_type == SERVER_HELLO) + { + if(extension_size != 2) + throw TLS_Exception(Alert::DECODE_ERROR, "Server provided a malformed PSK extension"); + + m_impl = std::make_unique(Server_PSK{reader.get_uint16_t()}); + } + else if(message_type == CLIENT_HELLO) + { + std::vector psks; + + const auto identities_length = reader.get_uint16_t(); + const auto identities_offset = reader.read_so_far(); + + while(reader.has_remaining() && (reader.read_so_far() - identities_offset) < identities_length) + { + auto& psk = psks.emplace_back(); + psk.identity = reader.get_tls_length_value(2); + psk.obfuscated_ticket_age = reader.get_uint32_t(); + } + + if(reader.read_so_far() - identities_offset != identities_length) + { + throw TLS_Exception(Alert::DECODE_ERROR, "Inconsistent PSK identity list"); + } + + const auto binders_length = reader.get_uint16_t(); + const auto binders_offset = reader.read_so_far(); + + for(auto& psk : psks) + { + if(!reader.has_remaining() || reader.read_so_far() - binders_offset >= binders_length) + { + throw TLS_Exception(Alert::DECODE_ERROR, "Not enough PSK binders"); + } + + psk.binder = reader.get_tls_length_value(1); + } + + if(reader.read_so_far() - binders_offset != binders_length) + { + throw TLS_Exception(Alert::DECODE_ERROR, "Inconsistent PSK binders list"); + } + + m_impl = std::make_unique(std::move(psks)); + } + else + { + throw TLS_Exception(Alert::DECODE_ERROR, "Found a PSK extension in an unexpected handshake message"); + } + } + + +PSK::PSK(const Session& session_to_resume, Callbacks& callbacks) + { + std::vector psks; + auto& cpsk = psks.emplace_back(); + + cpsk.identity = session_to_resume.session_ticket(); + + const auto age = + std::chrono::duration_cast( + callbacks.tls_current_timestamp() - session_to_resume.start_time()); + + cpsk.obfuscated_ticket_age = + obfuscate_ticket_age(age, session_to_resume.session_age_add()); + + auto psk = session_to_resume.master_secret(); + cpsk.hash_algorithm = session_to_resume.ciphersuite().prf_algo(); + cpsk.cipher_state = Cipher_State::init_with_psk(CLIENT, + Cipher_State::PSK_Type::RESUMPTION, + std::move(psk), + session_to_resume.ciphersuite()); + + // RFC 8446 4.2.11.2 + // Each entry in the binders list is computed as an HMAC over a transcript + // hash (see Section 4.4.1) containing a partial ClientHello up to and + // including the PreSharedKeyExtension.identities field. That is, it + // includes all of the ClientHello but not the binders list itself. The + // length fields for the message (including the overall length, the length + // of the extensions block, and the length of the "pre_shared_key" + // extension) are all set as if binders of the correct lengths were + // present. + // + // Hence, we fill the binders with dummy values of the correct length and use + // `Client_Hello_13::truncate()` to split them off before calculating the + // transcript hash that underpins the PSK binders. S.a. `calculate_binders()` + const auto binder_length = HashFunction::create_or_throw(cpsk.hash_algorithm)->output_length(); + cpsk.binder = std::vector(binder_length); + + m_impl = std::make_unique(std::move(psks)); + } + + +PSK::~PSK() = default; + + +bool PSK::empty() const + { + if(std::holds_alternative(m_impl->psk)) + return false; + + BOTAN_ASSERT_NOMSG(std::holds_alternative>(m_impl->psk)); + return std::get>(m_impl->psk).empty(); + } + + +std::unique_ptr PSK::select_cipher_state(const PSK& server_psk, const Ciphersuite& cipher) + { + BOTAN_STATE_CHECK(std::holds_alternative>(m_impl->psk)); + BOTAN_STATE_CHECK(std::holds_alternative(server_psk.m_impl->psk)); + + const auto id = std::get(server_psk.m_impl->psk).selected_identity; + auto& ids = std::get>(m_impl->psk); + + // RFC 8446 4.2.11 + // Clients MUST verify that the server's selected_identity is within the + // range supplied by the client, [...]. If these values are not + // consistent, the client MUST abort the handshake with an + // "illegal_parameter" alert. + if(id >= ids.size()) + { + throw TLS_Exception(Alert::ILLEGAL_PARAMETER, "PSK identity selected by server is out of bounds"); + } + + auto cipher_state = std::exchange(ids[id].cipher_state, nullptr); + BOTAN_ASSERT_NONNULL(cipher_state); + + // RFC 8446 4.2.11 + // Clients MUST verify that [...] the server selected a cipher suite + // indicating a Hash associated with the PSK [...]. If these values + // are not consistent, the client MUST abort the handshake with an + // "illegal_parameter" alert. + if(!cipher_state->is_compatible_with(cipher)) + { + throw TLS_Exception(Alert::ILLEGAL_PARAMETER, "PSK and ciphersuite selected by server are not compatible"); + } + + // destroy cipher states and PSKs that were not selected by the server + ids.clear(); + + return cipher_state; + } + + +void PSK::filter(const Ciphersuite& cipher) + { + BOTAN_STATE_CHECK(std::holds_alternative>(m_impl->psk)); + auto& psks = std::get>(m_impl->psk); + + const auto r = std::remove_if(psks.begin(), psks.end(), [&](const auto& psk) + { return psk.hash_algorithm != cipher.prf_algo(); }); + psks.erase(r, psks.end()); + } + + +std::vector PSK::serialize(Connection_Side side) const + { + std::vector result; + + std::visit(overloaded + { + [&](const Server_PSK& psk) + { + BOTAN_STATE_CHECK(side == SERVER); + result.reserve(2); + result.push_back(get_byte<0>(psk.selected_identity)); + result.push_back(get_byte<1>(psk.selected_identity)); + }, + [&](const std::vector& psks) + { + BOTAN_STATE_CHECK(side == CLIENT); + + std::vector identities; + std::vector binders; + for(const auto& psk : psks) + { + append_tls_length_value(identities, psk.identity, 2); + identities.push_back(get_byte<0>(psk.obfuscated_ticket_age)); + identities.push_back(get_byte<1>(psk.obfuscated_ticket_age)); + identities.push_back(get_byte<2>(psk.obfuscated_ticket_age)); + identities.push_back(get_byte<3>(psk.obfuscated_ticket_age)); + + append_tls_length_value(binders, psk.binder, 1); + } + + append_tls_length_value(result, identities, 2); + append_tls_length_value(result, binders, 2); + }, + }, + m_impl->psk); + + return result; + } + +// See RFC 8446 4.2.11.2 for details on how these binders are calculated +void PSK::calculate_binders(const Transcript_Hash_State& truncated_transcript_hash) + { + BOTAN_ASSERT_NOMSG(std::holds_alternative>(m_impl->psk)); + for(auto& psk : std::get>(m_impl->psk)) + { + auto tth = truncated_transcript_hash.clone(); + tth.set_algorithm(psk.hash_algorithm); + BOTAN_ASSERT_NONNULL(psk.cipher_state); + psk.binder = psk.cipher_state->psk_binder_mac(tth.truncated()); + } + } + +} // Botan::TLS + +#endif // HAS_TLS_13 diff --git a/src/lib/tls/tls13/tls_handshake_layer_13.cpp b/src/lib/tls/tls13/tls_handshake_layer_13.cpp index f27d59bca59..63adb1ea59b 100644 --- a/src/lib/tls/tls13/tls_handshake_layer_13.cpp +++ b/src/lib/tls/tls13/tls_handshake_layer_13.cpp @@ -110,7 +110,7 @@ std::optional parse_message(TLS::TLS_Data_Reader& reader, const Policy switch(type) { case NEW_SESSION_TICKET: - return New_Session_Ticket_13(msg); + return New_Session_Ticket_13(msg, peer_side); case KEY_UPDATE: return Key_Update(msg); default: diff --git a/src/lib/tls/tls13/tls_handshake_layer_13.h b/src/lib/tls/tls13/tls_handshake_layer_13.h index 2431c583a6f..e573767aecc 100644 --- a/src/lib/tls/tls13/tls_handshake_layer_13.h +++ b/src/lib/tls/tls13/tls_handshake_layer_13.h @@ -66,7 +66,7 @@ class BOTAN_TEST_API Handshake_Layer * * @return the marshalled handshake message */ - std::vector prepare_message(const Handshake_Message_13_Ref message, Transcript_Hash_State& transcript_hash); + static std::vector prepare_message(const Handshake_Message_13_Ref message, Transcript_Hash_State& transcript_hash); /** * Marshalls one post-handshake message for sending in an (encrypted) record. @@ -75,7 +75,7 @@ class BOTAN_TEST_API Handshake_Layer * * @return the marshalled post-handshake message */ - std::vector prepare_post_handshake_message(const Post_Handshake_Message_13& message); + static std::vector prepare_post_handshake_message(const Post_Handshake_Message_13& message); /** * Check if the Handshake_Layer has stored a partial message in its internal buffer. diff --git a/src/lib/tls/tls13/tls_handshake_state_13.h b/src/lib/tls/tls13/tls_handshake_state_13.h index 0ccb9d82c36..284ac7aaedf 100644 --- a/src/lib/tls/tls13/tls_handshake_state_13.h +++ b/src/lib/tls/tls13/tls_handshake_state_13.h @@ -25,6 +25,7 @@ class BOTAN_TEST_API Handshake_State_13_Base { public: bool has_client_hello() const { return m_client_hello.has_value(); } + bool has_server_certificate_chain() const { return m_server_certificate.has_value(); } bool has_hello_retry_request() const { return m_hello_retry_request.has_value(); } bool has_certificate_request() const { return m_certificate_request.has_value(); } bool has_server_finished() const { return m_server_finished.has_value(); } diff --git a/src/lib/tls/tls13/tls_transcript_hash_13.cpp b/src/lib/tls/tls13/tls_transcript_hash_13.cpp index a3f82f759ef..12f19643298 100644 --- a/src/lib/tls/tls13/tls_transcript_hash_13.cpp +++ b/src/lib/tls/tls13/tls_transcript_hash_13.cpp @@ -8,6 +8,10 @@ #include +#include +#include +#include + #include namespace Botan::TLS { @@ -22,6 +26,7 @@ Transcript_Hash_State::Transcript_Hash_State(const Transcript_Hash_State& other) , m_unprocessed_transcript(other.m_unprocessed_transcript) , m_current(other.m_current) , m_previous(other.m_previous) + , m_truncated(other.m_truncated) {} @@ -60,15 +65,125 @@ Transcript_Hash_State Transcript_Hash_State::recreate_after_hello_retry_request( return ths; } +namespace { + +// TODO: This is a massive code duplication of the client hello parsing code, +// as well as basic parsing of extensions. We should resolve this. +// +// Ad-hoc idea: When parsing the production objects, we could keep markers into +// the original buffer. E.g. the PSK extensions would keep its off- +// set into the entire client hello buffer. Using that offset we +// could quickly identify the offset of the binders list slice the +// buffer without re-parsing it. +// +// Finds the truncation offset in a serialization of Client Hello as defined in +// RFC 8446 4.2.11.2 used for the calculation of PSK binder MACs. +size_t find_client_hello_truncation_mark(std::vector client_hello) + { + // TODO: C++20: it would be great if TLS_Data_Reader could optionally deal + // with std::span. This would have saved one allocation of std::vector. + TLS_Data_Reader reader("Client Hello Truncation", client_hello); + + // handshake message type + BOTAN_ASSERT_NOMSG(reader.get_byte() == CLIENT_HELLO); + + // message length + reader.discard_next(3); + + // legacy version + reader.discard_next(2); + + // random + reader.discard_next(32); + + // session ID + const auto session_id_length = reader.get_byte(); + reader.discard_next(session_id_length); + + // TODO: DTLS contains a hello_cookie in this location + // Currently we don't support DTLS 1.3 + + // cipher suites + const auto ciphersuites_length = reader.get_uint16_t(); + reader.discard_next(ciphersuites_length); + + // compression methods + const auto compression_methods_length = reader.get_byte(); + reader.discard_next(compression_methods_length); + + // extensions + const auto extensions_length = reader.get_uint16_t(); + const auto extensions_offset = reader.read_so_far(); + while(reader.has_remaining() && reader.read_so_far() - extensions_offset < extensions_length) + { + const auto ext_type = static_cast(reader.get_uint16_t()); + const auto ext_length = reader.get_uint16_t(); + + // skip over all extensions, finding the PSK extension to be truncated + if(ext_type != TLSEXT_PSK) + { + reader.discard_next(ext_length); + continue; + } + + // PSK identities list + const auto identities_length = reader.get_uint16_t(); + reader.discard_next(identities_length); + + // check that only the binders are left in the buffer... + const auto binders_length = reader.peek_uint16_t(); + if(binders_length != reader.remaining_bytes() - 2 /* binders_length */) + { + throw TLS_Exception(Alert::DECODE_ERROR, + "Failed to truncate Client Hello that doesn't end on the PSK binders list"); + } + + // the reader now points to the truncation point + break; + } + + // if no PSK extension was found, this will point to the end of the buffer + return reader.read_so_far(); + } + +std::vector read_hash_state(std::unique_ptr& hash) + { + // Botan does not support finalizing a HashFunction without resetting + // the internal state of the hash. Hence we first copy the internal + // state and then finalize the transient HashFunction. + return hash->copy_state()->final_stdvec(); + } + +} // namespace + void Transcript_Hash_State::update(const uint8_t* serialized_message, const size_t serialized_message_length) { if(m_hash != nullptr) { - // Botan does not support finalizing a HashFunction without resetting - // the internal state of the hash. Hence we first copy the internal - // state and then finalize the transient HashFunction. - m_hash->update(serialized_message, serialized_message_length); - m_previous = std::exchange(m_current, m_hash->copy_state()->final_stdvec()); + auto truncation_mark = serialized_message_length; + + // Check whether we should generate a truncated hash for supporting PSK + // binder calculation or verification. See RFC 8446 4.2.11.2. + if(serialized_message_length > 0 && *serialized_message == CLIENT_HELLO) + { + truncation_mark = + find_client_hello_truncation_mark({serialized_message, + serialized_message + serialized_message_length}); + } + + if(truncation_mark < serialized_message_length) + { + m_hash->update(serialized_message, truncation_mark); + m_truncated = read_hash_state(m_hash); + m_hash->update(serialized_message + truncation_mark, serialized_message_length - truncation_mark); + } + else + { + m_truncated.clear(); + m_hash->update(serialized_message, serialized_message_length); + } + + m_previous = std::exchange(m_current, read_hash_state(m_hash)); } else { @@ -88,6 +203,12 @@ const Transcript_Hash& Transcript_Hash_State::previous() const return m_previous; } +const Transcript_Hash& Transcript_Hash_State::truncated() const + { + BOTAN_STATE_CHECK(!m_truncated.empty()); + return m_truncated; + } + void Transcript_Hash_State::set_algorithm(const std::string& algo_spec) { BOTAN_STATE_CHECK(m_hash == nullptr || m_hash->name() == algo_spec); diff --git a/src/lib/tls/tls13/tls_transcript_hash_13.h b/src/lib/tls/tls13/tls_transcript_hash_13.h index b20f6b142dd..db8cc5eb4f8 100644 --- a/src/lib/tls/tls13/tls_transcript_hash_13.h +++ b/src/lib/tls/tls13/tls_transcript_hash_13.h @@ -69,6 +69,18 @@ class BOTAN_TEST_API Transcript_Hash_State */ const Transcript_Hash& previous() const; + /** + * returns a truncated transcript hash (see RFC 8446 4.2.11.2) + * + * This is useful for implementing PSK binders in the PSK extension of + * client hello. It is a transcript over a partially marshalled client + * hello message. This hash is available only if the last processed + * message was a client hello with a PSK extension. + * + * throws if no 'truncated' hash is available + */ + const Transcript_Hash& truncated() const; + void set_algorithm(const std::string& algo_spec); Transcript_Hash_State clone() const; @@ -85,6 +97,7 @@ class BOTAN_TEST_API Transcript_Hash_State Transcript_Hash m_current; Transcript_Hash m_previous; + Transcript_Hash m_truncated; }; } diff --git a/src/lib/tls/tls_callbacks.cpp b/src/lib/tls/tls_callbacks.cpp index 6edb0f1ff4b..b2cd494252e 100644 --- a/src/lib/tls/tls_callbacks.cpp +++ b/src/lib/tls/tls_callbacks.cpp @@ -55,6 +55,15 @@ std::string TLS::Callbacks::tls_decode_group_param(Group_Params group_param) return group_param_to_string(group_param); } + +bool TLS::Callbacks::tls_session_ticket_received(const Session& session) + { + // RFC 8446 4.6.1 + // [A ticket_lifetime] of zero indicates that the ticket should be discarded + // immediately. + return session.lifetime_hint().count() > 0; + } + void TLS::Callbacks::tls_verify_cert_chain( const std::vector& cert_chain, const std::vector>& ocsp_responses, diff --git a/src/lib/tls/tls_callbacks.h b/src/lib/tls/tls_callbacks.h index a6fa6302597..af32dc4fa33 100644 --- a/src/lib/tls/tls_callbacks.h +++ b/src/lib/tls/tls_callbacks.h @@ -98,6 +98,21 @@ class BOTAN_PUBLIC_API(2,0) Callbacks */ virtual void tls_session_activated() {} + /** + * Optional callback: New session ticket received + * Called when we receive a session ticket from the server at any point + * after the initial handshake has finished. Clients may decide to keep or + * discard the session ticket in the configured session manager. + * + * Note: this is called for connections that negotiated TLS 1.3 only. + * + * @param session the session descriptor + * + * @return false to prevent the session from being cached, and true to + * cache the session in the configured session manager + */ + virtual bool tls_session_ticket_received(const Session& session); + /** * Optional callback with default impl: verify cert chain * diff --git a/src/lib/tls/tls_extensions.cpp b/src/lib/tls/tls_extensions.cpp index b224e4800c6..18f99defb3d 100644 --- a/src/lib/tls/tls_extensions.cpp +++ b/src/lib/tls/tls_extensions.cpp @@ -68,6 +68,12 @@ std::unique_ptr make_extension(TLS_Data_Reader& reader, return std::make_unique(reader, size, from); #if defined(BOTAN_HAS_TLS_13) + case TLSEXT_PSK: + return std::make_unique(reader, size, message_type); + + case TLSEXT_EARLY_DATA: + return std::make_unique(reader, size, message_type); + case TLSEXT_COOKIE: return std::make_unique(reader, size); @@ -843,5 +849,48 @@ std::vector Signature_Algorithms_Cert::serialize(Connection_Side whoami return m_siganture_algorithms.serialize(whoami); } +std::vector EarlyDataIndication::serialize(Connection_Side) const + { + std::vector result; + if(m_max_early_data_size.has_value()) + { + const auto max_data = m_max_early_data_size.value(); + result.push_back(get_byte<0>(max_data)); + result.push_back(get_byte<1>(max_data)); + result.push_back(get_byte<2>(max_data)); + result.push_back(get_byte<3>(max_data)); + } + return result; + } + +EarlyDataIndication::EarlyDataIndication(TLS_Data_Reader& reader, + uint16_t extension_size, + Handshake_Type message_type) + { + if(message_type == NEW_SESSION_TICKET) + { + if(extension_size != 4) + { + throw TLS_Exception(Alert::DECODE_ERROR, + "Received an early_data extension in a NewSessionTicket message " + "without maximum early data size indication"); + } + + m_max_early_data_size = reader.get_uint32_t(); + } + else if(extension_size != 0) + { + throw TLS_Exception(Alert::DECODE_ERROR, + "Received an early_data extension containing an unexpected data " + "size indication"); + } + } + +bool EarlyDataIndication::empty() const + { + // This extension may be empty by definition but still carry information + return false; + } + #endif } diff --git a/src/lib/tls/tls_extensions.h b/src/lib/tls/tls_extensions.h index a799317696b..b21e383ad85 100644 --- a/src/lib/tls/tls_extensions.h +++ b/src/lib/tls/tls_extensions.h @@ -34,6 +34,10 @@ namespace TLS { #if defined(BOTAN_HAS_TLS_13) class Callbacks; +class Session; +class Cipher_State; +class Ciphersuite; +class Transcript_Hash_State; enum class PSK_Key_Exchange_Mode : uint8_t { PSK_KE = 0, @@ -67,6 +71,8 @@ enum Handshake_Extension_Type { TLSEXT_SUPPORTED_VERSIONS = 43, #if defined(BOTAN_HAS_TLS_13) + TLSEXT_PSK = 41, + TLSEXT_EARLY_DATA = 42, TLSEXT_COOKIE = 44, TLSEXT_PSK_KEY_EXCHANGE_MODES = 45, @@ -586,6 +592,50 @@ class BOTAN_UNSTABLE_API Certificate_Authorities final : public Extension std::vector m_distinguished_names; }; +/** + * Pre-Shared Key extension from RFC 8446 4.2.11 + */ +class BOTAN_UNSTABLE_API PSK final : public Extension + { + public: + static Handshake_Extension_Type static_type() { return TLSEXT_PSK; } + Handshake_Extension_Type type() const override { return static_type(); } + + std::vector serialize(Connection_Side side) const override; + + /** + * Returns the cipher state representing the PSK selected by the server. + * Note that this destructs the list of offered PSKs and its cipher states + * and must therefore not be called more than once. + */ + std::unique_ptr select_cipher_state(const PSK& server_psk, + const Ciphersuite& cipher); + + /** + * Remove PSK identities from the list in \p m_psk that are not compatible + * with the passed in \p cipher suite. + * This is useful to react to Hello Retry Requests. See RFC 8446 4.1.4. + */ + void filter(const Ciphersuite& cipher); + + bool empty() const override; + + PSK(TLS_Data_Reader& reader, uint16_t extension_size, Handshake_Type message_type); + + PSK(const Session& session_to_resume, Callbacks& callbacks); + + ~PSK(); + + void calculate_binders(const Transcript_Hash_State& truncated_transcript_hash); + + // TODO: Implement pure PSK negotiation that is not used for session + // resumption. + + private: + class PSK_Internal; + std::unique_ptr m_impl; + }; + /** * Signature_Algorithms_Cert from RFC 8446 */ @@ -658,6 +708,40 @@ class BOTAN_UNSTABLE_API Key_Share final : public Extension class Key_Share_Impl; std::unique_ptr m_impl; }; + +/** + * Indicates usage or support of early data as described in RFC 8446 4.2.10. + */ +class BOTAN_UNSTABLE_API EarlyDataIndication final : public Extension + { + public: + static Handshake_Extension_Type static_type() + { return TLSEXT_EARLY_DATA; } + + Handshake_Extension_Type type() const override { return static_type(); } + std::vector serialize(Connection_Side whoami) const override; + + bool empty() const override; + + std::optional max_early_data_size() const + { return m_max_early_data_size; } + + EarlyDataIndication(TLS_Data_Reader& reader, + uint16_t extension_size, + Handshake_Type message_type); + + /** + * The max_early_data_size is exclusively provided by servers when using + * this extension in the NewSessionTicket message! Otherwise it stays + * std::nullopt and results in an empty extension. (RFC 8446 4.2.10). + */ + EarlyDataIndication(std::optional max_early_data_size = std::nullopt) + : m_max_early_data_size(std::move(max_early_data_size)) {} + + private: + std::optional m_max_early_data_size; + }; + #endif /** diff --git a/src/lib/tls/tls_messages.h b/src/lib/tls/tls_messages.h index f23e955bda3..1c7d18563bb 100644 --- a/src/lib/tls/tls_messages.h +++ b/src/lib/tls/tls_messages.h @@ -220,14 +220,25 @@ class BOTAN_UNSTABLE_API Client_Hello_13 final : public Client_Hello Callbacks& cb, RandomNumberGenerator& rng, const std::string& hostname, - const std::vector& next_protocols); - + const std::vector& next_protocols, + const std::optional& session = std::nullopt); void retry(const Hello_Retry_Request& hrr, + const Transcript_Hash_State& transcript_hash_state, Callbacks& cb, RandomNumberGenerator& rng); std::vector supported_versions() const; + + private: + /** + * If the Client Hello contains a PSK extensions with identities this will + * generate the PSK binders as described in RFC 8446 4.2.11.2. + * Note that the passed in \p transcript_hash_state might be virgin for + * the initial Client Hello and should be primed with ClientHello1 and + * HelloRetryRequest for an updated Client Hello. + */ + void calculate_psk_binders(Transcript_Hash_State transcript_hash_state); }; #endif // BOTAN_HAS_TLS_13 @@ -866,13 +877,35 @@ class BOTAN_UNSTABLE_API New_Session_Ticket_13 final : public Handshake_Message public: Handshake_Type type() const override { return NEW_SESSION_TICKET; } - explicit New_Session_Ticket_13(const std::vector& buf); + New_Session_Ticket_13(const std::vector& buf, + Connection_Side from); std::vector serialize() const override; - private: + const std::vector& ticket() const { return m_ticket; } + const std::vector& nonce() const { return m_ticket_nonce; } + uint32_t ticket_age_add() const { return m_ticket_age_add; } + uint32_t lifetime_hint() const { return m_ticket_lifetime_hint; } + + /** + * @return the number of bytes allowed for early data or std::nullopt + * when early data is not allowed at all + */ + std::optional early_data_byte_limit() const; - // TODO: implement this message fully + private: + // RFC 8446 4.6.1 + // Clients MUST NOT cache tickets for longer than 7 days, regardless of + // the ticket_lifetime, and MAY delete tickets earlier based on local + // policy. A server MAY treat a ticket as valid for a shorter period + // of time than what is stated in the ticket_lifetime. + // + // ... hence we call it 'lifetime hint'. + uint32_t m_ticket_lifetime_hint; + uint32_t m_ticket_age_add; + std::vector m_ticket_nonce; + std::vector m_ticket; + Extensions m_extensions; }; #endif diff --git a/src/lib/tls/tls_session.cpp b/src/lib/tls/tls_session.cpp index 9f3c8787e53..fb99fb7377c 100644 --- a/src/lib/tls/tls_session.cpp +++ b/src/lib/tls/tls_session.cpp @@ -17,6 +17,20 @@ namespace Botan::TLS { +Session::Session() : + m_start_time(std::chrono::system_clock::time_point::min()), + m_version(), + m_ciphersuite(0), + m_connection_side(static_cast(0)), + m_srtp_profile(0), + m_extended_master_secret(false), + m_encrypt_then_mac(false), + m_early_data_allowed(false), + m_max_early_data_bytes(0), + m_ticket_age_add(0), + m_lifetime_hint(0) + {} + Session::Session(const std::vector& session_identifier, const secure_vector& master_secret, Protocol_Version version, @@ -40,8 +54,61 @@ Session::Session(const std::vector& session_identifier, m_extended_master_secret(extended_master_secret), m_encrypt_then_mac(encrypt_then_mac), m_peer_certs(certs), - m_server_info(server_info) + m_server_info(server_info), + m_early_data_allowed(false), + m_max_early_data_bytes(0), + m_ticket_age_add(0), + m_lifetime_hint(0) { + BOTAN_ARG_CHECK(version.is_pre_tls_13(), + "Instantiated a TLS 1.2 session object with a TLS version newer than 1.2"); + } + +Session::Session(const std::vector& session_ticket, + const secure_vector& session_psk, + const std::optional& max_early_data_bytes, + uint32_t ticket_age_add, + uint32_t lifetime_hint, + Protocol_Version version, + uint16_t ciphersuite, + Connection_Side side, + const std::vector& peer_certs, + const Server_Information& server_info, + std::chrono::system_clock::time_point current_timestamp) : + m_start_time(current_timestamp), + + // In TLS 1.3 the PSK and Session Resumption concepts were merged and the + // explicit SessionID was retired. Instead an opaque session ticket is used + // to identify sessions during resumption. Hence, we deliberately set the + // legacy m_identifier as "empty". + m_identifier(), + m_session_ticket(session_ticket), + m_master_secret(session_psk), + m_version(version), + m_ciphersuite(ciphersuite), + m_connection_side(side), + + // TODO: Might become necessary when DTLS 1.3 is being implemented. + m_srtp_profile(0), + + // RFC 8446 Appendix D + // Because TLS 1.3 always hashes in the transcript up to the server + // Finished, implementations which support both TLS 1.3 and earlier + // versions SHOULD indicate the use of the Extended Master Secret + // extension in their APIs whenever TLS 1.3 is used. + m_extended_master_secret(true), + + // TLS 1.3 uses AEADs, so technically encrypt-then-MAC is not applicable. + m_encrypt_then_mac(false), + m_peer_certs(peer_certs), + m_server_info(server_info), + m_early_data_allowed(max_early_data_bytes.has_value()), + m_max_early_data_bytes(max_early_data_bytes.value_or(0)), + m_ticket_age_add(ticket_age_add), + m_lifetime_hint(lifetime_hint) + { + BOTAN_ARG_CHECK(!version.is_pre_tls_13(), + "Instantiated a TLS 1.3 session object with a TLS version older than 1.3"); } Session::Session(const std::string& pem) @@ -92,6 +159,10 @@ Session::Session(const uint8_t ber[], size_t ber_len) .decode(server_port) .decode(srp_identifier_str) .decode(srtp_profile) + .decode(m_early_data_allowed) + .decode_integer_type(m_max_early_data_bytes) + .decode_integer_type(m_ticket_age_add) + .decode_integer_type(m_lifetime_hint) .end_cons() .verify_end(); @@ -169,6 +240,12 @@ secure_vector Session::DER_encode() const .encode(static_cast(m_server_info.port())) .encode(ASN1_String("", ASN1_Type::Utf8String)) // old srp identifier .encode(static_cast(m_srtp_profile)) + + // the fields below were introduced for TLS 1.3 session tickets + .encode(m_early_data_allowed) + .encode(static_cast(m_max_early_data_bytes)) + .encode(static_cast(m_ticket_age_add)) + .encode(static_cast(m_lifetime_hint)) .end_cons() .get_contents(); } @@ -189,6 +266,19 @@ Ciphersuite Session::ciphersuite() const return suite.value(); } +const std::vector& Session::session_id() const + { + if(m_version.is_pre_tls_13()) + return m_identifier; + + // RFC 8446 4.6.1 + // ticket: The value of the ticket to be used as the PSK identity. The + // ticket itself is an opaque label. It MAY be either a database + // lookup key or a self-encrypted and self-authenticated value. + BOTAN_ASSERT_NOMSG(m_identifier.empty()); + return m_session_ticket; + } + namespace { // The output length of the HMAC must be a valid keylength for the AEAD diff --git a/src/lib/tls/tls_session.h b/src/lib/tls/tls_session.h index 3f5b2a121ec..8702e7db2aa 100644 --- a/src/lib/tls/tls_session.h +++ b/src/lib/tls/tls_session.h @@ -31,18 +31,10 @@ class BOTAN_PUBLIC_API(2,0) Session final /** * Uninitialized session */ - Session() : - m_start_time(std::chrono::system_clock::time_point::min()), - m_version(), - m_ciphersuite(0), - m_connection_side(static_cast(0)), - m_srtp_profile(0), - m_extended_master_secret(false), - m_encrypt_then_mac(false) - {} + Session(); /** - * New session (sets session start time) + * New TLS 1.2 session (sets session start time) */ Session(const std::vector& session_id, const secure_vector& master_secret, @@ -57,6 +49,21 @@ class BOTAN_PUBLIC_API(2,0) Session final uint16_t srtp_profile, std::chrono::system_clock::time_point current_timestamp); + /** + * New TLS 1.3 session (sets session start time) + */ + Session(const std::vector& session_ticket, + const secure_vector& session_psk, + const std::optional& max_early_data_bytes, + uint32_t ticket_age_add, + uint32_t lifetime_hint, + Protocol_Version version, + uint16_t ciphersuite, + Connection_Side side, + const std::vector& peer_certs, + const Server_Information& server_info, + std::chrono::system_clock::time_point current_timestamp); + /** * Load a session from DER representation (created by DER_encode) * @param ber DER representation buffer @@ -140,8 +147,11 @@ class BOTAN_PUBLIC_API(2,0) Session final /** * Get the session identifier + * + * Note that for TLS 1.3 sessions this will be equal to the session ticket + * and might therefore be a up to 2^16-1 bytes long. */ - const std::vector& session_id() const { return m_identifier; } + const std::vector& session_id() const; /** * Get the negotiated DTLS-SRTP algorithm (RFC 5764) @@ -152,6 +162,8 @@ class BOTAN_PUBLIC_API(2,0) Session final bool supports_encrypt_then_mac() const { return m_encrypt_then_mac; } + bool supports_early_data() const { return m_early_data_allowed; } + /** * Return the certificate chain of the peer (possibly empty) */ @@ -162,6 +174,16 @@ class BOTAN_PUBLIC_API(2,0) Session final */ std::chrono::system_clock::time_point start_time() const { return m_start_time; } + /** + * Return the ticket obfuscation adder + */ + uint32_t session_age_add() const { return m_ticket_age_add; } + + /** + * Return the number of bytes allowed for 0-RTT early data + */ + uint32_t max_early_data_bytes() const { return m_max_early_data_bytes; } + /** * Return the session ticket the server gave us */ @@ -172,8 +194,25 @@ class BOTAN_PUBLIC_API(2,0) Session final */ const Server_Information& server_info() const { return m_server_info; } + /** + * @return the lifetime of the ticket as defined by the TLS server + */ + std::chrono::seconds lifetime_hint() const { return std::chrono::seconds(m_lifetime_hint); } + private: - enum { TLS_SESSION_PARAM_STRUCT_VERSION = 20160812 }; + // Struct Version history + // + // 20160812 - Pre TLS 1.3 + // 20220505 - Introduction of TLS 1.3 sessions + // - added fields: + // - m_early_data_allowed + // - m_max_early_data_bytes + // - m_ticket_age_add + // - m_lifetime_hint + enum + { + TLS_SESSION_PARAM_STRUCT_VERSION = 20220505 + }; std::chrono::system_clock::time_point m_start_time; @@ -190,6 +229,11 @@ class BOTAN_PUBLIC_API(2,0) Session final std::vector m_peer_certs; Server_Information m_server_info; // optional + + bool m_early_data_allowed; + uint32_t m_max_early_data_bytes; + uint32_t m_ticket_age_add; + uint32_t m_lifetime_hint; // in milliseconds }; } diff --git a/src/lib/utils/stl_util.h b/src/lib/utils/stl_util.h index 19ebfbc0a7f..e46b2f196c9 100644 --- a/src/lib/utils/stl_util.h +++ b/src/lib/utils/stl_util.h @@ -141,6 +141,12 @@ constexpr bool holds_any_of(const std::variant& v) noexcept { return (std::holds_alternative(v) || ...); } +// This is a helper utility to emulate pattern matching with std::visit. +// See https://en.cppreference.com/w/cpp/utility/variant/visit for more info. +template struct overloaded : Ts... { using Ts::operator()...; }; +// explicit deduction guide (not needed as of C++20) +template overloaded(Ts...) -> overloaded; + } #endif diff --git a/src/tests/data/tls_13_rfc8448/transcripts.vec b/src/tests/data/tls_13_rfc8448/transcripts.vec index b2337431aa9..f303b00b875 100644 --- a/src/tests/data/tls_13_rfc8448/transcripts.vec +++ b/src/tests/data/tls_13_rfc8448/transcripts.vec @@ -6,6 +6,7 @@ ServerHello = 160303005a020000560303a6af06a4121860dc5e6e60249cd34c95930c8ac5cb14 ServerHandshakeMessages = 17030302a2d1ff334a56f5bff6594a07cc87b580233f500f45e489e7f33af35edf7869fcf40aa40aa2b8ea73f848a7ca07612ef9f945cb960b4068905123ea78b111b429ba9191cd05d2a389280f526134aadc7fc78c4b729df828b5ecf7b13bd9aefb0e57f271585b8ea9bb355c7c79020716cfb9b1183ef3ab20e37d57a6b9d7477609aee6e122a4cf51427325250c7d0e509289444c9b3a648f1d71035d2ed65b0e3cdd0cbae8bf2d0b227812cbb360987255cc744110c453baa4fcd610928d809810e4b7ed1a8fd991f06aa6248204797e36a6a73b70a2559c09ead686945ba246ab66e5edd8044b4c6de3fcf2a89441ac66272fd8fb330ef8190579b3684596c960bd596eea520a56a8d650f563aad27409960dca63d3e688611ea5e22f4415cf9538d51a200c27034272968a264ed6540c84838d89f72c24461aad6d26f59ecaba9acbbb317b66d902f4f292a36ac1b639c637ce343117b659622245317b49eeda0c6258f100d7d961ffb138647e92ea330faeea6dfa31c7a84dc3bd7e1b7a6c7178af36879018e3f252107f243d243dc7339d5684c8b0378bf30244da8c87c843f5e56eb4c5e8280a2b48052cf93b16499a66db7cca71e4599426f7d461e66f99882bd89fc50800becca62d6c74116dbd2972fda1fa80f85df881edbe5a37668936b335583b599186dc5c6918a396fa48a181d6b6fa4f9d62d513afbb992f2b992f67f8afe67f76913fa388cb5630c8ca01e0c65d11c66a1e2ac4c85977b7c7a6999bbf10dc35ae69f5515614636c0b9b68c19ed2e31c0b3b66763038ebba42f3b38edc0399f3a9f23faa63978c317fc9fa66a73f60f0504de93b5b845e275592c12335ee340bbc4fddd502784016e4b3be7ef04dda49f4b440a30cb5d2af939828fd4ae3794e44f94df5a631ede42c1719bfdabf0253fe5175be898e750edc53370d2b ClientFinished = 170303003575ec4dc238cce60b298044a71e219c56cc77b0517fe9b93c7a4bfc44d87f38f80338ac98fc46deb384bd1caeacab6867d726c40546 NewSessionTicket = 17030300de3a6b8f90414a97d6959c3487680de5134a2b240e6cffac116e95d41d6af8f6b580dcf3d11d63c758db289a015940252f55713e061dc13e078891a38efbcf5753ad8ef170ad3c7353d16d9da773b9ca7f2b9fa1b6c0d4a3d03f75e09c30ba1e62972ac46f75f7b981be63439b2999ce13064615139891d5e4c5b406f16e3fc181a77ca475840025db2f0a77f81b5ab05b94c01346755f69232c86519d86cbeeac87aac347d143f9605d64f650db4d023e70e952ca49fe5137121c74bc2697687e248746d6df353005f3bce18696129c8153556b3b6c6779b37bf15985684f +SessionTicket = 308202d5020401348a5902046274df7202010302010404000481b22c035d829359ee5ff7af4ec900000000262a6494dc486d2c8a34cb33fa90bf1b0070ad3c498883c9367c09a2be785abc55cd226097a3a982117283f82a03a143efd3ff5dd36d64e861be7fd61d2827db279cce145077d454a3664d4e6da4d29ee03725a6a4dafcd0fc67d2aea70529513e3da2677fa5906c5b3f7d8f92f228bda40dda721470f9fbf297b5aea617646fac5c03272e970727c621a79141ef5f7de6505e5bfbc388e93343694093934ae4d357020213010201000201010201000101ff01010004204ecd0eb6ec3b4d87f5d6028f922ca4c5851a277fd41311c9e62d2c9492e1c4f3048201b0308201ac30820115a003020102020102300d06092a864886f70d01010b0500300e310c300a06035504031303727361301e170d3136303733303031323335395a170d3236303733303031323335395a300e310c300a0603550403130372736130819f300d06092a864886f70d010101050003818d0030818902818100b4bb498f8279303d980836399b36c6988c0c68de55e1bdb826d3901a2461eafd2de49a91d015abbc9a95137ace6c1af19eaa6af98c7ced43120998e187a80ee0ccb0524b1b018c3e0b63264d449a6d38e22a5fda430846748030530ef0461c8ca9d9efbfae8ea6d1d03e2bd193eff0ab9a8002c47428a6d35a8d88d79f7f1e3f0203010001a31a301830090603551d1304023000300b0603551d0f0404030205a0300d06092a864886f70d01010b05000381810085aad2a0e5b9276b908c65f73a7267170618a54c5f8a7b337d2df7a594365417f2eae8f8a58c8f8172f9319cf36b7fd6c55b80f21a03015156726096fd335e5e67f2dbf102702e608ccae6bec1fc63a42a99be5c3eb7107c3c54e9b9eb2bd5203b1c3b84e0a8b2f759409ba3eac9d91d402dcc0cc8f8961229ac9187b42b4de10c067365727665720c000201000c000201000101ff02020400020500fad6aac502011e Client_AppData = 000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f3031 Client_AppData_Record = 1703030043a23f7054b62c94d0affafe8228ba55cbefacea42f914aa66bcab3f2b9819a8a5b46b395bd54a9a20441e2b62974e1f5a6292a2977014bd1e3deae63aeebb21694915e4 Server_AppData = 000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f3031 @@ -13,6 +14,26 @@ Server_AppData_Record = 17030300432e937e11ef4ac740e538ad36005fc4a46932fc3225d05f Client_CloseNotify = 1703030013c9872760655666b74d7ff1153efd6db6d0b0e3 Server_CloseNotify = 1703030013b58fd67166ebf599d24720cfbe7efa7a8864a9 +[Resumed_0RTT_Handshake] +RNG_Pool = 1bc3ceb6bbe39cff938355b5a50adb6db21b7a6af649d7b4bc419d7876487d95bff91188283846dd6a2134ef7180ca2b0b14fb10dce707b5098c0dddc813b2df +CurrentTimestamp = 1651826546006 +SessionTicket = 308202d5020401348a5902046274df7202010302010404000481b22c035d829359ee5ff7af4ec900000000262a6494dc486d2c8a34cb33fa90bf1b0070ad3c498883c9367c09a2be785abc55cd226097a3a982117283f82a03a143efd3ff5dd36d64e861be7fd61d2827db279cce145077d454a3664d4e6da4d29ee03725a6a4dafcd0fc67d2aea70529513e3da2677fa5906c5b3f7d8f92f228bda40dda721470f9fbf297b5aea617646fac5c03272e970727c621a79141ef5f7de6505e5bfbc388e93343694093934ae4d357020213010201000201010201000101ff01010004204ecd0eb6ec3b4d87f5d6028f922ca4c5851a277fd41311c9e62d2c9492e1c4f3048201b0308201ac30820115a003020102020102300d06092a864886f70d01010b0500300e310c300a06035504031303727361301e170d3136303733303031323335395a170d3236303733303031323335395a300e310c300a0603550403130372736130819f300d06092a864886f70d010101050003818d0030818902818100b4bb498f8279303d980836399b36c6988c0c68de55e1bdb826d3901a2461eafd2de49a91d015abbc9a95137ace6c1af19eaa6af98c7ced43120998e187a80ee0ccb0524b1b018c3e0b63264d449a6d38e22a5fda430846748030530ef0461c8ca9d9efbfae8ea6d1d03e2bd193eff0ab9a8002c47428a6d35a8d88d79f7f1e3f0203010001a31a301830090603551d1304023000300b0603551d0f0404030205a0300d06092a864886f70d01010b05000381810085aad2a0e5b9276b908c65f73a7267170618a54c5f8a7b337d2df7a594365417f2eae8f8a58c8f8172f9319cf36b7fd6c55b80f21a03015156726096fd335e5e67f2dbf102702e608ccae6bec1fc63a42a99be5c3eb7107c3c54e9b9eb2bd5203b1c3b84e0a8b2f759409ba3eac9d91d402dcc0cc8f8961229ac9187b42b4de10c067365727665720c000201000c000201000101ff02020400020500fad6aac502011e +ClientHello_1 = 1603010200010001fc03031bc3ceb6bbe39cff938355b5a50adb6db21b7a6af649d7b4bc419d7876487d95000006130113031302010001cd0000000b0009000006736572766572ff01000100000a00140012001d00170018001901000101010201030104003300260024001d0020e4ffb68ac05f8d96c99da26698346c6be16482badddafe051a66b4f18d668f0b002a0000002b0003020304000d0020001e040305030603020308040805080604010501060102010402050206020202002d00020101001c0002400100150057000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002900dd00b800b22c035d829359ee5ff7af4ec900000000262a6494dc486d2c8a34cb33fa90bf1b0070ad3c498883c9367c09a2be785abc55cd226097a3a982117283f82a03a143efd3ff5dd36d64e861be7fd61d2827db279cce145077d454a3664d4e6da4d29ee03725a6a4dafcd0fc67d2aea70529513e3da2677fa5906c5b3f7d8f92f228bda40dda721470f9fbf297b5aea617646fac5c03272e970727c621a79141ef5f7de6505e5bfbc388e93343694093934ae4d357fad6aacb0021203add4fb2d8fdf822a0ca3cf7678ef5e88dae990141c5924d57bb6fa31b9e5f9d +Client_EarlyAppData = 414243444546 +Client_EarlyAppData_Record = 1703030017ab1df420e75c457a7cc5d2844f76d5aee4b4edbf049be0 +ServerHello = 16030300600200005c03033ccfd2dec890222763472ae8136777c9d7358777bb66e91ea5122495f559ea2d00130100003400290002000000330024001d0020121761ee42c333e1b9e77b60dd57c2053cd94512ab47f115e86eff50942cea31002b00020304 +ServerHandshakeMessages = 1703030061dc48237b4b879f50d0d4d262ea8b4716eb40ddc1eb957e11126e8a7149c2d012d37a7115957e64ce30008b9e0323f2c05a9c1c77b4f37849a695ab255060a33fee770ca95cb8486bfd0843b87024865ca35cc41c4e515c64dcb1369f98635bc7a5 +# ClientFinished contains two records: +# * end of early data +# * client finished +ClientFinished = 1703030015aca6fc944841298df99593725f9bf9754429b12f09170303003500f8b467d14cf22a4b3f0b6ae0d8e6cc8d08e0db3515ef5c2bdf1922eafbb70009964716d834fb70c3d2a56c5b1f5f6bdba6c333cf +Client_AppData = 000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f3031 +Client_AppData_Record = 1703030043b1cebce242aa201be9ae5e1cb2a9aa4b33d4e866af1edb068919237741aa031d7a74d491c99b9d4e232b74206bc6fbaa04fe78be44a9b4f54320a17eb76992afac3103 +Server_AppData = 000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f3031 +Server_AppData_Record = 1703030043275e9f20acff57bc000657d3867df039cccf79047884cf75771746f740b5a83f462a0954c3581393a203a25a7dd14141ef1a37900cdb62ff62dee1ba39ab2590cbf194 +Client_CloseNotify = 17030300130facce3246bdfc6369838d6a82ae6de5d422dc +Server_CloseNotify = 17030300135b18af444e8e1eec7158fb62d8f2577d37ba5d + [HelloRetryRequest_Handshake] RNG_Pool = b0b1c5a5aa37c5919f2ed1d5c6fff7fcb7849716945a2b8cee9258a346677b6f0ed02f8e8117efc75ca7ac32aa7e34eda64cdc0ddad154a5e85289f959f63204ab5473467e19346ceb0a0414e41da21d4d2445bc3025afe97c4e8dc8d513da39 CurrentTimestamp = 1651826546000 diff --git a/src/tests/test_tls_cipher_state.cpp b/src/tests/test_tls_cipher_state.cpp index bac94b9fadd..891749d2b00 100644 --- a/src/tests/test_tls_cipher_state.cpp +++ b/src/tests/test_tls_cipher_state.cpp @@ -260,6 +260,15 @@ std::vector test_secret_derivation_rfc8448_rtt1() return { + Botan_Tests::CHECK("ciphersuite compatibility", [&](Test::Result& result) + { + result.confirm("self-compatibility", cs->is_compatible_with(cipher)); + result.confirm("fully defined state is not compatible to other suites", + !cs->is_compatible_with(Ciphersuite::from_name("CHACHA20_POLY1305_SHA256").value()) && + !cs->is_compatible_with(Ciphersuite::from_name("AES_128_CCM_SHA256").value()) && + !cs->is_compatible_with(Ciphersuite::from_name("PSK_WITH_AES_128_GCM_SHA256").value())); + }), + Botan_Tests::CHECK("handshake traffic without PSK (client side)", [&](Test::Result& result) { result.confirm("can not yet write application data", !cs->can_encrypt_application_traffic()); @@ -338,10 +347,241 @@ std::vector test_secret_derivation_rfc8448_rtt1() }; } +std::vector test_secret_derivation_rfc8448_rtt0() + { + // this is the PSK that was negotiated for session resumption in RFC 8448 (see test case above) + const auto psk = Botan::hex_decode_locked( + "4e cd 0e b6 ec 3b 4d 87 f5 d6 02 8f 92 2c a4 c5" + "85 1a 27 7f d4 13 11 c9 e6 2d 2c 94 92 e1 c4 f3"); + + // this shared secret is obtained by a key exchange performed additionally to + // the pre-shared-key. + const auto shared_secret = Botan::hex_decode_locked( + "f4 41 94 75 6f f9 ec 9d 25 18 06 35 d6 6e a6 82" + "4c 6a b3 bf 17 99 77 be 37 f7 23 57 0e 7c cb 2e"); + + // transcript hash of the client hello up to (including) the PSK modes + // (used to calculate the PSK binder) + const auto th_client_hello_prefix = Botan::hex_decode( + "63 22 4b 2e 45 73 f2 d3 45 4c a8 4b 9d 00 9a 04" + "f6 be 9e 05 71 1a 83 96 47 3a ef a0 1e 92 4a 14"); + + // transcript hash of the client hello including the PSK identity and binders + const auto th_client_hello = Botan::hex_decode( + "08 ad 0f a0 5d 7c 72 33 b1 77 5b a2 ff 9f 4c 5b" + "8b 59 27 6b 7f 22 7f 13 a9 76 24 5f 5d 96 09 13"); + + const auto th_server_hello = Botan::hex_decode( + "f7 36 cb 34 fe 25 e7 01 55 1b ee 6f d2 4c 1c c7" + "10 2a 7d af 94 05 cb 15 d9 7a af e1 6f 75 7d 03"); + + // this is not directly exposed in RFC 8448 but calculated as + // SHA-256(ClientHello..EncryptedExtensions) + const auto th_pre_server_finished = Botan::hex_decode( + "04 05 54 55 ef 74 b2 32 2b a3 66 cb c4 cf e0 27" + "23 43 4b 37 b9 b3 67 1b b5 a3 00 60 56 d8 f0 2f"); + + const auto th_server_finished = Botan::hex_decode( + "b0 ae ff c4 6a 2c fe 33 11 4e 6f d7 d5 1f 9f 04" + "b1 ca 3c 49 7d ab 08 93 4a 77 4a 9d 9a d7 db f3"); + + // this is not directly exposed in RFC 8448 but calculated as + // SHA-256(ClientHello..EndOfEarlyData) + const auto th_end_of_early_data = Botan::hex_decode( + "9f 23 a0 0c 1f 08 fc 18 80 7b 8b 68 23 7b 56 5d" + "f2 0d 4c 50 dd 8e 49 ef 61 2e 1a 5b b1 6c 58 67"); + + const auto th_client_finished = Botan::hex_decode( + "c3 c1 22 e0 bd 90 7a 4a 3f f6 11 2d 8f d5 3d bf" + "89 c7 73 d9 55 2e 8b 6b 9d 56 d3 61 b3 a9 7b f6"); + + const auto expected_psk_binder = Botan::hex_decode( + "3a dd 4f b2 d8 fd f8 22 a0 ca 3c f7 67 8e f5 e8" + "8d ae 99 01 41 c5 92 4d 57 bb 6f a3 1b 9e 5f 9d"); + + // this is not part of RFC 8448 + const std::string export_label = "export_test_label"; + const std::string export_context = "rfc8448_psk"; + const auto expected_key_export = Botan::hex_decode_locked( + "b5 89 bc b4 0b 7f 94 d7 6f 2d d6 fc f9 e5 87 8e"); + + // this is not part of RFC 8448 + const std::string early_export_label = "export_test_label_early"; + const std::string early_export_context = "rfc8448_psk_early"; + const auto early_expected_key_export = Botan::hex_decode_locked( + "b6 fb 7d 9e b5 4e 97 59 6f e4 ed 93 cc b0 bf 0c"); + + // encrypted with server_handshake_traffic_secret + const auto encrypted_extensions = RFC8448_TestData + ( + "encrypted_extensions", + Botan::hex_decode("17 03 03 00 61"), + Botan::hex_decode_locked( + "dc 48 23 7b 4b 87 9f" + "50 d0 d4 d2 62 ea 8b 47 16 eb 40 dd c1 eb 95 7e 11 12 6e 8a 71" + "49 c2 d0 12 d3 7a 71 15 95 7e 64 ce 30 00 8b 9e 03 23 f2 c0 5a" + "9c 1c 77 b4 f3 78 49 a6 95 ab 25 50 60 a3 3f ee 77 0c a9 5c b8" + "48 6b fd 08 43 b8 70 24 86 5c a3 5c c4 1c 4e 51 5c 64 dc b1 36" + "9f 98 63 5b c7 a5"), + Botan::hex_decode_locked( + "08 00 00 28 00 26 00 0a 00 14 00 12 00 1d 00" + "17 00 18 00 19 01 00 01 01 01 02 01 03 01 04 00 1c 00 02 40 01" + "00 00 00 00 00 2a 00 00 14 00 00 20 48 d3 e0 e1 b3 d9 07 c6 ac" + "ff 14 5e 16 09 03 88 c7 7b 05 c0 50 b6 34 ab 1a 88 bb d0 dd 1a" + "34 b2" + "16" /* to-be-encrypted content type */) + ); + + // encrypted with client_handshake_traffic_secret + const auto encrypted_client_finished_message = RFC8448_TestData + ( + "encrypted_client_finished_message", + Botan::hex_decode("17 03 03 00 35"), + Botan::hex_decode_locked("00 f8 b4 67 d1 4c f2" + "2a 4b 3f 0b 6a e0 d8 e6 cc 8d 08 e0 db 35 15 ef 5c 2b df 19 22" + "ea fb b7 00 09 96 47 16 d8 34 fb 70 c3 d2 a5 6c 5b 1f 5f 6b db" + "a6 c3 33 cf"), + Botan::hex_decode_locked("14 00 00 20 72 30 a9 c9 52 c2 5c d6 13 8f" + "c5 e6 62 83 08 c4 1c 53 35 dd 81 b9 f9 6b ce a5 0f d3 2b da 41 6d" + "16" /* to-be-encrypted content type */) + ); + + // encrypted with client_application_traffic_secret + const auto encrypted_application_data_client = RFC8448_TestData + ( + "encrypted_application_data_client", + Botan::hex_decode("17 03 03 00 43"), + Botan::hex_decode_locked("b1 ce bc e2 42 aa 20" + "1b e9 ae 5e 1c b2 a9 aa 4b 33 d4 e8 66 af 1e db 06 89 19 23 77" + "41 aa 03 1d 7a 74 d4 91 c9 9b 9d 4e 23 2b 74 20 6b c6 fb aa 04" + "fe 78 be 44 a9 b4 f5 43 20 a1 7e b7 69 92 af ac 31 03"), + Botan::hex_decode_locked( + "00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e" + "0f 10 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f 20 21 22 23" + "24 25 26 27 28 29 2a 2b 2c 2d 2e 2f 30 31" + "17" /* to-be-encrypted content type */) + ); + + // encrypted with server_application_traffic_secret + const auto encrypted_application_data_server = RFC8448_TestData + ( + "encrypted_application_data_server", + Botan::hex_decode("17 03 03 00 43"), + Botan::hex_decode_locked("27 5e 9f 20 ac ff 57" + "bc 00 06 57 d3 86 7d f0 39 cc cf 79 04 78 84 cf 75 77 17 46 f7" + "40 b5 a8 3f 46 2a 09 54 c3 58 13 93 a2 03 a2 5a 7d d1 41 41 ef" + "1a 37 90 0c db 62 ff 62 de e1 ba 39 ab 25 90 cb f1 94"), + Botan::hex_decode_locked( + "00 01 02 03 04 05 06 07 08 09 0a 0b 0c 0d 0e" + "0f 10 11 12 13 14 15 16 17 18 19 1a 1b 1c 1d 1e 1f 20 21 22 23" + "24 25 26 27 28 29 2a 2b 2c 2d 2e 2f 30 31" + "17" /* to-be-encrypted content type */) + ); + + auto cipher = Ciphersuite::from_name("AES_128_GCM_SHA256").value(); + + auto cs = Cipher_State::init_with_psk(Connection_Side::CLIENT, + Cipher_State::PSK_Type::RESUMPTION, + secure_vector(psk.begin(), psk.end()), + cipher); + + return + { + Botan_Tests::CHECK("calculating PSK binder (client side)", [&] (Test::Result& result) + { + const auto mac = cs->psk_binder_mac(th_client_hello_prefix); + result.test_eq("PSK binder is as expected", mac, expected_psk_binder); + }), + + Botan_Tests::CHECK("ciphersuite compatibility", [&](Test::Result& result) + { + result.confirm("self-compatibility", cs->is_compatible_with(cipher)); + result.confirm("partially defined state is compatible with suites using the same hash", + cs->is_compatible_with(Ciphersuite::from_name("CHACHA20_POLY1305_SHA256").value()) && + cs->is_compatible_with(Ciphersuite::from_name("AES_128_CCM_SHA256").value()) && + cs->is_compatible_with(Ciphersuite::from_name("AES_128_CCM_8_SHA256").value())); + + result.confirm("partially defined state is not compatible with other hashes or protocol versions", + !cs->is_compatible_with(Ciphersuite::from_name("PSK_WITH_AES_128_GCM_SHA256").value()) && + !cs->is_compatible_with(Ciphersuite::from_name("AES_256_GCM_SHA384").value())); + }), + + Botan_Tests::CHECK("calculate the early traffic secrets", [&] (Test::Result& result) + { + cs->advance_with_client_hello(th_client_hello); + result.require("early key export is possible", cs->can_export_keys()); + result.test_eq("early key export produces expected result", cs->export_key(early_export_label, early_export_context, 16), early_expected_key_export); + + // TODO: Once 0-RTT traffic is implemented this will likely allow encrypting + // application traffic in this state. + result.confirm("can not yet write application data", !cs->can_encrypt_application_traffic()); + }), + + Botan_Tests::CHECK("handshake traffic after PSK (client side)", [&](Test::Result& result) + { + cs->advance_with_server_hello(cipher, secure_vector(shared_secret), th_server_hello); + + // decrypt encrypted extensions from server + encrypted_extensions.decrypt(result, cs.get()); + + // TODO: Handling of early traffic is left out as 0-RTT is not implemented yet. + + // validate the MAC we receive in server Finished message + const auto expected_server_mac = Botan::hex_decode("48 d3 e0 e1 b3 d9 07 c6 ac ff 14 5e 16 09 03 88" + "c7 7b 05 c0 50 b6 34 ab 1a 88 bb d0 dd 1a 34 b2"); + result.confirm("expecting the correct MAC for server finished", cs->verify_peer_finished_mac(th_pre_server_finished, + expected_server_mac)); + + // generate the MAC for the client Finished message + const auto expected_client_mac = Botan::hex_decode("72 30 a9 c9 52 c2 5c d6 13 8f c5 e6 62 83 08 c4" + "1c 53 35 dd 81 b9 f9 6b ce a5 0f d3 2b da 41 6d"); + result.test_eq("generating the correct MAC for client finished", cs->finished_mac(th_end_of_early_data), expected_client_mac); + + // encrypt client Finished message by client + // (under the client handshake traffic secret) + encrypted_client_finished_message.encrypt(result, cs.get()); + }), + + Botan_Tests::CHECK("application traffic after PSK (client side)", [&](Test::Result& result) + { + // advance Cipher_State with client_hello...server_Finished + // (allows sending of application data) + result.test_no_throw("state advancement is legal", [&] + { + cs->advance_with_server_finished(th_server_finished); + }); + + result.confirm("can write application data", cs->can_encrypt_application_traffic()); + result.confirm("can export key material", cs->can_export_keys()); + result.test_eq("key export produces expected result", cs->export_key(export_label, export_context, 16), expected_key_export); + + // encrypt application data by client + encrypted_application_data_client.encrypt(result, cs.get()); + + // decrypt application data from server + // (encrypted under the application traffic secret -- and a new sequence number) + encrypted_application_data_server.decrypt(result, cs.get()); + }), + + Botan_Tests::CHECK("final state advancement", [&](Test::Result& result) + { + // advance Cipher_State with client_hello...client_Finished + // (allows generation of resumption PSKs) + result.test_no_throw("state advancement is legal", [&] + { + cs->advance_with_client_finished(th_client_finished); + }); + + result.confirm("can export key material still", cs->can_export_keys()); + result.test_eq("key export result did not change", cs->export_key(export_label, export_context, 16), expected_key_export); + }) + }; + } + } // namespace namespace Botan_Tests { -BOTAN_REGISTER_TEST_FN("tls", "tls_cipher_state", test_secret_derivation_rfc8448_rtt1); +BOTAN_REGISTER_TEST_FN("tls", "tls_cipher_state", test_secret_derivation_rfc8448_rtt1, test_secret_derivation_rfc8448_rtt0); } #endif diff --git a/src/tests/test_tls_messages.cpp b/src/tests/test_tls_messages.cpp index da8518ab981..17f569aaa26 100644 --- a/src/tests/test_tls_messages.cpp +++ b/src/tests/test_tls_messages.cpp @@ -460,6 +460,49 @@ class TLS_13_Message_Parsing_Test final : public Text_Based_Test Test::Result result("TLS 1.3 " + algo + " parsing"); + if(algo == "client_hello") + { + try + { + const std::string extensions = vars.get_req_str("AdditionalData"); + + // TODO: When implementing a TLS 1.3 server we will likely switch to + // a Client_Hello::parse() factory method that distinguishes + // TLS 1.2 and TLS 1.3 client hellos while parsing and returns + // a std::variant<> similarly to Server_Hello::parse(). + // + // For now we perform a hard-coded distinction. + if(msg_type == "client_hello_13") + { + Botan::TLS::Client_Hello_13 ch(buffer); + + std::vector exts_buffer; + for(Botan::TLS::Handshake_Extension_Type const& type : ch.extensions().extension_types()) + { + uint16_t u16type = static_cast(type); + exts_buffer.push_back(Botan::get_byte<0>(u16type)); + exts_buffer.push_back(Botan::get_byte<1>(u16type)); + } + result.test_eq("Hello extensions", Botan::hex_encode(exts_buffer), extensions); + + std::vector ciphersuites_buffer; + for(const auto& cs : ch.ciphersuites()) + { + ciphersuites_buffer.push_back(Botan::get_byte<0>(cs)); + ciphersuites_buffer.push_back(Botan::get_byte<1>(cs)); + } + result.test_eq("Supported ciphersuites", ciphersuites_buffer, ciphersuite); + } + + result.confirm("this is a positive test that should not have failed yet", is_positive_test); + } + catch (const std::exception &ex) + { + result.test_eq("correct error produced", ex.what(), exception); + result.confirm("negative test", !is_positive_test); + } + } + if(algo == "server_hello") { const std::string extensions = vars.get_req_str("AdditionalData"); @@ -498,7 +541,7 @@ class TLS_13_Message_Parsing_Test final : public Text_Based_Test catch(const std::exception &ex) { result.test_eq("correct error produced", ex.what(), exception); - result.require("negative test", !is_positive_test); + result.confirm("negative test", !is_positive_test); } } diff --git a/src/tests/test_tls_rfc8448.cpp b/src/tests/test_tls_rfc8448.cpp index 6d91a71989c..9cc60cc3b49 100644 --- a/src/tests/test_tls_rfc8448.cpp +++ b/src/tests/test_tls_rfc8448.cpp @@ -27,6 +27,7 @@ #include // TODO: replace me, otherwise we depend on auto_rng module #include #include + #include #include #include #include @@ -188,6 +189,12 @@ class Test_TLS_13_Callbacks : public Botan::TLS::Callbacks session_activated_called = true; } + bool tls_session_ticket_received(const Session&) override + { + count_callback_invocation("tls_session_ticket_received"); + return true; // should always store the session + } + void tls_verify_cert_chain( const std::vector& cert_chain, const std::vector>&, @@ -411,6 +418,92 @@ class RFC8448_Text_Policy : public Botan::TLS::Text_Policy } }; +/** + * In-Memory Session Manager that stores sessions verbatim, without encryption. + * Therefor it is not dependent on a random number generator and can easily be + * instrumented for test inspection. + */ +class RFC8448_Session_Manager : public Botan::TLS::Session_Manager + { + private: + template + bool load(const K& key, const M& map, Session& session) + { + const auto session_itr = map.find(key); + if(session_itr == map.end()) + return false; + + const auto& der = session_itr->second; + session = Session(der.data(), der.size()); + return true; + } + + public: + std::vector> all_sessions() const + { + std::vector> sessions; + std::transform(m_sessions_by_si.cbegin(), m_sessions_by_si.cend(), std::back_inserter(sessions), + [](const auto& session) { return session.second; }); + return sessions; + } + + bool load_from_session_id(const std::vector& session_id, + Session& session) override + { + return load(id(session_id), m_sessions_by_sid, session); + } + + bool load_from_server_info(const Server_Information& info, + Session& session) override + { + return load(info, m_sessions_by_si, session); + } + + void remove_entry(const std::vector& session_id) override + { + Session s; + const auto session_found = load_from_session_id(session_id, s); + if(!session_found) + return; + + const auto sid = id(session_id); + m_sessions_by_sid.erase(sid); + m_sessions_by_si.erase(s.server_info()); + } + + size_t remove_all() override + { + const auto sessions = m_sessions_by_sid.size(); + m_sessions_by_sid.clear(); + m_sessions_by_si.clear(); + return sessions; + } + + void save(const Session& session) override + { + const auto sid = id(session.session_id()); + const auto der = unlock(session.DER_encode()); + m_sessions_by_sid[sid] = der; + m_sessions_by_si[session.server_info()] = der; + } + + std::chrono::seconds session_lifetime() const override + { + return std::chrono::seconds(42); + } + + private: + std::string id(const std::vector& session_id) const + { + auto h = HashFunction::create_or_throw("SHA-256"); + return Botan::hex_encode(h->process(session_id)); + } + + private: + std::map> m_sessions_by_sid; + std::map> m_sessions_by_si; + }; + /** * This steers the TLS client handle and is the central entry point for the * test cases to interact with the TLS 1.3 implementation. @@ -424,12 +517,17 @@ class TLS_Context RFC8448_Text_Policy policy, Modify_Exts_Fn modify_exts_cb, MockSignature_Fn mock_signature_cb, - uint64_t timestamp) + uint64_t timestamp, + std::optional> session) : m_callbacks(std::move(modify_exts_cb), std::move(mock_signature_cb), timestamp) , m_rng(std::move(rng_in)) - , m_session_mgr(*m_rng) , m_policy(std::move(policy)) - {} + { + if(session.has_value()) + { + m_session_mgr.save(Session(session->data(), session->size())); + } + } public: virtual ~TLS_Context() = default; @@ -477,6 +575,11 @@ class TLS_Context m_callbacks.reset_callback_invocation_counters(); } + std::vector> stored_sessions() const + { + return m_session_mgr.all_sessions(); + } + const std::vector& certs_verified() const { return m_callbacks.certificate_chain; @@ -489,7 +592,7 @@ class TLS_Context Test_Client_Credentials m_creds; std::unique_ptr m_rng; - Botan::TLS::Session_Manager_In_Memory m_session_mgr; + RFC8448_Session_Manager m_session_mgr; RFC8448_Text_Policy m_policy; }; @@ -500,8 +603,9 @@ class Client_Context : public TLS_Context RFC8448_Text_Policy policy, uint64_t timestamp, Modify_Exts_Fn modify_exts_cb, + std::optional> session = std::nullopt, MockSignature_Fn mock_signature_cb = [](auto,auto,auto) { return std::vector(); }) - : TLS_Context(std::move(rng_in), std::move(policy), std::move(modify_exts_cb), std::move(mock_signature_cb), timestamp) + : TLS_Context(std::move(rng_in), std::move(policy), std::move(modify_exts_cb), std::move(mock_signature_cb), timestamp, std::move(session)) , client(m_callbacks, m_session_mgr, m_creds, m_policy, *m_rng, Botan::TLS::Server_Information("server"), Botan::TLS::Protocol_Version::TLS_V13) @@ -530,6 +634,7 @@ void sort_extensions(Botan::TLS::Extensions& exts, Botan::TLS::Connection_Side s Botan::TLS::Handshake_Extension_Type::TLSEXT_SUPPORTED_GROUPS, Botan::TLS::Handshake_Extension_Type::TLSEXT_SESSION_TICKET, Botan::TLS::Handshake_Extension_Type::TLSEXT_KEY_SHARE, + Botan::TLS::Handshake_Extension_Type::TLSEXT_EARLY_DATA, Botan::TLS::Handshake_Extension_Type::TLSEXT_SUPPORTED_VERSIONS, Botan::TLS::Handshake_Extension_Type::TLSEXT_SIGNATURE_ALGORITHMS, Botan::TLS::Handshake_Extension_Type::TLSEXT_COOKIE, @@ -549,15 +654,6 @@ void sort_extensions(Botan::TLS::Extensions& exts, Botan::TLS::Connection_Side s } } -void add_psk_exchange_modes(Botan::TLS::Extensions& exts) - { - // Currently we do not support PSK and session resumption in TLS 1.3. - // Hence, we add this extension to please the test vector. The actual - // resumption is not exercised in this test, though. Once PSK is - // implemented, this should be removed and added in Client_Hello_13. - exts.add(new PSK_Key_Exchange_Modes({PSK_Key_Exchange_Mode::PSK_DHE_KE})); - } - void add_renegotiation_extension(Botan::TLS::Extensions& exts) { // Renegotiation is not possible in TLS 1.3. Nevertheless, RFC 8448 requires @@ -565,6 +661,11 @@ void add_renegotiation_extension(Botan::TLS::Extensions& exts) exts.add(new Renegotiation_Extension()); } +void add_early_data_indication(Botan::TLS::Extensions& exts) + { + exts.add(new Botan::TLS::EarlyDataIndication()); + } + } // namespace /** @@ -594,13 +695,15 @@ class Test_TLS_RFC8448 final : public Text_Based_Test { public: Test_TLS_RFC8448() - : Text_Based_Test("tls_13_rfc8448/transcripts.vec", "RNG_Pool,CurrentTimestamp,ClientHello_1,ServerHello,ServerHandshakeMessages,ClientFinished,Client_CloseNotify,Server_CloseNotify", "HelloRetryRequest,ClientHello_2,NewSessionTicket,Client_AppData,Client_AppData_Record,Server_AppData,Server_AppData_Record,MessageToSign,MessageSignature") {} + : Text_Based_Test("tls_13_rfc8448/transcripts.vec", "RNG_Pool,CurrentTimestamp,ClientHello_1,ServerHello,ServerHandshakeMessages,ClientFinished,Client_CloseNotify,Server_CloseNotify", "HelloRetryRequest,ClientHello_2,NewSessionTicket,Client_AppData,Client_AppData_Record,Server_AppData,Server_AppData_Record,Client_EarlyAppData,Client_EarlyAppData_Record,SessionTicket,MessageToSign,MessageSignature") {} Test::Result run_one_test(const std::string& header, const VarMap& vars) override { if(header == "Simple_1RTT_Handshake") return Test::Result("Simple 1-RTT (Client side)", simple_1_rtt_client_hello(vars)); + else if(header == "Resumed_0RTT_Handshake") + return Test::Result("Resumption with 0-RTT data", resumed_handshake_with_0_rtt(vars)); else if(header == "HelloRetryRequest_Handshake") return Test::Result("Handshake involving Hello Retry Request (Client side)", hello_retry_request(vars)); else if(header == "Client_Authentication_Handshake") @@ -615,7 +718,6 @@ class Test_TLS_RFC8448 final : public Text_Based_Test static std::vector simple_1_rtt_client_hello(const VarMap& vars) { auto rng = std::make_unique(""); - rng->add_entropy(std::vector(32).data(), 32); // used by session mgr for session key // 32 - for client hello random // 32 - for KeyShare (eph. x25519 key pair) @@ -628,7 +730,6 @@ class Test_TLS_RFC8448 final : public Text_Based_Test // this obsoleted extension in a TLS 1.3 client. exts.add(new Botan::TLS::Session_Ticket()); - add_psk_exchange_modes(exts); add_renegotiation_extension(exts); sort_extensions(exts, side); }; @@ -648,6 +749,7 @@ class Test_TLS_RFC8448 final : public Text_Based_Test Botan_Tests::CHECK("Server Hello", [&](Test::Result& result) { + result.require("ctx is available", ctx != nullptr); const auto server_hello = vars.get_req_bin("ServerHello"); // splitting the input data to test partial reads const std::vector server_hello_a(server_hello.begin(), server_hello.begin() + 20); @@ -664,6 +766,7 @@ class Test_TLS_RFC8448 final : public Text_Based_Test Botan_Tests::CHECK("Server HS messages .. Client Finished", [&](Test::Result& result) { + result.require("ctx is available", ctx != nullptr); ctx->client.received_data(vars.get_req_bin("ServerHandshakeMessages")); ctx->check_callback_invocations(result, "encrypted handshake messages received", @@ -687,14 +790,24 @@ class Test_TLS_RFC8448 final : public Text_Based_Test Botan_Tests::CHECK("Post-Handshake: NewSessionTicket", [&](Test::Result& result) { + result.require("ctx is available", ctx != nullptr); + result.require("no sessions so far", ctx->stored_sessions().empty()); ctx->client.received_data(vars.get_req_bin("NewSessionTicket")); - // TODO: once we implement session resumption, this should probably expect some callback - ctx->check_callback_invocations(result, "new session ticket received", { }); + ctx->check_callback_invocations(result, "new session ticket received", + { + "tls_session_ticket_received", + "tls_current_timestamp" + }); + if(result.test_eq("session was stored", ctx->stored_sessions().size(), 1)) + { + result.test_eq("session was serialized as expected", ctx->stored_sessions().front(), vars.get_req_bin("SessionTicket")); + } }), Botan_Tests::CHECK("Send Application Data", [&](Test::Result& result) { + result.require("ctx is available", ctx != nullptr); ctx->send(vars.get_req_bin("Client_AppData")); ctx->check_callback_invocations(result, "application data sent", { "tls_emit_data" }); @@ -705,6 +818,7 @@ class Test_TLS_RFC8448 final : public Text_Based_Test Botan_Tests::CHECK("Receive Application Data", [&](Test::Result& result) { + result.require("ctx is available", ctx != nullptr); ctx->client.received_data(vars.get_req_bin("Server_AppData_Record")); ctx->check_callback_invocations(result, "application data sent", { "tls_record_received" }); @@ -716,6 +830,7 @@ class Test_TLS_RFC8448 final : public Text_Based_Test Botan_Tests::CHECK("Close Connection", [&](Test::Result& result) { + result.require("ctx is available", ctx != nullptr); ctx->client.close(); result.test_eq("close payload", ctx->pull_send_buffer(), vars.get_req_bin("Client_CloseNotify")); @@ -729,6 +844,51 @@ class Test_TLS_RFC8448 final : public Text_Based_Test }; } + static std::vector resumed_handshake_with_0_rtt(const VarMap& vars) + { + auto rng = std::make_unique(""); + + // 32 - for client hello random + // 32 - for KeyShare (eph. x25519 key pair) + add_entropy(*rng, vars.get_req_bin("RNG_Pool")); + + auto add_extensions_and_sort = [](Botan::TLS::Extensions& exts, Botan::TLS::Connection_Side side) + { + exts.add(new Padding(87)); + + add_renegotiation_extension(exts); + + // TODO: Implement early data support and remove this 'hack'. + // + // Currently, the production implementation will never add this + // extension even if the resumed session would allow early data. + add_early_data_indication(exts); + sort_extensions(exts, side); + }; + + std::unique_ptr ctx; + + return { + Botan_Tests::CHECK("Client Hello", [&](Test::Result& result) + { + ctx = std::make_unique(std::move(rng), + read_tls_policy("rfc8448_1rtt"), + vars.get_req_u64("CurrentTimestamp"), + add_extensions_and_sort, + vars.get_req_bin("SessionTicket")); + + result.confirm("client not closed", !ctx->client.is_closed()); + ctx->check_callback_invocations(result, "client hello prepared", { "tls_emit_data", "tls_inspect_handshake_msg_client_hello", "tls_modify_extensions", "tls_current_timestamp" }); + + result.test_eq("TLS client hello", ctx->pull_send_buffer(), vars.get_req_bin("ClientHello_1")); + }) + + // TODO: The rest of this test vector requires 0-RTT which is not + // yet implemented. For now we can only test the client's + // ability to offer a session resumption via PSK. + }; + } + static std::vector hello_retry_request(const VarMap& vars) { auto add_extensions_and_sort = [flights = 0](Botan::TLS::Extensions& exts, Botan::TLS::Connection_Side side) mutable @@ -737,7 +897,6 @@ class Test_TLS_RFC8448 final : public Text_Based_Test if(flights == 1) { - add_psk_exchange_modes(exts); add_renegotiation_extension(exts); } @@ -755,8 +914,6 @@ class Test_TLS_RFC8448 final : public Text_Based_Test auto& fallback_rng = Test::rng(); auto rng = std::make_unique(fallback_rng); - rng->add_entropy(std::vector(32).data(), 32); // used by session mgr for session key - // 32 - client hello random // 32 - eph. x25519 key pair // 32 - eph. P-256 key pair @@ -777,6 +934,7 @@ class Test_TLS_RFC8448 final : public Text_Based_Test Botan_Tests::CHECK("Hello Retry Request .. second Client Hello", [&](Test::Result& result) { + result.require("ctx is available", ctx != nullptr); ctx->client.received_data(vars.get_req_bin("HelloRetryRequest")); ctx->check_callback_invocations(result, "hello retry request received", @@ -794,6 +952,7 @@ class Test_TLS_RFC8448 final : public Text_Based_Test Botan_Tests::CHECK("Server Hello", [&](Test::Result& result) { + result.require("ctx is available", ctx != nullptr); ctx->client.received_data(vars.get_req_bin("ServerHello")); ctx->check_callback_invocations(result, "server hello received", { "tls_inspect_handshake_msg_server_hello", "tls_examine_extensions", "tls_decode_group_param" }); @@ -801,6 +960,7 @@ class Test_TLS_RFC8448 final : public Text_Based_Test Botan_Tests::CHECK("Server HS Messages .. Client Finished", [&](Test::Result& result) { + result.require("ctx is available", ctx != nullptr); ctx->client.received_data(vars.get_req_bin("ServerHandshakeMessages")); ctx->check_callback_invocations(result, "encrypted handshake messages received", @@ -821,6 +981,7 @@ class Test_TLS_RFC8448 final : public Text_Based_Test Botan_Tests::CHECK("Close Connection", [&](Test::Result& result) { + result.require("ctx is available", ctx != nullptr); ctx->client.close(); ctx->check_callback_invocations(result, "encrypted handshake messages received", { "tls_emit_data" }); result.test_eq("client close notify", ctx->pull_send_buffer(), vars.get_req_bin("Client_CloseNotify")); @@ -836,7 +997,6 @@ class Test_TLS_RFC8448 final : public Text_Based_Test static std::vector client_authentication(const VarMap& vars) { auto rng = std::make_unique(""); - rng->add_entropy(std::vector(32).data(), 32); // used by session mgr for session key // 32 - for client hello random // 32 - for eph. x25519 key pair @@ -844,7 +1004,6 @@ class Test_TLS_RFC8448 final : public Text_Based_Test auto add_extensions_and_sort = [&](Botan::TLS::Extensions& exts, Botan::TLS::Connection_Side side) { - add_psk_exchange_modes(exts); add_renegotiation_extension(exts); sort_extensions(exts, side); }; @@ -876,7 +1035,7 @@ class Test_TLS_RFC8448 final : public Text_Based_Test return { Botan_Tests::CHECK("Client Hello", [&](Test::Result& result) { - ctx = std::make_unique(std::move(rng), read_tls_policy("rfc8448_1rtt"), vars.get_req_u64("CurrentTimestamp"), add_extensions_and_sort, sign_certificate_verify); + ctx = std::make_unique(std::move(rng), read_tls_policy("rfc8448_1rtt"), vars.get_req_u64("CurrentTimestamp"), add_extensions_and_sort, std::nullopt, sign_certificate_verify); ctx->check_callback_invocations(result, "initial callbacks", { "tls_emit_data", @@ -942,7 +1101,6 @@ class Test_TLS_RFC8448 final : public Text_Based_Test static std::vector middlebox_compatibility(const VarMap& vars) { auto rng = std::make_unique(""); - rng->add_entropy(std::vector(32).data(), 32); // used by session mgr for session key // 32 - client hello random // 32 - legacy session ID @@ -952,7 +1110,6 @@ class Test_TLS_RFC8448 final : public Text_Based_Test auto add_extensions_and_sort = [&](Botan::TLS::Extensions& exts, Botan::TLS::Connection_Side side) { add_renegotiation_extension(exts); - add_psk_exchange_modes(exts); sort_extensions(exts, side); }; @@ -968,6 +1125,7 @@ class Test_TLS_RFC8448 final : public Text_Based_Test Botan_Tests::CHECK("Server Hello + other handshake messages", [&](Test::Result& result) { + result.require("ctx is available", ctx != nullptr); ctx->client.received_data(Botan::concat(vars.get_req_bin("ServerHello"), // ServerHandshakeMessages contains the expected ChangeCipherSpec record vars.get_req_bin("ServerHandshakeMessages"))); @@ -981,6 +1139,7 @@ class Test_TLS_RFC8448 final : public Text_Based_Test Botan_Tests::CHECK("Close connection", [&](Test::Result& result) { + result.require("ctx is available", ctx != nullptr); ctx->client.close(); result.test_eq("Client close_notify", ctx->pull_send_buffer(), vars.get_req_bin("Client_CloseNotify")); diff --git a/src/tests/test_tls_transcript_hash_13.cpp b/src/tests/test_tls_transcript_hash_13.cpp index 39bd0cf4272..e142a060781 100644 --- a/src/tests/test_tls_transcript_hash_13.cpp +++ b/src/tests/test_tls_transcript_hash_13.cpp @@ -12,6 +12,7 @@ #include #include +#include using namespace Botan::TLS; @@ -25,6 +26,39 @@ std::vector transcript_hash() return Botan::unlock(Botan::HashFunction::create_or_throw("SHA-256")->process(Botan::hex_decode(str))); }; + // Client Hello taken from RFC 8448 0-RTT + const auto psk_client_hello = Botan::hex_decode( + "01 00 01 fc 03 03 1b c3 ce b6 bb e3 9c ff" + "93 83 55 b5 a5 0a db 6d b2 1b 7a 6a f6 49 d7 b4 bc 41 9d 78 76" + "48 7d 95 00 00 06 13 01 13 03 13 02 01 00 01 cd 00 00 00 0b 00" + "09 00 00 06 73 65 72 76 65 72 ff 01 00 01 00 00 0a 00 14 00 12" + "00 1d 00 17 00 18 00 19 01 00 01 01 01 02 01 03 01 04 00 33 00" + "26 00 24 00 1d 00 20 e4 ff b6 8a c0 5f 8d 96 c9 9d a2 66 98 34" + "6c 6b e1 64 82 ba dd da fe 05 1a 66 b4 f1 8d 66 8f 0b 00 2a 00" + "00 00 2b 00 03 02 03 04 00 0d 00 20 00 1e 04 03 05 03 06 03 02" + "03 08 04 08 05 08 06 04 01 05 01 06 01 02 01 04 02 05 02 06 02" + "02 02 00 2d 00 02 01 01 00 1c 00 02 40 01 00 15 00 57 00 00 00" + "00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00" + "00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00" + "00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00" + "00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00" + "00 29 00 dd 00 b8 00 b2 2c 03 5d 82 93 59 ee 5f f7 af 4e c9 00" + "00 00 00 26 2a 64 94 dc 48 6d 2c 8a 34 cb 33 fa 90 bf 1b 00 70" + "ad 3c 49 88 83 c9 36 7c 09 a2 be 78 5a bc 55 cd 22 60 97 a3 a9" + "82 11 72 83 f8 2a 03 a1 43 ef d3 ff 5d d3 6d 64 e8 61 be 7f d6" + "1d 28 27 db 27 9c ce 14 50 77 d4 54 a3 66 4d 4e 6d a4 d2 9e e0" + "37 25 a6 a4 da fc d0 fc 67 d2 ae a7 05 29 51 3e 3d a2 67 7f a5" + "90 6c 5b 3f 7d 8f 92 f2 28 bd a4 0d da 72 14 70 f9 fb f2 97 b5" + "ae a6 17 64 6f ac 5c 03 27 2e 97 07 27 c6 21 a7 91 41 ef 5f 7d" + "e6 50 5e 5b fb c3 88 e9 33 43 69 40 93 93 4a e4 d3 57 fa d6 aa" + "cb 00 21 20 3a dd 4f b2 d8 fd f8 22 a0 ca 3c f7 67 8e f5 e8 8d" + "ae 99 01 41 c5 92 4d 57 bb 6f a3 1b 9e 5f 9d"); + + const auto sha256_truncated_ch = Botan::hex_decode( + "63224b2e4573f2d3454ca84b9d009a04f6be9e05711a8396473aefa01e924a14"); + const auto sha256_full_ch = Botan::hex_decode( + "08ad0fa05d7c7233b1775ba2ff9f4c5b8b59276b7f227f13a976245f5d960913"); + return { Botan_Tests::CHECK("trying to get 'previous' or 'current' with invalid state", [](Test::Result& result) @@ -144,6 +178,25 @@ std::vector transcript_hash() const std::string transcript = "fe000020" + hash_of_client_hello + "c001f00d"; result.test_eq("transcript hash of hello retry request", h2.current(), sha256(transcript)); }), + + Botan_Tests::CHECK("truncated transcript hash in client hellos with PSK", [&](Test::Result& result) + { + Transcript_Hash_State h1; + + const auto truncation_mark = 477u; + auto truncated_ch = psk_client_hello; + truncated_ch.resize(truncation_mark); + + h1.update(psk_client_hello); + h1.set_algorithm("SHA-256"); + + result.test_eq("truncated hash", h1.truncated(), sha256_truncated_ch); + result.test_eq("current hash", h1.current(), sha256_full_ch); + + // truncated hash is cleared as soon as new messages are read + h1.update({0xc0, 0xca, 0xc0, 0x1a} /* server hello */); + result.test_throws("truncated hash is cleared", [&] { h1.truncated(); }); + }), }; } diff --git a/src/tests/tests.h b/src/tests/tests.h index 21634db174f..98a7a07ced3 100644 --- a/src/tests/tests.h +++ b/src/tests/tests.h @@ -282,7 +282,7 @@ class Test { if(!confirm(what, expr, expected)) { - throw Test_Aborted("test aborted, because required condition was not met"); + throw Test_Aborted("test aborted, because required condition was not met: " + what); } }