Skip to content

Commit cb7b1ce

Browse files
committed
crypto: add raw key formats support to the KeyObject APIs
PR-URL: #62240 Reviewed-By: James M Snell <jasnell@gmail.com>
1 parent 60e41ba commit cb7b1ce

17 files changed

+1670
-195
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)