Skip to content

Commit 40b7a66

Browse files
committed
crypto: support loading private keys through STORE loaders
Accept WHATWG URL objects in private-key inputs and load referenced keys through OpenSSL STORE loaders. Pass optional property queries and passphrases while preserving provider-owned EVP_PKEY objects for ordinary KeyObject and CryptoKey operations. Signed-off-by: Filip Skokan <panva.ip@gmail.com>
1 parent 0618e9f commit 40b7a66

36 files changed

Lines changed: 1946 additions & 112 deletions

.github/workflows/build-shared.yml

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@ on:
2222
required: false
2323
type: string
2424
default: ''
25+
pkcs11-store-test:
26+
description: Whether to enable the PKCS#11-backed crypto STORE test
27+
required: false
28+
type: boolean
29+
default: false
2530
secrets:
2631
CACHIX_AUTH_TOKEN:
2732
description: Cachix auth token for nodejs.cachix.org.
@@ -75,10 +80,12 @@ jobs:
7580
7681
- name: Build Node.js and run tests
7782
shell: bash
83+
env:
84+
NODE_TEST_PKCS11_NIX: ${{ inputs.pkcs11-store-test && '1' || '0' }}
7885
run: |
7986
nix-shell \
8087
-I "nixpkgs=$TAR_DIR/tools/nix/pkgs.nix" \
81-
--pure --keep TAR_DIR --keep FLAKY_TESTS \
88+
--pure --keep TAR_DIR --keep FLAKY_TESTS --keep NODE_TEST_PKCS11_NIX \
8289
--keep SCCACHE_GHA_ENABLED --keep ACTIONS_CACHE_SERVICE_V2 --keep ACTIONS_RESULTS_URL --keep ACTIONS_RUNTIME_TOKEN \
8390
--arg loadJSBuiltinsDynamically false \
8491
--arg ccache "${NIX_SCCACHE:-null}" \

.github/workflows/test-shared.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,7 @@ jobs:
243243
with:
244244
runner: ubuntu-24.04-arm
245245
v8-nar: ${{ needs.build-aarch64-linux-v8.outputs.local-cache && 'libv8-aarch64-linux.nar' }}
246+
pkcs11-store-test: ${{ matrix.openssl.attr == 'openssl_3_5' }}
246247
# Override just the `openssl` attr of the default shared-lib set with
247248
# the matrix-selected nixpkgs attribute (e.g. `openssl_3_6`). All
248249
# other shared libs (brotli, cares, libuv, …) keep their defaults.

deps/ncrypto/ncrypto.cc

Lines changed: 182 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,13 @@
44
#include <openssl/dh.h>
55
#include <openssl/evp.h>
66
#include <openssl/hmac.h>
7+
#include <openssl/pem.h>
78
#include <openssl/pkcs12.h>
89
#include <openssl/rand.h>
910
#include <openssl/x509v3.h>
1011
#if NCRYPTO_USE_BORINGSSL_EVP_DO_ALL_FALLBACK
1112
#include <openssl/bytestring.h>
1213
#include <openssl/cipher.h>
13-
#include <openssl/pem.h>
1414
#endif
1515
#include <algorithm>
1616
#include <array>
@@ -21,6 +21,8 @@
2121
#include <openssl/core_names.h>
2222
#include <openssl/params.h>
2323
#include <openssl/provider.h>
24+
#include <openssl/store.h>
25+
#include <openssl/ui.h>
2426
#if OPENSSL_WITH_ARGON2
2527
#include <openssl/thread.h>
2628
#endif
@@ -75,6 +77,17 @@ namespace {
7577
using BignumCtxPointer = DeleteFnPtr<BN_CTX, BN_CTX_free>;
7678
using BignumGenCallbackPointer = DeleteFnPtr<BN_GENCB, BN_GENCB_free>;
7779
using NetscapeSPKIPointer = DeleteFnPtr<NETSCAPE_SPKI, NETSCAPE_SPKI_free>;
80+
using X509PubKeyPointer = DeleteFnPtr<X509_PUBKEY, X509_PUBKEY_free>;
81+
82+
#if OPENSSL_VERSION_MAJOR >= 3 && !defined(OPENSSL_IS_BORINGSSL)
83+
// OSSL_STORE_close() returns int, so it needs a void-returning adapter to be
84+
// usable as a DeleteFnPtr deleter.
85+
void CloseStoreCtx(OSSL_STORE_CTX* ctx) {
86+
OSSL_STORE_close(ctx);
87+
}
88+
using StoreCtxPointer = DeleteFnPtr<OSSL_STORE_CTX, CloseStoreCtx>;
89+
using UIMethodPointer = DeleteFnPtr<UI_METHOD, UI_destroy_method>;
90+
#endif
7891

7992
const EVP_CIPHER* GetCipherCtxCipher(const EVP_CIPHER_CTX* ctx) {
8093
#if NCRYPTO_USE_OPENSSL3_PROVIDER
@@ -332,7 +345,7 @@ ClearErrorOnReturn::~ClearErrorOnReturn() {
332345
ERR_clear_error();
333346
}
334347

335-
int ClearErrorOnReturn::peekError() {
348+
unsigned long ClearErrorOnReturn::peekError() { // NOLINT(runtime/int)
336349
return ERR_peek_error();
337350
}
338351

@@ -346,7 +359,7 @@ MarkPopErrorOnReturn::~MarkPopErrorOnReturn() {
346359
ERR_pop_to_mark();
347360
}
348361

349-
int MarkPopErrorOnReturn::peekError() {
362+
unsigned long MarkPopErrorOnReturn::peekError() { // NOLINT(runtime/int)
350363
return ERR_peek_error();
351364
}
352365

@@ -851,6 +864,26 @@ int PasswordCallback(char* buf, int size, int rwflag, void* u) {
851864
return -1;
852865
}
853866

867+
struct StorePassphraseData {
868+
Buffer<char> passphrase{.data = nullptr, .len = 0};
869+
bool has_passphrase = false;
870+
bool missing_passphrase = false;
871+
};
872+
873+
int StorePasswordCallback(char* buf, int size, int rwflag, void* u) {
874+
auto data = static_cast<StorePassphraseData*>(u);
875+
if (data == nullptr || !data->has_passphrase) {
876+
if (data != nullptr) data->missing_passphrase = true;
877+
return -1;
878+
}
879+
880+
size_t buflen = static_cast<size_t>(size);
881+
size_t len = data->passphrase.len;
882+
if (buflen < len) return -1;
883+
memcpy(buf, reinterpret_cast<const char*>(data->passphrase.data), len);
884+
return len;
885+
}
886+
854887
// Algorithm: http://howardhinnant.github.io/date_algorithms.html
855888
constexpr int days_from_epoch(int y, unsigned m, unsigned d) {
856889
y -= m <= 2;
@@ -3584,7 +3617,7 @@ EVPKeyPointer::ParseKeyResult EVPKeyPointer::TryParsePrivateKey(
35843617
const Buffer<const unsigned char>& buffer) {
35853618
static constexpr auto keyOrError = [](EVPKeyPointer pkey,
35863619
bool had_passphrase = false) {
3587-
if (int err = ERR_peek_error()) {
3620+
if (unsigned long err = ERR_peek_error()) { // NOLINT(runtime/int)
35883621
if (ERR_GET_LIB(err) == ERR_LIB_PEM &&
35893622
ERR_GET_REASON(err) == PEM_R_BAD_PASSWORD_READ && !had_passphrase) {
35903623
return ParseKeyResult(PKParseError::NEED_PASSPHRASE);
@@ -3644,6 +3677,97 @@ EVPKeyPointer::ParseKeyResult EVPKeyPointer::TryParsePrivateKey(
36443677
};
36453678
}
36463679

3680+
EVPKeyPointer::ParseKeyResult EVPKeyPointer::TryLoadPrivateKeyFromStore(
3681+
const StorePrivateKeyConfig& config) {
3682+
#if defined(OPENSSL_IS_BORINGSSL) || OPENSSL_VERSION_MAJOR < 3
3683+
return ParseKeyResult(PKParseError::FAILED);
3684+
#else
3685+
// Note: the OpenSSL error queue is deliberately left populated on failure so
3686+
// that the caller can surface a `code` and an `opensslErrorStack`, matching
3687+
// TryParsePrivateKey(). It is cleared explicitly on success, because loaders
3688+
// that decline the URI (OSSL_STORE_open_ex() always probes the `file` loader
3689+
// first) leave their errors behind even when a later loader succeeds.
3690+
std::string uri_str(config.uri);
3691+
std::string properties_str;
3692+
const char* properties = nullptr;
3693+
if (config.properties.has_value()) {
3694+
properties_str.assign(config.properties->data(), config.properties->size());
3695+
properties = properties_str.c_str();
3696+
}
3697+
3698+
std::string passphrase_str;
3699+
Buffer<char> passbuf{.data = nullptr, .len = 0};
3700+
if (config.passphrase.has_value()) {
3701+
passphrase_str.assign(config.passphrase->data, config.passphrase->len);
3702+
passbuf.data = passphrase_str.data();
3703+
passbuf.len = passphrase_str.size();
3704+
}
3705+
StorePassphraseData passphrase_data{
3706+
.passphrase = passbuf,
3707+
.has_passphrase = config.passphrase.has_value(),
3708+
};
3709+
UIMethodPointer ui_method(
3710+
UI_UTIL_wrap_read_pem_callback(StorePasswordCallback, 0));
3711+
if (!ui_method) return ParseKeyResult(PKParseError::FAILED);
3712+
3713+
// A loader that declines the URI reports it through the error queue, so the
3714+
// most recent entry describes the loader that actually handled it. Peeking
3715+
// the oldest entry would instead report the `file` loader probe that
3716+
// OSSL_STORE_open_ex() always performs first for URIs without an authority.
3717+
const auto failed = [&](bool missing_passphrase) {
3718+
if (missing_passphrase)
3719+
return ParseKeyResult(PKParseError::NEED_PASSPHRASE);
3720+
return ParseKeyResult(PKParseError::FAILED, ERR_peek_last_error());
3721+
};
3722+
3723+
const OSSL_PARAM store_params[] = {OSSL_PARAM_END};
3724+
StoreCtxPointer ctx(OSSL_STORE_open_ex(uri_str.c_str(),
3725+
nullptr,
3726+
properties,
3727+
ui_method.get(),
3728+
&passphrase_data,
3729+
store_params,
3730+
nullptr,
3731+
nullptr));
3732+
if (!ctx) return failed(passphrase_data.missing_passphrase);
3733+
3734+
if (!OSSL_STORE_expect(ctx.get(), OSSL_STORE_INFO_PKEY)) {
3735+
return failed(passphrase_data.missing_passphrase);
3736+
}
3737+
3738+
EVPKeyPointer pkey;
3739+
bool store_error = false;
3740+
while (!OSSL_STORE_eof(ctx.get())) {
3741+
OSSL_STORE_INFO* info = OSSL_STORE_load(ctx.get());
3742+
if (info == nullptr) {
3743+
if (OSSL_STORE_error(ctx.get())) {
3744+
store_error = true;
3745+
break;
3746+
}
3747+
continue;
3748+
}
3749+
if (OSSL_STORE_INFO_get_type(info) == OSSL_STORE_INFO_PKEY) {
3750+
EVP_PKEY* raw_pkey = OSSL_STORE_INFO_get1_PKEY(info);
3751+
if (raw_pkey != nullptr) {
3752+
pkey = EVPKeyPointer(raw_pkey);
3753+
} else {
3754+
store_error = true;
3755+
}
3756+
}
3757+
OSSL_STORE_INFO_free(info);
3758+
if (pkey || store_error) break;
3759+
}
3760+
3761+
if (passphrase_data.missing_passphrase || store_error) {
3762+
return failed(passphrase_data.missing_passphrase);
3763+
}
3764+
if (!pkey) return ParseKeyResult(PKParseError::NOT_RECOGNIZED);
3765+
3766+
ERR_clear_error();
3767+
return ParseKeyResult(std::move(pkey));
3768+
#endif
3769+
}
3770+
36473771
Result<BIOPointer, bool> EVPKeyPointer::writePrivateKey(
36483772
const PrivateKeyEncodingConfig& config) const {
36493773
if (config.format == PKFormatType::JWK) {
@@ -3685,6 +3809,8 @@ Result<BIOPointer, bool> EVPKeyPointer::writePrivateKey(
36853809
#else
36863810
RSA* rsa = EVP_PKEY_get0_RSA(get());
36873811
#endif
3812+
if (rsa == nullptr) return Result<BIOPointer, bool>(false);
3813+
36883814
switch (config.format) {
36893815
case PKFormatType::PEM: {
36903816
err = PEM_write_bio_RSAPrivateKey(
@@ -3760,6 +3886,8 @@ Result<BIOPointer, bool> EVPKeyPointer::writePrivateKey(
37603886
#else
37613887
EC_KEY* ec = EVP_PKEY_get0_EC_KEY(get());
37623888
#endif
3889+
if (ec == nullptr) return Result<BIOPointer, bool>(false);
3890+
37633891
switch (config.format) {
37643892
case PKFormatType::PEM: {
37653893
err = PEM_write_bio_ECPrivateKey(
@@ -3826,6 +3954,8 @@ Result<BIOPointer, bool> EVPKeyPointer::writePublicKey(
38263954
#else
38273955
RSA* rsa = EVP_PKEY_get0_RSA(get());
38283956
#endif
3957+
if (rsa == nullptr) return Result<BIOPointer, bool>(false);
3958+
38293959
if (config.format == ncrypto::EVPKeyPointer::PKFormatType::PEM) {
38303960
// Encode PKCS#1 as PEM.
38313961
if (PEM_write_bio_RSAPublicKey(bio.get(), rsa) != 1) {
@@ -3854,10 +3984,28 @@ Result<BIOPointer, bool> EVPKeyPointer::writePublicKey(
38543984

38553985
if (config.format == ncrypto::EVPKeyPointer::PKFormatType::PEM) {
38563986
// Encode SPKI as PEM.
3987+
#if OPENSSL_VERSION_MAJOR == 3
3988+
// Build the SubjectPublicKeyInfo wrapper explicitly before PEM encoding.
3989+
// Provider-backed keys can fail the direct PEM_write_bio_PUBKEY() path even
3990+
// when OpenSSL can materialize the public wrapper with X509_PUBKEY_set().
3991+
X509_PUBKEY* pubkey = nullptr;
3992+
if (X509_PUBKEY_set(&pubkey, get()) != 1) {
3993+
X509_PUBKEY_free(pubkey);
3994+
return Result<BIOPointer, bool>(false,
3995+
mark_pop_error_on_return.peekError());
3996+
}
3997+
X509PubKeyPointer pubkey_ptr(pubkey);
3998+
if (PEM_write_bio_X509_PUBKEY(bio.get(), pubkey_ptr.get()) != 1) {
3999+
return Result<BIOPointer, bool>(false,
4000+
mark_pop_error_on_return.peekError());
4001+
}
4002+
#else
4003+
// Non-OpenSSL 3 builds do not all declare PEM_write_bio_X509_PUBKEY().
38574004
if (PEM_write_bio_PUBKEY(bio.get(), get()) != 1) {
38584005
return Result<BIOPointer, bool>(false,
38594006
mark_pop_error_on_return.peekError());
38604007
}
4008+
#endif
38614009
return bio;
38624010
}
38634011

@@ -3928,21 +4076,37 @@ std::optional<uint32_t> EVPKeyPointer::getBytesOfRS() const {
39284076
bits = BignumPointer::GetBitCount(q.get());
39294077
#else
39304078
const DSA* dsa_key = EVP_PKEY_get0_DSA(get());
4079+
bool has_bits = false;
39314080
// Both r and s are computed mod q, so their width is limited by that of q.
3932-
bits = BignumPointer::GetBitCount(DSA_get0_q(dsa_key));
4081+
if (dsa_key != nullptr) {
4082+
const BIGNUM* q = DSA_get0_q(dsa_key);
4083+
if (q != nullptr) {
4084+
bits = BignumPointer::GetBitCount(q);
4085+
has_bits = true;
4086+
}
4087+
}
4088+
if (!has_bits) return std::nullopt;
39334089
#endif
39344090
} else if (id == EVP_PKEY_EC) {
39354091
#if NCRYPTO_USE_OPENSSL3_PROVIDER
39364092
Ec ec(get());
39374093
if (!ec) return std::nullopt;
3938-
bits = EC_GROUP_order_bits(ec.getGroup());
4094+
const EC_GROUP* group = ec.getGroup();
4095+
if (group == nullptr) return std::nullopt;
4096+
bits = EC_GROUP_order_bits(group);
39394097
#else
3940-
bits = EC_GROUP_order_bits(ECKeyPointer::GetGroup(*this));
4098+
const EC_KEY* ec_key = EVP_PKEY_get0_EC_KEY(get());
4099+
if (ec_key == nullptr) return std::nullopt;
4100+
const EC_GROUP* group = ECKeyPointer::GetGroup(ec_key);
4101+
if (group == nullptr) return std::nullopt;
4102+
bits = EC_GROUP_order_bits(group);
39414103
#endif
39424104
} else {
39434105
return std::nullopt;
39444106
}
39454107

4108+
if (bits <= 0) return std::nullopt;
4109+
39464110
return (bits + 7) / 8;
39474111
}
39484112

@@ -3981,12 +4145,12 @@ EVPKeyPointer::operator Dsa() const {
39814145

39824146
bool EVPKeyPointer::validateDsaParameters() const {
39834147
if (!pkey_) return false;
3984-
/* Validate DSA2 parameters from FIPS 186-4 */
39854148
#if OPENSSL_VERSION_MAJOR >= 3
39864149
if (EVP_default_properties_is_fips_enabled(nullptr) && EVP_PKEY_DSA == id()) {
39874150
#else
39884151
if (FIPS_mode() && EVP_PKEY_DSA == id()) {
39894152
#endif
4153+
// Validate DSA2 parameters from FIPS 186-4.
39904154
#if NCRYPTO_USE_OPENSSL3_PROVIDER
39914155
DeleteFnPtr<BIGNUM, BN_free> p;
39924156
DeleteFnPtr<BIGNUM, BN_free> q;
@@ -3998,9 +4162,11 @@ bool EVPKeyPointer::validateDsaParameters() const {
39984162
const BIGNUM* q_value = q.get();
39994163
#else
40004164
const DSA* dsa = EVP_PKEY_get0_DSA(pkey_.get());
4165+
if (dsa == nullptr) return false;
40014166
const BIGNUM* p;
40024167
const BIGNUM* q;
40034168
DSA_get0_pqg(dsa, &p, &q, nullptr);
4169+
if (p == nullptr || q == nullptr) return false;
40044170
const BIGNUM* p_value = p;
40054171
const BIGNUM* q_value = q;
40064172
#endif
@@ -6445,9 +6611,14 @@ DataPointer EVPMDCtxPointer::sign(
64456611

64466612
bool EVPMDCtxPointer::verify(const Buffer<const unsigned char>& buf,
64476613
const Buffer<const unsigned char>& sig) const {
6448-
if (!ctx_) return false;
6449-
int ret = EVP_DigestVerify(ctx_.get(), sig.data, sig.len, buf.data, buf.len);
6450-
return ret == 1;
6614+
return verifyOneShot(buf, sig) == 1;
6615+
}
6616+
6617+
int EVPMDCtxPointer::verifyOneShot(
6618+
const Buffer<const unsigned char>& buf,
6619+
const Buffer<const unsigned char>& sig) const {
6620+
if (!ctx_) return -1;
6621+
return EVP_DigestVerify(ctx_.get(), sig.data, sig.len, buf.data, buf.len);
64516622
}
64526623

64536624
EVPMDCtxPointer EVPMDCtxPointer::New() {

0 commit comments

Comments
 (0)