Skip to content

Commit 46ff6e0

Browse files
committed
crypto: add raw key formats support to the KeyObject APIs
Add raw key format support (raw-public, raw-private, raw-seed) to KeyObject.prototype.export(), crypto.createPrivateKey(), and crypto.createPublicKey() for applicable asymmetric keys (EC, CFRG curves, ML-DSA, ML-KEM, and SLH-DSA). Also wire these to the Web Cryptography APIs and remove the unnecessary KeyExportJob. The KeyExportJob classes were removed because the export operations they performed are not computationally expensive. They're just serialization of already-available key data (SPKI, PKCS8, raw bytes). Running them as async CryptoJobs on the libuv thread pool added unnecessary overhead and complexity for operations that complete instantly.
1 parent 65b521f commit 46ff6e0

34 files changed

+1929
-730
lines changed

benchmark/crypto/create-keyobject.js

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,8 +30,22 @@ if (hasOpenSSL(3, 5)) {
3030

3131
const bench = common.createBenchmark(main, {
3232
keyType: Object.keys(keyFixtures),
33-
keyFormat: ['pkcs8', 'spki', 'der-pkcs8', 'der-spki', 'jwk-public', 'jwk-private'],
33+
keyFormat: ['pkcs8', 'spki', 'der-pkcs8', 'der-spki', 'jwk-public', 'jwk-private',
34+
'raw-public', 'raw-private', 'raw-seed'],
3435
n: [1e3],
36+
}, {
37+
combinationFilter(p) {
38+
// raw-private is not supported for rsa and ml-dsa
39+
if (p.keyFormat === 'raw-private')
40+
return p.keyType !== 'rsa' && !p.keyType.startsWith('ml-');
41+
// raw-public is not supported by rsa
42+
if (p.keyFormat === 'raw-public')
43+
return p.keyType !== 'rsa';
44+
// raw-seed is only supported for ml-dsa
45+
if (p.keyFormat === 'raw-seed')
46+
return p.keyType.startsWith('ml-');
47+
return true;
48+
},
3549
});
3650

3751
function measure(n, fn, input) {
@@ -82,6 +96,29 @@ function main({ n, keyFormat, keyType }) {
8296
fn = crypto.createPrivateKey;
8397
break;
8498
}
99+
case 'raw-public': {
100+
const exportedKey = keyPair.publicKey.export({ format: 'raw-public' });
101+
key = { key: exportedKey, format: 'raw-public', asymmetricKeyType: keyType };
102+
if (keyType === 'ec') key.namedCurve = keyPair.publicKey.asymmetricKeyDetails.namedCurve;
103+
fn = crypto.createPublicKey;
104+
break;
105+
}
106+
case 'raw-private': {
107+
const exportedKey = keyPair.privateKey.export({ format: 'raw-private' });
108+
key = { key: exportedKey, format: 'raw-private', asymmetricKeyType: keyType };
109+
if (keyType === 'ec') key.namedCurve = keyPair.privateKey.asymmetricKeyDetails.namedCurve;
110+
fn = crypto.createPrivateKey;
111+
break;
112+
}
113+
case 'raw-seed': {
114+
key = {
115+
key: keyPair.privateKey.export({ format: 'raw-seed' }),
116+
format: 'raw-seed',
117+
asymmetricKeyType: keyType,
118+
};
119+
fn = crypto.createPrivateKey;
120+
break;
121+
}
85122
default:
86123
throw new Error('not implemented');
87124
}

benchmark/crypto/kem.js

Lines changed: 112 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -11,22 +11,29 @@ function readKey(name) {
1111
return fs.readFileSync(`${fixtures_keydir}/${name}.pem`, 'utf8');
1212
}
1313

14+
function readKeyPair(publicKeyName, privateKeyName) {
15+
return {
16+
publicKey: readKey(publicKeyName),
17+
privateKey: readKey(privateKeyName),
18+
};
19+
}
20+
1421
const keyFixtures = {};
1522

1623
if (hasOpenSSL(3, 5)) {
17-
keyFixtures['ml-kem-512'] = readKey('ml_kem_512_private');
18-
keyFixtures['ml-kem-768'] = readKey('ml_kem_768_private');
19-
keyFixtures['ml-kem-1024'] = readKey('ml_kem_1024_private');
24+
keyFixtures['ml-kem-512'] = readKeyPair('ml_kem_512_public', 'ml_kem_512_private');
25+
keyFixtures['ml-kem-768'] = readKeyPair('ml_kem_768_public', 'ml_kem_768_private');
26+
keyFixtures['ml-kem-1024'] = readKeyPair('ml_kem_1024_public', 'ml_kem_1024_private');
2027
}
2128
if (hasOpenSSL(3, 2)) {
22-
keyFixtures['p-256'] = readKey('ec_p256_private');
23-
keyFixtures['p-384'] = readKey('ec_p384_private');
24-
keyFixtures['p-521'] = readKey('ec_p521_private');
25-
keyFixtures.x25519 = readKey('x25519_private');
26-
keyFixtures.x448 = readKey('x448_private');
29+
keyFixtures['p-256'] = readKeyPair('ec_p256_public', 'ec_p256_private');
30+
keyFixtures['p-384'] = readKeyPair('ec_p384_public', 'ec_p384_private');
31+
keyFixtures['p-521'] = readKeyPair('ec_p521_public', 'ec_p521_private');
32+
keyFixtures.x25519 = readKeyPair('x25519_public', 'x25519_private');
33+
keyFixtures.x448 = readKeyPair('x448_public', 'x448_private');
2734
}
2835
if (hasOpenSSL(3, 0)) {
29-
keyFixtures.rsa = readKey('rsa_private_2048');
36+
keyFixtures.rsa = readKeyPair('rsa_public_2048', 'rsa_private_2048');
3037
}
3138

3239
if (Object.keys(keyFixtures).length === 0) {
@@ -37,32 +44,46 @@ if (Object.keys(keyFixtures).length === 0) {
3744
const bench = common.createBenchmark(main, {
3845
keyType: Object.keys(keyFixtures),
3946
mode: ['sync', 'async', 'async-parallel'],
40-
keyFormat: ['keyObject', 'keyObject.unique'],
47+
keyFormat: ['keyObject', 'keyObject.unique', 'pem', 'der', 'jwk',
48+
'raw-public', 'raw-private', 'raw-seed'],
4149
op: ['encapsulate', 'decapsulate'],
4250
n: [1e3],
4351
}, {
4452
combinationFilter(p) {
4553
// "keyObject.unique" allows to compare the result with "keyObject" to
4654
// assess whether mutexes over the key material impact the operation
47-
return p.keyFormat !== 'keyObject.unique' ||
48-
(p.keyFormat === 'keyObject.unique' && p.mode === 'async-parallel');
55+
if (p.keyFormat === 'keyObject.unique')
56+
return p.mode === 'async-parallel';
57+
// JWK is not supported for ml-kem for now
58+
if (p.keyFormat === 'jwk')
59+
return !p.keyType.startsWith('ml-');
60+
// raw-public is only supported for encapsulate, not rsa
61+
if (p.keyFormat === 'raw-public')
62+
return p.keyType !== 'rsa' && p.op === 'encapsulate';
63+
// raw-private is not supported for rsa and ml-kem, only for decapsulate
64+
if (p.keyFormat === 'raw-private')
65+
return p.keyType !== 'rsa' && !p.keyType.startsWith('ml-') && p.op === 'decapsulate';
66+
// raw-seed is only supported for ml-kem
67+
if (p.keyFormat === 'raw-seed')
68+
return p.keyType.startsWith('ml-');
69+
return true;
4970
},
5071
});
5172

52-
function measureSync(n, op, privateKey, keys, ciphertexts) {
73+
function measureSync(n, op, key, keys, ciphertexts) {
5374
bench.start();
5475
for (let i = 0; i < n; ++i) {
55-
const key = privateKey || keys[i];
76+
const k = key || keys[i];
5677
if (op === 'encapsulate') {
57-
crypto.encapsulate(key);
78+
crypto.encapsulate(k);
5879
} else {
59-
crypto.decapsulate(key, ciphertexts[i]);
80+
crypto.decapsulate(k, ciphertexts[i]);
6081
}
6182
}
6283
bench.end(n);
6384
}
6485

65-
function measureAsync(n, op, privateKey, keys, ciphertexts) {
86+
function measureAsync(n, op, key, keys, ciphertexts) {
6687
let remaining = n;
6788
function done() {
6889
if (--remaining === 0)
@@ -72,44 +93,98 @@ function measureAsync(n, op, privateKey, keys, ciphertexts) {
7293
}
7394

7495
function one() {
75-
const key = privateKey || keys[n - remaining];
96+
const k = key || keys[n - remaining];
7697
if (op === 'encapsulate') {
77-
crypto.encapsulate(key, done);
98+
crypto.encapsulate(k, done);
7899
} else {
79-
crypto.decapsulate(key, ciphertexts[n - remaining], done);
100+
crypto.decapsulate(k, ciphertexts[n - remaining], done);
80101
}
81102
}
82103
bench.start();
83104
one();
84105
}
85106

86-
function measureAsyncParallel(n, op, privateKey, keys, ciphertexts) {
107+
function measureAsyncParallel(n, op, key, keys, ciphertexts) {
87108
let remaining = n;
88109
function done() {
89110
if (--remaining === 0)
90111
bench.end(n);
91112
}
92113
bench.start();
93114
for (let i = 0; i < n; ++i) {
94-
const key = privateKey || keys[i];
115+
const k = key || keys[i];
95116
if (op === 'encapsulate') {
96-
crypto.encapsulate(key, done);
117+
crypto.encapsulate(k, done);
97118
} else {
98-
crypto.decapsulate(key, ciphertexts[i], done);
119+
crypto.decapsulate(k, ciphertexts[i], done);
99120
}
100121
}
101122
}
102123

103124
function main({ n, mode, keyFormat, keyType, op }) {
104-
const pems = [...Buffer.alloc(n)].map(() => keyFixtures[keyType]);
105-
const keyObjects = pems.map(crypto.createPrivateKey);
125+
const isEncapsulate = op === 'encapsulate';
126+
const pemSource = isEncapsulate ?
127+
keyFixtures[keyType].publicKey :
128+
keyFixtures[keyType].privateKey;
129+
const createKeyFn = isEncapsulate ? crypto.createPublicKey : crypto.createPrivateKey;
130+
const pems = [...Buffer.alloc(n)].map(() => pemSource);
131+
const keyObjects = pems.map(createKeyFn);
106132

107-
let privateKey, keys, ciphertexts;
133+
// Warm up OpenSSL's provider operation cache for each key object
134+
if (isEncapsulate) {
135+
for (const keyObject of keyObjects) {
136+
crypto.encapsulate(keyObject);
137+
}
138+
} else {
139+
const warmupCiphertext = crypto.encapsulate(keyObjects[0]).ciphertext;
140+
for (const keyObject of keyObjects) {
141+
crypto.decapsulate(keyObject, warmupCiphertext);
142+
}
143+
}
144+
145+
const asymmetricKeyType = keyObjects[0].asymmetricKeyType;
146+
let key, keys, ciphertexts;
108147

109148
switch (keyFormat) {
110149
case 'keyObject':
111-
privateKey = keyObjects[0];
150+
key = keyObjects[0];
151+
break;
152+
case 'pem':
153+
key = pems[0];
112154
break;
155+
case 'jwk': {
156+
key = { key: keyObjects[0].export({ format: 'jwk' }), format: 'jwk' };
157+
break;
158+
}
159+
case 'der': {
160+
const type = isEncapsulate ? 'spki' : 'pkcs8';
161+
key = { key: keyObjects[0].export({ format: 'der', type }), format: 'der', type };
162+
break;
163+
}
164+
case 'raw-public': {
165+
const exportedKey = keyObjects[0].export({ format: 'raw-public' });
166+
const keyOpts = { key: exportedKey, format: 'raw-public', asymmetricKeyType };
167+
if (asymmetricKeyType === 'ec') keyOpts.namedCurve = keyObjects[0].asymmetricKeyDetails.namedCurve;
168+
key = keyOpts;
169+
break;
170+
}
171+
case 'raw-private': {
172+
const exportedKey = keyObjects[0].export({ format: 'raw-private' });
173+
const keyOpts = { key: exportedKey, format: 'raw-private', asymmetricKeyType };
174+
if (asymmetricKeyType === 'ec') keyOpts.namedCurve = keyObjects[0].asymmetricKeyDetails.namedCurve;
175+
key = keyOpts;
176+
break;
177+
}
178+
case 'raw-seed': {
179+
// raw-seed requires a private key to export from
180+
const privateKeyObject = crypto.createPrivateKey(keyFixtures[keyType].privateKey);
181+
key = {
182+
key: privateKeyObject.export({ format: 'raw-seed' }),
183+
format: 'raw-seed',
184+
asymmetricKeyType,
185+
};
186+
break;
187+
}
113188
case 'keyObject.unique':
114189
keys = keyObjects;
115190
break;
@@ -118,23 +193,25 @@ function main({ n, mode, keyFormat, keyType, op }) {
118193
}
119194

120195
// Pre-generate ciphertexts for decapsulate operations
121-
if (op === 'decapsulate') {
122-
if (privateKey) {
123-
ciphertexts = [...Buffer.alloc(n)].map(() => crypto.encapsulate(privateKey).ciphertext);
196+
if (!isEncapsulate) {
197+
const encapKey = crypto.createPublicKey(
198+
crypto.createPrivateKey(keyFixtures[keyType].privateKey));
199+
if (key) {
200+
ciphertexts = [...Buffer.alloc(n)].map(() => crypto.encapsulate(encapKey).ciphertext);
124201
} else {
125-
ciphertexts = keys.map((key) => crypto.encapsulate(key).ciphertext);
202+
ciphertexts = keys.map(() => crypto.encapsulate(encapKey).ciphertext);
126203
}
127204
}
128205

129206
switch (mode) {
130207
case 'sync':
131-
measureSync(n, op, privateKey, keys, ciphertexts);
208+
measureSync(n, op, key, keys, ciphertexts);
132209
break;
133210
case 'async':
134-
measureAsync(n, op, privateKey, keys, ciphertexts);
211+
measureAsync(n, op, key, keys, ciphertexts);
135212
break;
136213
case 'async-parallel':
137-
measureAsyncParallel(n, op, privateKey, keys, ciphertexts);
214+
measureAsyncParallel(n, op, key, keys, ciphertexts);
138215
break;
139216
}
140217
}

benchmark/crypto/oneshot-sign.js

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,21 @@ let keyObjects;
2929
const bench = common.createBenchmark(main, {
3030
keyType: Object.keys(keyFixtures),
3131
mode: ['sync', 'async', 'async-parallel'],
32-
keyFormat: ['pem', 'der', 'jwk', 'keyObject', 'keyObject.unique'],
32+
keyFormat: ['pem', 'der', 'jwk', 'keyObject', 'keyObject.unique', 'raw-private', 'raw-seed'],
3333
n: [1e3],
3434
}, {
3535
combinationFilter(p) {
3636
// "keyObject.unique" allows to compare the result with "keyObject" to
3737
// assess whether mutexes over the key material impact the operation
38-
return p.keyFormat !== 'keyObject.unique' ||
39-
(p.keyFormat === 'keyObject.unique' && p.mode === 'async-parallel');
38+
if (p.keyFormat === 'keyObject.unique')
39+
return p.mode === 'async-parallel';
40+
// raw-private is not supported for rsa and ml-dsa
41+
if (p.keyFormat === 'raw-private')
42+
return p.keyType !== 'rsa' && !p.keyType.startsWith('ml-');
43+
// raw-seed is only supported for ml-dsa
44+
if (p.keyFormat === 'raw-seed')
45+
return p.keyType.startsWith('ml-');
46+
return true;
4047
},
4148
});
4249

@@ -91,6 +98,12 @@ function main({ n, mode, keyFormat, keyType }) {
9198
pems ||= [...Buffer.alloc(n)].map(() => keyFixtures[keyType]);
9299
keyObjects ||= pems.map(crypto.createPrivateKey);
93100

101+
// Warm up OpenSSL's provider operation cache for each key object
102+
for (const keyObject of keyObjects) {
103+
crypto.sign(keyType === 'rsa' || keyType === 'ec' ? 'sha256' : null,
104+
data, keyObject);
105+
}
106+
94107
let privateKey, keys, digest;
95108

96109
switch (keyType) {
@@ -120,6 +133,21 @@ function main({ n, mode, keyFormat, keyType }) {
120133
privateKey = { key: keyObjects[0].export({ format: 'der', type: 'pkcs8' }), format: 'der', type: 'pkcs8' };
121134
break;
122135
}
136+
case 'raw-private': {
137+
const exportedKey = keyObjects[0].export({ format: 'raw-private' });
138+
const keyOpts = { key: exportedKey, format: 'raw-private', asymmetricKeyType: keyType };
139+
if (keyType === 'ec') keyOpts.namedCurve = keyObjects[0].asymmetricKeyDetails.namedCurve;
140+
privateKey = keyOpts;
141+
break;
142+
}
143+
case 'raw-seed': {
144+
privateKey = {
145+
key: keyObjects[0].export({ format: 'raw-seed' }),
146+
format: 'raw-seed',
147+
asymmetricKeyType: keyType,
148+
};
149+
break;
150+
}
123151
case 'keyObject.unique':
124152
keys = keyObjects;
125153
break;

0 commit comments

Comments
 (0)