Skip to content

Commit d4fa60c

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. PR-URL: #62240 Reviewed-By: Matteo Collina <matteo.collina@gmail.com> Reviewed-By: Anna Henningsen <anna@addaleax.net> Reviewed-By: Yagiz Nizipli <yagiz@nizipli.com> Reviewed-By: James M Snell <jasnell@gmail.com>
1 parent 1ea93c7 commit d4fa60c

34 files changed

Lines changed: 1826 additions & 744 deletions

‎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
constbench=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+
returnp.keyType!=='rsa'&&!p.keyType.startsWith('ml-');
41+
// raw-public is not supported by rsa
42+
if(p.keyFormat==='raw-public')
43+
returnp.keyType!=='rsa';
44+
// raw-seed is only supported for ml-dsa
45+
if(p.keyFormat==='raw-seed')
46+
returnp.keyType.startsWith('ml-');
47+
returntrue;
48+
},
3549
});
3650

3751
functionmeasure(n,fn,input){
@@ -82,6 +96,29 @@ function main({ n, keyFormat, keyType }) {
8296
fn=crypto.createPrivateKey;
8397
break;
8498
}
99+
case'raw-public': {
100+
constexportedKey=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+
constexportedKey=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
thrownewError('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
returnfs.readFileSync(`${fixtures_keydir}/${name}.pem`,'utf8');
1212
}
1313

14+
functionreadKeyPair(publicKeyName,privateKeyName){
15+
return{
16+
publicKey: readKey(publicKeyName),
17+
privateKey: readKey(privateKeyName),
18+
};
19+
}
20+
1421
constkeyFixtures={};
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
constbench=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-
returnp.keyFormat!=='keyObject.unique'||
48-
(p.keyFormat==='keyObject.unique'&&p.mode==='async-parallel');
55+
if(p.keyFormat==='keyObject.unique')
56+
returnp.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+
returnp.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+
returnp.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+
returnp.keyType.startsWith('ml-');
69+
returntrue;
4970
},
5071
});
5172

52-
functionmeasureSync(n,op,privateKey,keys,ciphertexts){
73+
functionmeasureSync(n,op,key,keys,ciphertexts){
5374
bench.start();
5475
for(leti=0;i<n;++i){
55-
constkey=privateKey||keys[i];
76+
constk=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-
functionmeasureAsync(n,op,privateKey,keys,ciphertexts){
86+
functionmeasureAsync(n,op,key,keys,ciphertexts){
6687
letremaining=n;
6788
functiondone(){
6889
if(--remaining===0)
@@ -72,44 +93,98 @@ function measureAsync(n, op, privateKey, keys, ciphertexts) {
7293
}
7394

7495
functionone(){
75-
constkey=privateKey||keys[n-remaining];
96+
constk=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-
functionmeasureAsyncParallel(n,op,privateKey,keys,ciphertexts){
107+
functionmeasureAsyncParallel(n,op,key,keys,ciphertexts){
87108
letremaining=n;
88109
functiondone(){
89110
if(--remaining===0)
90111
bench.end(n);
91112
}
92113
bench.start();
93114
for(leti=0;i<n;++i){
94-
constkey=privateKey||keys[i];
115+
constk=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
functionmain({ n, mode, keyFormat, keyType, op }){
104-
constpems=[...Buffer.alloc(n)].map(()=>keyFixtures[keyType]);
105-
constkeyObjects=pems.map(crypto.createPrivateKey);
125+
constisEncapsulate=op==='encapsulate';
126+
constpemSource=isEncapsulate ?
127+
keyFixtures[keyType].publicKey :
128+
keyFixtures[keyType].privateKey;
129+
constcreateKeyFn=isEncapsulate ? crypto.createPublicKey : crypto.createPrivateKey;
130+
constpems=[...Buffer.alloc(n)].map(()=>pemSource);
131+
constkeyObjects=pems.map(createKeyFn);
106132

107-
letprivateKey,keys,ciphertexts;
133+
// Warm up OpenSSL's provider operation cache for each key object
134+
if(isEncapsulate){
135+
for(constkeyObjectofkeyObjects){
136+
crypto.encapsulate(keyObject);
137+
}
138+
}else{
139+
constwarmupCiphertext=crypto.encapsulate(keyObjects[0]).ciphertext;
140+
for(constkeyObjectofkeyObjects){
141+
crypto.decapsulate(keyObject,warmupCiphertext);
142+
}
143+
}
144+
145+
constasymmetricKeyType=keyObjects[0].asymmetricKeyType;
146+
letkey,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+
consttype=isEncapsulate ? 'spki' : 'pkcs8';
161+
key={key: keyObjects[0].export({format: 'der', type }),format: 'der', type };
162+
break;
163+
}
164+
case'raw-public': {
165+
constexportedKey=keyObjects[0].export({format: 'raw-public'});
166+
constkeyOpts={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+
constexportedKey=keyObjects[0].export({format: 'raw-private'});
173+
constkeyOpts={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+
constprivateKeyObject=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+
constencapKey=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
constbench=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-
returnp.keyFormat!=='keyObject.unique'||
39-
(p.keyFormat==='keyObject.unique'&&p.mode==='async-parallel');
38+
if(p.keyFormat==='keyObject.unique')
39+
returnp.mode==='async-parallel';
40+
// raw-private is not supported for rsa and ml-dsa
41+
if(p.keyFormat==='raw-private')
42+
returnp.keyType!=='rsa'&&!p.keyType.startsWith('ml-');
43+
// raw-seed is only supported for ml-dsa
44+
if(p.keyFormat==='raw-seed')
45+
returnp.keyType.startsWith('ml-');
46+
returntrue;
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(constkeyObjectofkeyObjects){
103+
crypto.sign(keyType==='rsa'||keyType==='ec' ? 'sha256' : null,
104+
data,keyObject);
105+
}
106+
94107
letprivateKey,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+
constexportedKey=keyObjects[0].export({format: 'raw-private'});
138+
constkeyOpts={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)