Skip to content

Commit

Permalink
src: move hkdf impl to ncrypto
Browse files Browse the repository at this point in the history
  • Loading branch information
jasnell committed Aug 29, 2024
1 parent ff5ef70 commit 27447ca
Show file tree
Hide file tree
Showing 3 changed files with 115 additions and 51 deletions.
79 changes: 79 additions & 0 deletions deps/ncrypto/ncrypto.cc
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#include <openssl/dh.h>
#include <openssl/bn.h>
#include <openssl/evp.h>
#include <openssl/hmac.h>
#include <openssl/pkcs12.h>
#include <openssl/x509v3.h>
#if OPENSSL_VERSION_MAJOR >= 3
Expand Down Expand Up @@ -1252,4 +1253,82 @@ DataPointer DHPointer::stateless(const EVPKeyPointer& ourKey,
return out;
}

// ============================================================================
// HKDF

const EVP_MD* getDigestByName(const std::string_view name) {
return EVP_get_digestbyname(name.data());
}

bool checkHkdfLength(const EVP_MD* md, size_t length) {
// HKDF-Expand computes up to 255 HMAC blocks, each having as many bits as
// the output of the hash function. 255 is a hard limit because HKDF appends
// an 8-bit counter to each HMAC'd message, starting at 1.
static constexpr size_t kMaxDigestMultiplier = 255;
size_t max_length = EVP_MD_size(md) * kMaxDigestMultiplier;
if (length > max_length) return false;
return true;
}

DataPointer hkdf(const EVP_MD* md,
const Buffer<const unsigned char>& key,
const Buffer<const unsigned char>& info,
const Buffer<const unsigned char>& salt,
size_t length) {
if (!checkHkdfLength(md, length) ||
info.len > INT_MAX ||
salt.len > INT_MAX) {
return {};
}

EVPKeyCtxPointer ctx =
EVPKeyCtxPointer(EVP_PKEY_CTX_new_id(EVP_PKEY_HKDF, nullptr));
if (!ctx ||
!EVP_PKEY_derive_init(ctx.get()) ||
!EVP_PKEY_CTX_set_hkdf_md(ctx.get(), md) ||
!EVP_PKEY_CTX_add1_hkdf_info(ctx.get(), info.data, info.len)) {
return {};
}

std::string_view actual_salt;
static const char default_salt[EVP_MAX_MD_SIZE] = {0};
if (salt.len > 0) {
actual_salt = {reinterpret_cast<const char*>(salt.data), salt.len};
} else {
actual_salt = {default_salt, static_cast<unsigned>(EVP_MD_size(md))};
}

// We do not use EVP_PKEY_HKDF_MODE_EXTRACT_AND_EXPAND because and instead
// implement the extraction step ourselves because EVP_PKEY_derive does not
// handle zero-length keys, which are required for Web Crypto.
// TODO: Once OpenSSL 1.1.1 support is dropped completely, and once BoringSSL
// is confirmed to support it, wen can hopefully drop this and use EVP_KDF
// directly which does support zero length keys.
unsigned char pseudorandom_key[EVP_MAX_MD_SIZE];
unsigned pseudorandom_key_len = sizeof(pseudorandom_key);

if (HMAC(md,
actual_salt.data(),
actual_salt.size(),
key.data,
key.len,
pseudorandom_key,
&pseudorandom_key_len) == nullptr) {
return {};
}
if (!EVP_PKEY_CTX_hkdf_mode(ctx.get(), EVP_PKEY_HKDEF_MODE_EXPAND_ONLY) ||
!EVP_PKEY_CTX_set1_hkdf_key(ctx.get(), pseudorandom_key, pseudorandom_key_len)) {
return {};
}

auto buf = DataPointer::Alloc(length);
if (!buf) return {};

if (EVP_PKEY_derive(ctx.get(), static_cast<unsigned char*>(buf.get()), &length) <= 0) {
return {};
}

return buf;
}

} // namespace ncrypto
16 changes: 16 additions & 0 deletions deps/ncrypto/ncrypto.h
Original file line number Diff line number Diff line change
Expand Up @@ -588,6 +588,22 @@ BIOPointer ExportPublicKey(const char* input, size_t length);
// The caller takes ownership of the returned Buffer<char>
Buffer<char> ExportChallenge(const char* input, size_t length);

// ============================================================================
// HKDF

const EVP_MD* getDigestByName(const std::string_view name);

// Verify that the specified HKDF output length is valid for the given digest.
// The maximum length for HKDF output for a given digest is 255 times the
// hash size for the given digest algorithm.
bool checkHkdfLength(const EVP_MD* md, size_t length);

DataPointer hkdf(const EVP_MD* md,
const Buffer<const unsigned char>& key,
const Buffer<const unsigned char>& info,
const Buffer<const unsigned char>& salt,
size_t length);

// ============================================================================
// Version metadata
#define NCRYPTO_VERSION "0.0.1"
Expand Down
71 changes: 20 additions & 51 deletions src/crypto/crypto_hkdf.cc
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ Maybe<bool> HKDFTraits::AdditionalConfig(
CHECK(args[offset + 4]->IsUint32()); // Length

Utf8Value hash(env->isolate(), args[offset]);
params->digest = EVP_get_digestbyname(*hash);
params->digest = ncrypto::getDigestByName(hash.ToStringView());
if (params->digest == nullptr) {
THROW_ERR_CRYPTO_INVALID_DIGEST(env, "Invalid digest: %s", *hash);
return Nothing<bool>();
Expand Down Expand Up @@ -90,9 +90,7 @@ Maybe<bool> HKDFTraits::AdditionalConfig(
// HKDF-Expand computes up to 255 HMAC blocks, each having as many bits as the
// output of the hash function. 255 is a hard limit because HKDF appends an
// 8-bit counter to each HMAC'd message, starting at 1.
constexpr size_t kMaxDigestMultiplier = 255;
size_t max_length = EVP_MD_size(params->digest) * kMaxDigestMultiplier;
if (params->length > max_length) {
if (!ncrypto::checkHkdfLength(params->digest, params->length)) {
THROW_ERR_CRYPTO_INVALID_KEYLEN(env);
return Nothing<bool>();
}
Expand All @@ -104,53 +102,24 @@ bool HKDFTraits::DeriveBits(
Environment* env,
const HKDFConfig& params,
ByteSource* out) {
EVPKeyCtxPointer ctx =
EVPKeyCtxPointer(EVP_PKEY_CTX_new_id(EVP_PKEY_HKDF, nullptr));
if (!ctx || !EVP_PKEY_derive_init(ctx.get()) ||
!EVP_PKEY_CTX_set_hkdf_md(ctx.get(), params.digest) ||
!EVP_PKEY_CTX_add1_hkdf_info(
ctx.get(), params.info.data<unsigned char>(), params.info.size())) {
return false;
}

// TODO(panva): Once support for OpenSSL 1.1.1 is dropped the whole
// of HKDFTraits::DeriveBits can be refactored to use
// EVP_KDF which does handle zero length key.

std::string_view salt;
if (params.salt.size() != 0) {
salt = {params.salt.data<char>(), params.salt.size()};
} else {
static const char default_salt[EVP_MAX_MD_SIZE] = {0};
salt = {default_salt, static_cast<unsigned>(EVP_MD_size(params.digest))};
}

// We do not use EVP_PKEY_HKDEF_MODE_EXTRACT_AND_EXPAND and instead implement
// the extraction step ourselves because EVP_PKEY_derive does not handle
// zero-length keys, which are required for Web Crypto.
unsigned char pseudorandom_key[EVP_MAX_MD_SIZE];
unsigned int prk_len = sizeof(pseudorandom_key);
if (HMAC(
params.digest,
salt.data(),
salt.size(),
reinterpret_cast<const unsigned char*>(params.key->GetSymmetricKey()),
params.key->GetSymmetricKeySize(),
pseudorandom_key,
&prk_len) == nullptr) {
return false;
}
if (!EVP_PKEY_CTX_hkdf_mode(ctx.get(), EVP_PKEY_HKDEF_MODE_EXPAND_ONLY) ||
!EVP_PKEY_CTX_set1_hkdf_key(ctx.get(), pseudorandom_key, prk_len)) {
return false;
}

size_t length = params.length;
ByteSource::Builder buf(length);
if (EVP_PKEY_derive(ctx.get(), buf.data<unsigned char>(), &length) <= 0)
return false;

*out = std::move(buf).release();
auto dp = ncrypto::hkdf(
params.digest,
ncrypto::Buffer<const unsigned char> {
.data = reinterpret_cast<const unsigned char*>(params.key->GetSymmetricKey()),
.len = params.key->GetSymmetricKeySize(),
},
ncrypto::Buffer<const unsigned char> {
.data = params.info.data<const unsigned char>(),
.len = params.info.size(),
},
ncrypto::Buffer<const unsigned char> {
.data = params.salt.data<const unsigned char>(),
.len = params.salt.size(),
},
params.length);
if (!dp) return false;

*out = ByteSource::Allocated(dp.release());
return true;
}

Expand Down

0 comments on commit 27447ca

Please sign in to comment.