@@ -7,6 +7,15 @@ const { hasOpenSSL } = require('../common/crypto');
77if ( ! 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+
1019const assert = require ( 'assert' ) ;
1120const fs = require ( 'fs' ) ;
1221const path = require ( 'path' ) ;
@@ -30,258 +39,38 @@ const { subtle } = globalThis.crypto;
3039const kData = Buffer . from (
3140 Array . from ( { length : 256 } , ( _ , i ) => ( i * 17 + 43 ) & 0xff ) ) ;
3241const kProperties = 'provider=pkcs11' ;
33- const kOpenSSLConfig = process . env . NODE_TEST_PKCS11_OPENSSL_CONF ;
34- const kPin = process . env . NODE_TEST_PKCS11_PIN ;
3542const kExpectedPrivateExportFailure =
3643 / F a i l e d t o e n c o d e p r i v a t e k e y | F a i l e d t o e x p o r t J W K | F a i l e d t o e x p o r t R S A p r i v a t e k e y | F a i l e d t o e x p o r t E C .* k e y | F a i l e d t o g e t r a w .* k e y | k e y m g m t e x p o r t f a i l u r e | n o t e x p o r t a b l e | o p e r a t i o n n o t s u p p o r t e d | n o t s u p p o r t e d | i n c o m p a t i b l e / 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
27266function 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