Skip to content

Commit 8271265

Browse files
panvaaduh95
authored andcommitted
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> PR-URL: #63949 Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
1 parent daee546 commit 8271265

38 files changed

Lines changed: 2105 additions & 140 deletions

.github/workflows/build-shared.yml

Lines changed: 6 additions & 0 deletions
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.
@@ -84,6 +89,7 @@ jobs:
8489
--arg ccache "${NIX_SCCACHE:-null}" \
8590
--arg devTools '[]' \
8691
--arg benchmarkTools '[]' \
92+
--arg pkcs11 ${{ inputs.pkcs11-store-test }} \
8793
${{ inputs.extra-nix-flags }} \
8894
--run '
8995
make -C "$TAR_DIR" run-ci -j4 V=1 TEST_CI_ARGS="-p actions --measure-flakiness 9 --skip-tests=$CI_SKIP_TESTS"

.github/workflows/nix-changes.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ jobs:
6969
++ builtins.attrValues (
7070
{ inherit (import <nixpkgs> {}) nixfmt-tree sccache; }
7171
// import ./tools/nix/openssl-matrix.nix {}
72+
// import ./tools/nix/pkcs11.nix {}
7273
)")" \
7374
| xargs nix-store --realise \
7475
| xargs nix-store --query --requisites \
@@ -77,6 +78,7 @@ jobs:
7778
7879
- name: Compute requisites before change
7980
shell: bash # See https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#exit-codes-and-error-action-preference, we want the pipefail option.
81+
# TODO(panva): add `// import ./tools/nix/pkcs11.nix {}` once landed
8082
run: |
8183
git reset HEAD^ --hard
8284
nix-store --query --references "$(

.github/workflows/test-shared.yml

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

deps/ncrypto/ncrypto.cc

Lines changed: 190 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
@@ -76,6 +78,17 @@ 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>;
7880

81+
#if NCRYPTO_USE_OPENSSL3_PROVIDER
82+
using X509PubKeyPointer = DeleteFnPtr<X509_PUBKEY, X509_PUBKEY_free>;
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
91+
7992
const EVP_CIPHER* GetCipherCtxCipher(const EVP_CIPHER_CTX* ctx) {
8093
#if NCRYPTO_USE_OPENSSL3_PROVIDER
8194
return EVP_CIPHER_CTX_get0_cipher(ctx);
@@ -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

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

841854
int PasswordCallback(char* buf, int size, int rwflag, void* u) {
842855
auto passphrase = static_cast<const Buffer<char>*>(u);
856+
if (size <= 0) return -1;
843857
if (passphrase != nullptr) {
844858
size_t buflen = static_cast<size_t>(size);
845859
size_t len = passphrase->len;
@@ -851,6 +865,31 @@ int PasswordCallback(char* buf, int size, int rwflag, void* u) {
851865
return -1;
852866
}
853867

868+
#if NCRYPTO_USE_OPENSSL3_PROVIDER
869+
namespace {
870+
struct StorePassphraseData {
871+
Buffer<char> passphrase{.data = nullptr, .len = 0};
872+
bool has_passphrase = false;
873+
bool missing_passphrase = false;
874+
};
875+
876+
int StorePasswordCallback(char* buf, int size, int rwflag, void* u) {
877+
auto data = static_cast<StorePassphraseData*>(u);
878+
if (data == nullptr || !data->has_passphrase) {
879+
if (data != nullptr) data->missing_passphrase = true;
880+
return -1;
881+
}
882+
883+
if (size <= 0) return -1;
884+
size_t buflen = static_cast<size_t>(size);
885+
size_t len = data->passphrase.len;
886+
if (buflen < len) return -1;
887+
memcpy(buf, reinterpret_cast<const char*>(data->passphrase.data), len);
888+
return len;
889+
}
890+
} // namespace
891+
#endif
892+
854893
// Algorithm: http://howardhinnant.github.io/date_algorithms.html
855894
constexpr int days_from_epoch(int y, unsigned m, unsigned d) {
856895
y -= m <= 2;
@@ -3585,7 +3624,7 @@ EVPKeyPointer::ParseKeyResult EVPKeyPointer::TryParsePrivateKey(
35853624
const Buffer<const unsigned char>& buffer) {
35863625
static constexpr auto keyOrError = [](EVPKeyPointer pkey,
35873626
bool had_passphrase = false) {
3588-
if (int err = ERR_peek_error()) {
3627+
if (unsigned long err = ERR_peek_error()) { // NOLINT(runtime/int)
35893628
if (ERR_GET_LIB(err) == ERR_LIB_PEM &&
35903629
ERR_GET_REASON(err) == PEM_R_BAD_PASSWORD_READ && !had_passphrase) {
35913630
return ParseKeyResult(PKParseError::NEED_PASSPHRASE);
@@ -3645,6 +3684,99 @@ EVPKeyPointer::ParseKeyResult EVPKeyPointer::TryParsePrivateKey(
36453684
};
36463685
}
36473686

3687+
EVPKeyPointer::ParseKeyResult EVPKeyPointer::TryLoadPrivateKeyFromStore(
3688+
const StorePrivateKeyConfig& config) {
3689+
#if !NCRYPTO_USE_OPENSSL3_PROVIDER
3690+
return ParseKeyResult(PKParseError::FAILED);
3691+
#else
3692+
// The error queue is left populated on failure so the caller can surface a
3693+
// `code` and an `opensslErrorStack`, matching TryParsePrivateKey(), and is
3694+
// cleared on success because decoders leave entries behind either way.
3695+
std::string uri_str(config.uri);
3696+
std::string properties_str;
3697+
const char* properties = nullptr;
3698+
if (config.properties.has_value() && !config.properties->empty()) {
3699+
properties_str.assign(config.properties->data(), config.properties->size());
3700+
properties = properties_str.c_str();
3701+
}
3702+
3703+
// config.passphrase outlives this call, so no copy is needed.
3704+
Buffer<char> passbuf{.data = nullptr, .len = 0};
3705+
if (config.passphrase.has_value()) {
3706+
passbuf.data = const_cast<char*>(config.passphrase->data);
3707+
passbuf.len = config.passphrase->len;
3708+
}
3709+
StorePassphraseData passphrase_data{
3710+
.passphrase = passbuf,
3711+
.has_passphrase = config.passphrase.has_value(),
3712+
};
3713+
// Declared before ctx so that reverse destruction closes the store first;
3714+
// it holds both for its lifetime.
3715+
UIMethodPointer ui_method(
3716+
UI_UTIL_wrap_read_pem_callback(StorePasswordCallback, 0));
3717+
if (!ui_method) return ParseKeyResult(PKParseError::FAILED);
3718+
3719+
// Errors from loaders that declined the URI are retained oldest-first, so the
3720+
// newest entry is the loader that actually handled it. Must run before ctx is
3721+
// destroyed, since OSSL_STORE_close() can push errors of its own.
3722+
const auto failed = [&](bool missing_passphrase) {
3723+
if (missing_passphrase)
3724+
return ParseKeyResult(PKParseError::NEED_PASSPHRASE);
3725+
return ParseKeyResult(PKParseError::FAILED, ERR_peek_last_error());
3726+
};
3727+
3728+
const OSSL_PARAM store_params[] = {OSSL_PARAM_END};
3729+
StoreCtxPointer ctx(OSSL_STORE_open_ex(uri_str.c_str(),
3730+
nullptr,
3731+
properties,
3732+
ui_method.get(),
3733+
&passphrase_data,
3734+
store_params,
3735+
nullptr,
3736+
nullptr));
3737+
if (!ctx) return failed(passphrase_data.missing_passphrase);
3738+
3739+
if (!OSSL_STORE_expect(ctx.get(), OSSL_STORE_INFO_PKEY)) {
3740+
return failed(passphrase_data.missing_passphrase);
3741+
}
3742+
3743+
EVPKeyPointer pkey;
3744+
bool store_error = false;
3745+
while (!OSSL_STORE_eof(ctx.get())) {
3746+
OSSL_STORE_INFO* info = OSSL_STORE_load(ctx.get());
3747+
if (info == nullptr) {
3748+
if (OSSL_STORE_error(ctx.get())) {
3749+
store_error = true;
3750+
break;
3751+
}
3752+
continue;
3753+
}
3754+
if (OSSL_STORE_INFO_get_type(info) == OSSL_STORE_INFO_PKEY) {
3755+
EVP_PKEY* raw_pkey = OSSL_STORE_INFO_get1_PKEY(info);
3756+
if (raw_pkey != nullptr) {
3757+
pkey = EVPKeyPointer(raw_pkey);
3758+
} else {
3759+
store_error = true;
3760+
}
3761+
}
3762+
OSSL_STORE_INFO_free(info);
3763+
if (pkey || store_error) break;
3764+
}
3765+
3766+
// missing_passphrase is sticky, so a key that loaded anyway wins over it.
3767+
if (pkey) {
3768+
ctx.reset();
3769+
ERR_clear_error();
3770+
return ParseKeyResult(std::move(pkey));
3771+
}
3772+
3773+
if (passphrase_data.missing_passphrase || store_error) {
3774+
return failed(passphrase_data.missing_passphrase);
3775+
}
3776+
return ParseKeyResult(PKParseError::NOT_RECOGNIZED);
3777+
#endif
3778+
}
3779+
36483780
Result<BIOPointer, bool> EVPKeyPointer::writePrivateKey(
36493781
const PrivateKeyEncodingConfig& config) const {
36503782
if (config.format == PKFormatType::JWK) {
@@ -3686,6 +3818,8 @@ Result<BIOPointer, bool> EVPKeyPointer::writePrivateKey(
36863818
#else
36873819
RSA* rsa = EVP_PKEY_get0_RSA(get());
36883820
#endif
3821+
if (rsa == nullptr) return Result<BIOPointer, bool>(false);
3822+
36893823
switch (config.format) {
36903824
case PKFormatType::PEM: {
36913825
err = PEM_write_bio_RSAPrivateKey(
@@ -3761,6 +3895,8 @@ Result<BIOPointer, bool> EVPKeyPointer::writePrivateKey(
37613895
#else
37623896
EC_KEY* ec = EVP_PKEY_get0_EC_KEY(get());
37633897
#endif
3898+
if (ec == nullptr) return Result<BIOPointer, bool>(false);
3899+
37643900
switch (config.format) {
37653901
case PKFormatType::PEM: {
37663902
err = PEM_write_bio_ECPrivateKey(
@@ -3827,6 +3963,8 @@ Result<BIOPointer, bool> EVPKeyPointer::writePublicKey(
38273963
#else
38283964
RSA* rsa = EVP_PKEY_get0_RSA(get());
38293965
#endif
3966+
if (rsa == nullptr) return Result<BIOPointer, bool>(false);
3967+
38303968
if (config.format == ncrypto::EVPKeyPointer::PKFormatType::PEM) {
38313969
// Encode PKCS#1 as PEM.
38323970
if (PEM_write_bio_RSAPublicKey(bio.get(), rsa) != 1) {
@@ -3855,10 +3993,28 @@ Result<BIOPointer, bool> EVPKeyPointer::writePublicKey(
38553993

38563994
if (config.format == ncrypto::EVPKeyPointer::PKFormatType::PEM) {
38573995
// Encode SPKI as PEM.
3996+
#if NCRYPTO_USE_OPENSSL3_PROVIDER
3997+
// Build the SubjectPublicKeyInfo wrapper explicitly before PEM encoding.
3998+
// Provider-backed keys can fail the direct PEM_write_bio_PUBKEY() path even
3999+
// when OpenSSL can materialize the public wrapper with X509_PUBKEY_set().
4000+
X509_PUBKEY* pubkey = nullptr;
4001+
if (X509_PUBKEY_set(&pubkey, get()) != 1) {
4002+
X509_PUBKEY_free(pubkey);
4003+
return Result<BIOPointer, bool>(false,
4004+
mark_pop_error_on_return.peekError());
4005+
}
4006+
X509PubKeyPointer pubkey_ptr(pubkey);
4007+
if (PEM_write_bio_X509_PUBKEY(bio.get(), pubkey_ptr.get()) != 1) {
4008+
return Result<BIOPointer, bool>(false,
4009+
mark_pop_error_on_return.peekError());
4010+
}
4011+
#else
4012+
// Non-OpenSSL >= 3 builds do not all declare PEM_write_bio_X509_PUBKEY().
38584013
if (PEM_write_bio_PUBKEY(bio.get(), get()) != 1) {
38594014
return Result<BIOPointer, bool>(false,
38604015
mark_pop_error_on_return.peekError());
38614016
}
4017+
#endif
38624018
return bio;
38634019
}
38644020

@@ -3929,21 +4085,37 @@ std::optional<uint32_t> EVPKeyPointer::getBytesOfRS() const {
39294085
bits = BignumPointer::GetBitCount(q.get());
39304086
#else
39314087
const DSA* dsa_key = EVP_PKEY_get0_DSA(get());
4088+
bool has_bits = false;
39324089
// Both r and s are computed mod q, so their width is limited by that of q.
3933-
bits = BignumPointer::GetBitCount(DSA_get0_q(dsa_key));
4090+
if (dsa_key != nullptr) {
4091+
const BIGNUM* q = DSA_get0_q(dsa_key);
4092+
if (q != nullptr) {
4093+
bits = BignumPointer::GetBitCount(q);
4094+
has_bits = true;
4095+
}
4096+
}
4097+
if (!has_bits) return std::nullopt;
39344098
#endif
39354099
} else if (id == EVP_PKEY_EC) {
39364100
#if NCRYPTO_USE_OPENSSL3_PROVIDER
39374101
Ec ec(get());
39384102
if (!ec) return std::nullopt;
3939-
bits = EC_GROUP_order_bits(ec.getGroup());
4103+
const EC_GROUP* group = ec.getGroup();
4104+
if (group == nullptr) return std::nullopt;
4105+
bits = EC_GROUP_order_bits(group);
39404106
#else
3941-
bits = EC_GROUP_order_bits(ECKeyPointer::GetGroup(*this));
4107+
const EC_KEY* ec_key = EVP_PKEY_get0_EC_KEY(get());
4108+
if (ec_key == nullptr) return std::nullopt;
4109+
const EC_GROUP* group = ECKeyPointer::GetGroup(ec_key);
4110+
if (group == nullptr) return std::nullopt;
4111+
bits = EC_GROUP_order_bits(group);
39424112
#endif
39434113
} else {
39444114
return std::nullopt;
39454115
}
39464116

4117+
if (bits <= 0) return std::nullopt;
4118+
39474119
return (bits + 7) / 8;
39484120
}
39494121

@@ -3982,12 +4154,12 @@ EVPKeyPointer::operator Dsa() const {
39824154

39834155
bool EVPKeyPointer::validateDsaParameters() const {
39844156
if (!pkey_) return false;
3985-
/* Validate DSA2 parameters from FIPS 186-4 */
39864157
#if OPENSSL_VERSION_MAJOR >= 3
39874158
if (EVP_default_properties_is_fips_enabled(nullptr) && EVP_PKEY_DSA == id()) {
39884159
#else
39894160
if (FIPS_mode() && EVP_PKEY_DSA == id()) {
39904161
#endif
4162+
// Validate DSA2 parameters from FIPS 186-4.
39914163
#if NCRYPTO_USE_OPENSSL3_PROVIDER
39924164
DeleteFnPtr<BIGNUM, BN_free> p;
39934165
DeleteFnPtr<BIGNUM, BN_free> q;
@@ -3999,9 +4171,11 @@ bool EVPKeyPointer::validateDsaParameters() const {
39994171
const BIGNUM* q_value = q.get();
40004172
#else
40014173
const DSA* dsa = EVP_PKEY_get0_DSA(pkey_.get());
4174+
if (dsa == nullptr) return false;
40024175
const BIGNUM* p;
40034176
const BIGNUM* q;
40044177
DSA_get0_pqg(dsa, &p, &q, nullptr);
4178+
if (p == nullptr || q == nullptr) return false;
40054179
const BIGNUM* p_value = p;
40064180
const BIGNUM* q_value = q;
40074181
#endif
@@ -6452,9 +6626,14 @@ DataPointer EVPMDCtxPointer::sign(
64526626

64536627
bool EVPMDCtxPointer::verify(const Buffer<const unsigned char>& buf,
64546628
const Buffer<const unsigned char>& sig) const {
6455-
if (!ctx_) return false;
6456-
int ret = EVP_DigestVerify(ctx_.get(), sig.data, sig.len, buf.data, buf.len);
6457-
return ret == 1;
6629+
return verifyOneShot(buf, sig) == 1;
6630+
}
6631+
6632+
int EVPMDCtxPointer::verifyOneShot(
6633+
const Buffer<const unsigned char>& buf,
6634+
const Buffer<const unsigned char>& sig) const {
6635+
if (!ctx_) return -1;
6636+
return EVP_DigestVerify(ctx_.get(), sig.data, sig.len, buf.data, buf.len);
64586637
}
64596638

64606639
EVPMDCtxPointer EVPMDCtxPointer::New() {

0 commit comments

Comments
 (0)