Skip to content

Commit 90a0903

Browse files
committed
fixup! crypto: support loading private keys through STORE loaders
1 parent 7725852 commit 90a0903

4 files changed

Lines changed: 83 additions & 7 deletions

File tree

doc/api/crypto.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4051,6 +4051,16 @@ loaded through an OpenSSL STORE loader. The URL is passed to OpenSSL as a URI,
40514051
for example a `file:` URI or a provider-backed scheme such as `pkcs11:`. When
40524052
the [Permission Model][] is enabled, [`--allow-openssl-store`][] is required.
40534053

4054+
> **Warning**: A URI scheme does not pin an OpenSSL STORE loader or prove where
4055+
> the returned key came from. Node.js forwards the URI to OpenSSL, which chooses
4056+
> loaders according to its version and configuration. For example, OpenSSL may
4057+
> offer an opaque URI such as `pkcs11:object=...` (one without `//` after the
4058+
> scheme) to its `file` loader before trying the `pkcs11` loader. If the complete
4059+
> URI is a valid local path and that file exists, it may be loaded instead.
4060+
> Node.js does not verify which loader supplied the key. Do not rely on a
4061+
> provider-specific URI scheme as proof that a key came from that provider or
4062+
> from a hardware device.
4063+
40544064
Configured OpenSSL STORE loaders have broad authority and may access files,
40554065
devices, tokens, or the network. Access performed by a loader is not constrained
40564066
by the `fs.read`, `fs.write`, or `net` permission scopes.

lib/internal/crypto/keys.js

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ const {
77
ObjectSetPrototypeOf,
88
SafeSet,
99
StringPrototypeIncludes,
10+
StringPrototypeStartsWith,
1011
SymbolToStringTag,
1112
Uint8Array,
1213
} = primordials;
@@ -74,7 +75,11 @@ const {
7475
isArrayBufferView,
7576
} = require('internal/util/types');
7677

77-
const { fileURLToPath, isURLInstance, URL } = require('internal/url');
78+
const {
79+
fileURLToPath,
80+
getURLHref,
81+
isURLInstance,
82+
} = require('internal/url');
7883

7984
const {
8085
customInspectSymbol: kInspect,
@@ -542,13 +547,12 @@ function getKeyTypes({ allowKeyObject, allowURL = false }) {
542547
}
543548

544549
function prepareStorePrivateKey(url, name, passphrase, encoding, properties) {
545-
const normalizedURL = new URL(url.href);
546-
let uri = normalizedURL.href;
547-
if (normalizedURL.protocol === 'file:')
548-
uri = fileURLToPath(normalizedURL);
550+
// Read the canonical serialization from private URL state. Public accessors
551+
// can be overridden by subclasses or through prototype mutation.
552+
let uri = getURLHref(url);
553+
if (StringPrototypeStartsWith(uri, 'file:'))
554+
uri = fileURLToPath(uri);
549555

550-
// A subclass may override the `href` getter, so the value is not guaranteed
551-
// to be a string even for a genuine URL instance.
552556
validateString(uri, name);
553557
// The URI is handed to OpenSSL as a NUL-terminated C string. Reject embedded
554558
// NUL bytes, which `fileURLToPath()` happily decodes from `%00`, rather than

lib/internal/url.js

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,11 @@ let setURLSearchParams;
230230
* @type {(value: unknown) => value is URL}
231231
*/
232232
let isURLInstance;
233+
/**
234+
* Returns the canonical serialization of a URL from its private state.
235+
* @type {(value: URL) => string}
236+
*/
237+
let getURLHref;
233238

234239
class URLSearchParamsIterator {
235240
#target;
@@ -821,6 +826,11 @@ class URL {
821826
static {
822827
isURLInstance = (value) => typeof value === 'object' && value !== null && #context in value;
823828

829+
getURLHref = (value) => {
830+
value.#ensureSearchParamsUpdated();
831+
return value.#context.href;
832+
};
833+
824834
setURLSearchParamsModified = (obj) => {
825835
// When URLSearchParams changes, we lazily update URL on the next read/write for performance.
826836
obj.#searchParamsModified = true;
@@ -1734,6 +1744,7 @@ module.exports = {
17341744
encodeStr,
17351745
isURL,
17361746
isURLInstance,
1747+
getURLHref,
17371748

17381749
urlUpdateActions: updateActions,
17391750
getURLOrigin,

test/parallel/test-crypto-key-store.js

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,57 @@ const data = Buffer.from('hello store');
233233
{ code: 'ERR_CRYPTO_INVALID_JWK' });
234234
}
235235

236+
{
237+
// Read the URI from the URL's private state. Public accessors on a branded
238+
// subclass must not redirect a previously created URL to a different key.
239+
const trustedHref = pathToFileURL(
240+
path.join(tmpdir.path, 'priv.pem')).href;
241+
const redirectHref = pathToFileURL(
242+
path.join(tmpdir.path, 'rsa.pem')).href;
243+
class RedirectingURL extends URL {
244+
get href() { return redirectHref; }
245+
}
246+
const subclassURL = new RedirectingURL(trustedHref);
247+
assert.strictEqual(
248+
URL.prototype.toString.call(subclassURL), trustedHref);
249+
assert.strictEqual(
250+
createPrivateKey(subclassURL).asymmetricKeyType, 'ed25519');
251+
252+
// The same applies when the accessor is replaced on URL.prototype.
253+
const prototypeURL = new URL(trustedHref);
254+
const hrefDescriptor = Object.getOwnPropertyDescriptor(URL.prototype, 'href');
255+
try {
256+
Object.defineProperty(URL.prototype, 'href', {
257+
...hrefDescriptor,
258+
get() { return redirectHref; },
259+
});
260+
assert.strictEqual(
261+
URL.prototype.toString.call(prototypeURL), trustedHref);
262+
assert.strictEqual(
263+
createPrivateKey(prototypeURL).asymmetricKeyType, 'ed25519');
264+
} finally {
265+
Object.defineProperty(URL.prototype, 'href', hrefDescriptor);
266+
}
267+
268+
// Replacing `protocol` must not turn an opaque STORE URI into a file path.
269+
const filePathname = pathToFileURL(
270+
path.join(tmpdir.path, 'priv.pem')).pathname;
271+
const opaqueURL = new URL(`pkcs11:${filePathname}`);
272+
const protocolDescriptor = Object.getOwnPropertyDescriptor(
273+
URL.prototype, 'protocol');
274+
try {
275+
Object.defineProperty(URL.prototype, 'protocol', {
276+
...protocolDescriptor,
277+
get() { return 'file:'; },
278+
});
279+
assert.throws(() => createPrivateKey(opaqueURL), {
280+
code: 'ERR_OSSL_OSSL_STORE_UNSUPPORTED',
281+
});
282+
} finally {
283+
Object.defineProperty(URL.prototype, 'protocol', protocolDescriptor);
284+
}
285+
}
286+
236287
{
237288
// The URI is handed to OpenSSL as a NUL-terminated C string, so an embedded
238289
// NUL must be rejected rather than silently truncating the path.

0 commit comments

Comments
 (0)