fizz/protocol/ech/Encryption.cpp (310 lines of code) (raw):
/*
* Copyright (c) 2018-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree.
*/
#include <fizz/protocol/ech/Encryption.h>
#include "fizz/record/Types.h"
#include <fizz/crypto/Sha256.h>
#include <fizz/crypto/Sha384.h>
#include <fizz/crypto/hpke/Utils.h>
#include <fizz/protocol/Protocol.h>
#include <fizz/protocol/ech/ECHExtensions.h>
#include <fizz/protocol/ech/Types.h>
#include <iterator>
namespace fizz {
namespace ech {
namespace {
std::unique_ptr<folly::IOBuf> makeClientHelloOuterForAad(
const ClientHello& clientHelloOuter) {
// Copy client hello outer
ClientHello chloCopy = clientHelloOuter.clone();
// Remove ech extension from the copy
auto it =
findExtension(chloCopy.extensions, ExtensionType::encrypted_client_hello);
chloCopy.extensions.erase(it);
// Get the serialized version of the client hello outer
// without the ECH extension to use
auto clientHelloOuterAad = encode(chloCopy);
return clientHelloOuterAad;
}
std::unique_ptr<folly::IOBuf> extractEncodedClientHelloInner(
ECHVersion version,
std::unique_ptr<folly::IOBuf> configId,
const ECHCipherSuite& cipherSuite,
const std::unique_ptr<folly::IOBuf>& encapsulatedKey,
std::unique_ptr<folly::IOBuf> encryptedCh,
hpke::HpkeContext& context,
const ClientHello& clientHelloOuter) {
std::unique_ptr<folly::IOBuf> encodedClientHelloInner;
switch (version) {
case ECHVersion::Draft9: {
auto aadCH = makeClientHelloOuterForAad(clientHelloOuter);
auto chloOuterAad =
makeClientHelloAad(cipherSuite, configId, encapsulatedKey, aadCH);
encodedClientHelloInner =
context.open(chloOuterAad.get(), std::move(encryptedCh));
}
}
return encodedClientHelloInner;
}
std::unique_ptr<folly::IOBuf> makeHpkeContextInfoParam(
const ECHConfig& echConfig) {
switch (echConfig.version) {
case ECHVersion::Draft9: {
// The "info" parameter to setupWithEncap is the
// concatenation of "tls ech", a zero byte, and the serialized
// ECHConfig.
std::string tlsEchPrefix = "tls ech";
tlsEchPrefix += '\0';
auto bufContents = folly::IOBuf::copyBuffer(tlsEchPrefix);
bufContents->prependChain(encode(echConfig));
return bufContents;
}
}
return nullptr;
}
} // namespace
std::unique_ptr<folly::IOBuf> constructConfigId(
hpke::KDFId kdfId,
ECHConfig echConfig) {
std::unique_ptr<HkdfImpl> hkdf;
// Draft 9 set this to a fixed value
const size_t hashLen = 8;
switch (kdfId) {
case (hpke::KDFId::Sha256): {
hkdf = std::make_unique<HkdfImpl>(HkdfImpl::create<Sha256>());
break;
}
case (hpke::KDFId::Sha384): {
hkdf = std::make_unique<HkdfImpl>(HkdfImpl::create<Sha384>());
break;
}
default: {
throw std::runtime_error("kdf: not implemented");
}
}
auto extractedChlo = hkdf->extract(
folly::IOBuf::copyBuffer("")->coalesce(),
encode(std::move(echConfig))->coalesce());
return hkdf->expand(
extractedChlo, *folly::IOBuf::copyBuffer("tls ech config id"), hashLen);
}
folly::Optional<SupportedECHConfig> selectECHConfig(
const std::vector<ECHConfig>& configs,
std::vector<hpke::KEMId> supportedKEMs,
std::vector<hpke::AeadId> supportedAeads) {
// Received set of configs is in order of server preference so
// we should be selecting the first one that we can support.
for (const auto& config : configs) {
folly::io::Cursor cursor(config.ech_config_content.get());
if (config.version == ECHVersion::Draft9) {
auto echConfig = decode<ECHConfigContentDraft>(cursor);
// Check if we (client) support the server's chosen KEM.
auto result = std::find(
supportedKEMs.begin(), supportedKEMs.end(), echConfig.kem_id);
if (result == supportedKEMs.end()) {
continue;
}
// Check if we (client) support the HPKE cipher suite.
auto cipherSuites = echConfig.cipher_suites;
for (auto& suite : cipherSuites) {
auto isCipherSupported =
std::find(
supportedAeads.begin(), supportedAeads.end(), suite.aead_id) !=
supportedAeads.end();
if (isCipherSupported) {
auto associatedCipherKdf =
hpke::getKDFId(getHashFunction(getCipherSuite(suite.aead_id)));
if (suite.kdf_id == associatedCipherKdf) {
auto supportedConfig = config;
return SupportedECHConfig{supportedConfig, suite};
}
}
}
}
}
return folly::none;
}
static hpke::SetupParam getSetupParam(
std::unique_ptr<DHKEM> dhkem,
std::unique_ptr<folly::IOBuf> prefix,
hpke::KEMId kemId,
const ECHCipherSuite& cipherSuite) {
// Get suite id
auto group = getKexGroup(kemId);
auto hash = getHashFunction(cipherSuite.kdf_id);
auto suite = getCipherSuite(cipherSuite.aead_id);
auto suiteId = hpke::generateHpkeSuiteId(group, hash, suite);
auto hkdf = hpke::makeHpkeHkdf(std::move(prefix), cipherSuite.kdf_id);
return hpke::SetupParam{
std::move(dhkem),
makeCipher(cipherSuite.aead_id),
std::move(hkdf),
std::move(suiteId)};
}
std::unique_ptr<folly::IOBuf> getRecordDigest(
const ECHConfig& echConfig,
hpke::KDFId id) {
switch (id) {
case hpke::KDFId::Sha256: {
std::array<uint8_t, fizz::Sha256::HashLen> recordDigest;
fizz::Sha256::hash(
*encode(echConfig),
folly::MutableByteRange(recordDigest.data(), recordDigest.size()));
return folly::IOBuf::copyBuffer(recordDigest);
}
case hpke::KDFId::Sha384: {
std::array<uint8_t, fizz::Sha384::HashLen> recordDigest;
fizz::Sha384::hash(
*encode(echConfig),
folly::MutableByteRange(recordDigest.data(), recordDigest.size()));
return folly::IOBuf::copyBuffer(recordDigest);
}
default:
throw std::runtime_error("kdf: not implemented");
}
}
hpke::SetupResult constructHpkeSetupResult(
std::unique_ptr<KeyExchange> kex,
const SupportedECHConfig& supportedConfig) {
const std::unique_ptr<folly::IOBuf> prefix =
folly::IOBuf::copyBuffer("HPKE-07");
folly::io::Cursor cursor(supportedConfig.config.ech_config_content.get());
auto config = decode<ECHConfigContentDraft>(cursor);
auto cipherSuite = supportedConfig.cipherSuite;
// Get shared secret
auto hkdf = hpke::makeHpkeHkdf(prefix->clone(), cipherSuite.kdf_id);
std::unique_ptr<DHKEM> dhkem = std::make_unique<DHKEM>(
std::move(kex), getKexGroup(config.kem_id), std::move(hkdf));
// Get context
std::unique_ptr<folly::IOBuf> info =
makeHpkeContextInfoParam(supportedConfig.config);
return setupWithEncap(
hpke::Mode::Base,
config.public_key->clone()->coalesce(),
std::move(info),
folly::none,
getSetupParam(
std::move(dhkem), prefix->clone(), config.kem_id, cipherSuite));
}
std::unique_ptr<folly::IOBuf> makeClientHelloAad(
ECHCipherSuite cipherSuite,
const std::unique_ptr<folly::IOBuf>& configId,
const std::unique_ptr<folly::IOBuf>& enc,
const std::unique_ptr<folly::IOBuf>& clientHello) {
auto aad = folly::IOBuf::create(0);
folly::io::Appender appender(aad.get(), 32);
detail::write<ech::ECHCipherSuite>(cipherSuite, appender);
detail::writeBuf<uint8_t>(configId, appender);
detail::writeBuf<uint16_t>(enc, appender);
detail::writeBuf<detail::bits24>(clientHello, appender);
return aad;
}
ClientECH encryptClientHello(
const SupportedECHConfig& supportedConfig,
const ClientHello& clientHelloInner,
const ClientHello& clientHelloOuter,
hpke::SetupResult setupResult) {
// Create ECH extension
ClientECH echExtension;
echExtension.cipher_suite = supportedConfig.cipherSuite;
echExtension.config_id = constructConfigId(
supportedConfig.cipherSuite.kdf_id, supportedConfig.config);
echExtension.enc = std::move(setupResult.enc);
// Remove legacy_session_id and serialize the client hello inner
auto chloInnerCopy = clientHelloInner.clone();
chloInnerCopy.legacy_session_id = folly::IOBuf::copyBuffer("");
auto encodedClientHelloInner = encode(chloInnerCopy);
// Compute the AAD for sealing
auto clientHelloOuterEnc = encode(clientHelloOuter);
auto clientHelloOuterAad = makeClientHelloAad(
supportedConfig.cipherSuite,
echExtension.config_id,
echExtension.enc,
clientHelloOuterEnc);
// Encrypt inner client hello
echExtension.payload = setupResult.context.seal(
clientHelloOuterAad.get(), std::move(encodedClientHelloInner));
return echExtension;
}
folly::Optional<ClientHello> tryToDecryptECH(
const ClientHello& clientHelloOuter,
const ECHConfig& echConfig,
ECHCipherSuite cipherSuite,
std::unique_ptr<folly::IOBuf> encapsulatedKey,
std::unique_ptr<folly::IOBuf> encryptedCh,
std::unique_ptr<KeyExchange> kex,
ECHVersion version) {
const std::unique_ptr<folly::IOBuf> prefix =
folly::IOBuf::copyBuffer("HPKE-07");
// Get crypto primitive types used for decrypting
hpke::KDFId kdfId = cipherSuite.kdf_id;
folly::io::Cursor echConfigCursor(echConfig.ech_config_content.get());
auto decodedConfigContent = decode<ECHConfigContentDraft>(echConfigCursor);
auto kemId = decodedConfigContent.kem_id;
NamedGroup group = hpke::getKexGroup(kemId);
// Try to decrypt and get the client hello inner
try {
auto dhkem = std::make_unique<DHKEM>(
std::move(kex), group, hpke::makeHpkeHkdf(prefix->clone(), kdfId));
auto aeadId = cipherSuite.aead_id;
auto suiteId = hpke::generateHpkeSuiteId(
group, hpke::getHashFunction(kdfId), hpke::getCipherSuite(aeadId));
hpke::SetupParam setupParam{
std::move(dhkem),
makeCipher(aeadId),
hpke::makeHpkeHkdf(prefix->clone(), kdfId),
std::move(suiteId)};
std::unique_ptr<folly::IOBuf> info = makeHpkeContextInfoParam(echConfig);
auto context = hpke::setupWithDecap(
hpke::Mode::Base,
encapsulatedKey->coalesce(),
std::move(info),
folly::none,
std::move(setupParam));
auto encodedClientHelloInner = extractEncodedClientHelloInner(
version,
constructConfigId(cipherSuite.kdf_id, echConfig),
cipherSuite,
encapsulatedKey,
std::move(encryptedCh),
context,
clientHelloOuter);
// Set actual client hello, ECH acceptance
folly::io::Cursor encodedECHInnerCursor(encodedClientHelloInner.get());
auto decodedChlo = decode<ClientHello>(encodedECHInnerCursor);
decodedChlo.originalEncoding = encodeHandshake(decodedChlo);
// Replace legacy_session_id that got removed during encryption
decodedChlo.legacy_session_id = clientHelloOuter.legacy_session_id->clone();
// TODO: Scan for outer_extensions extension.
return decodedChlo;
} catch (const std::exception&) {
}
return folly::none;
}
std::vector<Extension> substituteOuterExtensions(
std::vector<Extension>&& innerExt,
const std::vector<Extension>& outerExt) {
std::vector<Extension> expandedInnerExt;
// validate that the innerExt has no duplicate extensions.
Protocol::checkDuplicateExtensions(innerExt);
// locate echOuterExtension
auto echOuterExtension = std::find_if(
innerExt.cbegin(), innerExt.cend(), [](const Extension& ext) {
return ext.extension_type == fizz::ExtensionType::ech_outer_extensions;
});
// Return innerExt if no ech_outer_extensions extension was found.
if (echOuterExtension == innerExt.end()) {
return std::move(innerExt);
}
folly::io::Cursor cursor(echOuterExtension->extension_data.get());
std::vector<ExtensionType> outerExtsToCopy;
fizz::detail::readVector<std::uint8_t>(outerExtsToCopy, cursor);
// Insert all innerExt extensions before ech_outer_extensions.
std::transform(
innerExt.cbegin(),
echOuterExtension,
std::back_inserter(expandedInnerExt),
[](const Extension& ext) { return ext.clone(); });
/**
* Expand ech_outer_extensions using the following two pointer approach:
* Iterate over the outerExt and when both pointers have
* equivalent extensions types, we increment idxInner
* –indicating that we've validated an extension is present in
* outerExt and maintains its relative ordering.
*/
std::size_t idxInner = 0;
for (const auto& ext : outerExt) {
if (idxInner >= outerExtsToCopy.size()) {
break;
}
if ((ext.extension_type == outerExtsToCopy[idxInner])) {
expandedInnerExt.push_back(ext.clone());
idxInner++;
}
}
/**
* If we've reached the end of the outerExtensionPtrs, the following
* requirements have been satisfied:
*
* 1. All extensions in outerExtension are present in clientOuterHello
* 2. The extensions in outerExtension preserve the relative ordering of
* extensions in clientOuterHello.
*
* Otherwise we've violated one of the requirements.
**/
if (idxInner < outerExtsToCopy.size()) {
throw fizz::FizzException(
"Requirements for OuterExtensions not met.",
AlertDescription::decode_error);
}
// Add all extensions that follow the ech_outer_extensions; resulting vector
// is the list of substituted extensions
std::transform(
std::next(echOuterExtension, 1),
innerExt.cend(),
std::back_inserter(expandedInnerExt),
[](const Extension& ext) { return ext.clone(); });
// Verify that there are no duplicate extensions.
std::unordered_set<ExtensionType> expandedExtTypes;
std::transform(
expandedInnerExt.cbegin(),
expandedInnerExt.cend(),
std::inserter(expandedExtTypes, expandedExtTypes.begin()),
[](const Extension& ext) { return ext.extension_type; });
if (expandedExtTypes.size() != expandedInnerExt.size()) {
throw FizzException(
"Duplicate extensions", AlertDescription::illegal_parameter);
}
return expandedInnerExt;
}
} // namespace ech
} // namespace fizz