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
61 changes: 59 additions & 2 deletions .github/actions/build-shared/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ inputs:
description: Cachix auth token for nodejs.cachix.org.
required: false
default: ''
pkcs11-store-provider-test:
description: >
Whether to make pkcs11-provider, SoftHSM, and OpenSC available to the
regular test suite for the OpenSSL STORE provider-backed key test.
required: false
default: 'false'

runs:
using: composite
Expand Down Expand Up @@ -51,15 +57,66 @@ runs:
- name: Build Node.js and run tests
shell: bash
run: |
dev_tools='[]'
if [ "${{ inputs.pkcs11-store-provider-test }}" = "true" ]; then
# The external STORE provider test dlopens both the OpenSSL provider
# and the PKCS#11 module into the test Node.js process. Build both
# against the same OpenSSL as the shared-OpenSSL matrix entry to avoid
# mixing libcrypto ABIs from the nixpkgs default and the matrix build.
# Keep this in sync with the pkcs11StoreProviderTest gate in
# .github/workflows/test-shared.yml.
openssl_attr="${OPENSSL_ATTR:-openssl_3_5}"
shared_openssl="(import $TAR_DIR/tools/nix/openssl-matrix.nix { inherit pkgs; }).$openssl_attr"
export NODE_TEST_PKCS11_PROVIDER_PACKAGE="$(
nix-build \
-I "nixpkgs=$TAR_DIR/tools/nix/pkgs.nix" \
--no-out-link \
-E "
let
pkgs = import <nixpkgs> {};
openssl = $shared_openssl;
in (pkgs.pkcs11-provider.override { inherit openssl; }).overrideAttrs (_: {
doCheck = false;
})
"
)"
export NODE_TEST_SOFTHSM_PACKAGE="$(
nix-build \
-I "nixpkgs=$TAR_DIR/tools/nix/pkgs.nix" \
--no-out-link \
-E "
let
pkgs = import <nixpkgs> {};
openssl = $shared_openssl;
in (pkgs.softhsm.override { inherit openssl; }).overrideAttrs (_: {
doCheck = false;
})
"
)"
dev_tools='with import <nixpkgs> {}; [ opensc ]'
fi

nix-shell \
-I "nixpkgs=$TAR_DIR/tools/nix/pkgs.nix" \
--pure --keep TAR_DIR --keep FLAKY_TESTS \
--keep NODE_TEST_PKCS11_PROVIDER_PACKAGE --keep NODE_TEST_SOFTHSM_PACKAGE \
--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}" \
--arg devTools '[]' \
--arg devTools "$dev_tools" \
--arg benchmarkTools '[]' \
${{ inputs.extra-nix-flags }} \
--run '
make -C "$TAR_DIR" run-ci -j4 V=1 TEST_CI_ARGS="-p actions --measure-flakiness 9 --skip-tests=$CI_SKIP_TESTS"
set -e
if [ "${{ inputs.pkcs11-store-provider-test }}" = "true" ]; then
provider=$(find "$NODE_TEST_PKCS11_PROVIDER_PACKAGE" \( -path "*/lib/ossl-modules/pkcs11.so" -o -path "*/lib/ossl-modules/pkcs11.dylib" \) -print -quit)
softhsm=$(find "$NODE_TEST_SOFTHSM_PACKAGE" \( -path "*/lib/softhsm/libsofthsm2.so" -o -path "*/lib/softhsm/libsofthsm2.dylib" \) -print -quit)
test -n "$provider"
test -n "$softhsm"
export PATH="$NODE_TEST_SOFTHSM_PACKAGE/bin:$PATH"
export NODE_TEST_PKCS11_PROVIDER=1
export NODE_TEST_PKCS11_PROVIDER_MODULE="$provider"
export NODE_TEST_SOFTHSM_MODULE="$softhsm"
fi
make -C "$TAR_DIR" run-ci -j4 V=1 TEST_CI_ARGS="-p actions --measure-flakiness 9 --skip-tests=${CI_SKIP_TESTS:-}"
' "$TAR_DIR/shell.nix"
30 changes: 27 additions & 3 deletions .github/workflows/test-shared.yml
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ jobs:
name: Build and test Node.js
with:
cachix-auth-token: ${{ secrets.CACHIX_AUTH_TOKEN }}
pkcs11-store-provider-test: 'true'
extra-nix-flags: |
--arg useSeparateDerivationForV8 true \
${{ endsWith(matrix.system, '-darwin') && '--arg withAmaro false --arg withLief false --arg withSQLite false --arg withFFI false --arg extraConfigFlags ''["--without-inspector" "--without-node-options"]'' \' || '\' }}
Expand Down Expand Up @@ -211,7 +212,7 @@ jobs:

- name: Build V8 to cache it
if: ${{ steps.v8-drv.outputs.ALREADY_CACHED != 'true' }}
run: nix-store --export "$(nix-build "$V8_DRV")" > libv8-aarch64-linux.nar
run: nix-store --export "$(nix-build --fallback "$V8_DRV")" > libv8-aarch64-linux.nar
env:
V8_DRV: ${{ steps.v8-drv.outputs.V8_DRV }}

Expand All @@ -229,9 +230,31 @@ jobs:
echo "matrix=$(
nix-instantiate --eval --strict --json -E "
let
matrix = import $TAR_DIR/tools/nix/openssl-matrix.nix {};
pkgs = import $TAR_DIR/tools/nix/pkgs.nix {
config.permittedInsecurePackages = [ \"openssl-1.1.1w\" ];
};
matrix = import $TAR_DIR/tools/nix/openssl-matrix.nix {
inherit pkgs;
};
in
builtins.map (attr: { inherit attr; inherit (builtins.getAttr attr matrix) name; }) (builtins.attrNames matrix)
builtins.map (attr:
let
openssl = builtins.getAttr attr matrix;
in
{
inherit attr;
inherit (openssl) name;
# The real pkcs11 STORE test needs pkcs11-provider and
# SoftHSM built against the same OpenSSL as Node. The pinned
# SoftHSM package currently builds against OpenSSL 3.x but
# not OpenSSL 4.x, so keep this to the known-working range and
# revisit when https://github.com/softhsm/SoftHSMv2/issues/868
# is fixed and the pinned nixpkgs provider stack supports 4.x.
pkcs11StoreProviderTest =
openssl.pname == \"openssl\" &&
pkgs.lib.versionAtLeast openssl.version \"3\" &&
pkgs.lib.versionOlder openssl.version \"4\";
}) (builtins.attrNames matrix)
"
)" >> "$GITHUB_OUTPUT"

Expand Down Expand Up @@ -267,6 +290,7 @@ jobs:
name: Build and test Node.js
with:
cachix-auth-token: ${{ secrets.CACHIX_AUTH_TOKEN }}
pkcs11-store-provider-test: ${{ matrix.openssl.pkcs11StoreProviderTest }}
# 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
23 changes: 21 additions & 2 deletions deps/ncrypto/ncrypto.cc
Original file line number Diff line number Diff line change
Expand Up @@ -2839,9 +2839,28 @@ std::optional<uint32_t> EVPKeyPointer::getBytesOfRS() const {
if (id == EVP_PKEY_DSA) {
const DSA* dsa_key = EVP_PKEY_get0_DSA(get());
// 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) {
bits = BignumPointer::GetBitCount(DSA_get0_q(dsa_key));
#if OPENSSL_VERSION_MAJOR >= 3 && !defined(OPENSSL_IS_BORINGSSL)
} else if (EVP_PKEY_get_int_param(
get(), OSSL_PKEY_PARAM_FFC_QBITS, &bits) != 1) {
return std::nullopt;
#endif
} else {
return std::nullopt;
}
} else if (id == EVP_PKEY_EC) {
bits = EC_GROUP_order_bits(ECKeyPointer::GetGroup(*this));
const EC_KEY* ec_key = EVP_PKEY_get0_EC_KEY(get());
if (ec_key != nullptr) {
bits = EC_GROUP_order_bits(ECKeyPointer::GetGroup(ec_key));
#if OPENSSL_VERSION_MAJOR >= 3 && !defined(OPENSSL_IS_BORINGSSL)
} else if (EVP_PKEY_get_int_param(get(), OSSL_PKEY_PARAM_BITS, &bits) !=
1) {
return std::nullopt;
#endif
} else {
return std::nullopt;
}
} else {
return std::nullopt;
}
Expand Down
26 changes: 26 additions & 0 deletions doc/api/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,28 @@ This behavior also applies to `child_process.spawn()`, but in that case, the
flags are propagated via the `NODE_OPTIONS` environment variable rather than
directly through the process arguments.

### `--allow-crypto-store`

<!-- YAML
added: REPLACEME
-->

> Stability: 1.1 - Active development

When using the [Permission Model][], the process will not be able to load
private keys from OpenSSL STORE provider URLs by default. Attempts to do so
will throw an `ERR_ACCESS_DENIED` unless the user explicitly passes the
`--allow-crypto-store` flag when starting Node.js.

This permission only applies to OpenSSL STORE provider URLs accepted by
[`crypto.createPrivateKey()`][]. It does not grant access to Node.js file
system or network APIs. Configured OpenSSL providers may still perform their
own I/O, credential handling, hardware access, or daemon communication outside
of Node.js `fs` and `net` permission scopes.

Node.js does not pass URL input to OpenSSL's built-in `default` or `base` STORE
loaders, so local file STORE loading is not exposed through this API.

### `--allow-ffi`

<!-- YAML
Expand Down Expand Up @@ -2322,6 +2344,7 @@ following permissions are restricted:
* File System - manageable through
[`--allow-fs-read`][], [`--allow-fs-write`][] flags
* Network - manageable through [`--allow-net`][] flag
* OpenSSL STORE - manageable through [`--allow-crypto-store`][] flag
* Child Process - manageable through [`--allow-child-process`][] flag
* Worker Threads - manageable through [`--allow-worker`][] flag
* WASI - manageable through [`--allow-wasi`][] flag
Expand Down Expand Up @@ -3777,6 +3800,7 @@ one is included in the list below.

* `--allow-addons`
* `--allow-child-process`
* `--allow-crypto-store`
* `--allow-ffi`
* `--allow-fs-read`
* `--allow-fs-write`
Expand Down Expand Up @@ -4420,6 +4444,7 @@ node --stack-trace-limit=12 -p -e "Error.stackTraceLimit" # prints 12
[`"type"`]: packages.md#type
[`--allow-addons`]: #--allow-addons
[`--allow-child-process`]: #--allow-child-process
[`--allow-crypto-store`]: #--allow-crypto-store
[`--allow-fs-read`]: #--allow-fs-read
[`--allow-fs-write`]: #--allow-fs-write
[`--allow-net`]: #--allow-net
Expand Down Expand Up @@ -4453,6 +4478,7 @@ node --stack-trace-limit=12 -p -e "Error.stackTraceLimit" # prints 12
[`NO_COLOR`]: https://no-color.org
[`Web Storage`]: https://developer.mozilla.org/en-US/docs/Web/API/Web_Storage_API
[`YoungGenerationSizeFromSemiSpaceSize`]: https://chromium.googlesource.com/v8/v8.git/+/refs/tags/10.3.129/src/heap/heap.cc#328
[`crypto.createPrivateKey()`]: crypto.md#cryptocreateprivatekeykey
[`dns.lookup()`]: dns.md#dnslookuphostname-options-callback
[`dns.setDefaultResultOrder()`]: dns.md#dnssetdefaultresultorderorder
[`dnsPromises.lookup()`]: dns.md#dnspromiseslookuphostname-options
Expand Down
22 changes: 20 additions & 2 deletions doc/api/crypto.md
Original file line number Diff line number Diff line change
Expand Up @@ -2493,6 +2493,9 @@ added:

Converts a `KeyObject` instance to a `CryptoKey`.

Private keys loaded from OpenSSL STORE providers can only be converted with
`extractable` set to `false`.

### `keyObject.type`

<!-- YAML
Expand Down Expand Up @@ -3973,8 +3976,8 @@ changes:

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

* `key` {Object|string|ArrayBuffer|Buffer|TypedArray|DataView}
* `key` {string|ArrayBuffer|Buffer|TypedArray|DataView|Object} The key
* `key` {Object|string|ArrayBuffer|Buffer|TypedArray|DataView|URL}
* `key` {string|ArrayBuffer|Buffer|TypedArray|DataView|Object|URL} The key
material, either in PEM, DER, JWK, or raw format.
* `format` {string} Must be `'pem'`, `'der'`, `'jwk'`, `'raw-private'`,
or `'raw-seed'`. **Default:** `'pem'`.
Expand All @@ -3995,6 +3998,18 @@ Creates and returns a new key object containing a private key. If `key` is a
string or `Buffer`, `format` is assumed to be `'pem'`; otherwise, `key`
must be an object with the properties described above.

If `key` is a WHATWG {URL}, it is loaded through OpenSSL STORE providers.
Only provider-backed URLs are supported. Node.js does not pass URL input to
OpenSSL's built-in `default` or `base` STORE loaders, so local file STORE
loading is not exposed through this API. Provider-specific URL grammar and
parameters are handled by the configured provider.

When `key` is a {URL}, `passphrase` is passed separately to OpenSSL's password
callback and is not encoded into the URL.

Private keys loaded from OpenSSL STORE providers are not exportable through
Node.js, and cannot be passed where a public key is required.

If the private key is encrypted, a `passphrase` must be specified. The length
of the passphrase is limited to 1024 bytes.

Expand Down Expand Up @@ -4068,6 +4083,7 @@ returned `KeyObject` will be `'public'` and that the private key cannot be
extracted from the returned `KeyObject`. Similarly, if a `KeyObject` with type
`'private'` is given, a new `KeyObject` with type `'public'` will be returned
and it will be impossible to extract the private key from the returned object.
This does not apply to private keys loaded from OpenSSL STORE providers.

### `crypto.createSecretKey(key[, encoding])`

Expand Down Expand Up @@ -5381,6 +5397,7 @@ object, the `padding` property can be passed. Otherwise, this function uses

Because RSA public keys can be derived from private keys, a private key may
be passed instead of a public key.
This does not apply to private keys loaded from OpenSSL STORE providers.

### `crypto.publicEncrypt(key, buffer)`

Expand Down Expand Up @@ -5440,6 +5457,7 @@ object, the `padding` property can be passed. Otherwise, this function uses

Because RSA public keys can be derived from private keys, a private key may
be passed instead of a public key.
This does not apply to private keys loaded from OpenSSL STORE providers.

### `crypto.randomBytes(size[, callback])`

Expand Down
10 changes: 10 additions & 0 deletions doc/api/errors.md
Original file line number Diff line number Diff line change
Expand Up @@ -1123,6 +1123,16 @@ added: v24.7.0
Attempted to use KEM operations while Node.js was not compiled with
OpenSSL with KEM support.

<a id="ERR_CRYPTO_KEY_NOT_EXPORTABLE"></a>

### `ERR_CRYPTO_KEY_NOT_EXPORTABLE`

<!-- YAML
added: REPLACEME
-->

A private key could not be exported because the key material is not exportable.

<a id="ERR_CRYPTO_OPERATION_FAILED"></a>

### `ERR_CRYPTO_OPERATION_FAILED`
Expand Down
9 changes: 7 additions & 2 deletions doc/api/permissions.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,11 @@ Error: Access to this API has been restricted
Allowing access to spawning a process and creating worker threads can be done
using the [`--allow-child-process`][] and [`--allow-worker`][] respectively.

To allow network access, use [`--allow-net`][] and for allowing native addons
when using permission model, use the [`--allow-addons`][]
To allow network access, use [`--allow-net`][]. To allow loading private keys
from OpenSSL STORE provider URLs, use [`--allow-crypto-store`][]. This does not
grant access to Node.js file system or network APIs, but configured OpenSSL
providers may perform their own I/O outside those permission scopes. For
allowing native addons when using permission model, use the [`--allow-addons`][]
flag. For WASI, use the [`--allow-wasi`][] flag. For FFI, use the
[`--allow-ffi`][] flag. The [`node:ffi`](ffi.md) module also requires the
`--experimental-ffi` flag and is only available in builds with FFI support.
Expand Down Expand Up @@ -206,6 +209,7 @@ Example `node.config.json`:
"allow-fs-write": ["./bar"],
"allow-child-process": true,
"allow-worker": true,
"allow-crypto-store": true,
"allow-net": true,
"allow-addons": false,
"allow-ffi": false
Expand Down Expand Up @@ -318,6 +322,7 @@ Developers relying on --permission to sandbox untrusted code should be aware tha
[Security Policy]: https://github.com/nodejs/node/blob/main/SECURITY.md
[`--allow-addons`]: cli.md#--allow-addons
[`--allow-child-process`]: cli.md#--allow-child-process
[`--allow-crypto-store`]: cli.md#--allow-crypto-store
[`--allow-ffi`]: cli.md#--allow-ffi
[`--allow-fs-read`]: cli.md#--allow-fs-read
[`--allow-fs-write`]: cli.md#--allow-fs-write
Expand Down
1 change: 1 addition & 0 deletions doc/api/process.md
Original file line number Diff line number Diff line change
Expand Up @@ -3210,6 +3210,7 @@ The available scopes are the same as [`process.permission.has()`][]:
* `child` - Child process spawning operations
* `worker` - Worker thread spawning operation
* `net` - Network operations
* `crypto.store` - OpenSSL STORE provider private key loading
* `inspector` - Inspector operations
* `wasi` - WASI operations
* `addon` - Native addon operations
Expand Down
18 changes: 18 additions & 0 deletions doc/node.1
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,20 @@ This behavior also applies to \fBchild_process.spawn()\fR, but in that case, the
flags are propagated via the \fBNODE_OPTIONS\fR environment variable rather than
directly through the process arguments.
.
.It Fl -allow-crypto-store
When using the Permission Model, the process will not be able to load private
keys from OpenSSL STORE provider URLs by default.
Attempts to do so will throw an \fBERR_ACCESS_DENIED\fR unless the user
explicitly passes the \fB--allow-crypto-store\fR flag when starting Node.js.
This permission only applies to OpenSSL STORE provider URLs accepted by
\fBcrypto.createPrivateKey()\fR. It does not grant access to Node.js file system
or network APIs. Configured OpenSSL providers may still perform their own I/O,
credential handling, hardware access, or daemon communication outside of
Node.js \fBfs\fR and \fBnet\fR permission scopes.
Node.js does not pass URL input to OpenSSL's built-in \fBdefault\fR or
\fBbase\fR STORE loaders, so local file STORE loading is not exposed through
this API.
.
.It Fl -allow-ffi
When using the Permission Model, the process will not be able to use
\fBnode:ffi\fR by default.
Expand Down Expand Up @@ -1152,6 +1166,8 @@ File System - manageable through
.It
Network - manageable through \fB--allow-net\fR flag
.It
OpenSSL STORE - manageable through \fB--allow-crypto-store\fR flag
.It
Child Process - manageable through \fB--allow-child-process\fR flag
.It
Worker Threads - manageable through \fB--allow-worker\fR flag
Expand Down Expand Up @@ -1875,6 +1891,8 @@ one is included in the list below.
.It
\fB--allow-child-process\fR
.It
\fB--allow-crypto-store\fR
.It
\fB--allow-ffi\fR
.It
\fB--allow-fs-read\fR
Expand Down
Loading
Loading