Skip to content

Commit cacbfb1

Browse files
committed
fixup! crypto: add raw key formats support to the KeyObject APIs
1 parent 729072d commit cacbfb1

File tree

1 file changed

+57
-173
lines changed

1 file changed

+57
-173
lines changed

doc/api/crypto.md

Lines changed: 57 additions & 173 deletions
Original file line numberDiff line numberDiff line change
@@ -112,8 +112,8 @@ The following table lists the asymmetric key types recognized by the
112112

113113
Asymmetric keys can be represented in several formats. **The recommended
114114
approach is to import key material into a [`KeyObject`][] once and reuse it**
115-
for all subsequent operations. A [`KeyObject`][] avoids repeated parsing
116-
and delivers the best performance.
115+
for all subsequent operations, as this avoids repeated parsing and delivers
116+
the best performance.
117117

118118
When a [`KeyObject`][] is not practical - for example, when key material
119119
arrives in a protocol message and is used only once - most cryptographic
@@ -124,46 +124,31 @@ options accepted by each format.
124124

125125
#### KeyObject
126126

127-
A [`KeyObject`][] is the in-memory representation of a parsed key and is
128-
**the preferred way to work with keys** in `node:crypto`. It is created by
129-
[`crypto.createPublicKey()`][], [`crypto.createPrivateKey()`][],
127+
A [`KeyObject`][] is the in-memory representation of a parsed key. It is
128+
created by [`crypto.createPublicKey()`][], [`crypto.createPrivateKey()`][],
130129
[`crypto.createSecretKey()`][], or key generation functions such as
131-
[`crypto.generateKeyPair()`][].
132-
133-
Because the key material is parsed once at creation time, reusing a
134-
[`KeyObject`][] across multiple operations avoids repeated parsing and
135-
delivers the best performance - for example, each Ed25519 signing operation
136-
with a reused [`KeyObject`][] is over 2× faster than passing a PEM string,
137-
and the savings compound with every subsequent use of the same [`KeyObject`][].
138-
Always prefer creating a [`KeyObject`][] up front when the same key is used
139-
more than once. The first cryptographic operation with a given
130+
[`crypto.generateKeyPair()`][]. The first cryptographic operation with a given
140131
[`KeyObject`][] may be slower than subsequent ones because OpenSSL lazily
141-
initializes internal caches on first use, but it will still generally be faster
142-
than passing key material in any other format.
132+
initializes internal caches on first use.
143133

144134
#### PEM and DER
145135

146136
PEM and DER are the traditional encoding formats for asymmetric keys based on
147-
ASN.1 structures. For private keys, these structures typically carry both the
148-
private and public key components, so no additional computation is needed
149-
during import - however, the ASN.1 parsing itself is the main cost.
137+
ASN.1 structures.
150138

151139
* **PEM** is a text encoding that wraps Base64-encoded DER data between
152140
header and footer lines (e.g. `-----BEGIN PUBLIC KEY-----`). PEM strings can
153-
be passed directly to most cryptographic operations. Public keys typically use
154-
the `'spki'` type and private keys typically use `'pkcs8'`.
141+
be passed directly to most cryptographic operations.
155142
* **DER** is the binary encoding of the same ASN.1 structures. When providing
156143
DER input, the `type` (typically `'spki'` or `'pkcs8'`) must be specified
157-
explicitly. DER avoids the Base64 decoding overhead of PEM, and the explicit
158-
`type` lets the parser skip type detection, making DER slightly faster to
159-
import than PEM.
144+
explicitly.
160145

161146
#### JSON Web Key (JWK)
162147

163148
JSON Web Key (JWK) is a JSON-based key representation defined in
164-
[RFC 7517][]. Instead of wrapping key material in ASN.1 structures, JWK
165-
encodes each key component as an individual Base64url-encoded value inside a
166-
JSON object.
149+
[RFC 7517][]. JWK encodes each key component as an individual Base64url-encoded
150+
value inside a JSON object. For RSA keys, JWK avoids ASN.1 parsing overhead
151+
and is the fastest serialized import format.
167152

168153
#### Raw key formats
169154

@@ -174,15 +159,12 @@ importing and exporting raw key material without any encoding wrapper.
174159
See [`keyObject.export()`][], [`crypto.createPublicKey()`][], and
175160
[`crypto.createPrivateKey()`][] for usage details.
176161

177-
The `'raw-public'` format is generally the fastest way to import a public key
178-
because no ASN.1 or Base64 decoding is needed. The `'raw-private'` and
179-
`'raw-seed'` formats, however, are not always faster than PEM, DER, or JWK
180-
because those formats only contain the private scalar or seed - importing them
181-
requires mathematically deriving the public key component for the purpose of
182-
committing the public key to the cryptographic output (e.g. elliptic curve
183-
point multiplication or seed expansion), which can be expensive depending on
184-
the key type. Other formats like PEM, DER, or JWK include both private and
185-
public components, avoiding that computation.
162+
`'raw-public'` is generally the fastest way to import a public key.
163+
`'raw-private'` and `'raw-seed'` are not always faster than other formats
164+
because they only contain the private scalar or seed - importing them requires
165+
deriving the public key component (e.g. elliptic curve point multiplication or
166+
seed expansion), which can be expensive. Other formats include both private
167+
and public components, avoiding that computation.
186168

187169
### Choosing a key format
188170

@@ -203,8 +185,7 @@ or expanding a seed). Which part dominates depends on the key type. For
203185
example:
204186

205187
* Public keys - `'raw-public'` is the fastest serialized format because the
206-
raw format skips all ASN.1 and Base64 decoding. For Ed25519 public key
207-
import, `'raw-public'` can be over 2× faster than PEM.
188+
raw format skips all ASN.1 and Base64 decoding.
208189
* EC private keys - `'raw-private'` is faster than PEM or DER because it
209190
avoids ASN.1 parsing. However, for larger curves (e.g. P-384, P-521) the
210191
required derivation of the public point from the private scalar becomes
@@ -229,7 +210,7 @@ signing or verification, the import cost is a larger fraction of the total,
229210
so a faster format like `'raw-public'` or `'raw-private'` can meaningfully
230211
improve throughput.
231212

232-
If the same key material is used only a few times, it is worth importing it
213+
Even if the same key material is used only a few times, it is worth importing it
233214
into a [`KeyObject`][] rather than passing the raw or PEM representation
234215
repeatedly.
235216

@@ -250,174 +231,77 @@ const signature = sign(null, data, privateKey);
250231
verify(null, data, publicKey, signature);
251232
```
252233

253-
Example: Importing a PEM-encoded key into a [`KeyObject`][]:
234+
Example: Importing keys of various formats into [`KeyObject`][]s:
254235

255236
```mjs
256237
import { promisify } from 'node:util';
257238
const {
258-
createPrivateKey, createPublicKey, generateKeyPair, sign, verify,
239+
createPrivateKey, createPublicKey, generateKeyPair,
259240
} = await import('node:crypto');
260241

261-
// PEM-encoded keys, e.g. read from a file or environment variable.
262242
const generated = await promisify(generateKeyPair)('ed25519');
243+
244+
// PEM
263245
const privatePem = generated.privateKey.export({ format: 'pem', type: 'pkcs8' });
264246
const publicPem = generated.publicKey.export({ format: 'pem', type: 'spki' });
247+
createPrivateKey(privatePem);
248+
createPublicKey(publicPem);
265249

266-
const privateKey = createPrivateKey(privatePem);
267-
const publicKey = createPublicKey(publicPem);
268-
269-
const data = new TextEncoder().encode('message to sign');
270-
const signature = sign(null, data, privateKey);
271-
verify(null, data, publicKey, signature);
272-
```
273-
274-
Example: Importing a JWK into a [`KeyObject`][]:
275-
276-
```mjs
277-
import { promisify } from 'node:util';
278-
const {
279-
createPrivateKey, createPublicKey, generateKeyPair, sign, verify,
280-
} = await import('node:crypto');
250+
// DER - requires explicit type
251+
const privateDer = generated.privateKey.export({ format: 'der', type: 'pkcs8' });
252+
const publicDer = generated.publicKey.export({ format: 'der', type: 'spki' });
253+
createPrivateKey({ key: privateDer, format: 'der', type: 'pkcs8' });
254+
createPublicKey({ key: publicDer, format: 'der', type: 'spki' });
281255

282-
// JWK objects, e.g. from a JSON configuration or API response.
283-
const generated = await promisify(generateKeyPair)('ed25519');
256+
// JWK
284257
const privateJwk = generated.privateKey.export({ format: 'jwk' });
285258
const publicJwk = generated.publicKey.export({ format: 'jwk' });
259+
createPrivateKey({ key: privateJwk, format: 'jwk' });
260+
createPublicKey({ key: publicJwk, format: 'jwk' });
286261

287-
const privateKey = createPrivateKey({ key: privateJwk, format: 'jwk' });
288-
const publicKey = createPublicKey({ key: publicJwk, format: 'jwk' });
289-
290-
const data = new TextEncoder().encode('message to sign');
291-
const signature = sign(null, data, privateKey);
292-
verify(null, data, publicKey, signature);
262+
// Raw
263+
const rawPriv = generated.privateKey.export({ format: 'raw-private' });
264+
const rawPub = generated.publicKey.export({ format: 'raw-public' });
265+
createPrivateKey({ key: rawPriv, format: 'raw-private', asymmetricKeyType: 'ed25519' });
266+
createPublicKey({ key: rawPub, format: 'raw-public', asymmetricKeyType: 'ed25519' });
293267
```
294268

295-
Example: Importing a DER-encoded key into a [`KeyObject`][]:
269+
Example: Passing key material directly to [`crypto.sign()`][] and
270+
[`crypto.verify()`][] without creating a [`KeyObject`][] first:
296271

297272
```mjs
298273
import { promisify } from 'node:util';
299-
const {
300-
createPrivateKey, createPublicKey, generateKeyPair, sign, verify,
301-
} = await import('node:crypto');
274+
const { generateKeyPair, sign, verify } = await import('node:crypto');
302275

303-
// DER-encoded keys, e.g. read from binary files or hex/base64url-decoded
304-
// from environment variables.
305276
const generated = await promisify(generateKeyPair)('ed25519');
306-
const privateDer = generated.privateKey.export({ format: 'der', type: 'pkcs8' });
307-
const publicDer = generated.publicKey.export({ format: 'der', type: 'spki' });
308-
309-
const privateKey = createPrivateKey({
310-
key: privateDer,
311-
format: 'der',
312-
type: 'pkcs8',
313-
});
314-
const publicKey = createPublicKey({
315-
key: publicDer,
316-
format: 'der',
317-
type: 'spki',
318-
});
319277

320278
const data = new TextEncoder().encode('message to sign');
321-
const signature = sign(null, data, privateKey);
322-
verify(null, data, publicKey, signature);
323-
```
324-
325-
Example: Passing PEM strings directly to [`crypto.sign()`][] and
326-
[`crypto.verify()`][]:
327279

328-
```mjs
329-
import { promisify } from 'node:util';
330-
const { generateKeyPair, sign, verify } = await import('node:crypto');
331-
332-
const generated = await promisify(generateKeyPair)('ed25519');
280+
// PEM strings
333281
const privatePem = generated.privateKey.export({ format: 'pem', type: 'pkcs8' });
334282
const publicPem = generated.publicKey.export({ format: 'pem', type: 'spki' });
283+
const sig1 = sign(null, data, privatePem);
284+
verify(null, data, publicPem, sig1);
335285

336-
// PEM strings can be passed directly without creating a KeyObject first.
337-
const data = new TextEncoder().encode('message to sign');
338-
const signature = sign(null, data, privatePem);
339-
verify(null, data, publicPem, signature);
340-
```
341-
342-
Example: Passing JWK objects directly to [`crypto.sign()`][] and
343-
[`crypto.verify()`][]:
344-
345-
```mjs
346-
import { promisify } from 'node:util';
347-
const { generateKeyPair, sign, verify } = await import('node:crypto');
348-
349-
const generated = await promisify(generateKeyPair)('ed25519');
286+
// JWK objects
350287
const privateJwk = generated.privateKey.export({ format: 'jwk' });
351288
const publicJwk = generated.publicKey.export({ format: 'jwk' });
352-
353-
// JWK objects can be passed directly without creating a KeyObject first.
354-
const data = new TextEncoder().encode('message to sign');
355-
const signature = sign(null, data, { key: privateJwk, format: 'jwk' });
356-
verify(null, data, { key: publicJwk, format: 'jwk' }, signature);
357-
```
358-
359-
Example: Passing raw key bytes directly to [`crypto.sign()`][] and
360-
[`crypto.verify()`][]:
361-
362-
```mjs
363-
import { promisify } from 'node:util';
364-
const { generateKeyPair, sign, verify } = await import('node:crypto');
365-
366-
const generated = await promisify(generateKeyPair)('ed25519');
367-
const rawPrivateKey = generated.privateKey.export({ format: 'raw-private' });
368-
const rawPublicKey = generated.publicKey.export({ format: 'raw-public' });
369-
370-
// Raw key bytes can be passed directly without creating a KeyObject first.
371-
const data = new TextEncoder().encode('message to sign');
372-
const signature = sign(null, data, {
373-
key: rawPrivateKey,
374-
format: 'raw-private',
375-
asymmetricKeyType: 'ed25519',
289+
const sig2 = sign(null, data, { key: privateJwk, format: 'jwk' });
290+
verify(null, data, { key: publicJwk, format: 'jwk' }, sig2);
291+
292+
// Raw key bytes
293+
const rawPriv = generated.privateKey.export({ format: 'raw-private' });
294+
const rawPub = generated.publicKey.export({ format: 'raw-public' });
295+
const sig3 = sign(null, data, {
296+
key: rawPriv, format: 'raw-private', asymmetricKeyType: 'ed25519',
376297
});
377298
verify(null, data, {
378-
key: rawPublicKey,
379-
format: 'raw-public',
380-
asymmetricKeyType: 'ed25519',
381-
}, signature);
382-
```
383-
384-
Example: Exporting raw keys and importing them:
385-
386-
```mjs
387-
import { promisify } from 'node:util';
388-
const {
389-
createPrivateKey, createPublicKey, generateKeyPair, sign, verify,
390-
} = await import('node:crypto');
391-
392-
const generated = await promisify(generateKeyPair)('ed25519');
393-
394-
// Export the raw public key (32 bytes for Ed25519).
395-
const rawPublicKey = generated.publicKey.export({ format: 'raw-public' });
396-
397-
// Export the raw private key (32 bytes for Ed25519).
398-
const rawPrivateKey = generated.privateKey.export({ format: 'raw-private' });
399-
400-
// Import the raw public key.
401-
const publicKey = createPublicKey({
402-
key: rawPublicKey,
403-
format: 'raw-public',
404-
asymmetricKeyType: 'ed25519',
405-
});
406-
407-
// Import the raw private key.
408-
const privateKey = createPrivateKey({
409-
key: rawPrivateKey,
410-
format: 'raw-private',
411-
asymmetricKeyType: 'ed25519',
412-
});
413-
414-
const data = new TextEncoder().encode('message to sign');
415-
const signature = sign(null, data, privateKey);
416-
verify(null, data, publicKey, signature);
299+
key: rawPub, format: 'raw-public', asymmetricKeyType: 'ed25519',
300+
}, sig3);
417301
```
418302

419303
Example: For EC keys, the `namedCurve` option is required when importing
420-
`'raw-public'` keys:
304+
raw keys:
421305

422306
```mjs
423307
import { promisify } from 'node:util';

0 commit comments

Comments
 (0)