0

Implement QUIC key extraction.

Added a new subkey_secret output to crypto::HKDF which is
saved by the forward-secure key derivation and used for a new
ExportKeyingMaterial method on QuicCryptoStream. This will be used
in Chromium for WebRTC on QUIC.

Generated some tests by making a straightforward alternative
implementation in Python.

Written by Daniel Ziegler.

Merge internal CL: 72073257

R=agl@chromium.org,dmziegler@chromium.org
BUG=

Review URL: https://codereview.chromium.org/423333002

git-svn-id: svn://svn.chromium.org/chrome/trunk/src@286738 0039d316-1c4b-4281-b951-d872f2087c98
This commit is contained in:
wtc@chromium.org
2014-07-31 11:36:37 +00:00
parent cc405e4789
commit 2fe8b63061
12 changed files with 254 additions and 38 deletions

@ -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<char*>(&output_[j]),
iv_bytes_to_generate);
j += iv_bytes_to_generate;
}
if (subkey_secret_bytes_to_generate) {
subkey_secret_ = base::StringPiece(reinterpret_cast<char*>(&output_[j]),
subkey_secret_bytes_to_generate);
}
}

@ -8,9 +8,7 @@
#include <vector>
#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<uint8> 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

@ -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(),

@ -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.

@ -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,24 +101,60 @@ 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<uint32>::max()) {
LOG(ERROR) << "Context value longer than 2^32";
return false;
}
uint32 context_length = static_cast<uint32>(context.length());
string info = label.as_string();
info.push_back('\0');
info.append(reinterpret_cast<char*>(&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;
}
} // namespace net

@ -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);

@ -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

@ -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;
}

@ -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,
&params->initial_crypters)) {
&params->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, &params->forward_secure_crypters)) {
CryptoUtils::SERVER, &params->forward_secure_crypters,
&params->subkey_secret)) {
*error_details = "Symmetric key setup failed";
return QUIC_CRYPTO_SYMMETRIC_KEY_SETUP_FAILED;
}

@ -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_;

@ -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_; }

@ -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