Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion .github/workflows/build-shared.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,11 @@ on:
required: false
type: string
default: ''
pkcs11-store-test:
description: Whether to enable the PKCS#11-backed crypto STORE test
required: false
type: boolean
default: false
secrets:
CACHIX_AUTH_TOKEN:
description: Cachix auth token for nodejs.cachix.org.
Expand Down Expand Up @@ -75,10 +80,12 @@ jobs:

- name: Build Node.js and run tests
shell: bash
env:
NODE_TEST_PKCS11_NIX: ${{ inputs.pkcs11-store-test && '1' || '0' }}
run: |
nix-shell \
-I "nixpkgs=$TAR_DIR/tools/nix/pkgs.nix" \
--pure --keep TAR_DIR --keep FLAKY_TESTS \
--pure --keep TAR_DIR --keep FLAKY_TESTS --keep NODE_TEST_PKCS11_NIX \
--keep SCCACHE_GHA_ENABLED --keep ACTIONS_CACHE_SERVICE_V2 --keep ACTIONS_RESULTS_URL --keep ACTIONS_RUNTIME_TOKEN \
--arg loadJSBuiltinsDynamically false \
--arg ccache "${NIX_SCCACHE:-null}" \
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/test-shared.yml
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,7 @@ jobs:
with:
runner: ubuntu-24.04-arm
v8-nar: ${{ needs.build-aarch64-linux-v8.outputs.local-cache && 'libv8-aarch64-linux.nar' }}
pkcs11-store-test: ${{ matrix.openssl.attr == 'openssl_3_5' }}
# Override just the `openssl` attr of the default shared-lib set with
# the matrix-selected nixpkgs attribute (e.g. `openssl_3_6`). All
# other shared libs (brotli, cares, libuv, …) keep their defaults.
Expand Down
200 changes: 189 additions & 11 deletions deps/ncrypto/ncrypto.cc
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,13 @@
#include <openssl/dh.h>
#include <openssl/evp.h>
#include <openssl/hmac.h>
#include <openssl/pem.h>
#include <openssl/pkcs12.h>
#include <openssl/rand.h>
#include <openssl/x509v3.h>
#if NCRYPTO_USE_BORINGSSL_EVP_DO_ALL_FALLBACK
#include <openssl/bytestring.h>
#include <openssl/cipher.h>
#include <openssl/pem.h>
#endif
#include <algorithm>
#include <array>
Expand All @@ -21,6 +21,8 @@
#include <openssl/core_names.h>
#include <openssl/params.h>
#include <openssl/provider.h>
#include <openssl/store.h>
#include <openssl/ui.h>
#if OPENSSL_WITH_ARGON2
#include <openssl/thread.h>
#endif
Expand Down Expand Up @@ -76,6 +78,17 @@ using BignumCtxPointer = DeleteFnPtr<BN_CTX, BN_CTX_free>;
using BignumGenCallbackPointer = DeleteFnPtr<BN_GENCB, BN_GENCB_free>;
using NetscapeSPKIPointer = DeleteFnPtr<NETSCAPE_SPKI, NETSCAPE_SPKI_free>;

#if NCRYPTO_USE_OPENSSL3_PROVIDER
using X509PubKeyPointer = DeleteFnPtr<X509_PUBKEY, X509_PUBKEY_free>;
// OSSL_STORE_close() returns int, so it needs a void-returning adapter to be
// usable as a DeleteFnPtr deleter.
void CloseStoreCtx(OSSL_STORE_CTX* ctx) {
OSSL_STORE_close(ctx);
}
using StoreCtxPointer = DeleteFnPtr<OSSL_STORE_CTX, CloseStoreCtx>;
using UIMethodPointer = DeleteFnPtr<UI_METHOD, UI_destroy_method>;
#endif

const EVP_CIPHER* GetCipherCtxCipher(const EVP_CIPHER_CTX* ctx) {
#if NCRYPTO_USE_OPENSSL3_PROVIDER
return EVP_CIPHER_CTX_get0_cipher(ctx);
Expand Down Expand Up @@ -332,7 +345,7 @@ ClearErrorOnReturn::~ClearErrorOnReturn() {
ERR_clear_error();
}

int ClearErrorOnReturn::peekError() {
unsigned long ClearErrorOnReturn::peekError() { // NOLINT(runtime/int)
return ERR_peek_error();
}

Expand All @@ -346,7 +359,7 @@ MarkPopErrorOnReturn::~MarkPopErrorOnReturn() {
ERR_pop_to_mark();
}

int MarkPopErrorOnReturn::peekError() {
unsigned long MarkPopErrorOnReturn::peekError() { // NOLINT(runtime/int)
return ERR_peek_error();
}

Expand Down Expand Up @@ -840,6 +853,7 @@ int NoPasswordCallback(char* buf, int size, int rwflag, void* u) {

int PasswordCallback(char* buf, int size, int rwflag, void* u) {
auto passphrase = static_cast<const Buffer<char>*>(u);
if (size <= 0) return -1;
if (passphrase != nullptr) {
size_t buflen = static_cast<size_t>(size);
size_t len = passphrase->len;
Expand All @@ -851,6 +865,31 @@ int PasswordCallback(char* buf, int size, int rwflag, void* u) {
return -1;
}

#if NCRYPTO_USE_OPENSSL3_PROVIDER
namespace {
struct StorePassphraseData {
Buffer<char> passphrase{.data = nullptr, .len = 0};
bool has_passphrase = false;
bool missing_passphrase = false;
};

int StorePasswordCallback(char* buf, int size, int rwflag, void* u) {
auto data = static_cast<StorePassphraseData*>(u);
if (data == nullptr || !data->has_passphrase) {
if (data != nullptr) data->missing_passphrase = true;
return -1;
}

if (size <= 0) return -1;
size_t buflen = static_cast<size_t>(size);
size_t len = data->passphrase.len;
if (buflen < len) return -1;
memcpy(buf, reinterpret_cast<const char*>(data->passphrase.data), len);
return len;
}
} // namespace
#endif

// Algorithm: http://howardhinnant.github.io/date_algorithms.html
constexpr int days_from_epoch(int y, unsigned m, unsigned d) {
y -= m <= 2;
Expand Down Expand Up @@ -3584,7 +3623,7 @@ EVPKeyPointer::ParseKeyResult EVPKeyPointer::TryParsePrivateKey(
const Buffer<const unsigned char>& buffer) {
static constexpr auto keyOrError = [](EVPKeyPointer pkey,
bool had_passphrase = false) {
if (int err = ERR_peek_error()) {
if (unsigned long err = ERR_peek_error()) { // NOLINT(runtime/int)
if (ERR_GET_LIB(err) == ERR_LIB_PEM &&
ERR_GET_REASON(err) == PEM_R_BAD_PASSWORD_READ && !had_passphrase) {
return ParseKeyResult(PKParseError::NEED_PASSPHRASE);
Expand Down Expand Up @@ -3644,6 +3683,98 @@ EVPKeyPointer::ParseKeyResult EVPKeyPointer::TryParsePrivateKey(
};
}

EVPKeyPointer::ParseKeyResult EVPKeyPointer::TryLoadPrivateKeyFromStore(
const StorePrivateKeyConfig& config) {
#if !NCRYPTO_USE_OPENSSL3_PROVIDER
return ParseKeyResult(PKParseError::FAILED);
#else
// The error queue is left populated on failure so the caller can surface a
// `code` and an `opensslErrorStack`, matching TryParsePrivateKey(), and is
// cleared on success because decoders leave entries behind either way.
std::string uri_str(config.uri);
std::string properties_str;
const char* properties = nullptr;
if (config.properties.has_value() && !config.properties->empty()) {
properties_str.assign(config.properties->data(), config.properties->size());
properties = properties_str.c_str();
}

// config.passphrase outlives this call, so no copy is needed.
Buffer<char> passbuf{.data = nullptr, .len = 0};
if (config.passphrase.has_value()) {
passbuf.data = const_cast<char*>(config.passphrase->data);
passbuf.len = config.passphrase->len;
}
StorePassphraseData passphrase_data{
.passphrase = passbuf,
.has_passphrase = config.passphrase.has_value(),
};
// Declared before ctx so that reverse destruction closes the store first;
// it holds both for its lifetime.
UIMethodPointer ui_method(
UI_UTIL_wrap_read_pem_callback(StorePasswordCallback, 0));
if (!ui_method) return ParseKeyResult(PKParseError::FAILED);

// Errors from loaders that declined the URI are retained oldest-first, so the
// newest entry is the loader that actually handled it. Must run before ctx is
// destroyed, since OSSL_STORE_close() can push errors of its own.
const auto failed = [&](bool missing_passphrase) {
if (missing_passphrase)
return ParseKeyResult(PKParseError::NEED_PASSPHRASE);
return ParseKeyResult(PKParseError::FAILED, ERR_peek_last_error());
};

const OSSL_PARAM store_params[] = {OSSL_PARAM_END};
StoreCtxPointer ctx(OSSL_STORE_open_ex(uri_str.c_str(),
nullptr,
properties,
ui_method.get(),
&passphrase_data,
store_params,
nullptr,
nullptr));
if (!ctx) return failed(passphrase_data.missing_passphrase);

if (!OSSL_STORE_expect(ctx.get(), OSSL_STORE_INFO_PKEY)) {
return failed(passphrase_data.missing_passphrase);
}

EVPKeyPointer pkey;
bool store_error = false;
while (!OSSL_STORE_eof(ctx.get())) {
OSSL_STORE_INFO* info = OSSL_STORE_load(ctx.get());
if (info == nullptr) {
if (OSSL_STORE_error(ctx.get())) {
store_error = true;
break;
}
continue;
}
if (OSSL_STORE_INFO_get_type(info) == OSSL_STORE_INFO_PKEY) {
EVP_PKEY* raw_pkey = OSSL_STORE_INFO_get1_PKEY(info);
if (raw_pkey != nullptr) {
pkey = EVPKeyPointer(raw_pkey);
} else {
store_error = true;
}
}
OSSL_STORE_INFO_free(info);
if (pkey || store_error) break;
}

// missing_passphrase is sticky, so a key that loaded anyway wins over it.
if (pkey) {
ERR_clear_error();
return ParseKeyResult(std::move(pkey));
}

if (passphrase_data.missing_passphrase || store_error) {
return failed(passphrase_data.missing_passphrase);
}
return ParseKeyResult(PKParseError::NOT_RECOGNIZED);
#endif
}

Result<BIOPointer, bool> EVPKeyPointer::writePrivateKey(
const PrivateKeyEncodingConfig& config) const {
if (config.format == PKFormatType::JWK) {
Expand Down Expand Up @@ -3685,6 +3816,8 @@ Result<BIOPointer, bool> EVPKeyPointer::writePrivateKey(
#else
RSA* rsa = EVP_PKEY_get0_RSA(get());
#endif
if (rsa == nullptr) return Result<BIOPointer, bool>(false);

switch (config.format) {
case PKFormatType::PEM: {
err = PEM_write_bio_RSAPrivateKey(
Expand Down Expand Up @@ -3760,6 +3893,8 @@ Result<BIOPointer, bool> EVPKeyPointer::writePrivateKey(
#else
EC_KEY* ec = EVP_PKEY_get0_EC_KEY(get());
#endif
if (ec == nullptr) return Result<BIOPointer, bool>(false);

switch (config.format) {
case PKFormatType::PEM: {
err = PEM_write_bio_ECPrivateKey(
Expand Down Expand Up @@ -3826,6 +3961,8 @@ Result<BIOPointer, bool> EVPKeyPointer::writePublicKey(
#else
RSA* rsa = EVP_PKEY_get0_RSA(get());
#endif
if (rsa == nullptr) return Result<BIOPointer, bool>(false);

if (config.format == ncrypto::EVPKeyPointer::PKFormatType::PEM) {
// Encode PKCS#1 as PEM.
if (PEM_write_bio_RSAPublicKey(bio.get(), rsa) != 1) {
Expand Down Expand Up @@ -3854,10 +3991,28 @@ Result<BIOPointer, bool> EVPKeyPointer::writePublicKey(

if (config.format == ncrypto::EVPKeyPointer::PKFormatType::PEM) {
// Encode SPKI as PEM.
#if NCRYPTO_USE_OPENSSL3_PROVIDER
// Build the SubjectPublicKeyInfo wrapper explicitly before PEM encoding.
// Provider-backed keys can fail the direct PEM_write_bio_PUBKEY() path even
// when OpenSSL can materialize the public wrapper with X509_PUBKEY_set().
X509_PUBKEY* pubkey = nullptr;
if (X509_PUBKEY_set(&pubkey, get()) != 1) {
X509_PUBKEY_free(pubkey);
return Result<BIOPointer, bool>(false,
mark_pop_error_on_return.peekError());
}
X509PubKeyPointer pubkey_ptr(pubkey);
if (PEM_write_bio_X509_PUBKEY(bio.get(), pubkey_ptr.get()) != 1) {
return Result<BIOPointer, bool>(false,
mark_pop_error_on_return.peekError());
}
#else
// Non-OpenSSL >= 3 builds do not all declare PEM_write_bio_X509_PUBKEY().
if (PEM_write_bio_PUBKEY(bio.get(), get()) != 1) {
return Result<BIOPointer, bool>(false,
mark_pop_error_on_return.peekError());
}
#endif
return bio;
}

Expand Down Expand Up @@ -3928,21 +4083,37 @@ std::optional<uint32_t> EVPKeyPointer::getBytesOfRS() const {
bits = BignumPointer::GetBitCount(q.get());
#else
const DSA* dsa_key = EVP_PKEY_get0_DSA(get());
bool has_bits = false;
// Both r and s are computed mod q, so their width is limited by that of q.
bits = BignumPointer::GetBitCount(DSA_get0_q(dsa_key));
if (dsa_key != nullptr) {
const BIGNUM* q = DSA_get0_q(dsa_key);
if (q != nullptr) {
bits = BignumPointer::GetBitCount(q);
has_bits = true;
}
}
if (!has_bits) return std::nullopt;
#endif
} else if (id == EVP_PKEY_EC) {
#if NCRYPTO_USE_OPENSSL3_PROVIDER
Ec ec(get());
if (!ec) return std::nullopt;
bits = EC_GROUP_order_bits(ec.getGroup());
const EC_GROUP* group = ec.getGroup();
if (group == nullptr) return std::nullopt;
bits = EC_GROUP_order_bits(group);
#else
bits = EC_GROUP_order_bits(ECKeyPointer::GetGroup(*this));
const EC_KEY* ec_key = EVP_PKEY_get0_EC_KEY(get());
if (ec_key == nullptr) return std::nullopt;
const EC_GROUP* group = ECKeyPointer::GetGroup(ec_key);
if (group == nullptr) return std::nullopt;
bits = EC_GROUP_order_bits(group);
#endif
} else {
return std::nullopt;
}

if (bits <= 0) return std::nullopt;

return (bits + 7) / 8;
}

Expand Down Expand Up @@ -3981,12 +4152,12 @@ EVPKeyPointer::operator Dsa() const {

bool EVPKeyPointer::validateDsaParameters() const {
if (!pkey_) return false;
/* Validate DSA2 parameters from FIPS 186-4 */
#if OPENSSL_VERSION_MAJOR >= 3
if (EVP_default_properties_is_fips_enabled(nullptr) && EVP_PKEY_DSA == id()) {
#else
if (FIPS_mode() && EVP_PKEY_DSA == id()) {
#endif
// Validate DSA2 parameters from FIPS 186-4.
#if NCRYPTO_USE_OPENSSL3_PROVIDER
DeleteFnPtr<BIGNUM, BN_free> p;
DeleteFnPtr<BIGNUM, BN_free> q;
Expand All @@ -3998,9 +4169,11 @@ bool EVPKeyPointer::validateDsaParameters() const {
const BIGNUM* q_value = q.get();
#else
const DSA* dsa = EVP_PKEY_get0_DSA(pkey_.get());
if (dsa == nullptr) return false;
const BIGNUM* p;
const BIGNUM* q;
DSA_get0_pqg(dsa, &p, &q, nullptr);
if (p == nullptr || q == nullptr) return false;
const BIGNUM* p_value = p;
const BIGNUM* q_value = q;
#endif
Expand Down Expand Up @@ -6445,9 +6618,14 @@ DataPointer EVPMDCtxPointer::sign(

bool EVPMDCtxPointer::verify(const Buffer<const unsigned char>& buf,
const Buffer<const unsigned char>& sig) const {
if (!ctx_) return false;
int ret = EVP_DigestVerify(ctx_.get(), sig.data, sig.len, buf.data, buf.len);
return ret == 1;
return verifyOneShot(buf, sig) == 1;
}

int EVPMDCtxPointer::verifyOneShot(
const Buffer<const unsigned char>& buf,
const Buffer<const unsigned char>& sig) const {
if (!ctx_) return -1;
return EVP_DigestVerify(ctx_.get(), sig.data, sig.len, buf.data, buf.len);
}

EVPMDCtxPointer EVPMDCtxPointer::New() {
Expand Down
Loading
Loading