Index: net/quic/crypto/crypto_server_config.cc |
diff --git a/net/quic/crypto/crypto_server_config.cc b/net/quic/crypto/crypto_server_config.cc |
new file mode 100644 |
index 0000000000000000000000000000000000000000..10dcf73fa7f35feff92cc816be48bbe54d24d459 |
--- /dev/null |
+++ b/net/quic/crypto/crypto_server_config.cc |
@@ -0,0 +1,586 @@ |
+// Copyright (c) 2013 The Chromium Authors. All rights reserved. |
+// Use of this source code is governed by a BSD-style license that can be |
+// found in the LICENSE file. |
+ |
+#include "net/quic/crypto/crypto_server_config.h" |
+ |
+#include "base/stl_util.h" |
+#include "crypto/hkdf.h" |
+#include "crypto/secure_hash.h" |
+#include "net/quic/crypto/aes_128_gcm_decrypter.h" |
+#include "net/quic/crypto/aes_128_gcm_encrypter.h" |
+#include "net/quic/crypto/crypto_framer.h" |
+#include "net/quic/crypto/crypto_server_config_protobuf.h" |
+#include "net/quic/crypto/crypto_utils.h" |
+#include "net/quic/crypto/curve25519_key_exchange.h" |
+#include "net/quic/crypto/key_exchange.h" |
+#include "net/quic/crypto/p256_key_exchange.h" |
+#include "net/quic/crypto/proof_source.h" |
+#include "net/quic/crypto/quic_decrypter.h" |
+#include "net/quic/crypto/quic_encrypter.h" |
+#include "net/quic/crypto/quic_random.h" |
+#include "net/quic/crypto/source_address_token.h" |
+#include "net/quic/crypto/strike_register.h" |
+#include "net/quic/quic_clock.h" |
+#include "net/quic/quic_protocol.h" |
+ |
+using base::StringPiece; |
+using crypto::SecureHash; |
+using std::map; |
+using std::string; |
+using std::vector; |
+ |
+namespace net { |
+ |
+// static |
+const char QuicCryptoServerConfig::TESTING[] = "secret string for testing"; |
+ |
+QuicCryptoServerConfig::QuicCryptoServerConfig( |
+ StringPiece source_address_token_secret) |
+ // AES-GCM is used to encrypt and authenticate source address tokens. The |
+ // full, 96-bit nonce is used but we must ensure that an attacker cannot |
+ // obtain two source address tokens with the same nonce. This occurs with |
+ // probability 0.5 after 2**48 values. We assume that obtaining 2**48 |
+ // source address tokens is not possible: at a rate of 10M packets per |
+ // second, it would still take the attacker a year to obtain the needed |
+ // number of packets. |
+ // |
+ // TODO(agl): switch to an encrypter with a larger nonce space (i.e. |
+ // Salsa20+Poly1305). |
+ : strike_register_lock_(), |
+ source_address_token_encrypter_(new Aes128GcmEncrypter), |
+ source_address_token_decrypter_(new Aes128GcmDecrypter) { |
+ crypto::HKDF hkdf(source_address_token_secret, StringPiece() /* no salt */, |
+ "QUIC source address token key", |
+ source_address_token_encrypter_->GetKeySize(), |
+ 0 /* no fixed IV needed */); |
+ source_address_token_encrypter_->SetKey(hkdf.server_write_key()); |
+ source_address_token_decrypter_->SetKey(hkdf.server_write_key()); |
+} |
+ |
+QuicCryptoServerConfig::~QuicCryptoServerConfig() { |
+ STLDeleteValues(&configs_); |
+} |
+ |
+// static |
+QuicServerConfigProtobuf* QuicCryptoServerConfig::DefaultConfig( |
+ QuicRandom* rand, |
+ const QuicClock* clock, |
+ const CryptoHandshakeMessage& extra_tags) { |
+ CryptoHandshakeMessage msg; |
+ |
+ const string curve25519_private_key = |
+ Curve25519KeyExchange::NewPrivateKey(rand); |
+ scoped_ptr<Curve25519KeyExchange> curve25519( |
+ Curve25519KeyExchange::New(curve25519_private_key)); |
+ StringPiece curve25519_public_value = curve25519->public_value(); |
+ |
+ const string p256_private_key = |
+ P256KeyExchange::NewPrivateKey(); |
+ scoped_ptr<P256KeyExchange> p256( |
+ P256KeyExchange::New(p256_private_key)); |
+ StringPiece p256_public_value = p256->public_value(); |
+ |
+ string encoded_public_values; |
+ // First two bytes encode the length of the public value. |
+ encoded_public_values.push_back(curve25519_public_value.size()); |
+ encoded_public_values.push_back(curve25519_public_value.size() >> 8); |
+ encoded_public_values.append(curve25519_public_value.data(), |
+ curve25519_public_value.size()); |
+ encoded_public_values.push_back(p256_public_value.size()); |
+ encoded_public_values.push_back(p256_public_value.size() >> 8); |
+ encoded_public_values.append(p256_public_value.data(), |
+ p256_public_value.size()); |
+ |
+ msg.set_tag(kSCFG); |
+ msg.SetTaglist(kKEXS, kC255, kP256, 0); |
+ msg.SetTaglist(kAEAD, kAESG, 0); |
+ msg.SetValue(kVERS, static_cast<uint16>(0)); |
+ msg.SetStringPiece(kPUBS, encoded_public_values); |
+ msg.Insert(extra_tags.tag_value_map().begin(), |
+ extra_tags.tag_value_map().end()); |
+ |
+ char scid_bytes[16]; |
+ rand->RandBytes(scid_bytes, sizeof(scid_bytes)); |
+ msg.SetStringPiece(kSCID, StringPiece(scid_bytes, sizeof(scid_bytes))); |
+ |
+ char orbit_bytes[kOrbitSize]; |
+ rand->RandBytes(orbit_bytes, sizeof(orbit_bytes)); |
+ msg.SetStringPiece(kORBT, StringPiece(orbit_bytes, sizeof(orbit_bytes))); |
+ |
+ scoped_ptr<QuicData> serialized( |
+ CryptoFramer::ConstructHandshakeMessage(msg)); |
+ |
+ scoped_ptr<QuicServerConfigProtobuf> config(new QuicServerConfigProtobuf); |
+ config->set_config(serialized->AsStringPiece()); |
+ QuicServerConfigProtobuf::PrivateKey* curve25519_key = config->add_key(); |
+ curve25519_key->set_tag(kC255); |
+ curve25519_key->set_private_key(curve25519_private_key); |
+ QuicServerConfigProtobuf::PrivateKey* p256_key = config->add_key(); |
+ p256_key->set_tag(kP256); |
+ p256_key->set_private_key(p256_private_key); |
+ |
+ return config.release(); |
+} |
+ |
+CryptoHandshakeMessage* QuicCryptoServerConfig::AddConfig( |
+ QuicServerConfigProtobuf* protobuf) { |
+ scoped_ptr<CryptoHandshakeMessage> msg( |
+ CryptoFramer::ParseMessage(protobuf->config())); |
+ |
+ if (!msg.get()) { |
+ LOG(WARNING) << "Failed to parse server config message"; |
+ return NULL; |
+ } |
+ if (msg->tag() != kSCFG) { |
+ LOG(WARNING) << "Server config message has tag " |
+ << msg->tag() << " expected " |
+ << kSCFG; |
+ return NULL; |
+ } |
+ |
+ scoped_ptr<Config> config(new Config); |
+ config->serialized = protobuf->config(); |
+ |
+ StringPiece scid; |
+ if (!msg->GetStringPiece(kSCID, &scid)) { |
+ LOG(WARNING) << "Server config message is missing SCID"; |
+ return NULL; |
+ } |
+ config->id = scid.as_string(); |
+ |
+ const CryptoTag* aead_tags; |
+ size_t aead_len; |
+ if (msg->GetTaglist(kAEAD, &aead_tags, &aead_len) != QUIC_NO_ERROR) { |
+ LOG(WARNING) << "Server config message is missing AEAD"; |
+ return NULL; |
+ } |
+ config->aead = vector<CryptoTag>(aead_tags, aead_tags + aead_len); |
+ |
+ const CryptoTag* kexs_tags; |
+ size_t kexs_len; |
+ if (msg->GetTaglist(kKEXS, &kexs_tags, &kexs_len) != QUIC_NO_ERROR) { |
+ LOG(WARNING) << "Server config message is missing KEXS"; |
+ return NULL; |
+ } |
+ |
+ StringPiece orbit; |
+ if (!msg->GetStringPiece(kORBT, &orbit)) { |
+ LOG(WARNING) << "Server config message is missing OBIT"; |
+ return NULL; |
+ } |
+ |
+ if (orbit.size() != kOrbitSize) { |
+ LOG(WARNING) << "Orbit value in server config is the wrong length." |
+ " Got " << orbit.size() << " want " << kOrbitSize; |
+ return NULL; |
+ } |
+ COMPILE_ASSERT(sizeof(config->orbit) == kOrbitSize, orbit_incorrect_size); |
+ memcpy(config->orbit, orbit.data(), sizeof(config->orbit)); |
+ |
+ if (kexs_len != protobuf->key_size()) { |
+ LOG(WARNING) << "Server config has " |
+ << kexs_len |
+ << " key exchange methods configured, but " |
+ << protobuf->key_size() |
+ << " private keys"; |
+ return NULL; |
+ } |
+ |
+ for (size_t i = 0; i < kexs_len; i++) { |
+ const CryptoTag tag = kexs_tags[i]; |
+ string private_key; |
+ |
+ config->kexs.push_back(tag); |
+ |
+ for (size_t j = 0; j < protobuf->key_size(); j++) { |
+ const QuicServerConfigProtobuf::PrivateKey& key = protobuf->key(i); |
+ if (key.tag() == tag) { |
+ private_key = key.private_key(); |
+ break; |
+ } |
+ } |
+ |
+ if (private_key.empty()) { |
+ LOG(WARNING) << "Server config contains key exchange method without " |
+ "corresponding private key: " |
+ << tag; |
+ return NULL; |
+ } |
+ |
+ scoped_ptr<KeyExchange> ka; |
+ switch (tag) { |
+ case kC255: |
+ ka.reset(Curve25519KeyExchange::New(private_key)); |
+ if (!ka.get()) { |
+ LOG(WARNING) << "Server config contained an invalid curve25519" |
+ " private key."; |
+ return NULL; |
+ } |
+ break; |
+ case kP256: |
+ ka.reset(P256KeyExchange::New(private_key)); |
+ if (!ka.get()) { |
+ LOG(WARNING) << "Server config contained an invalid P-256" |
+ " private key."; |
+ return NULL; |
+ } |
+ break; |
+ default: |
+ LOG(WARNING) << "Server config message contains unknown key exchange " |
+ "method: " |
+ << tag; |
+ return NULL; |
+ } |
+ |
+ for (vector<KeyExchange*>::const_iterator i = config->key_exchanges.begin(); |
+ i != config->key_exchanges.end(); ++i) { |
+ if ((*i)->tag() == tag) { |
+ LOG(WARNING) << "Duplicate key exchange in config: " << tag; |
+ return NULL; |
+ } |
+ } |
+ |
+ config->key_exchanges.push_back(ka.release()); |
+ } |
+ |
+ if (msg->GetUint16(kVERS, &config->version) != QUIC_NO_ERROR) { |
+ LOG(WARNING) << "Server config message is missing version"; |
+ return NULL; |
+ } |
+ |
+ if (config->version != QuicCryptoConfig::CONFIG_VERSION) { |
+ LOG(WARNING) << "Server config specifies an unsupported version"; |
+ return NULL; |
+ } |
+ |
+ scoped_ptr<SecureHash> sha256(SecureHash::Create(SecureHash::SHA256)); |
+ sha256->Update(protobuf->config().data(), protobuf->config().size()); |
+ char id_bytes[16]; |
+ sha256->Finish(id_bytes, sizeof(id_bytes)); |
+ const string id(id_bytes, sizeof(id_bytes)); |
+ |
+ configs_[id] = config.release(); |
+ active_config_ = id; |
+ |
+ return msg.release(); |
+} |
+ |
+CryptoHandshakeMessage* QuicCryptoServerConfig::AddDefaultConfig( |
+ QuicRandom* rand, |
+ const QuicClock* clock, |
+ const CryptoHandshakeMessage& extra_tags) { |
+ scoped_ptr<QuicServerConfigProtobuf> config(DefaultConfig( |
+ rand, clock, extra_tags)); |
+ return AddConfig(config.get()); |
+} |
+ |
+QuicErrorCode QuicCryptoServerConfig::ProcessClientHello( |
+ const CryptoHandshakeMessage& client_hello, |
+ QuicGuid guid, |
+ const IPEndPoint& client_ip, |
+ QuicTime::Delta now_since_unix_epoch, |
+ QuicRandom* rand, |
+ QuicCryptoNegotiatedParameters *params, |
+ CryptoHandshakeMessage* out, |
+ string* error_details) const { |
+ DCHECK(error_details); |
+ |
+ CHECK(!configs_.empty()); |
+ |
+ // FIXME(agl): we should use the client's SCID, not just the active config. |
+ map<ServerConfigID, Config*>::const_iterator it = |
+ configs_.find(active_config_); |
+ if (it == configs_.end()) { |
+ *error_details = "No valid server config loaded"; |
+ return QUIC_CRYPTO_INTERNAL_ERROR; |
+ } |
+ const Config* const config(it->second); |
+ |
+ bool valid_source_address_token = false; |
+ StringPiece srct; |
+ if (client_hello.GetStringPiece(kSRCT, &srct) && |
+ ValidateSourceAddressToken(srct, client_ip, now_since_unix_epoch)) { |
+ valid_source_address_token = true; |
+ } |
+ |
+ const string fresh_source_address_token = |
+ NewSourceAddressToken(client_ip, rand, now_since_unix_epoch); |
+ |
+ // If we previously sent a REJ to this client then we may have stored a |
+ // server nonce in |params|. In which case, we know that the connection |
+ // is unique because the server nonce will be mixed into the key generation. |
+ bool unique_by_server_nonce = !params->server_nonce.empty(); |
+ // If we can't ensure uniqueness by a server nonce, then we will try and use |
+ // the strike register. |
+ bool unique_by_strike_register = false; |
+ |
+ StringPiece client_nonce; |
+ bool client_nonce_well_formed = false; |
+ if (client_hello.GetStringPiece(kNONC, &client_nonce) && |
+ client_nonce.size() == kNonceSize) { |
+ client_nonce_well_formed = true; |
+ base::AutoLock auto_lock(strike_register_lock_); |
+ |
+ if (strike_register_.get() == NULL) { |
+ strike_register_.reset(new StrikeRegister( |
+ // TODO(agl): these magic numbers should come from config. |
+ 1024 /* max entries */, |
+ static_cast<uint32>(now_since_unix_epoch.ToSeconds()), |
+ 600 /* window secs */, config->orbit)); |
+ } |
+ unique_by_strike_register = strike_register_->Insert( |
+ reinterpret_cast<const uint8*>(client_nonce.data()), |
+ static_cast<uint32>(now_since_unix_epoch.ToSeconds())); |
+ } |
+ |
+ out->Clear(); |
+ |
+ StringPiece sni; |
+ client_hello.GetStringPiece(kSNI, &sni); |
+ |
+ StringPiece scid; |
+ if (!client_hello.GetStringPiece(kSCID, &scid) || |
+ scid.as_string() != config->id || |
+ !valid_source_address_token || |
+ !client_nonce_well_formed || |
+ (!unique_by_strike_register && |
+ !unique_by_server_nonce)) { |
+ // If the client didn't provide a server config ID, or gave the wrong one, |
+ // then the handshake cannot possibly complete. We reject the handshake and |
+ // give the client enough information to do better next time. |
+ out->set_tag(kREJ); |
+ out->SetStringPiece(kSCFG, config->serialized); |
+ out->SetStringPiece(kSRCT, fresh_source_address_token); |
+ if (params->server_nonce.empty()) { |
+ CryptoUtils::GenerateNonce( |
+ now_since_unix_epoch, rand, |
+ StringPiece(reinterpret_cast<const char*>(config->orbit), |
+ sizeof(config->orbit)), |
+ ¶ms->server_nonce); |
+ } |
+ out->SetStringPiece(kNONC, params->server_nonce); |
+ |
+ // The client may have requested a certificate chain. |
+ const CryptoTag* their_proof_demands; |
+ size_t num_their_proof_demands; |
+ |
+ if (valid_source_address_token && |
+ proof_source_.get() != NULL && |
+ !sni.empty() && |
+ client_hello.GetTaglist(kPDMD, &their_proof_demands, |
+ &num_their_proof_demands) == QUIC_NO_ERROR) { |
+ for (size_t i = 0; i < num_their_proof_demands; i++) { |
+ if (their_proof_demands[i] != kX509) { |
+ continue; |
+ } |
+ |
+ // TODO(agl): in the future we will hopefully have a cached-info like |
+ // mechanism where we can omit certificates that the client already has. |
+ // In that case, the certificate chain may be small enough to include |
+ // without a source-address token. But, for now, we always send the full |
+ // chain and we always need a valid source-address token. |
+ |
+ const vector<string>* certs; |
+ string signature; |
+ if (!proof_source_->GetProof(sni.as_string(), config->serialized, |
+ &certs, &signature)) { |
+ break; |
+ } |
+ |
+ // TODO(agl): compress and omit certificates where possible based on |
+ // the client's cached certificates. |
+ size_t cert_bytes = 0; |
+ for (vector<string>::const_iterator i = certs->begin(); |
+ i != certs->end(); ++i) { |
+ cert_bytes += i->size(); |
+ } |
+ // There's a three byte length-prefix for each certificate. |
+ cert_bytes += certs->size()*3; |
+ scoped_ptr<char[]> buf(new char[cert_bytes]); |
+ |
+ size_t j = 0; |
+ for (vector<string>::const_iterator i = certs->begin(); |
+ i != certs->end(); ++i) { |
+ size_t len = i->size(); |
+ buf[j++] = len; |
+ buf[j++] = len >> 8; |
+ buf[j++] = len >> 16; |
+ memcpy(&buf[j], i->data(), i->size()); |
+ j += i->size(); |
+ } |
+ |
+ DCHECK_EQ(j, cert_bytes); |
+ |
+ out->SetStringPiece(kCERT, StringPiece(buf.get(), cert_bytes)); |
+ out->SetStringPiece(kPROF, signature); |
+ break; |
+ } |
+ } |
+ |
+ return QUIC_NO_ERROR; |
+ } |
+ |
+ const CryptoTag* their_aeads; |
+ const CryptoTag* their_key_exchanges; |
+ size_t num_their_aeads, num_their_key_exchanges; |
+ if (client_hello.GetTaglist(kAEAD, &their_aeads, |
+ &num_their_aeads) != QUIC_NO_ERROR || |
+ client_hello.GetTaglist(kKEXS, &their_key_exchanges, |
+ &num_their_key_exchanges) != QUIC_NO_ERROR || |
+ num_their_aeads != 1 || |
+ num_their_key_exchanges != 1) { |
+ *error_details = "Missing or invalid AEAD or KEXS"; |
+ return QUIC_INVALID_CRYPTO_MESSAGE_PARAMETER; |
+ } |
+ |
+ size_t key_exchange_index; |
+ if (!CryptoUtils::FindMutualTag(config->aead, |
+ their_aeads, num_their_aeads, |
+ CryptoUtils::LOCAL_PRIORITY, |
+ ¶ms->aead, |
+ NULL) || |
+ !CryptoUtils::FindMutualTag(config->kexs, |
+ their_key_exchanges, num_their_key_exchanges, |
+ CryptoUtils::LOCAL_PRIORITY, |
+ ¶ms->key_exchange, |
+ &key_exchange_index)) { |
+ *error_details = "Unsupported AEAD or KEXS"; |
+ return QUIC_CRYPTO_NO_SUPPORT; |
+ } |
+ |
+ StringPiece public_value; |
+ if (!client_hello.GetStringPiece(kPUBS, &public_value)) { |
+ *error_details = "Missing public value"; |
+ return QUIC_INVALID_CRYPTO_MESSAGE_PARAMETER; |
+ } |
+ |
+ if (!config->key_exchanges[key_exchange_index]->CalculateSharedKey( |
+ public_value, ¶ms->premaster_secret)) { |
+ *error_details = "Invalid public value"; |
+ return QUIC_INVALID_CRYPTO_MESSAGE_PARAMETER; |
+ } |
+ |
+ params->server_config_id = scid.as_string(); |
+ |
+ string hkdf_input(QuicCryptoConfig::kLabel, |
+ strlen(QuicCryptoConfig::kLabel) + 1); |
+ hkdf_input.append(reinterpret_cast<char*>(&guid), sizeof(guid)); |
+ |
+ const QuicData& client_hello_serialized = client_hello.GetSerialized(); |
+ hkdf_input.append(client_hello_serialized.data(), |
+ client_hello_serialized.length()); |
+ hkdf_input.append(config->serialized); |
+ |
+ CryptoUtils::DeriveKeys(params, client_nonce, hkdf_input, |
+ CryptoUtils::SERVER); |
+ |
+ out->set_tag(kSHLO); |
+ out->SetStringPiece(kSRCT, fresh_source_address_token); |
+ return QUIC_NO_ERROR; |
+} |
+ |
+void QuicCryptoServerConfig::SetProofSource(ProofSource* proof_source) { |
+ proof_source_.reset(proof_source); |
+} |
+ |
+string QuicCryptoServerConfig::NewSourceAddressToken( |
+ const IPEndPoint& ip, |
+ QuicRandom* rand, |
+ QuicTime::Delta now_since_epoch) const { |
+ SourceAddressToken source_address_token; |
+ source_address_token.set_ip(ip.ToString()); |
+ source_address_token.set_timestamp(now_since_epoch.ToSeconds()); |
+ |
+ string plaintext = source_address_token.SerializeAsString(); |
+ char nonce[12]; |
+ DCHECK_EQ(sizeof(nonce), |
+ source_address_token_encrypter_->GetNoncePrefixSize() + |
+ sizeof(QuicPacketSequenceNumber)); |
+ rand->RandBytes(nonce, sizeof(nonce)); |
+ |
+ size_t ciphertext_size = |
+ source_address_token_encrypter_->GetCiphertextSize(plaintext.size()); |
+ string result; |
+ result.resize(sizeof(nonce) + ciphertext_size); |
+ memcpy(&result[0], &nonce, sizeof(nonce)); |
+ |
+ if (!source_address_token_encrypter_->Encrypt( |
+ StringPiece(nonce, sizeof(nonce)), StringPiece(), plaintext, |
+ reinterpret_cast<unsigned char*>(&result[sizeof(nonce)]))) { |
+ DCHECK(false); |
+ return string(); |
+ } |
+ |
+ return result; |
+} |
+ |
+bool QuicCryptoServerConfig::ValidateSourceAddressToken( |
+ StringPiece token, |
+ const IPEndPoint& ip, |
+ QuicTime::Delta now_since_epoch) const { |
+ char nonce[12]; |
+ DCHECK_EQ(sizeof(nonce), |
+ source_address_token_encrypter_->GetNoncePrefixSize() + |
+ sizeof(QuicPacketSequenceNumber)); |
+ |
+ if (token.size() <= sizeof(nonce)) { |
+ return false; |
+ } |
+ memcpy(&nonce, token.data(), sizeof(nonce)); |
+ token.remove_prefix(sizeof(nonce)); |
+ |
+ unsigned char plaintext_stack[128]; |
+ scoped_ptr<unsigned char[]> plaintext_heap; |
+ unsigned char* plaintext; |
+ if (token.size() <= sizeof(plaintext_stack)) { |
+ plaintext = plaintext_stack; |
+ } else { |
+ plaintext_heap.reset(new unsigned char[token.size()]); |
+ plaintext = plaintext_heap.get(); |
+ } |
+ size_t plaintext_length; |
+ |
+ if (!source_address_token_decrypter_->Decrypt( |
+ StringPiece(nonce, sizeof(nonce)), StringPiece(), token, |
+ plaintext, &plaintext_length)) { |
+ return false; |
+ } |
+ |
+ SourceAddressToken source_address_token; |
+ if (!source_address_token.ParseFromArray(plaintext, plaintext_length)) { |
+ return false; |
+ } |
+ |
+ if (source_address_token.ip() != ip.ToString()) { |
+ // It's for a different IP address. |
+ return false; |
+ } |
+ |
+ const QuicTime::Delta delta(now_since_epoch.Subtract( |
+ QuicTime::Delta::FromSeconds(source_address_token.timestamp()))); |
+ const int64 delta_secs = delta.ToSeconds(); |
+ |
+ // TODO(agl): consider whether and how these magic values should be moved to |
+ // a config. |
+ if (delta_secs < -3600) { |
+ // We only allow timestamps to be from an hour in the future. |
+ return false; |
+ } |
+ |
+ if (delta_secs > 86400) { |
+ // We allow one day into the past. |
+ return false; |
+ } |
+ |
+ return true; |
+} |
+ |
+QuicCryptoServerConfig::Config::Config() { |
+} |
+ |
+QuicCryptoServerConfig::Config::~Config() { |
+ STLDeleteElements(&key_exchanges); |
+} |
+ |
+} // namespace net |