diff --git a/crypto/hkdf.cc b/crypto/hkdf.cc index 1cd8468977e3e6..82aae24679e45a 100644 --- a/crypto/hkdf.cc +++ b/crypto/hkdf.cc @@ -5,6 +5,7 @@ #include "crypto/hkdf.h" #include "base/logging.h" +#include "base/memory/scoped_ptr.h" #include "crypto/hmac.h" namespace crypto { @@ -15,7 +16,8 @@ HKDF::HKDF(const base::StringPiece& secret, const base::StringPiece& salt, const base::StringPiece& info, size_t key_bytes_to_generate, - size_t iv_bytes_to_generate) { + size_t iv_bytes_to_generate, + size_t subkey_secret_bytes_to_generate) { // https://tools.ietf.org/html/rfc5869#section-2.2 base::StringPiece actual_salt = salt; char zeros[kSHA256HashLength]; @@ -40,8 +42,9 @@ HKDF::HKDF(const base::StringPiece& secret, // https://tools.ietf.org/html/rfc5869#section-2.3 // Perform the Expand phase to turn the pseudorandom key // and info into the output keying material. - const size_t material_length = - 2*key_bytes_to_generate + 2*iv_bytes_to_generate; + const size_t material_length = 2 * key_bytes_to_generate + + 2 * iv_bytes_to_generate + + subkey_secret_bytes_to_generate; const size_t n = (material_length + kSHA256HashLength-1) / kSHA256HashLength; DCHECK_LT(n, 256u); @@ -90,6 +93,11 @@ HKDF::HKDF(const base::StringPiece& secret, j += iv_bytes_to_generate; server_write_iv_ = base::StringPiece(reinterpret_cast(&output_[j]), iv_bytes_to_generate); + j += iv_bytes_to_generate; + } + if (subkey_secret_bytes_to_generate) { + subkey_secret_ = base::StringPiece(reinterpret_cast(&output_[j]), + subkey_secret_bytes_to_generate); } } diff --git a/crypto/hkdf.h b/crypto/hkdf.h index 1d7a87673a1d7b..e91bccf5b4f38c 100644 --- a/crypto/hkdf.h +++ b/crypto/hkdf.h @@ -8,9 +8,7 @@ #include #include "base/basictypes.h" -#include "base/memory/scoped_ptr.h" #include "base/strings/string_piece.h" -#include "build/build_config.h" #include "crypto/crypto_export.h" namespace crypto { @@ -20,7 +18,7 @@ namespace crypto { // See https://tools.ietf.org/html/rfc5869 for details. class CRYPTO_EXPORT HKDF { public: - // |secret|: The input shared secret (or, from RFC 5869, the IKM). + // |secret|: the input shared secret (or, from RFC 5869, the IKM). // |salt|: an (optional) public salt / non-secret random value. While // optional, callers are strongly recommended to provide a salt. There is no // added security value in making this larger than the SHA-256 block size of @@ -28,13 +26,18 @@ class CRYPTO_EXPORT HKDF { // |info|: an (optional) label to distinguish different uses of HKDF. It is // optional context and application specific information (can be a zero-length // string). - // |key_bytes_to_generate|: the number of bytes of key material to generate. - // |iv_bytes_to_generate|: the number of bytes of IV to generate. + // |key_bytes_to_generate|: the number of bytes of key material to generate + // for both client and server. + // |iv_bytes_to_generate|: the number of bytes of IV to generate for both + // client and server. + // |subkey_secret_bytes_to_generate|: the number of bytes of subkey secret to + // generate, shared between client and server. HKDF(const base::StringPiece& secret, const base::StringPiece& salt, const base::StringPiece& info, size_t key_bytes_to_generate, - size_t iv_bytes_to_generate); + size_t iv_bytes_to_generate, + size_t subkey_secret_bytes_to_generate); ~HKDF(); base::StringPiece client_write_key() const { @@ -49,6 +52,9 @@ class CRYPTO_EXPORT HKDF { base::StringPiece server_write_iv() const { return server_write_iv_; } + base::StringPiece subkey_secret() const { + return subkey_secret_; + } private: std::vector output_; @@ -57,6 +63,7 @@ class CRYPTO_EXPORT HKDF { base::StringPiece server_write_key_; base::StringPiece client_write_iv_; base::StringPiece server_write_iv_; + base::StringPiece subkey_secret_; }; } // namespace crypto diff --git a/crypto/hkdf_unittest.cc b/crypto/hkdf_unittest.cc index dc369d1b94f1b4..bcb19c5c10d185 100644 --- a/crypto/hkdf_unittest.cc +++ b/crypto/hkdf_unittest.cc @@ -82,7 +82,7 @@ TEST(HKDFTest, HKDF) { // We set the key_length to the length of the expected output and then take // the result from the first key, which is the client write key. - HKDF hkdf(key, salt, info, expected.size(), 0); + HKDF hkdf(key, salt, info, expected.size(), 0, 0); ASSERT_EQ(expected.size(), hkdf.client_write_key().size()); EXPECT_EQ(0, memcmp(expected.data(), hkdf.client_write_key().data(), diff --git a/net/quic/crypto/crypto_handshake.h b/net/quic/crypto/crypto_handshake.h index 3839a2154656f6..cc25570af6bd48 100644 --- a/net/quic/crypto/crypto_handshake.h +++ b/net/quic/crypto/crypto_handshake.h @@ -100,6 +100,8 @@ struct NET_EXPORT_PRIVATE QuicCryptoNegotiatedParameters { QuicTag aead; std::string initial_premaster_secret; std::string forward_secure_premaster_secret; + // subkey_secret is used as the PRK input to the HKDF used for key extraction. + std::string subkey_secret; CrypterPair initial_crypters; CrypterPair forward_secure_crypters; // Normalized SNI: converted to lower case and trailing '.' removed. diff --git a/net/quic/crypto/crypto_utils.cc b/net/quic/crypto/crypto_utils.cc index 2943396048946b..041c284b935871 100644 --- a/net/quic/crypto/crypto_utils.cc +++ b/net/quic/crypto/crypto_utils.cc @@ -15,6 +15,7 @@ #include "url/url_canon.h" using base::StringPiece; +using std::numeric_limits; using std::string; namespace net { @@ -83,11 +84,14 @@ bool CryptoUtils::DeriveKeys(StringPiece premaster_secret, StringPiece server_nonce, const string& hkdf_input, Perspective perspective, - CrypterPair* out) { - out->encrypter.reset(QuicEncrypter::Create(aead)); - out->decrypter.reset(QuicDecrypter::Create(aead)); - size_t key_bytes = out->encrypter->GetKeySize(); - size_t nonce_prefix_bytes = out->encrypter->GetNoncePrefixSize(); + CrypterPair* crypters, + string* subkey_secret) { + crypters->encrypter.reset(QuicEncrypter::Create(aead)); + crypters->decrypter.reset(QuicDecrypter::Create(aead)); + size_t key_bytes = crypters->encrypter->GetKeySize(); + size_t nonce_prefix_bytes = crypters->encrypter->GetNoncePrefixSize(); + size_t subkey_secret_bytes = + subkey_secret == NULL ? 0 : premaster_secret.length(); StringPiece nonce = client_nonce; string nonce_storage; @@ -97,23 +101,59 @@ bool CryptoUtils::DeriveKeys(StringPiece premaster_secret, } crypto::HKDF hkdf(premaster_secret, nonce, hkdf_input, key_bytes, - nonce_prefix_bytes); + nonce_prefix_bytes, subkey_secret_bytes); if (perspective == SERVER) { - if (!out->encrypter->SetKey(hkdf.server_write_key()) || - !out->encrypter->SetNoncePrefix(hkdf.server_write_iv()) || - !out->decrypter->SetKey(hkdf.client_write_key()) || - !out->decrypter->SetNoncePrefix(hkdf.client_write_iv())) { + if (!crypters->encrypter->SetKey(hkdf.server_write_key()) || + !crypters->encrypter->SetNoncePrefix(hkdf.server_write_iv()) || + !crypters->decrypter->SetKey(hkdf.client_write_key()) || + !crypters->decrypter->SetNoncePrefix(hkdf.client_write_iv())) { return false; } } else { - if (!out->encrypter->SetKey(hkdf.client_write_key()) || - !out->encrypter->SetNoncePrefix(hkdf.client_write_iv()) || - !out->decrypter->SetKey(hkdf.server_write_key()) || - !out->decrypter->SetNoncePrefix(hkdf.server_write_iv())) { + if (!crypters->encrypter->SetKey(hkdf.client_write_key()) || + !crypters->encrypter->SetNoncePrefix(hkdf.client_write_iv()) || + !crypters->decrypter->SetKey(hkdf.server_write_key()) || + !crypters->decrypter->SetNoncePrefix(hkdf.server_write_iv())) { return false; } } + if (subkey_secret != NULL) { + hkdf.subkey_secret().CopyToString(subkey_secret); + } + + return true; +} + +// static +bool CryptoUtils::ExportKeyingMaterial(StringPiece subkey_secret, + StringPiece label, + StringPiece context, + size_t result_len, + string* result) { + for (size_t i = 0; i < label.length(); i++) { + if (label[i] == '\0') { + LOG(ERROR) << "ExportKeyingMaterial label may not contain NULs"; + return false; + } + } + // Create HKDF info input: null-terminated label + length-prefixed context + if (context.length() >= numeric_limits::max()) { + LOG(ERROR) << "Context value longer than 2^32"; + return false; + } + uint32 context_length = static_cast(context.length()); + string info = label.as_string(); + info.push_back('\0'); + info.append(reinterpret_cast(&context_length), sizeof(context_length)); + info.append(context.data(), context.length()); + crypto::HKDF hkdf(subkey_secret, + StringPiece() /* no salt */, + info, + result_len, + 0 /* no fixed IV */, + 0 /* no subkey secret */); + hkdf.client_write_key().CopyToString(result); return true; } diff --git a/net/quic/crypto/crypto_utils.h b/net/quic/crypto/crypto_utils.h index ec6384c131e639..af3ecbb82671a7 100644 --- a/net/quic/crypto/crypto_utils.h +++ b/net/quic/crypto/crypto_utils.h @@ -49,19 +49,31 @@ class NET_EXPORT_PRIVATE CryptoUtils { // literals. IsValidSNI() should be called before calling NormalizeHostname(). static std::string NormalizeHostname(const char* hostname); - // DeriveKeys populates |out->encrypter| and |out->decrypter| given the - // contents of |premaster_secret|, |client_nonce|, |server_nonce| and - // |hkdf_input|. |aead| determines which cipher will be used. |perspective| - // controls whether the server's keys are assigned to |encrypter| or - // |decrypter|. |server_nonce| is optional and, if non-empty, is mixed into - // the key derivation. + // DeriveKeys populates |crypters->encrypter|, |crypters->decrypter|, and + // |subkey_secret| (optional -- may be null) given the contents of + // |premaster_secret|, |client_nonce|, |server_nonce| and |hkdf_input|. |aead| + // determines which cipher will be used. |perspective| controls whether the + // server's keys are assigned to |encrypter| or |decrypter|. |server_nonce| is + // optional and, if non-empty, is mixed into the key derivation. + // |subkey_secret| will have the same length as |premaster_secret|. static bool DeriveKeys(base::StringPiece premaster_secret, QuicTag aead, base::StringPiece client_nonce, base::StringPiece server_nonce, const std::string& hkdf_input, Perspective perspective, - CrypterPair* out); + CrypterPair* crypters, + std::string* subkey_secret); + + // Performs key extraction to derive a new secret of |result_len| bytes + // dependent on |subkey_secret|, |label|, and |context|. Returns false if the + // parameters are invalid (e.g. |label| contains null bytes); returns true on + // success. + static bool ExportKeyingMaterial(base::StringPiece subkey_secret, + base::StringPiece label, + base::StringPiece context, + size_t result_len, + std::string* result); private: DISALLOW_COPY_AND_ASSIGN(CryptoUtils); diff --git a/net/quic/crypto/crypto_utils_test.cc b/net/quic/crypto/crypto_utils_test.cc index 17eb19250eeb76..37ac583314a927 100644 --- a/net/quic/crypto/crypto_utils_test.cc +++ b/net/quic/crypto/crypto_utils_test.cc @@ -4,6 +4,7 @@ #include "net/quic/crypto/crypto_utils.h" +#include "net/quic/test_tools/quic_test_utils.h" #include "testing/gtest/include/gtest/gtest.h" namespace net { @@ -44,6 +45,87 @@ TEST(CryptoUtilsTest, NormalizeHostname) { } } +TEST(CryptoUtilsTest, TestExportKeyingMaterial) { + const struct TestVector { + // Input (strings of hexadecimal digits): + const char* subkey_secret; + const char* label; + const char* context; + size_t result_len; + + // Expected output (string of hexadecimal digits): + const char* expected; // Null if it should fail. + } test_vector[] = { + // Try a typical input + { "4823c1189ecc40fce888fbb4cf9ae6254f19ba12e6d9af54788f195a6f509ca3", + "e934f78d7a71dd85420fceeb8cea0317", + "b8d766b5d3c8aba0009c7ed3de553eba53b4de1030ea91383dcdf724cd8b7217", + 32, + "a9979da0d5f1c1387d7cbe68f5c4163ddb445a03c4ad6ee72cb49d56726d679e" + }, + // Don't let the label contain nulls + { "14fe51e082ffee7d1b4d8d4ab41f8c55", + "3132333435363700", + "58585858585858585858585858585858", + 16, + NULL + }, + // Make sure nulls in the context are fine + { "d862c2e36b0a42f7827c67ebc8d44df7", + "7a5b95e4e8378123", + "4142434445464700", + 16, + "12d418c6d0738a2e4d85b2d0170f76e1" + }, + // ... and give a different result than without + { "d862c2e36b0a42f7827c67ebc8d44df7", + "7a5b95e4e8378123", + "41424344454647", + 16, + "abfa1c479a6e3ffb98a11dee7d196408" + }, + // Try weird lengths + { "d0ec8a34f6cc9a8c96", + "49711798cc6251", + "933d4a2f30d22f089cfba842791116adc121e0", + 23, + "c9a46ed0757bd1812f1f21b4d41e62125fec8364a21db7" + }, + }; + + for (size_t i = 0; i < arraysize(test_vector); i++) { + // Decode the test vector. + string subkey_secret; + string label; + string context; + ASSERT_TRUE(DecodeHexString(test_vector[i].subkey_secret, &subkey_secret)); + ASSERT_TRUE(DecodeHexString(test_vector[i].label, &label)); + ASSERT_TRUE(DecodeHexString(test_vector[i].context, &context)); + size_t result_len = test_vector[i].result_len; + bool expect_ok = test_vector[i].expected != NULL; + string expected; + if (expect_ok) { + ASSERT_TRUE(DecodeHexString(test_vector[i].expected, &expected)); + } + + string result; + bool ok = CryptoUtils::ExportKeyingMaterial(subkey_secret, + label, + context, + result_len, + &result); + EXPECT_EQ(expect_ok, ok); + if (expect_ok) { + EXPECT_EQ(result_len, result.length()); + test::CompareCharArraysWithHexError("HKDF output", + result.data(), + result.length(), + expected.data(), + expected.length()); + } + } +} + } // namespace } // namespace test } // namespace net diff --git a/net/quic/crypto/quic_crypto_client_config.cc b/net/quic/crypto/quic_crypto_client_config.cc index ce2705d1e61009..93b11ef4f02cbf 100644 --- a/net/quic/crypto/quic_crypto_client_config.cc +++ b/net/quic/crypto/quic_crypto_client_config.cc @@ -515,7 +515,8 @@ QuicErrorCode QuicCryptoClientConfig::FillClientHello( if (!CryptoUtils::DeriveKeys(out_params->initial_premaster_secret, out_params->aead, out_params->client_nonce, out_params->server_nonce, hkdf_input, - CryptoUtils::CLIENT, &crypters)) { + CryptoUtils::CLIENT, &crypters, + NULL /* subkey secret */)) { *error_details = "Symmetric key setup failed"; return QUIC_CRYPTO_SYMMETRIC_KEY_SETUP_FAILED; } @@ -557,7 +558,8 @@ QuicErrorCode QuicCryptoClientConfig::FillClientHello( if (!CryptoUtils::DeriveKeys( out_params->initial_premaster_secret, out_params->aead, out_params->client_nonce, out_params->server_nonce, hkdf_input, - CryptoUtils::CLIENT, &out_params->initial_crypters)) { + CryptoUtils::CLIENT, &out_params->initial_crypters, + NULL /* subkey secret */)) { *error_details = "Symmetric key setup failed"; return QUIC_CRYPTO_SYMMETRIC_KEY_SETUP_FAILED; } @@ -737,7 +739,8 @@ QuicErrorCode QuicCryptoClientConfig::ProcessServerHello( if (!CryptoUtils::DeriveKeys( out_params->forward_secure_premaster_secret, out_params->aead, out_params->client_nonce, out_params->server_nonce, hkdf_input, - CryptoUtils::CLIENT, &out_params->forward_secure_crypters)) { + CryptoUtils::CLIENT, &out_params->forward_secure_crypters, + &out_params->subkey_secret)) { *error_details = "Symmetric key setup failed"; return QUIC_CRYPTO_SYMMETRIC_KEY_SETUP_FAILED; } diff --git a/net/quic/crypto/quic_crypto_server_config.cc b/net/quic/crypto/quic_crypto_server_config.cc index 6bddf2b3d8fa24..36592705b34d49 100644 --- a/net/quic/crypto/quic_crypto_server_config.cc +++ b/net/quic/crypto/quic_crypto_server_config.cc @@ -54,7 +54,8 @@ string DeriveSourceAddressTokenKey(StringPiece source_address_token_secret) { StringPiece() /* no salt */, "QUIC source address token key", CryptoSecretBoxer::GetKeySize(), - 0 /* no fixed IV needed */); + 0 /* no fixed IV needed */, + 0 /* no subkey secret */); return hkdf.server_write_key().as_string(); } @@ -682,7 +683,8 @@ QuicErrorCode QuicCryptoServerConfig::ProcessClientHello( CrypterPair crypters; if (!CryptoUtils::DeriveKeys(params->initial_premaster_secret, params->aead, info.client_nonce, info.server_nonce, - hkdf_input, CryptoUtils::SERVER, &crypters)) { + hkdf_input, CryptoUtils::SERVER, &crypters, + NULL /* subkey secret */)) { *error_details = "Symmetric key setup failed"; return QUIC_CRYPTO_SYMMETRIC_KEY_SETUP_FAILED; } @@ -723,7 +725,8 @@ QuicErrorCode QuicCryptoServerConfig::ProcessClientHello( if (!CryptoUtils::DeriveKeys(params->initial_premaster_secret, params->aead, info.client_nonce, info.server_nonce, hkdf_input, CryptoUtils::SERVER, - ¶ms->initial_crypters)) { + ¶ms->initial_crypters, + NULL /* subkey secret */)) { *error_details = "Symmetric key setup failed"; return QUIC_CRYPTO_SYMMETRIC_KEY_SETUP_FAILED; } @@ -756,7 +759,8 @@ QuicErrorCode QuicCryptoServerConfig::ProcessClientHello( if (!CryptoUtils::DeriveKeys( params->forward_secure_premaster_secret, params->aead, info.client_nonce, info.server_nonce, forward_secure_hkdf_input, - CryptoUtils::SERVER, ¶ms->forward_secure_crypters)) { + CryptoUtils::SERVER, ¶ms->forward_secure_crypters, + ¶ms->subkey_secret)) { *error_details = "Symmetric key setup failed"; return QUIC_CRYPTO_SYMMETRIC_KEY_SETUP_FAILED; } diff --git a/net/quic/quic_crypto_stream.cc b/net/quic/quic_crypto_stream.cc index 36b7c4550a2810..d53b736527f964 100644 --- a/net/quic/quic_crypto_stream.cc +++ b/net/quic/quic_crypto_stream.cc @@ -8,6 +8,7 @@ #include "base/strings/string_piece.h" #include "net/quic/crypto/crypto_handshake.h" +#include "net/quic/crypto/crypto_utils.h" #include "net/quic/quic_connection.h" #include "net/quic/quic_session.h" #include "net/quic/quic_utils.h" @@ -62,6 +63,24 @@ void QuicCryptoStream::SendHandshakeMessage( WriteOrBufferData(string(data.data(), data.length()), false, NULL); } +bool QuicCryptoStream::ExportKeyingMaterial( + StringPiece label, + StringPiece context, + size_t result_len, + string* result) const { + if (!handshake_confirmed()) { + DLOG(ERROR) << "ExportKeyingMaterial was called before forward-secure" + << "encryption was established."; + return false; + } + return CryptoUtils::ExportKeyingMaterial( + crypto_negotiated_params_.subkey_secret, + label, + context, + result_len, + result); +} + const QuicCryptoNegotiatedParameters& QuicCryptoStream::crypto_negotiated_params() const { return crypto_negotiated_params_; diff --git a/net/quic/quic_crypto_stream.h b/net/quic/quic_crypto_stream.h index 2c61a18fe782dc..e9d575ccc29444 100644 --- a/net/quic/quic_crypto_stream.h +++ b/net/quic/quic_crypto_stream.h @@ -46,6 +46,15 @@ class NET_EXPORT_PRIVATE QuicCryptoStream // TODO(wtc): return a success/failure status. void SendHandshakeMessage(const CryptoHandshakeMessage& message); + // Performs key extraction to derive a new secret of |result_len| bytes + // dependent on |label|, |context|, and the stream's negotiated subkey secret. + // Returns false if the handshake has not been confirmed or the parameters are + // invalid (e.g. |label| contains null bytes); returns true on success. + bool ExportKeyingMaterial(base::StringPiece label, + base::StringPiece context, + size_t result_len, + std::string* result) const; + bool encryption_established() const { return encryption_established_; } bool handshake_confirmed() const { return handshake_confirmed_; } diff --git a/net/quic/test_tools/crypto_test_utils.cc b/net/quic/test_tools/crypto_test_utils.cc index ca0681ac5b317f..0e3a81517a7745 100644 --- a/net/quic/test_tools/crypto_test_utils.cc +++ b/net/quic/test_tools/crypto_test_utils.cc @@ -452,6 +452,26 @@ void CryptoTestUtils::CompareClientAndServerKeys( StringPiece server_forward_secure_decrypter_iv = server_forward_secure_decrypter->GetNoncePrefix(); + StringPiece client_subkey_secret = + client->crypto_negotiated_params().subkey_secret; + StringPiece server_subkey_secret = + server->crypto_negotiated_params().subkey_secret; + + + const char kSampleLabel[] = "label"; + const char kSampleContext[] = "context"; + const size_t kSampleOutputLength = 32; + string client_key_extraction; + string server_key_extraction; + EXPECT_TRUE(client->ExportKeyingMaterial(kSampleLabel, + kSampleContext, + kSampleOutputLength, + &client_key_extraction)); + EXPECT_TRUE(server->ExportKeyingMaterial(kSampleLabel, + kSampleContext, + kSampleOutputLength, + &server_key_extraction)); + CompareCharArraysWithHexError("client write key", client_encrypter_key.data(), client_encrypter_key.length(), @@ -492,6 +512,16 @@ void CryptoTestUtils::CompareClientAndServerKeys( server_forward_secure_encrypter_iv.length(), client_forward_secure_decrypter_iv.data(), client_forward_secure_decrypter_iv.length()); + CompareCharArraysWithHexError("subkey secret", + client_subkey_secret.data(), + client_subkey_secret.length(), + server_subkey_secret.data(), + server_subkey_secret.length()); + CompareCharArraysWithHexError("sample key extraction", + client_key_extraction.data(), + client_key_extraction.length(), + server_key_extraction.data(), + server_key_extraction.length()); } // static