Skip to content

Commit f1d3884

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 da5843b commit f1d3884

27 files changed

+1172
-597
lines changed

lib/internal/crypto/cfrg.js

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,23 @@
22

33
const {
44
SafeSet,
5+
TypedArrayPrototypeGetBuffer,
56
} = primordials;
67

78
const { Buffer } = require('buffer');
89

910
const {
10-
ECKeyExportJob,
1111
KeyObjectHandle,
1212
SignJob,
1313
kCryptoJobAsync,
14+
kKeyFormatDER,
1415
kKeyTypePrivate,
1516
kKeyTypePublic,
1617
kSignJobModeSign,
1718
kSignJobModeVerify,
19+
kWebCryptoKeyFormatPKCS8,
20+
kWebCryptoKeyFormatRaw,
21+
kWebCryptoKeyFormatSPKI,
1822
} = internalBinding('crypto');
1923

2024
const {
@@ -195,10 +199,30 @@ async function cfrgGenerateKey(algorithm, extractable, keyUsages) {
195199
}
196200

197201
function cfrgExportKey(key, format) {
198-
return jobPromise(() => new ECKeyExportJob(
199-
kCryptoJobAsync,
200-
format,
201-
key[kKeyObject][kHandle]));
202+
try {
203+
switch (format) {
204+
case kWebCryptoKeyFormatRaw: {
205+
if (key[kKeyType] === 'private') {
206+
return TypedArrayPrototypeGetBuffer(key[kKeyObject][kHandle].rawPrivateKey());
207+
}
208+
return TypedArrayPrototypeGetBuffer(key[kKeyObject][kHandle].rawPublicKey());
209+
}
210+
case kWebCryptoKeyFormatSPKI: {
211+
return TypedArrayPrototypeGetBuffer(
212+
key[kKeyObject][kHandle].export(kKeyFormatDER, kWebCryptoKeyFormatSPKI));
213+
}
214+
case kWebCryptoKeyFormatPKCS8: {
215+
return TypedArrayPrototypeGetBuffer(
216+
key[kKeyObject][kHandle].export(kKeyFormatDER, kWebCryptoKeyFormatPKCS8, null, null));
217+
}
218+
default:
219+
return undefined;
220+
}
221+
} catch (err) {
222+
throw lazyDOMException(
223+
'The operation failed for an operation-specific reason',
224+
{ name: 'OperationError', cause: err });
225+
}
202226
}
203227

204228
function cfrgImportKey(

lib/internal/crypto/ec.js

Lines changed: 47 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,30 @@
22

33
const {
44
SafeSet,
5+
TypedArrayPrototypeGetBuffer,
6+
TypedArrayPrototypeGetByteLength,
57
} = primordials;
68

79
const {
8-
ECKeyExportJob,
910
KeyObjectHandle,
1011
SignJob,
1112
kCryptoJobAsync,
13+
kKeyFormatDER,
1214
kKeyTypePrivate,
1315
kSignJobModeSign,
1416
kSignJobModeVerify,
1517
kSigEncP1363,
18+
kWebCryptoKeyFormatPKCS8,
19+
kWebCryptoKeyFormatRaw,
20+
kWebCryptoKeyFormatSPKI,
1621
} = internalBinding('crypto');
1722

23+
const {
24+
crypto: {
25+
POINT_CONVERSION_UNCOMPRESSED,
26+
},
27+
} = internalBinding('constants');
28+
1829
const {
1930
getUsagesUnion,
2031
hasAnyNotIn,
@@ -41,6 +52,7 @@ const {
4152
PublicKeyObject,
4253
createPrivateKey,
4354
createPublicKey,
55+
kAlgorithm,
4456
kKeyType,
4557
} = require('internal/crypto/keys');
4658

@@ -139,10 +151,40 @@ async function ecGenerateKey(algorithm, extractable, keyUsages) {
139151
}
140152

141153
function ecExportKey(key, format) {
142-
return jobPromise(() => new ECKeyExportJob(
143-
kCryptoJobAsync,
144-
format,
145-
key[kKeyObject][kHandle]));
154+
try {
155+
const handle = key[kKeyObject][kHandle];
156+
switch (format) {
157+
case kWebCryptoKeyFormatRaw: {
158+
return TypedArrayPrototypeGetBuffer(
159+
handle.exportECPublicRaw(POINT_CONVERSION_UNCOMPRESSED));
160+
}
161+
case kWebCryptoKeyFormatSPKI: {
162+
let spki = handle.export(kKeyFormatDER, kWebCryptoKeyFormatSPKI);
163+
// WebCrypto requires uncompressed point format for SPKI exports.
164+
// This is a very rare edge case dependent on the imported key
165+
// using compressed point format.
166+
if (TypedArrayPrototypeGetByteLength(spki) !== {
167+
'__proto__': null, 'P-256': 91, 'P-384': 120, 'P-521': 158,
168+
}[key[kAlgorithm].namedCurve]) {
169+
const raw = handle.exportECPublicRaw(POINT_CONVERSION_UNCOMPRESSED);
170+
const tmp = new KeyObjectHandle();
171+
tmp.initECRaw(kNamedCurveAliases[key[kAlgorithm].namedCurve], raw);
172+
spki = tmp.export(kKeyFormatDER, kWebCryptoKeyFormatSPKI);
173+
}
174+
return TypedArrayPrototypeGetBuffer(spki);
175+
}
176+
case kWebCryptoKeyFormatPKCS8: {
177+
return TypedArrayPrototypeGetBuffer(
178+
handle.export(kKeyFormatDER, kWebCryptoKeyFormatPKCS8, null, null));
179+
}
180+
default:
181+
return undefined;
182+
}
183+
} catch (err) {
184+
throw lazyDOMException(
185+
'The operation failed for an operation-specific reason',
186+
{ name: 'OperationError', cause: err });
187+
}
146188
}
147189

148190
function ecImportKey(

lib/internal/crypto/keys.js

Lines changed: 139 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,15 @@ const {
2727
kKeyEncodingSEC1,
2828
} = internalBinding('crypto');
2929

30+
const hasPqcSupport = KeyObjectHandle.prototype.initPqcRaw !== undefined;
31+
32+
const {
33+
crypto: {
34+
POINT_CONVERSION_COMPRESSED,
35+
POINT_CONVERSION_UNCOMPRESSED,
36+
},
37+
} = internalBinding('constants');
38+
3039
const {
3140
validateObject,
3241
validateOneOf,
@@ -82,6 +91,7 @@ const kKeyUsages = Symbol('kKeyUsages');
8291
const kCachedAlgorithm = Symbol('kCachedAlgorithm');
8392
const kCachedKeyUsages = Symbol('kCachedKeyUsages');
8493

94+
8595
// Key input contexts.
8696
const kConsumePublic = 0;
8797
const kConsumePrivate = 1;
@@ -340,14 +350,27 @@ const {
340350
}
341351

342352
export(options) {
343-
if (options && options.format === 'jwk') {
344-
return this[kHandle].exportJwk({}, false);
353+
switch (options?.format) {
354+
case 'jwk':
355+
return this[kHandle].exportJwk({}, false);
356+
case 'raw-public': {
357+
if (this.asymmetricKeyType === 'ec') {
358+
const { type = 'compressed' } = options;
359+
validateOneOf(type, 'options.type', ['compressed', 'uncompressed']);
360+
const form = type === 'compressed' ?
361+
POINT_CONVERSION_COMPRESSED : POINT_CONVERSION_UNCOMPRESSED;
362+
return this[kHandle].exportECPublicRaw(form);
363+
}
364+
return this[kHandle].rawPublicKey();
365+
}
366+
default: {
367+
const {
368+
format,
369+
type,
370+
} = parsePublicKeyEncoding(options, this.asymmetricKeyType);
371+
return this[kHandle].export(format, type);
372+
}
345373
}
346-
const {
347-
format,
348-
type,
349-
} = parsePublicKeyEncoding(options, this.asymmetricKeyType);
350-
return this[kHandle].export(format, type);
351374
}
352375
}
353376

@@ -357,20 +380,32 @@ const {
357380
}
358381

359382
export(options) {
360-
if (options && options.format === 'jwk') {
361-
if (options.passphrase !== undefined) {
362-
throw new ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS(
363-
'jwk', 'does not support encryption');
383+
if (options?.passphrase !== undefined &&
384+
options.format !== 'pem' && options.format !== 'der') {
385+
throw new ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS(
386+
options.format, 'does not support encryption');
387+
}
388+
switch (options?.format) {
389+
case 'jwk':
390+
return this[kHandle].exportJwk({}, false);
391+
case 'raw-private': {
392+
if (this.asymmetricKeyType === 'ec') {
393+
return this[kHandle].exportECPrivateRaw();
394+
}
395+
return this[kHandle].rawPrivateKey();
396+
}
397+
case 'raw-seed':
398+
return this[kHandle].rawSeed();
399+
default: {
400+
const {
401+
format,
402+
type,
403+
cipher,
404+
passphrase,
405+
} = parsePrivateKeyEncoding(options, this.asymmetricKeyType);
406+
return this[kHandle].export(format, type, cipher, passphrase);
364407
}
365-
return this[kHandle].exportJwk({}, false);
366408
}
367-
const {
368-
format,
369-
type,
370-
cipher,
371-
passphrase,
372-
} = parsePrivateKeyEncoding(options, this.asymmetricKeyType);
373-
return this[kHandle].export(format, type, cipher, passphrase);
374409
}
375410
}
376411

@@ -549,7 +584,7 @@ function mlDsaPubLen(alg) {
549584

550585
function getKeyObjectHandleFromJwk(key, ctx) {
551586
validateObject(key, 'key');
552-
if (KeyObjectHandle.prototype.initPqcRaw) {
587+
if (hasPqcSupport) {
553588
validateOneOf(
554589
key.kty, 'key.kty', ['RSA', 'EC', 'OKP', 'AKP']);
555590
} else {
@@ -691,6 +726,82 @@ function getKeyObjectHandleFromJwk(key, ctx) {
691726
return handle;
692727
}
693728

729+
730+
function getKeyObjectHandleFromRaw(options, data, format) {
731+
if (!isStringOrBuffer(data)) {
732+
throw new ERR_INVALID_ARG_TYPE(
733+
'key.key',
734+
['ArrayBuffer', 'Buffer', 'TypedArray', 'DataView'],
735+
data);
736+
}
737+
738+
const keyData = getArrayBufferOrView(data, 'key.key');
739+
740+
validateString(options.asymmetricKeyType, 'key.asymmetricKeyType');
741+
const asymmetricKeyType = options.asymmetricKeyType;
742+
743+
const handle = new KeyObjectHandle();
744+
745+
switch (asymmetricKeyType) {
746+
case 'ec': {
747+
validateString(options.namedCurve, 'key.namedCurve');
748+
if (format === 'raw-public') {
749+
if (!handle.initECRaw(options.namedCurve, keyData)) {
750+
throw new ERR_INVALID_ARG_VALUE('key.key', keyData);
751+
}
752+
} else if (!handle.initECPrivateRaw(options.namedCurve, keyData)) {
753+
throw new ERR_INVALID_ARG_VALUE('key.key', keyData);
754+
}
755+
return handle;
756+
}
757+
case 'ed25519':
758+
case 'ed448':
759+
case 'x25519':
760+
case 'x448': {
761+
const keyType = format === 'raw-public' ? kKeyTypePublic : kKeyTypePrivate;
762+
if (!handle.initEDRaw(asymmetricKeyType, keyData, keyType)) {
763+
throw new ERR_INVALID_ARG_VALUE('key.key', keyData);
764+
}
765+
return handle;
766+
}
767+
case 'rsa':
768+
case 'rsa-pss':
769+
case 'dsa':
770+
case 'dh':
771+
throw new ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS(
772+
format, `is not supported for ${asymmetricKeyType} keys`);
773+
case 'ml-dsa-44':
774+
case 'ml-dsa-65':
775+
case 'ml-dsa-87':
776+
case 'ml-kem-512':
777+
case 'ml-kem-768':
778+
case 'ml-kem-1024':
779+
case 'slh-dsa-sha2-128f':
780+
case 'slh-dsa-sha2-128s':
781+
case 'slh-dsa-sha2-192f':
782+
case 'slh-dsa-sha2-192s':
783+
case 'slh-dsa-sha2-256f':
784+
case 'slh-dsa-sha2-256s':
785+
case 'slh-dsa-shake-128f':
786+
case 'slh-dsa-shake-128s':
787+
case 'slh-dsa-shake-192f':
788+
case 'slh-dsa-shake-192s':
789+
case 'slh-dsa-shake-256f':
790+
case 'slh-dsa-shake-256s': {
791+
if (!hasPqcSupport) {
792+
throw new ERR_INVALID_ARG_VALUE('asymmetricKeyType', asymmetricKeyType);
793+
}
794+
const keyType = format === 'raw-public' ? kKeyTypePublic : kKeyTypePrivate;
795+
if (!handle.initPqcRaw(asymmetricKeyType, keyData, keyType)) {
796+
throw new ERR_INVALID_ARG_VALUE('key.key', keyData);
797+
}
798+
return handle;
799+
}
800+
default:
801+
throw new ERR_INVALID_ARG_VALUE('asymmetricKeyType', asymmetricKeyType);
802+
}
803+
}
804+
694805
function prepareAsymmetricKey(key, ctx) {
695806
if (isKeyObject(key)) {
696807
// Best case: A key object, as simple as that.
@@ -712,6 +823,12 @@ function prepareAsymmetricKey(key, ctx) {
712823
else if (format === 'jwk') {
713824
validateObject(data, 'key.key');
714825
return { data: getKeyObjectHandleFromJwk(data, ctx), format: 'jwk' };
826+
} else if (format === 'raw-public' || format === 'raw-private' ||
827+
format === 'raw-seed') {
828+
return {
829+
data: getKeyObjectHandleFromRaw(key, data, format),
830+
format,
831+
};
715832
}
716833

717834
// Either PEM or DER using PKCS#1 or SPKI.
@@ -777,7 +894,7 @@ function createPublicKey(key) {
777894
const { format, type, data, passphrase } =
778895
prepareAsymmetricKey(key, kCreatePublic);
779896
let handle;
780-
if (format === 'jwk') {
897+
if (format === 'jwk' || format === 'raw-public') {
781898
handle = data;
782899
} else {
783900
handle = new KeyObjectHandle();
@@ -790,7 +907,7 @@ function createPrivateKey(key) {
790907
const { format, type, data, passphrase } =
791908
prepareAsymmetricKey(key, kCreatePrivate);
792909
let handle;
793-
if (format === 'jwk') {
910+
if (format === 'jwk' || format === 'raw-private' || format === 'raw-seed') {
794911
handle = data;
795912
} else {
796913
handle = new KeyObjectHandle();

0 commit comments

Comments
 (0)