Skip to content

Commit 9d23b6b

Browse files
committed
crypto: support OpenSSL STORE private keys
Signed-off-by: Filip Skokan <panva.ip@gmail.com>
1 parent af100d1 commit 9d23b6b

25 files changed

Lines changed: 585 additions & 9 deletions

deps/ncrypto/ncrypto.cc

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
#include <openssl/core_names.h>
2121
#include <openssl/params.h>
2222
#include <openssl/provider.h>
23+
#include <openssl/store.h>
2324
#if OPENSSL_WITH_ARGON2
2425
#include <openssl/thread.h>
2526
#endif
@@ -618,6 +619,26 @@ int PasswordCallback(char* buf, int size, int rwflag, void* u) {
618619
return -1;
619620
}
620621

622+
struct StorePassphraseData {
623+
Buffer<char> passphrase{.data = nullptr, .len = 0};
624+
bool has_passphrase = false;
625+
bool missing_passphrase = false;
626+
};
627+
628+
int StorePasswordCallback(char* buf, int size, int rwflag, void* u) {
629+
auto data = static_cast<StorePassphraseData*>(u);
630+
if (data == nullptr || !data->has_passphrase) {
631+
if (data != nullptr) data->missing_passphrase = true;
632+
return -1;
633+
}
634+
635+
size_t buflen = static_cast<size_t>(size);
636+
size_t len = data->passphrase.len;
637+
if (buflen < len) return -1;
638+
memcpy(buf, reinterpret_cast<const char*>(data->passphrase.data), len);
639+
return len;
640+
}
641+
621642
// Algorithm: http://howardhinnant.github.io/date_algorithms.html
622643
constexpr int days_from_epoch(int y, unsigned m, unsigned d) {
623644
y -= m <= 2;
@@ -2613,6 +2634,102 @@ EVPKeyPointer::ParseKeyResult EVPKeyPointer::TryParsePrivateKey(
26132634
};
26142635
}
26152636

2637+
EVPKeyPointer::ParseKeyResult EVPKeyPointer::TryLoadPrivateKeyFromStore(
2638+
const StorePrivateKeyConfig& config) {
2639+
#if defined(OPENSSL_IS_BORINGSSL) || OPENSSL_VERSION_MAJOR < 3
2640+
return ParseKeyResult(PKParseError::FAILED);
2641+
#else
2642+
ClearErrorOnReturn clear_error_on_return;
2643+
std::string uri_str(config.uri);
2644+
std::string properties_str;
2645+
const char* properties = nullptr;
2646+
if (config.properties.has_value()) {
2647+
properties_str.assign(config.properties->data(), config.properties->size());
2648+
properties = properties_str.c_str();
2649+
}
2650+
2651+
std::string passphrase_str;
2652+
Buffer<char> passbuf{.data = nullptr, .len = 0};
2653+
if (config.passphrase.has_value()) {
2654+
passphrase_str.assign(config.passphrase->data, config.passphrase->len);
2655+
passbuf.data = passphrase_str.data();
2656+
passbuf.len = passphrase_str.size();
2657+
}
2658+
StorePassphraseData passphrase_data{
2659+
.passphrase = passbuf,
2660+
.has_passphrase = config.passphrase.has_value(),
2661+
};
2662+
UI_METHOD* ui_method =
2663+
UI_UTIL_wrap_read_pem_callback(StorePasswordCallback, 0);
2664+
if (ui_method == nullptr) return ParseKeyResult(PKParseError::FAILED);
2665+
2666+
const OSSL_PARAM store_params[] = {OSSL_PARAM_END};
2667+
OSSL_STORE_CTX* ctx = OSSL_STORE_open_ex(uri_str.c_str(),
2668+
nullptr,
2669+
properties,
2670+
ui_method,
2671+
&passphrase_data,
2672+
store_params,
2673+
nullptr,
2674+
nullptr);
2675+
if (ctx == nullptr) {
2676+
bool missing_passphrase = passphrase_data.missing_passphrase;
2677+
int err = ERR_peek_error();
2678+
UI_destroy_method(ui_method);
2679+
if (missing_passphrase) {
2680+
return ParseKeyResult(PKParseError::NEED_PASSPHRASE);
2681+
}
2682+
return ParseKeyResult(PKParseError::FAILED, err);
2683+
}
2684+
2685+
if (!OSSL_STORE_expect(ctx, OSSL_STORE_INFO_PKEY)) {
2686+
bool missing_passphrase = passphrase_data.missing_passphrase;
2687+
int err = ERR_peek_error();
2688+
OSSL_STORE_close(ctx);
2689+
UI_destroy_method(ui_method);
2690+
if (missing_passphrase) {
2691+
return ParseKeyResult(PKParseError::NEED_PASSPHRASE);
2692+
}
2693+
return ParseKeyResult(PKParseError::FAILED, err);
2694+
}
2695+
2696+
EVPKeyPointer pkey;
2697+
int store_error = 0;
2698+
while (!OSSL_STORE_eof(ctx)) {
2699+
OSSL_STORE_INFO* info = OSSL_STORE_load(ctx);
2700+
if (info == nullptr) {
2701+
if (OSSL_STORE_error(ctx)) {
2702+
store_error = ERR_peek_error();
2703+
break;
2704+
}
2705+
continue;
2706+
}
2707+
if (OSSL_STORE_INFO_get_type(info) == OSSL_STORE_INFO_PKEY) {
2708+
EVP_PKEY* raw_pkey = OSSL_STORE_INFO_get1_PKEY(info);
2709+
if (raw_pkey != nullptr) {
2710+
pkey = EVPKeyPointer(raw_pkey);
2711+
} else {
2712+
store_error = ERR_peek_error();
2713+
}
2714+
}
2715+
OSSL_STORE_INFO_free(info);
2716+
if (pkey || store_error != 0) break;
2717+
}
2718+
2719+
OSSL_STORE_close(ctx);
2720+
UI_destroy_method(ui_method);
2721+
2722+
if (passphrase_data.missing_passphrase) {
2723+
return ParseKeyResult(PKParseError::NEED_PASSPHRASE);
2724+
}
2725+
if (store_error != 0) {
2726+
return ParseKeyResult(PKParseError::FAILED, store_error);
2727+
}
2728+
if (!pkey) return ParseKeyResult(PKParseError::NOT_RECOGNIZED);
2729+
return ParseKeyResult(std::move(pkey));
2730+
#endif
2731+
}
2732+
26162733
Result<BIOPointer, bool> EVPKeyPointer::writePrivateKey(
26172734
const PrivateKeyEncodingConfig& config) const {
26182735
if (config.format == PKFormatType::JWK) {

deps/ncrypto/ncrypto.h

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -946,6 +946,7 @@ class EVPKeyPointer final {
946946
RAW_PUBLIC,
947947
RAW_PRIVATE,
948948
RAW_SEED,
949+
STORE,
949950
};
950951

951952
enum class PKParseError { NOT_RECOGNIZED, NEED_PASSPHRASE, FAILED };
@@ -978,6 +979,12 @@ class EVPKeyPointer final {
978979
PrivateKeyEncodingConfig& operator=(const PrivateKeyEncodingConfig&);
979980
};
980981

982+
struct StorePrivateKeyConfig {
983+
std::string_view uri;
984+
std::optional<std::string_view> properties = std::nullopt;
985+
std::optional<Buffer<const char>> passphrase = std::nullopt;
986+
};
987+
981988
static ParseKeyResult TryParsePublicKey(
982989
const PublicKeyEncodingConfig& config,
983990
const Buffer<const unsigned char>& buffer);
@@ -989,6 +996,13 @@ class EVPKeyPointer final {
989996
const PrivateKeyEncodingConfig& config,
990997
const Buffer<const unsigned char>& buffer);
991998

999+
// Loads a private key from an OpenSSL OSSL_STORE URI (e.g. "file:", a
1000+
// provider-backed scheme such as "pkcs11:"). The optional passphrase is
1001+
// used as the PIN/passphrase for encrypted or token-protected keys.
1002+
// Returns NOT_RECOGNIZED when no private key is found at the URI.
1003+
static ParseKeyResult TryLoadPrivateKeyFromStore(
1004+
const StorePrivateKeyConfig& config);
1005+
9921006
EVPKeyPointer() = default;
9931007
explicit EVPKeyPointer(EVP_PKEY* pkey);
9941008
EVPKeyPointer(EVPKeyPointer&& other) noexcept;

doc/api/cli.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,21 @@ This behavior also applies to `child_process.spawn()`, but in that case, the
191191
flags are propagated via the `NODE_OPTIONS` environment variable rather than
192192
directly through the process arguments.
193193

194+
### `--allow-crypto-store`
195+
196+
<!-- YAML
197+
added: REPLACEME
198+
-->
199+
200+
> Stability: 1.1 - Active development
201+
202+
When using the [Permission Model][], the process will not be able to load
203+
private keys from OpenSSL store URIs (passed as a {URL} to
204+
[`crypto.createPrivateKey()`][]) by default. Attempts to do so will throw an
205+
`ERR_ACCESS_DENIED` unless the user explicitly passes the
206+
`--allow-crypto-store` flag. This permission can be dropped at runtime via
207+
[`permission.drop()`][].
208+
194209
### `--allow-ffi`
195210

196211
<!-- YAML
@@ -2339,6 +2354,7 @@ following permissions are restricted:
23392354
* WASI - manageable through [`--allow-wasi`][] flag
23402355
* Addons - manageable through [`--allow-addons`][] flag
23412356
* FFI - manageable through [`--allow-ffi`](#--allow-ffi) flag
2357+
* Crypto Store - manageable through [`--allow-crypto-store`][] flag
23422358

23432359
### `--permission-audit`
23442360

@@ -3789,6 +3805,7 @@ one is included in the list below.
37893805

37903806
* `--allow-addons`
37913807
* `--allow-child-process`
3808+
* `--allow-crypto-store`
37923809
* `--allow-ffi`
37933810
* `--allow-fs-read`
37943811
* `--allow-fs-write`
@@ -4433,6 +4450,7 @@ node --stack-trace-limit=12 -p -e "Error.stackTraceLimit" # prints 12
44334450
[`"type"`]: packages.md#type
44344451
[`--allow-addons`]: #--allow-addons
44354452
[`--allow-child-process`]: #--allow-child-process
4453+
[`--allow-crypto-store`]: #--allow-crypto-store
44364454
[`--allow-fs-read`]: #--allow-fs-read
44374455
[`--allow-fs-write`]: #--allow-fs-write
44384456
[`--allow-net`]: #--allow-net
@@ -4466,6 +4484,7 @@ node --stack-trace-limit=12 -p -e "Error.stackTraceLimit" # prints 12
44664484
[`NO_COLOR`]: https://no-color.org
44674485
[`Web Storage`]: https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API
44684486
[`YoungGenerationSizeFromSemiSpaceSize`]: https://chromium.googlesource.com/v8/v8.git/+/refs/tags/10.3.129/src/heap/heap.cc#328
4487+
[`crypto.createPrivateKey()`]: crypto.md#cryptocreateprivatekeykey
44694488
[`dns.lookup()`]: dns.md#dnslookuphostname-options-callback
44704489
[`dns.setDefaultResultOrder()`]: dns.md#dnssetdefaultresultorderorder
44714490
[`dnsPromises.lookup()`]: dns.md#dnspromiseslookuphostname-options
@@ -4476,6 +4495,7 @@ node --stack-trace-limit=12 -p -e "Error.stackTraceLimit" # prints 12
44764495
[`node:sqlite`]: sqlite.md
44774496
[`node:stream/iter`]: stream_iter.md
44784497
[`node:vfs`]: vfs.md
4498+
[`permission.drop()`]: permissions.md#permissiondropscope-reference
44794499
[`process.setUncaughtExceptionCaptureCallback()`]: process.md#processsetuncaughtexceptioncapturecallbackfn
44804500
[`tls.DEFAULT_MAX_VERSION`]: tls.md#tlsdefault_max_version
44814501
[`tls.DEFAULT_MIN_VERSION`]: tls.md#tlsdefault_min_version

doc/api/crypto.md

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3982,14 +3982,19 @@ changes:
39823982

39833983
<!--lint disable maximum-line-length remark-lint-->
39843984

3985-
* `key` {Object|string|ArrayBuffer|Buffer|TypedArray|DataView}
3986-
* `key` {string|ArrayBuffer|Buffer|TypedArray|DataView|Object} The key
3987-
material, either in PEM, DER, JWK, or raw format.
3985+
* `key` {Object|string|ArrayBuffer|Buffer|TypedArray|DataView|URL}
3986+
* `key` {string|ArrayBuffer|Buffer|TypedArray|DataView|Object|URL} The key
3987+
material, either in PEM, DER, JWK, or raw format, or a {URL} referencing an
3988+
OpenSSL store.
39883989
* `format` {string} Must be `'pem'`, `'der'`, `'jwk'`, `'raw-private'`,
39893990
or `'raw-seed'`. **Default:** `'pem'`.
39903991
* `type` {string} Must be `'pkcs1'`, `'pkcs8'` or `'sec1'`. This option is
39913992
required only if the `format` is `'der'` and ignored otherwise.
3992-
* `passphrase` {string | Buffer} The passphrase to use for decryption.
3993+
* `passphrase` {string | Buffer} The passphrase to use for decryption. When
3994+
`key` is a {URL}, this is the optional PIN/passphrase forwarded to the
3995+
store.
3996+
* `properties` {string} The optional OpenSSL property query used when
3997+
fetching the store loader for a {URL} key.
39933998
* `encoding` {string} The string encoding to use when `key` is a string.
39943999
* `asymmetricKeyType` {string} Required when `format` is `'raw-private'`
39954000
or `'raw-seed'` and ignored otherwise.
@@ -4007,6 +4012,19 @@ must be an object with the properties described above.
40074012
If the private key is encrypted, a `passphrase` must be specified. The length
40084013
of the passphrase is limited to 1024 bytes.
40094014

4015+
#### OpenSSL store {URL} keys
4016+
4017+
> Stability: 1.1 - Active development
4018+
4019+
If `key` is a {URL} (or an object whose `key` is a {URL}), the private key is
4020+
loaded from the corresponding OpenSSL store URI (for example a `file:` URI or a
4021+
provider-backed scheme such as `pkcs11:`). When the [Permission Model][] is
4022+
enabled, [`--allow-crypto-store`][] is required.
4023+
4024+
When `properties` is specified with a {URL} key, it is passed to OpenSSL as the
4025+
property query for selecting the store loader. It is not appended to the URL and
4026+
is distinct from provider-specific URI parameters.
4027+
40104028
### `crypto.createPublicKey(key)`
40114029

40124030
<!-- YAML
@@ -4080,6 +4098,10 @@ extracted from the returned `KeyObject`. Similarly, if a `KeyObject` with type
40804098
`'private'` is given, a new `KeyObject` with type `'public'` will be returned
40814099
and it will be impossible to extract the private key from the returned object.
40824100

4101+
A store-backed private key can be used as a public key by first loading it with
4102+
[`crypto.createPrivateKey()`][]; a {URL} cannot be passed to
4103+
`crypto.createPublicKey()` directly.
4104+
40834105
### `crypto.createSecretKey(key[, encoding])`
40844106

40854107
<!-- YAML
@@ -6960,6 +6982,7 @@ See the [list of SSL OP Flags][] for details.
69606982
[NIST SP 800-38D]: https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38d.pdf
69616983
[OpenSSL's FIPS README file]: https://github.com/openssl/openssl/blob/openssl-3.0/README-FIPS.md
69626984
[OpenSSL's SPKAC implementation]: https://www.openssl.org/docs/man3.0/man1/openssl-spkac.html
6985+
[Permission Model]: permissions.md#permission-model
69636986
[RFC 1421]: https://www.rfc-editor.org/rfc/rfc1421.txt
69646987
[RFC 2409]: https://www.rfc-editor.org/rfc/rfc2409.txt
69656988
[RFC 2818]: https://www.rfc-editor.org/rfc/rfc2818.txt
@@ -6973,6 +6996,7 @@ See the [list of SSL OP Flags][] for details.
69736996
[RFC 8032]: https://www.rfc-editor.org/rfc/rfc8032.txt
69746997
[RFC 9562]: https://www.rfc-editor.org/rfc/rfc9562.txt
69756998
[Web Crypto API documentation]: webcrypto.md
6999+
[`--allow-crypto-store`]: cli.md#--allow-crypto-store
69767000
[`BN_is_prime_ex`]: https://www.openssl.org/docs/man1.1.1/man3/BN_is_prime_ex.html
69777001
[`Buffer`]: buffer.md
69787002
[`DH_generate_key()`]: https://www.openssl.org/docs/man3.0/man3/DH_generate_key.html

doc/api/permissions.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,9 @@ flag. For WASI, use the [`--allow-wasi`][] flag. For FFI, use the
7474
[`--allow-ffi`][] flag. The [`node:ffi`](ffi.md) module also requires the
7575
`--experimental-ffi` flag and is only available in builds with FFI support.
7676

77+
To allow loading private keys from OpenSSL store URIs via
78+
[`crypto.createPrivateKey()`][], use the [`--allow-crypto-store`][] flag.
79+
7780
#### Runtime API
7881

7982
When enabling the Permission Model through the [`--permission`][]
@@ -208,7 +211,8 @@ Example `node.config.json`:
208211
"allow-worker": true,
209212
"allow-net": true,
210213
"allow-addons": false,
211-
"allow-ffi": false
214+
"allow-ffi": false,
215+
"allow-crypto-store": false
212216
}
213217
}
214218
```
@@ -318,12 +322,14 @@ Developers relying on --permission to sandbox untrusted code should be aware tha
318322
[Security Policy]: https://github.com/nodejs/node/blob/main/SECURITY.md
319323
[`--allow-addons`]: cli.md#--allow-addons
320324
[`--allow-child-process`]: cli.md#--allow-child-process
325+
[`--allow-crypto-store`]: cli.md#--allow-crypto-store
321326
[`--allow-ffi`]: cli.md#--allow-ffi
322327
[`--allow-fs-read`]: cli.md#--allow-fs-read
323328
[`--allow-fs-write`]: cli.md#--allow-fs-write
324329
[`--allow-net`]: cli.md#--allow-net
325330
[`--allow-wasi`]: cli.md#--allow-wasi
326331
[`--allow-worker`]: cli.md#--allow-worker
327332
[`--permission`]: cli.md#--permission
333+
[`crypto.createPrivateKey()`]: crypto.md#cryptocreateprivatekeykey
328334
[`npx`]: https://docs.npmjs.com/cli/commands/npx
329335
[`permission.has()`]: process.md#processpermissionhasscope-reference

doc/api/process.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3158,6 +3158,7 @@ The available scopes are:
31583158
* `fs.read` - File System read operations
31593159
* `fs.write` - File System write operations
31603160
* `child` - Child process spawning operations
3161+
* `crypto.store` - OpenSSL store URI private key loading
31613162
* `worker` - Worker thread spawning operation
31623163
* `ffi` - Foreign function interface operations
31633164
@@ -3208,6 +3209,7 @@ The available scopes are the same as [`process.permission.has()`][]:
32083209
* `fs.read` - File System read operations
32093210
* `fs.write` - File System write operations
32103211
* `child` - Child process spawning operations
3212+
* `crypto.store` - OpenSSL store URI private key loading
32113213
* `worker` - Worker thread spawning operation
32123214
* `net` - Network operations
32133215
* `inspector` - Inspector operations

doc/node-config-schema.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,10 @@
6161
"type": "boolean",
6262
"description": "allow use of child process when any permissions are set"
6363
},
64+
"allow-crypto-store": {
65+
"type": "boolean",
66+
"description": "allow loading private keys from OpenSSL store URIs when any permissions are set"
67+
},
6468
"allow-ffi": {
6569
"type": "boolean",
6670
"description": "allow use of FFI when any permissions are set"
@@ -826,6 +830,10 @@
826830
"type": "boolean",
827831
"description": "allow use of child process when any permissions are set"
828832
},
833+
"allow-crypto-store": {
834+
"type": "boolean",
835+
"description": "allow loading private keys from OpenSSL store URIs when any permissions are set"
836+
},
829837
"allow-ffi": {
830838
"type": "boolean",
831839
"description": "allow use of FFI when any permissions are set"

0 commit comments

Comments
 (0)