Skip to content

Commit 5ca4525

Browse files
committed
fixup! crypto: support loading private keys through STORE loaders
Signed-off-by: Filip Skokan <panva.ip@gmail.com>
1 parent cd2dd6d commit 5ca4525

4 files changed

Lines changed: 176 additions & 242 deletions

File tree

.github/workflows/build-shared.yml

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -80,17 +80,16 @@ jobs:
8080
8181
- name: Build Node.js and run tests
8282
shell: bash
83-
env:
84-
NODE_TEST_PKCS11_NIX: ${{ inputs.pkcs11-store-test && '1' || '0' }}
8583
run: |
8684
nix-shell \
8785
-I "nixpkgs=$TAR_DIR/tools/nix/pkgs.nix" \
88-
--pure --keep TAR_DIR --keep FLAKY_TESTS --keep NODE_TEST_PKCS11_NIX \
86+
--pure --keep TAR_DIR --keep FLAKY_TESTS \
8987
--keep SCCACHE_GHA_ENABLED --keep ACTIONS_CACHE_SERVICE_V2 --keep ACTIONS_RESULTS_URL --keep ACTIONS_RUNTIME_TOKEN \
9088
--arg loadJSBuiltinsDynamically false \
9189
--arg ccache "${NIX_SCCACHE:-null}" \
9290
--arg devTools '[]' \
9391
--arg benchmarkTools '[]' \
92+
--arg pkcs11 ${{ inputs.pkcs11-store-test }} \
9493
${{ inputs.extra-nix-flags }} \
9594
--run '
9695
make -C "$TAR_DIR" run-ci -j4 V=1 TEST_CI_ARGS="-p actions --measure-flakiness 9 --skip-tests=$CI_SKIP_TESTS"

shell.nix

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,12 @@
3333
}
3434
),
3535

36+
# PKCS#11 fixture for `test/parallel/test-crypto-key-store-pkcs11.js`. `true`
37+
# builds the one from tools/nix/pkcs11.nix against the OpenSSL being linked,
38+
# which needs a shared OpenSSL >= 3 and a platform nixpkgs ships
39+
# pkcs11-provider for; any other value is used as the fixture itself.
40+
pkcs11 ? false,
41+
3642
# dev tools (not needed to build Node.js, useful to maintain it)
3743
ncu-path ? null, # Provide this if you want to use a local version of NCU
3844
devTools ? import ./tools/nix/devTools.nix { inherit pkgs ncu-path; },
@@ -47,6 +53,16 @@ let
4753
useSharedTemporal = builtins.hasAttr "temporal_capi" sharedLibDeps;
4854
needsRustCompiler = withTemporal && !useSharedTemporal;
4955

56+
usePkcs11 = pkcs11 != false;
57+
pkcs11Fixture =
58+
if pkcs11 == true then
59+
import ./tools/nix/pkcs11.nix {
60+
inherit pkgs;
61+
inherit (sharedLibDeps) openssl;
62+
}
63+
else
64+
pkcs11;
65+
5066
nativeBuildInputs =
5167
pkgs.nodejs-slim_latest.nativeBuildInputs
5268
++ pkgs.lib.optionals needsRustCompiler [
@@ -131,4 +147,7 @@ pkgs.mkShell {
131147
)
132148
);
133149
NOSQLITE = pkgs.lib.optionalString (!withSQLite) "1";
150+
NODE_TEST_PKCS11_OPENSSL_CONF = pkgs.lib.optionalString usePkcs11 "${pkcs11Fixture.opensslConf}";
151+
NODE_TEST_PKCS11_PIN = pkgs.lib.optionalString usePkcs11 pkcs11Fixture.pin;
152+
NODE_TEST_PKCS11_SOFTHSM_DIR = pkgs.lib.optionalString usePkcs11 "${pkcs11Fixture.softhsmDir}";
134153
}

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

Lines changed: 28 additions & 239 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,15 @@ const { hasOpenSSL } = require('../common/crypto');
77
if (!hasOpenSSL(3, 0))
88
common.skip('requires OpenSSL 3.x');
99

10+
// The PKCS#11 token, the OpenSSL configuration that activates a provider for
11+
// it, and the PIN that unlocks it are all provided by the environment. See
12+
// tools/nix/pkcs11.nix for the fixture this repository ships, which `shell.nix`
13+
// exports when instantiated with `--arg pkcs11 true`.
14+
const kOpenSSLConfig = process.env.NODE_TEST_PKCS11_OPENSSL_CONF;
15+
const kPin = process.env.NODE_TEST_PKCS11_PIN;
16+
if (!kOpenSSLConfig || !kPin)
17+
common.skip('missing a PKCS#11 provider test fixture');
18+
1019
const assert = require('assert');
1120
const fs = require('fs');
1221
const path = require('path');
@@ -30,258 +39,38 @@ const { subtle } = globalThis.crypto;
3039
const kData = Buffer.from(
3140
Array.from({ length: 256 }, (_, i) => (i * 17 + 43) & 0xff));
3241
const kProperties = 'provider=pkcs11';
33-
const kOpenSSLConfig = process.env.NODE_TEST_PKCS11_OPENSSL_CONF;
34-
const kPin = process.env.NODE_TEST_PKCS11_PIN;
3542
const kExpectedPrivateExportFailure =
3643
/Failed to encode private key|Failed to export JWK|Failed to export RSA private key|Failed to export EC .* key|Failed to get raw .* key|keymgmt export failure|not exportable|operation not supported|not supported|incompatible/i;
3744

38-
function run(command, args, options = {}) {
39-
const result = spawnSync(command, args, {
40-
encoding: 'utf8',
41-
...options,
42-
});
43-
44-
if (result.status !== 0) {
45-
const output = [
46-
result.error?.message,
47-
result.stdout,
48-
result.stderr,
49-
].filter(Boolean).join('\n');
50-
assert.fail(`${command} ${args.join(' ')} failed\n${output}`);
51-
}
52-
53-
return result.stdout.trim();
54-
}
55-
56-
function commandExists(command) {
57-
const result = spawnSync(command, ['--version'], { stdio: 'ignore' });
58-
return result.status === 0;
59-
}
60-
61-
function findNixBuild() {
62-
return [
63-
'nix-build',
64-
'/nix/var/nix/profiles/default/bin/nix-build',
65-
'/run/current-system/sw/bin/nix-build',
66-
].find(commandExists);
67-
}
68-
69-
function lastStorePath(output) {
70-
const lines = output.split(/\r?\n/);
71-
for (let i = lines.length - 1; i >= 0; i--) {
72-
if (lines[i].startsWith('/nix/store/')) return lines[i];
73-
}
74-
}
75-
76-
function nixBuild(command, repo, expression) {
77-
const output = run(command, [
78-
'-I',
79-
`nixpkgs=${path.join(repo, 'tools/nix/pkgs.nix')}`,
80-
'-I',
81-
`node=${repo}`,
82-
'--no-out-link',
83-
'-E',
84-
expression,
85-
]);
86-
const storePath = lastStorePath(output);
87-
assert(storePath, `nix-build did not print a store path:\n${output}`);
88-
return storePath;
89-
}
90-
91-
function findFile(root, suffixes) {
92-
const stack = [root];
93-
while (stack.length > 0) {
94-
const dir = stack.pop();
95-
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
96-
const file = path.join(dir, entry.name);
97-
if (entry.isDirectory()) {
98-
stack.push(file);
99-
} else if (suffixes.some((suffix) => file.endsWith(suffix))) {
100-
return file;
101-
}
102-
}
103-
}
104-
}
105-
106-
function shouldCreateNixFixture() {
107-
// Enabled explicitly by the shared OpenSSL CI matrix.
108-
return process.env.NODE_TEST_PKCS11_NIX === '1';
109-
}
110-
111-
function createNixFixture() {
112-
if (!shouldCreateNixFixture()) {
113-
common.skip('requires a configured PKCS#11 provider test fixture');
114-
}
115-
const nixBuildCommand = findNixBuild();
116-
if (nixBuildCommand === undefined) {
117-
assert.fail('requires nix-build for the PKCS#11 provider test fixture');
118-
}
119-
120-
const repo = process.env.TAR_DIR || path.resolve(__dirname, '..', '..');
121-
const opensslExpression = `
122-
let
123-
pkgs = import <nixpkgs> {};
124-
openssl = (import <node/tools/nix/sharedLibDeps.nix> {
125-
inherit pkgs;
126-
}).openssl;
127-
in
128-
`;
129-
const pkcs11ProviderPackage = nixBuild(nixBuildCommand, repo, `
130-
${opensslExpression}
131-
(pkgs.pkcs11-provider.override { inherit openssl; })
132-
.overrideAttrs (_: { doCheck = false; })
133-
`);
134-
const softhsmPackage = nixBuild(nixBuildCommand, repo, `
135-
${opensslExpression}
136-
(pkgs.softhsm.override { inherit openssl; })
137-
.overrideAttrs (_: { doCheck = false; })
138-
`);
139-
const openscPackage = nixBuild(
140-
nixBuildCommand,
141-
repo,
142-
'let pkgs = import <nixpkgs> {}; in pkgs.opensc');
143-
144-
const pkcs11ProviderModule = findFile(pkcs11ProviderPackage, [
145-
'/lib/ossl-modules/pkcs11.so',
146-
'/lib/ossl-modules/pkcs11.dylib',
147-
]);
148-
const softhsmModule = findFile(softhsmPackage, [
149-
'/lib/softhsm/libsofthsm2.so',
150-
'/lib/softhsm/libsofthsm2.dylib',
151-
]);
152-
const softhsm2Util = path.join(softhsmPackage, 'bin/softhsm2-util');
153-
const pkcs11Tool = path.join(openscPackage, 'bin/pkcs11-tool');
154-
for (const file of [
155-
pkcs11ProviderModule,
156-
softhsmModule,
157-
softhsm2Util,
158-
pkcs11Tool,
159-
]) {
160-
assert(file && fs.existsSync(file), `missing PKCS#11 fixture file: ${file}`);
161-
}
45+
// tools/nix/pkcs11.nix ships a SoftHSM directory holding the token and the
46+
// configuration naming it. SoftHSM opens its token read-write, which a Nix
47+
// store path can never be, so run from a writable copy of that directory; the
48+
// configuration names the token relative to the working directory. A fixture
49+
// configured by hand, a real HSM for instance, sets no directory and is used
50+
// as it stands.
51+
function softhsmOptions() {
52+
const source = process.env.NODE_TEST_PKCS11_SOFTHSM_DIR;
53+
if (!source) return {};
16254

16355
tmpdir.refresh();
164-
const work = tmpdir.resolve('pkcs11-store');
165-
const tokens = path.join(work, 'tokens');
166-
fs.mkdirSync(tokens, { recursive: true });
167-
168-
const pin = '1234';
169-
const softhsmConf = path.join(work, 'softhsm2.conf');
170-
const opensslConf = path.join(work, 'openssl-pkcs11.cnf');
171-
fs.writeFileSync(softhsmConf, `
172-
directories.tokendir = ${tokens}
173-
objectstore.backend = file
174-
log.level = ERROR
175-
slots.removable = false
176-
`);
177-
fs.writeFileSync(opensslConf, `
178-
nodejs_conf = nodejs_init
179-
180-
[nodejs_init]
181-
providers = provider_sect
182-
183-
[provider_sect]
184-
default = default_sect
185-
pkcs11 = pkcs11_sect
186-
187-
[default_sect]
188-
activate = 1
189-
190-
[pkcs11_sect]
191-
module = ${pkcs11ProviderModule}
192-
pkcs11-module-path = ${softhsmModule}
193-
pkcs11-module-quirks = no-deinit
194-
activate = 1
195-
`);
196-
197-
const env = { ...process.env, SOFTHSM2_CONF: softhsmConf };
198-
run(softhsm2Util, [
199-
'--init-token',
200-
'--free',
201-
'--label',
202-
'node-test',
203-
'--pin',
204-
pin,
205-
'--so-pin',
206-
pin,
207-
], { env });
208-
209-
// Keep this fixture limited to key types that SoftHSM and pkcs11-provider can
210-
// both generate and operate. Node's PQC APIs require OpenSSL >= 3.5, but that
211-
// does not imply ML-DSA or ML-KEM support in this PKCS#11 stack.
212-
//
213-
// RSA decryption is likewise not covered: pkcs11-provider fails every padding
214-
// mode with `provider asym cipher failure` even for a key whose PKCS#11
215-
// attributes include the decrypt usage, so crypto.privateDecrypt() cannot be
216-
// exercised against this stack.
217-
for (const [keyType, id, label] of [
218-
['RSA:2048', '01', 'node-rsa'],
219-
['EC:prime256v1', '02', 'node-ec'],
220-
['EC:ED25519', '03', 'node-ed25519'],
221-
['EC:ED448', '04', 'node-ed448'],
222-
]) {
223-
run(pkcs11Tool, [
224-
'--module',
225-
softhsmModule,
226-
'--login',
227-
'--pin',
228-
pin,
229-
'--keypairgen',
230-
'--key-type',
231-
keyType,
232-
'--id',
233-
id,
234-
'--label',
235-
label,
236-
'--usage-sign',
237-
], { env });
56+
const cwd = tmpdir.resolve('softhsm');
57+
fs.cpSync(source, cwd, { recursive: true });
58+
fs.chmodSync(cwd, 0o700);
59+
for (const entry of fs.readdirSync(cwd, { recursive: true })) {
60+
fs.chmodSync(path.join(cwd, entry), 0o700);
23861
}
23962

240-
run(pkcs11Tool, [
241-
'--module',
242-
softhsmModule,
243-
'--login',
244-
'--pin',
245-
pin,
246-
'--keypairgen',
247-
'--key-type',
248-
'EC:prime256v1',
249-
'--id',
250-
'05',
251-
'--label',
252-
'node-ecdh',
253-
'--usage-derive',
254-
], { env });
255-
256-
return { opensslConf, pin, softhsmConf };
257-
}
258-
259-
function getFixture() {
260-
if (process.env.NODE_TEST_PKCS11_OPENSSL_CONF &&
261-
process.env.NODE_TEST_PKCS11_PIN) {
262-
return {
263-
opensslConf: process.env.NODE_TEST_PKCS11_OPENSSL_CONF,
264-
pin: process.env.NODE_TEST_PKCS11_PIN,
265-
softhsmConf: process.env.SOFTHSM2_CONF,
266-
};
267-
}
268-
269-
return createNixFixture();
63+
return { cwd, env: { SOFTHSM2_CONF: path.join(cwd, 'softhsm2.conf') } };
27064
}
27165

27266
function runInChild() {
273-
const fixture = getFixture();
67+
const { cwd, env } = softhsmOptions();
27468
const child = spawnSync(process.execPath, [
275-
`--openssl-config=${fixture.opensslConf}`,
69+
`--openssl-config=${kOpenSSLConfig}`,
27670
__filename,
27771
], {
278-
env: {
279-
...process.env,
280-
NODE_TEST_PKCS11_CHILD: '1',
281-
NODE_TEST_PKCS11_OPENSSL_CONF: fixture.opensslConf,
282-
NODE_TEST_PKCS11_PIN: fixture.pin,
283-
...(fixture.softhsmConf && { SOFTHSM2_CONF: fixture.softhsmConf }),
284-
},
72+
cwd,
73+
env: { ...process.env, ...env, NODE_TEST_PKCS11_CHILD: '1' },
28574
stdio: 'inherit',
28675
});
28776
assert.strictEqual(child.status, 0);

0 commit comments

Comments
 (0)