diff options
Diffstat (limited to 'src/ssl/ssl_test.cc')
-rw-r--r-- | src/ssl/ssl_test.cc | 407 |
1 files changed, 252 insertions, 155 deletions
diff --git a/src/ssl/ssl_test.cc b/src/ssl/ssl_test.cc index 03760b6e..2df005a2 100644 --- a/src/ssl/ssl_test.cc +++ b/src/ssl/ssl_test.cc @@ -4698,7 +4698,9 @@ static_assert(ssl_encryption_application < kNumQUICLevels, class MockQUICTransport { public: - MockQUICTransport() { + enum class Role { kClient, kServer }; + + explicit MockQUICTransport(Role role) : role_(role) { // The caller is expected to configure initial secrets. levels_[ssl_encryption_initial].write_secret = {1}; levels_[ssl_encryption_initial].read_secret = {1}; @@ -4735,18 +4737,38 @@ class MockQUICTransport { return false; } - if (level != ssl_encryption_early_data && - (read_secret == nullptr || write_secret == nullptr)) { - ADD_FAILURE() << "key was unexpectedly null"; - return false; + bool expect_read_secret = true, expect_write_secret = true; + if (level == ssl_encryption_early_data) { + if (role_ == Role::kClient) { + expect_read_secret = false; + } else { + expect_write_secret = false; + } } - if (read_secret != nullptr) { + + if (expect_read_secret) { + if (read_secret == nullptr) { + ADD_FAILURE() << "read secret was unexpectedly null"; + return false; + } levels_[level].read_secret.assign(read_secret, read_secret + secret_len); + } else if (read_secret != nullptr) { + ADD_FAILURE() << "unexpected read secret"; + return false; } - if (write_secret != nullptr) { + + if (expect_write_secret) { + if (write_secret == nullptr) { + ADD_FAILURE() << "write secret was unexpectedly null"; + return false; + } levels_[level].write_secret.assign(write_secret, write_secret + secret_len); + } else if (write_secret != nullptr) { + ADD_FAILURE() << "unexpected write secret"; + return false; } + levels_[level].cipher = SSL_CIPHER_get_id(cipher); return true; } @@ -4783,20 +4805,22 @@ class MockQUICTransport { ssl_encryption_level_t level, size_t num = std::numeric_limits<size_t>::max()) { if (levels_[level].read_secret.empty()) { - ADD_FAILURE() << "data read before keys configured"; + ADD_FAILURE() << "data read before keys configured in level " << level; return false; } // The peer may not have configured any keys yet. if (peer_->levels_[level].write_secret.empty()) { + out->clear(); return true; } // Check the peer computed the same key. if (peer_->levels_[level].write_secret != levels_[level].read_secret) { - ADD_FAILURE() << "peer write key does not match read key"; + ADD_FAILURE() << "peer write key does not match read key in level " + << level; return false; } if (peer_->levels_[level].cipher != levels_[level].cipher) { - ADD_FAILURE() << "peer cipher does not match"; + ADD_FAILURE() << "peer cipher does not match in level " << level; return false; } std::vector<uint8_t> *peer_data = &peer_->levels_[level].write_data; @@ -4807,6 +4831,7 @@ class MockQUICTransport { } private: + Role role_; MockQUICTransport *peer_ = nullptr; bool has_alert_ = false; @@ -4824,21 +4849,24 @@ class MockQUICTransport { class MockQUICTransportPair { public: - MockQUICTransportPair() { - server_.set_peer(&client_); + MockQUICTransportPair() + : client_(MockQUICTransport::Role::kClient), + server_(MockQUICTransport::Role::kServer) { client_.set_peer(&server_); + server_.set_peer(&client_); } ~MockQUICTransportPair() { - server_.set_peer(nullptr); client_.set_peer(nullptr); + server_.set_peer(nullptr); } MockQUICTransport *client() { return &client_; } MockQUICTransport *server() { return &server_; } bool SecretsMatch(ssl_encryption_level_t level) const { - return client_.PeerSecretsMatch(level); + return client_.HasSecrets(level) && server_.HasSecrets(level) && + client_.PeerSecretsMatch(level); } private: @@ -4890,24 +4918,80 @@ class QUICMethodTest : public testing::Test { SSL_set_connect_state(client_.get()); SSL_set_accept_state(server_.get()); - ex_data_.Set(client_.get(), transport_.client()); - ex_data_.Set(server_.get(), transport_.server()); + transport_.reset(new MockQUICTransportPair); + ex_data_.Set(client_.get(), transport_->client()); + ex_data_.Set(server_.get(), transport_->server()); return true; } - bool CreateSecondClientAndServer() { - client_.reset(SSL_new(client_ctx_.get())); - server_.reset(SSL_new(server_ctx_.get())); - if (!client_ || !server_) { - return false; + // CompleteHandshakesForQUIC runs |SSL_do_handshake| on |client_| and + // |server_| until each completes once. It returns true on success and false + // on failure. + bool CompleteHandshakesForQUIC() { + bool client_done = false, server_done = false; + while (!client_done || !server_done) { + if (!client_done) { + if (!ProvideHandshakeData(client_.get())) { + ADD_FAILURE() << "ProvideHandshakeData(client_) failed"; + return false; + } + int client_ret = SSL_do_handshake(client_.get()); + if (client_ret == 1) { + client_done = true; + } else { + EXPECT_EQ(client_ret, -1); + EXPECT_EQ(SSL_get_error(client_.get(), client_ret), + SSL_ERROR_WANT_READ); + } + } + + if (!server_done) { + if (!ProvideHandshakeData(server_.get())) { + ADD_FAILURE() << "ProvideHandshakeData(server_) failed"; + return false; + } + int server_ret = SSL_do_handshake(server_.get()); + if (server_ret == 1) { + server_done = true; + } else { + EXPECT_EQ(server_ret, -1); + EXPECT_EQ(SSL_get_error(server_.get(), server_ret), + SSL_ERROR_WANT_READ); + } + } } + return true; + } - SSL_set_connect_state(client_.get()); - SSL_set_accept_state(server_.get()); + bssl::UniquePtr<SSL_SESSION> CreateClientSessionForQUIC() { + g_last_session = nullptr; + SSL_CTX_sess_set_new_cb(client_ctx_.get(), SaveLastSession); + if (!CreateClientAndServer() || + !CompleteHandshakesForQUIC()) { + return nullptr; + } - ex_data_.Set(client_.get(), second_transport_.client()); - ex_data_.Set(server_.get(), second_transport_.server()); - return true; + // The server sent NewSessionTicket messages in the handshake. + if (!ProvideHandshakeData(client_.get()) || + !SSL_process_quic_post_handshake(client_.get())) { + return nullptr; + } + + return std::move(g_last_session); + } + + void ExpectHandshakeSuccess() { + EXPECT_TRUE(transport_->SecretsMatch(ssl_encryption_application)); + EXPECT_EQ(ssl_encryption_application, SSL_quic_read_level(client_.get())); + EXPECT_EQ(ssl_encryption_application, SSL_quic_write_level(client_.get())); + EXPECT_EQ(ssl_encryption_application, SSL_quic_read_level(server_.get())); + EXPECT_EQ(ssl_encryption_application, SSL_quic_write_level(server_.get())); + EXPECT_FALSE(transport_->client()->has_alert()); + EXPECT_FALSE(transport_->server()->has_alert()); + + // SSL_do_handshake is now idempotent. + EXPECT_EQ(SSL_do_handshake(client_.get()), 1); + EXPECT_EQ(SSL_do_handshake(server_.get()), 1); } // The following functions may be configured on an |SSL_QUIC_METHOD| as @@ -4942,8 +5026,7 @@ class QUICMethodTest : public testing::Test { bssl::UniquePtr<SSL_CTX> server_ctx_; static UnownedSSLExData<MockQUICTransport> ex_data_; - MockQUICTransportPair transport_; - MockQUICTransportPair second_transport_; + std::unique_ptr<MockQUICTransportPair> transport_; bssl::UniquePtr<SSL> client_; bssl::UniquePtr<SSL> server_; @@ -4966,33 +5049,13 @@ TEST_F(QUICMethodTest, Basic) { SSL_CTX_sess_set_new_cb(client_ctx_.get(), SaveLastSession); ASSERT_TRUE(SSL_CTX_set_quic_method(client_ctx_.get(), &quic_method)); ASSERT_TRUE(SSL_CTX_set_quic_method(server_ctx_.get(), &quic_method)); - ASSERT_TRUE(CreateClientAndServer()); - for (;;) { - ASSERT_TRUE(ProvideHandshakeData(client_.get())); - int client_ret = SSL_do_handshake(client_.get()); - if (client_ret != 1) { - ASSERT_EQ(client_ret, -1); - ASSERT_EQ(SSL_get_error(client_.get(), client_ret), SSL_ERROR_WANT_READ); - } - - ASSERT_TRUE(ProvideHandshakeData(server_.get())); - int server_ret = SSL_do_handshake(server_.get()); - if (server_ret != 1) { - ASSERT_EQ(server_ret, -1); - ASSERT_EQ(SSL_get_error(server_.get(), server_ret), SSL_ERROR_WANT_READ); - } - - if (client_ret == 1 && server_ret == 1) { - break; - } - } + ASSERT_TRUE(CreateClientAndServer()); + ASSERT_TRUE(CompleteHandshakesForQUIC()); - EXPECT_EQ(SSL_do_handshake(client_.get()), 1); - EXPECT_EQ(SSL_do_handshake(server_.get()), 1); - EXPECT_TRUE(transport_.SecretsMatch(ssl_encryption_application)); - EXPECT_FALSE(transport_.client()->has_alert()); - EXPECT_FALSE(transport_.server()->has_alert()); + ExpectHandshakeSuccess(); + EXPECT_FALSE(SSL_session_reused(client_.get())); + EXPECT_FALSE(SSL_session_reused(server_.get())); // The server sent NewSessionTicket messages in the handshake. EXPECT_FALSE(g_last_session); @@ -5001,35 +5064,13 @@ TEST_F(QUICMethodTest, Basic) { EXPECT_TRUE(g_last_session); // Create a second connection to verify resumption works. - ASSERT_TRUE(CreateSecondClientAndServer()); + ASSERT_TRUE(CreateClientAndServer()); bssl::UniquePtr<SSL_SESSION> session = std::move(g_last_session); SSL_set_session(client_.get(), session.get()); - for (;;) { - ASSERT_TRUE(ProvideHandshakeData(client_.get())); - int client_ret = SSL_do_handshake(client_.get()); - if (client_ret != 1) { - ASSERT_EQ(client_ret, -1); - ASSERT_EQ(SSL_get_error(client_.get(), client_ret), SSL_ERROR_WANT_READ); - } - - ASSERT_TRUE(ProvideHandshakeData(server_.get())); - int server_ret = SSL_do_handshake(server_.get()); - if (server_ret != 1) { - ASSERT_EQ(server_ret, -1); - ASSERT_EQ(SSL_get_error(server_.get(), server_ret), SSL_ERROR_WANT_READ); - } - - if (client_ret == 1 && server_ret == 1) { - break; - } - } + ASSERT_TRUE(CompleteHandshakesForQUIC()); - EXPECT_EQ(SSL_do_handshake(client_.get()), 1); - EXPECT_EQ(SSL_do_handshake(server_.get()), 1); - EXPECT_TRUE(transport_.SecretsMatch(ssl_encryption_application)); - EXPECT_FALSE(transport_.client()->has_alert()); - EXPECT_FALSE(transport_.server()->has_alert()); + ExpectHandshakeSuccess(); EXPECT_TRUE(SSL_session_reused(client_.get())); EXPECT_TRUE(SSL_session_reused(server_.get())); } @@ -5056,32 +5097,133 @@ TEST_F(QUICMethodTest, HelloRetryRequest) { OPENSSL_ARRAY_SIZE(kServerPrefs))); ASSERT_TRUE(CreateClientAndServer()); + ASSERT_TRUE(CompleteHandshakesForQUIC()); + ExpectHandshakeSuccess(); +} - for (;;) { - ASSERT_TRUE(ProvideHandshakeData(client_.get())); - int client_ret = SSL_do_handshake(client_.get()); - if (client_ret != 1) { - ASSERT_EQ(client_ret, -1); - ASSERT_EQ(SSL_get_error(client_.get(), client_ret), SSL_ERROR_WANT_READ); +TEST_F(QUICMethodTest, ZeroRTTAccept) { + const SSL_QUIC_METHOD quic_method = { + SetEncryptionSecretsCallback, + AddHandshakeDataCallback, + FlushFlightCallback, + SendAlertCallback, + }; + + SSL_CTX_set_session_cache_mode(client_ctx_.get(), SSL_SESS_CACHE_BOTH); + SSL_CTX_set_early_data_enabled(client_ctx_.get(), 1); + SSL_CTX_set_early_data_enabled(server_ctx_.get(), 1); + ASSERT_TRUE(SSL_CTX_set_quic_method(client_ctx_.get(), &quic_method)); + ASSERT_TRUE(SSL_CTX_set_quic_method(server_ctx_.get(), &quic_method)); + + bssl::UniquePtr<SSL_SESSION> session = CreateClientSessionForQUIC(); + ASSERT_TRUE(session); + + ASSERT_TRUE(CreateClientAndServer()); + SSL_set_session(client_.get(), session.get()); + + // The client handshake should return immediately into the early data state. + ASSERT_EQ(SSL_do_handshake(client_.get()), 1); + EXPECT_TRUE(SSL_in_early_data(client_.get())); + // The transport should have keys for sending 0-RTT data. + EXPECT_TRUE( + transport_->client()->HasSecrets(ssl_encryption_early_data)); + + // The server will consume the ClientHello and also enter the early data + // state. + ASSERT_TRUE(ProvideHandshakeData(server_.get())); + ASSERT_EQ(SSL_do_handshake(server_.get()), 1); + EXPECT_TRUE(SSL_in_early_data(server_.get())); + EXPECT_TRUE(transport_->SecretsMatch(ssl_encryption_early_data)); + // The transport should have keys for sending half-RTT data. + EXPECT_TRUE( + transport_->server()->HasSecrets(ssl_encryption_application)); + + // Finish up the client and server handshakes. + ASSERT_TRUE(CompleteHandshakesForQUIC()); + + // Both sides can now exchange 1-RTT data. + ExpectHandshakeSuccess(); + EXPECT_TRUE(SSL_session_reused(client_.get())); + EXPECT_TRUE(SSL_session_reused(server_.get())); + EXPECT_FALSE(SSL_in_early_data(client_.get())); + EXPECT_FALSE(SSL_in_early_data(server_.get())); + EXPECT_TRUE(SSL_early_data_accepted(client_.get())); + EXPECT_TRUE(SSL_early_data_accepted(server_.get())); +} + +TEST_F(QUICMethodTest, ZeroRTTReject) { + const SSL_QUIC_METHOD quic_method = { + SetEncryptionSecretsCallback, + AddHandshakeDataCallback, + FlushFlightCallback, + SendAlertCallback, + }; + + SSL_CTX_set_session_cache_mode(client_ctx_.get(), SSL_SESS_CACHE_BOTH); + SSL_CTX_set_early_data_enabled(client_ctx_.get(), 1); + SSL_CTX_set_early_data_enabled(server_ctx_.get(), 1); + ASSERT_TRUE(SSL_CTX_set_quic_method(client_ctx_.get(), &quic_method)); + ASSERT_TRUE(SSL_CTX_set_quic_method(server_ctx_.get(), &quic_method)); + + bssl::UniquePtr<SSL_SESSION> session = CreateClientSessionForQUIC(); + ASSERT_TRUE(session); + + for (bool reject_hrr : {false, true}) { + SCOPED_TRACE(reject_hrr); + + ASSERT_TRUE(CreateClientAndServer()); + if (reject_hrr) { + // Configure the server to prefer P-256, which will reject 0-RTT via + // HelloRetryRequest. + int p256 = NID_X9_62_prime256v1; + ASSERT_TRUE(SSL_set1_curves(server_.get(), &p256, 1)); + } else { + // Disable 0-RTT on the server, so it will reject it. + SSL_set_early_data_enabled(server_.get(), 0); } + SSL_set_session(client_.get(), session.get()); + + // The client handshake should return immediately into the early data state. + ASSERT_EQ(SSL_do_handshake(client_.get()), 1); + EXPECT_TRUE(SSL_in_early_data(client_.get())); + // The transport should have keys for sending 0-RTT data. + EXPECT_TRUE(transport_->client()->HasSecrets(ssl_encryption_early_data)); + // The server will consume the ClientHello, but it will not accept 0-RTT. ASSERT_TRUE(ProvideHandshakeData(server_.get())); - int server_ret = SSL_do_handshake(server_.get()); - if (server_ret != 1) { - ASSERT_EQ(server_ret, -1); - ASSERT_EQ(SSL_get_error(server_.get(), server_ret), SSL_ERROR_WANT_READ); - } + ASSERT_EQ(SSL_do_handshake(server_.get()), -1); + EXPECT_EQ(SSL_ERROR_WANT_READ, SSL_get_error(server_.get(), -1)); + EXPECT_FALSE(SSL_in_early_data(server_.get())); + EXPECT_FALSE(transport_->server()->HasSecrets(ssl_encryption_early_data)); - if (client_ret == 1 && server_ret == 1) { - break; + // The client consumes the server response and signals 0-RTT rejection. + for (;;) { + ASSERT_TRUE(ProvideHandshakeData(client_.get())); + ASSERT_EQ(-1, SSL_do_handshake(client_.get())); + int err = SSL_get_error(client_.get(), -1); + if (err == SSL_ERROR_EARLY_DATA_REJECTED) { + break; + } + ASSERT_EQ(SSL_ERROR_WANT_READ, err); } - } - EXPECT_EQ(SSL_do_handshake(client_.get()), 1); - EXPECT_EQ(SSL_do_handshake(server_.get()), 1); - EXPECT_TRUE(transport_.SecretsMatch(ssl_encryption_application)); - EXPECT_FALSE(transport_.client()->has_alert()); - EXPECT_FALSE(transport_.server()->has_alert()); + // As in TLS over TCP, 0-RTT rejection is sticky. + ASSERT_EQ(-1, SSL_do_handshake(client_.get())); + ASSERT_EQ(SSL_ERROR_EARLY_DATA_REJECTED, SSL_get_error(client_.get(), -1)); + + // Finish up the client and server handshakes. + SSL_reset_early_data_reject(client_.get()); + ASSERT_TRUE(CompleteHandshakesForQUIC()); + + // Both sides can now exchange 1-RTT data. + ExpectHandshakeSuccess(); + EXPECT_TRUE(SSL_session_reused(client_.get())); + EXPECT_TRUE(SSL_session_reused(server_.get())); + EXPECT_FALSE(SSL_in_early_data(client_.get())); + EXPECT_FALSE(SSL_in_early_data(server_.get())); + EXPECT_FALSE(SSL_early_data_accepted(client_.get())); + EXPECT_FALSE(SSL_early_data_accepted(server_.get())); + } } // Test only releasing data to QUIC one byte at a time on request, to maximize @@ -5137,11 +5279,7 @@ TEST_F(QUICMethodTest, Async) { } } - EXPECT_EQ(SSL_do_handshake(client_.get()), 1); - EXPECT_EQ(SSL_do_handshake(server_.get()), 1); - EXPECT_TRUE(transport_.SecretsMatch(ssl_encryption_application)); - EXPECT_FALSE(transport_.client()->has_alert()); - EXPECT_FALSE(transport_.server()->has_alert()); + ExpectHandshakeSuccess(); } // Test buffering write data until explicit flushes. @@ -5188,31 +5326,9 @@ TEST_F(QUICMethodTest, Buffered) { buffered_flights.Set(client_.get(), &client_flight); buffered_flights.Set(server_.get(), &server_flight); - for (;;) { - ASSERT_TRUE(ProvideHandshakeData(client_.get())); - int client_ret = SSL_do_handshake(client_.get()); - if (client_ret != 1) { - ASSERT_EQ(client_ret, -1); - ASSERT_EQ(SSL_get_error(client_.get(), client_ret), SSL_ERROR_WANT_READ); - } - - ASSERT_TRUE(ProvideHandshakeData(server_.get())); - int server_ret = SSL_do_handshake(server_.get()); - if (server_ret != 1) { - ASSERT_EQ(server_ret, -1); - ASSERT_EQ(SSL_get_error(server_.get(), server_ret), SSL_ERROR_WANT_READ); - } - - if (client_ret == 1 && server_ret == 1) { - break; - } - } + ASSERT_TRUE(CompleteHandshakesForQUIC()); - EXPECT_EQ(SSL_do_handshake(client_.get()), 1); - EXPECT_EQ(SSL_do_handshake(server_.get()), 1); - EXPECT_TRUE(transport_.SecretsMatch(ssl_encryption_application)); - EXPECT_FALSE(transport_.client()->has_alert()); - EXPECT_FALSE(transport_.server()->has_alert()); + ExpectHandshakeSuccess(); } // Test that excess data at one level is rejected. That is, if a single @@ -5262,13 +5378,13 @@ TEST_F(QUICMethodTest, ExcessProvidedData) { EXPECT_EQ(ERR_GET_REASON(err), SSL_R_BUFFERED_MESSAGES_ON_CIPHER_CHANGE); // The client sends an alert in response to this. - ASSERT_TRUE(transport_.client()->has_alert()); - EXPECT_EQ(transport_.client()->alert_level(), ssl_encryption_initial); - EXPECT_EQ(transport_.client()->alert(), SSL_AD_UNEXPECTED_MESSAGE); + ASSERT_TRUE(transport_->client()->has_alert()); + EXPECT_EQ(transport_->client()->alert_level(), ssl_encryption_initial); + EXPECT_EQ(transport_->client()->alert(), SSL_AD_UNEXPECTED_MESSAGE); // Sanity-check client did get far enough to process the ServerHello and // install keys. - EXPECT_TRUE(transport_.client()->HasSecrets(ssl_encryption_handshake)); + EXPECT_TRUE(transport_->client()->HasSecrets(ssl_encryption_handshake)); } // Test that |SSL_provide_quic_data| will reject data at the wrong level. @@ -5298,7 +5414,7 @@ TEST_F(QUICMethodTest, ProvideWrongLevel) { // Data cannot be provided at the next level. std::vector<uint8_t> data; ASSERT_TRUE( - transport_.client()->ReadHandshakeData(&data, ssl_encryption_initial)); + transport_->client()->ReadHandshakeData(&data, ssl_encryption_initial)); ASSERT_FALSE(SSL_provide_quic_data(client_.get(), ssl_encryption_handshake, data.data(), data.size())); ERR_clear_error(); @@ -5312,7 +5428,7 @@ TEST_F(QUICMethodTest, ProvideWrongLevel) { // Data cannot be provided at the previous level. ASSERT_TRUE( - transport_.client()->ReadHandshakeData(&data, ssl_encryption_handshake)); + transport_->client()->ReadHandshakeData(&data, ssl_encryption_handshake)); ASSERT_FALSE(SSL_provide_quic_data(client_.get(), ssl_encryption_initial, data.data(), data.size())); } @@ -5357,32 +5473,13 @@ TEST_F(QUICMethodTest, BadPostHandshake) { ASSERT_TRUE(SSL_CTX_set_quic_method(client_ctx_.get(), &quic_method)); ASSERT_TRUE(SSL_CTX_set_quic_method(server_ctx_.get(), &quic_method)); ASSERT_TRUE(CreateClientAndServer()); - - for (;;) { - ASSERT_TRUE(ProvideHandshakeData(client_.get())); - int client_ret = SSL_do_handshake(client_.get()); - if (client_ret != 1) { - ASSERT_EQ(client_ret, -1); - ASSERT_EQ(SSL_get_error(client_.get(), client_ret), SSL_ERROR_WANT_READ); - } - - ASSERT_TRUE(ProvideHandshakeData(server_.get())); - int server_ret = SSL_do_handshake(server_.get()); - if (server_ret != 1) { - ASSERT_EQ(server_ret, -1); - ASSERT_EQ(SSL_get_error(server_.get(), server_ret), SSL_ERROR_WANT_READ); - } - - if (client_ret == 1 && server_ret == 1) { - break; - } - } + ASSERT_TRUE(CompleteHandshakesForQUIC()); EXPECT_EQ(SSL_do_handshake(client_.get()), 1); EXPECT_EQ(SSL_do_handshake(server_.get()), 1); - EXPECT_TRUE(transport_.SecretsMatch(ssl_encryption_application)); - EXPECT_FALSE(transport_.client()->has_alert()); - EXPECT_FALSE(transport_.server()->has_alert()); + EXPECT_TRUE(transport_->SecretsMatch(ssl_encryption_application)); + EXPECT_FALSE(transport_->client()->has_alert()); + EXPECT_FALSE(transport_->server()->has_alert()); // Junk sent as part of post-handshake data should cause an error. uint8_t kJunk[] = {0x17, 0x0, 0x0, 0x4, 0xB, 0xE, 0xE, 0xF}; |