Skip to content

Commit ed7599b

Browse files
tniessentargos
authored andcommitted
crypto: allow deriving public from private keys
This change allows passing private key objects to crypto.createPublicKey, resulting in a key object that represents a valid public key for the given private key. The returned public key object can be used and exported safely without revealing information about the private key. Backport-PR-URL: #26688 PR-URL: #26278 Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Ben Noordhuis <info@bnoordhuis.nl> Reviewed-By: Sam Roberts <vieuxtech@gmail.com>
1 parent be3ea2a commit ed7599b

File tree

3 files changed

+78
-25
lines changed

3 files changed

+78
-25
lines changed

doc/api/crypto.md

+12-5
Original file line numberDiff line numberDiff line change
@@ -1817,28 +1817,35 @@ must be an object with the properties described above.
18171817
<!-- YAML
18181818
added: v11.6.0
18191819
changes:
1820+
- version: REPLACEME
1821+
pr-url: https://github.com/nodejs/node/pull/26278
1822+
description: The `key` argument can now be a `KeyObject` with type
1823+
`private`.
18201824
- version: v11.7.0
18211825
pr-url: https://github.com/nodejs/node/pull/25217
18221826
description: The `key` argument can now be a private key.
18231827
-->
1824-
* `key` {Object | string | Buffer}
1828+
* `key` {Object | string | Buffer | KeyObject}
18251829
- `key`: {string | Buffer}
18261830
- `format`: {string} Must be `'pem'` or `'der'`. **Default:** `'pem'`.
18271831
- `type`: {string} Must be `'pkcs1'` or `'spki'`. This option is required
18281832
only if the `format` is `'der'`.
18291833
* Returns: {KeyObject}
18301834

18311835
Creates and returns a new key object containing a public key. If `key` is a
1832-
string or `Buffer`, `format` is assumed to be `'pem'`; otherwise, `key`
1833-
must be an object with the properties described above.
1836+
string or `Buffer`, `format` is assumed to be `'pem'`; if `key` is a `KeyObject`
1837+
with type `'private'`, the public key is derived from the given private key;
1838+
otherwise, `key` must be an object with the properties described above.
18341839

18351840
If the format is `'pem'`, the `'key'` may also be an X.509 certificate.
18361841

18371842
Because public keys can be derived from private keys, a private key may be
18381843
passed instead of a public key. In that case, this function behaves as if
18391844
[`crypto.createPrivateKey()`][] had been called, except that the type of the
1840-
returned `KeyObject` will be `public` and that the private key cannot be
1841-
extracted from the returned `KeyObject`.
1845+
returned `KeyObject` will be `'public'` and that the private key cannot be
1846+
extracted from the returned `KeyObject`. Similarly, if a `KeyObject` with type
1847+
`'private'` is given, a new `KeyObject` with type `'public'` will be returned
1848+
and it will be impossible to extract the private key from the returned object.
18421849

18431850
### crypto.createSecretKey(key)
18441851
<!-- YAML

lib/internal/crypto/keys.js

+31-19
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,12 @@ const { isArrayBufferView } = require('internal/util/types');
2626

2727
const kKeyType = Symbol('kKeyType');
2828

29+
// Key input contexts.
30+
const kConsumePublic = 0;
31+
const kConsumePrivate = 1;
32+
const kCreatePublic = 2;
33+
const kCreatePrivate = 3;
34+
2935
const encodingNames = [];
3036
for (const m of [[kKeyEncodingPKCS1, 'pkcs1'], [kKeyEncodingPKCS8, 'pkcs8'],
3137
[kKeyEncodingSPKI, 'spki'], [kKeyEncodingSEC1, 'sec1']])
@@ -203,7 +209,7 @@ function parseKeyEncoding(enc, keyType, isPublic, objName) {
203209
// when this is used to parse an input encoding and must be a valid key type if
204210
// used to parse an output encoding.
205211
function parsePublicKeyEncoding(enc, keyType, objName) {
206-
return parseKeyFormatAndType(enc, keyType, true, objName);
212+
return parseKeyEncoding(enc, keyType, keyType ? true : undefined, objName);
207213
}
208214

209215
// Parses the private key encoding based on an object. keyType must be undefined
@@ -213,26 +219,31 @@ function parsePrivateKeyEncoding(enc, keyType, objName) {
213219
return parseKeyEncoding(enc, keyType, false, objName);
214220
}
215221

216-
function getKeyObjectHandle(key, isPublic, allowKeyObject) {
217-
if (!allowKeyObject) {
222+
function getKeyObjectHandle(key, ctx) {
223+
if (ctx === kCreatePrivate) {
218224
throw new ERR_INVALID_ARG_TYPE(
219225
'key',
220226
['string', 'Buffer', 'TypedArray', 'DataView'],
221227
key
222228
);
223229
}
224-
if (isPublic != null) {
225-
const expectedType = isPublic ? 'public' : 'private';
226-
if (key.type !== expectedType)
227-
throw new ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE(key.type, expectedType);
230+
231+
if (key.type !== 'private') {
232+
if (ctx === kConsumePrivate || ctx === kCreatePublic)
233+
throw new ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE(key.type, 'private');
234+
if (key.type !== 'public') {
235+
throw new ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE(key.type,
236+
'private or public');
237+
}
228238
}
239+
229240
return key[kHandle];
230241
}
231242

232-
function prepareAsymmetricKey(key, isPublic, allowKeyObject = true) {
243+
function prepareAsymmetricKey(key, ctx) {
233244
if (isKeyObject(key)) {
234245
// Best case: A key object, as simple as that.
235-
return { data: getKeyObjectHandle(key, isPublic, allowKeyObject) };
246+
return { data: getKeyObjectHandle(key, ctx) };
236247
} else if (typeof key === 'string' || isArrayBufferView(key)) {
237248
// Expect PEM by default, mostly for backward compatibility.
238249
return { format: kKeyFormatPEM, data: key };
@@ -241,32 +252,32 @@ function prepareAsymmetricKey(key, isPublic, allowKeyObject = true) {
241252
// The 'key' property can be a KeyObject as well to allow specifying
242253
// additional options such as padding along with the key.
243254
if (isKeyObject(data))
244-
return { data: getKeyObjectHandle(data, isPublic, allowKeyObject) };
255+
return { data: getKeyObjectHandle(data, ctx) };
245256
// Either PEM or DER using PKCS#1 or SPKI.
246257
if (!isStringOrBuffer(data)) {
247258
throw new ERR_INVALID_ARG_TYPE(
248259
'key',
249260
['string', 'Buffer', 'TypedArray', 'DataView',
250-
...(allowKeyObject ? ['KeyObject'] : [])],
261+
...(ctx !== kCreatePrivate ? ['KeyObject'] : [])],
251262
key);
252263
}
253-
return { data, ...parseKeyEncoding(key, undefined, isPublic) };
264+
return { data, ...parseKeyEncoding(key, undefined) };
254265
} else {
255266
throw new ERR_INVALID_ARG_TYPE(
256267
'key',
257268
['string', 'Buffer', 'TypedArray', 'DataView',
258-
...(allowKeyObject ? ['KeyObject'] : [])],
269+
...(ctx !== kCreatePrivate ? ['KeyObject'] : [])],
259270
key
260271
);
261272
}
262273
}
263274

264-
function preparePrivateKey(key, allowKeyObject) {
265-
return prepareAsymmetricKey(key, false, allowKeyObject);
275+
function preparePrivateKey(key) {
276+
return prepareAsymmetricKey(key, kConsumePrivate);
266277
}
267278

268-
function preparePublicOrPrivateKey(key, allowKeyObject) {
269-
return prepareAsymmetricKey(key, undefined, allowKeyObject);
279+
function preparePublicOrPrivateKey(key) {
280+
return prepareAsymmetricKey(key, kConsumePublic);
270281
}
271282

272283
function prepareSecretKey(key, bufferOnly = false) {
@@ -296,14 +307,15 @@ function createSecretKey(key) {
296307
}
297308

298309
function createPublicKey(key) {
299-
const { format, type, data } = preparePublicOrPrivateKey(key, false);
310+
const { format, type, data } = prepareAsymmetricKey(key, kCreatePublic);
300311
const handle = new KeyObjectHandle(kKeyTypePublic);
301312
handle.init(data, format, type);
302313
return new PublicKeyObject(handle);
303314
}
304315

305316
function createPrivateKey(key) {
306-
const { format, type, data, passphrase } = preparePrivateKey(key, false);
317+
const { format, type, data, passphrase } =
318+
prepareAsymmetricKey(key, kCreatePrivate);
307319
const handle = new KeyObjectHandle(kKeyTypePrivate);
308320
handle.init(data, format, type, passphrase);
309321
return new PrivateKeyObject(handle);

test/parallel/test-crypto-key-objects.js

+35-1
Original file line numberDiff line numberDiff line change
@@ -59,9 +59,27 @@ const privatePem = fixtures.readSync('test_rsa_privkey.pem', 'ascii');
5959
}
6060

6161
{
62-
// Passing an existing key object should throw.
62+
// Passing an existing public key object to createPublicKey should throw.
6363
const publicKey = createPublicKey(publicPem);
6464
common.expectsError(() => createPublicKey(publicKey), {
65+
type: TypeError,
66+
code: 'ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE',
67+
message: 'Invalid key object type public, expected private.'
68+
});
69+
70+
// Constructing a private key from a public key should be impossible, even
71+
// if the public key was derived from a private key.
72+
common.expectsError(() => createPrivateKey(createPublicKey(privatePem)), {
73+
type: TypeError,
74+
code: 'ERR_INVALID_ARG_TYPE',
75+
message: 'The "key" argument must be one of type string, Buffer, ' +
76+
'TypedArray, or DataView. Received type object'
77+
});
78+
79+
// Similarly, passing an existing private key object to createPrivateKey
80+
// should throw.
81+
const privateKey = createPrivateKey(privatePem);
82+
common.expectsError(() => createPrivateKey(privateKey), {
6583
type: TypeError,
6684
code: 'ERR_INVALID_ARG_TYPE',
6785
message: 'The "key" argument must be one of type string, Buffer, ' +
@@ -80,6 +98,12 @@ const privatePem = fixtures.readSync('test_rsa_privkey.pem', 'ascii');
8098
assert.strictEqual(privateKey.asymmetricKeyType, 'rsa');
8199
assert.strictEqual(privateKey.symmetricKeySize, undefined);
82100

101+
// It should be possible to derive a public key from a private key.
102+
const derivedPublicKey = createPublicKey(privateKey);
103+
assert.strictEqual(derivedPublicKey.type, 'public');
104+
assert.strictEqual(derivedPublicKey.asymmetricKeyType, 'rsa');
105+
assert.strictEqual(derivedPublicKey.symmetricKeySize, undefined);
106+
83107
const publicDER = publicKey.export({
84108
format: 'der',
85109
type: 'pkcs1'
@@ -95,8 +119,18 @@ const privatePem = fixtures.readSync('test_rsa_privkey.pem', 'ascii');
95119

96120
const plaintext = Buffer.from('Hello world', 'utf8');
97121
const ciphertexts = [
122+
// Encrypt using the public key.
98123
publicEncrypt(publicKey, plaintext),
99124
publicEncrypt({ key: publicKey }, plaintext),
125+
126+
// Encrypt using the private key.
127+
publicEncrypt(privateKey, plaintext),
128+
publicEncrypt({ key: privateKey }, plaintext),
129+
130+
// Encrypt using a public key derived from the private key.
131+
publicEncrypt(derivedPublicKey, plaintext),
132+
publicEncrypt({ key: derivedPublicKey }, plaintext),
133+
100134
// Test distinguishing PKCS#1 public and private keys based on the
101135
// DER-encoded data only.
102136
publicEncrypt({ format: 'der', type: 'pkcs1', key: publicDER }, plaintext),

0 commit comments

Comments
 (0)