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.

PR-URL: https://github.com/nodejs/node/pull/26278
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Ben Noordhuis <info@bnoordhuis.nl>
Reviewed-By: Sam Roberts <vieuxtech@gmail.com>
This commit is contained in:
Tobias Nießen 2019-01-26 13:28:55 +01:00
parent 84ebaaa339
commit fe7162915e
No known key found for this signature in database
GPG Key ID: 718207F8FD156B70
4 changed files with 79 additions and 26 deletions

View File

@ -1813,11 +1813,15 @@ must be an object with the properties described above.
<!-- YAML <!-- YAML
added: v11.6.0 added: v11.6.0
changes: changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/26278
description: The `key` argument can now be a `KeyObject` with type
`private`.
- version: v11.7.0 - version: v11.7.0
pr-url: https://github.com/nodejs/node/pull/25217 pr-url: https://github.com/nodejs/node/pull/25217
description: The `key` argument can now be a private key. description: The `key` argument can now be a private key.
--> -->
* `key` {Object | string | Buffer} * `key` {Object | string | Buffer | KeyObject}
- `key`: {string | Buffer} - `key`: {string | Buffer}
- `format`: {string} Must be `'pem'` or `'der'`. **Default:** `'pem'`. - `format`: {string} Must be `'pem'` or `'der'`. **Default:** `'pem'`.
- `type`: {string} Must be `'pkcs1'` or `'spki'`. This option is required - `type`: {string} Must be `'pkcs1'` or `'spki'`. This option is required
@ -1825,16 +1829,19 @@ changes:
* Returns: {KeyObject} * Returns: {KeyObject}
Creates and returns a new key object containing a public key. If `key` is a Creates and returns a new key object containing a public key. If `key` is a
string or `Buffer`, `format` is assumed to be `'pem'`; otherwise, `key` string or `Buffer`, `format` is assumed to be `'pem'`; if `key` is a `KeyObject`
must be an object with the properties described above. with type `'private'`, the public key is derived from the given private key;
otherwise, `key` must be an object with the properties described above.
If the format is `'pem'`, the `'key'` may also be an X.509 certificate. If the format is `'pem'`, the `'key'` may also be an X.509 certificate.
Because public keys can be derived from private keys, a private key may be Because public keys can be derived from private keys, a private key may be
passed instead of a public key. In that case, this function behaves as if passed instead of a public key. In that case, this function behaves as if
[`crypto.createPrivateKey()`][] had been called, except that the type of the [`crypto.createPrivateKey()`][] had been called, except that the type of the
returned `KeyObject` will be `public` and that the private key cannot be returned `KeyObject` will be `'public'` and that the private key cannot be
extracted from the returned `KeyObject`. extracted from the returned `KeyObject`. Similarly, if a `KeyObject` with type
`'private'` is given, a new `KeyObject` with type `'public'` will be returned
and it will be impossible to extract the private key from the returned object.
### crypto.createSecretKey(key) ### crypto.createSecretKey(key)
<!-- YAML <!-- YAML

View File

@ -26,6 +26,12 @@ const { isArrayBufferView } = require('internal/util/types');
const kKeyType = Symbol('kKeyType'); const kKeyType = Symbol('kKeyType');
// Key input contexts.
const kConsumePublic = 0;
const kConsumePrivate = 1;
const kCreatePublic = 2;
const kCreatePrivate = 3;
const encodingNames = []; const encodingNames = [];
for (const m of [[kKeyEncodingPKCS1, 'pkcs1'], [kKeyEncodingPKCS8, 'pkcs8'], for (const m of [[kKeyEncodingPKCS1, 'pkcs1'], [kKeyEncodingPKCS8, 'pkcs8'],
[kKeyEncodingSPKI, 'spki'], [kKeyEncodingSEC1, 'sec1']]) [kKeyEncodingSPKI, 'spki'], [kKeyEncodingSEC1, 'sec1']])
@ -203,7 +209,7 @@ function parseKeyEncoding(enc, keyType, isPublic, objName) {
// when this is used to parse an input encoding and must be a valid key type if // when this is used to parse an input encoding and must be a valid key type if
// used to parse an output encoding. // used to parse an output encoding.
function parsePublicKeyEncoding(enc, keyType, objName) { function parsePublicKeyEncoding(enc, keyType, objName) {
return parseKeyFormatAndType(enc, keyType, true, objName); return parseKeyEncoding(enc, keyType, keyType ? true : undefined, objName);
} }
// Parses the private key encoding based on an object. keyType must be undefined // Parses the private key encoding based on an object. keyType must be undefined
@ -213,26 +219,31 @@ function parsePrivateKeyEncoding(enc, keyType, objName) {
return parseKeyEncoding(enc, keyType, false, objName); return parseKeyEncoding(enc, keyType, false, objName);
} }
function getKeyObjectHandle(key, isPublic, allowKeyObject) { function getKeyObjectHandle(key, ctx) {
if (!allowKeyObject) { if (ctx === kCreatePrivate) {
throw new ERR_INVALID_ARG_TYPE( throw new ERR_INVALID_ARG_TYPE(
'key', 'key',
['string', 'Buffer', 'TypedArray', 'DataView'], ['string', 'Buffer', 'TypedArray', 'DataView'],
key key
); );
} }
if (isPublic != null) {
const expectedType = isPublic ? 'public' : 'private'; if (key.type !== 'private') {
if (key.type !== expectedType) if (ctx === kConsumePrivate || ctx === kCreatePublic)
throw new ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE(key.type, expectedType); throw new ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE(key.type, 'private');
if (key.type !== 'public') {
throw new ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE(key.type,
'private or public');
}
} }
return key[kHandle]; return key[kHandle];
} }
function prepareAsymmetricKey(key, isPublic, allowKeyObject = true) { function prepareAsymmetricKey(key, ctx) {
if (isKeyObject(key)) { if (isKeyObject(key)) {
// Best case: A key object, as simple as that. // Best case: A key object, as simple as that.
return { data: getKeyObjectHandle(key, isPublic, allowKeyObject) }; return { data: getKeyObjectHandle(key, ctx) };
} else if (typeof key === 'string' || isArrayBufferView(key)) { } else if (typeof key === 'string' || isArrayBufferView(key)) {
// Expect PEM by default, mostly for backward compatibility. // Expect PEM by default, mostly for backward compatibility.
return { format: kKeyFormatPEM, data: key }; return { format: kKeyFormatPEM, data: key };
@ -241,32 +252,32 @@ function prepareAsymmetricKey(key, isPublic, allowKeyObject = true) {
// The 'key' property can be a KeyObject as well to allow specifying // The 'key' property can be a KeyObject as well to allow specifying
// additional options such as padding along with the key. // additional options such as padding along with the key.
if (isKeyObject(data)) if (isKeyObject(data))
return { data: getKeyObjectHandle(data, isPublic, allowKeyObject) }; return { data: getKeyObjectHandle(data, ctx) };
// Either PEM or DER using PKCS#1 or SPKI. // Either PEM or DER using PKCS#1 or SPKI.
if (!isStringOrBuffer(data)) { if (!isStringOrBuffer(data)) {
throw new ERR_INVALID_ARG_TYPE( throw new ERR_INVALID_ARG_TYPE(
'key', 'key',
['string', 'Buffer', 'TypedArray', 'DataView', ['string', 'Buffer', 'TypedArray', 'DataView',
...(allowKeyObject ? ['KeyObject'] : [])], ...(ctx !== kCreatePrivate ? ['KeyObject'] : [])],
key); key);
} }
return { data, ...parseKeyEncoding(key, undefined, isPublic) }; return { data, ...parseKeyEncoding(key, undefined) };
} else { } else {
throw new ERR_INVALID_ARG_TYPE( throw new ERR_INVALID_ARG_TYPE(
'key', 'key',
['string', 'Buffer', 'TypedArray', 'DataView', ['string', 'Buffer', 'TypedArray', 'DataView',
...(allowKeyObject ? ['KeyObject'] : [])], ...(ctx !== kCreatePrivate ? ['KeyObject'] : [])],
key key
); );
} }
} }
function preparePrivateKey(key, allowKeyObject) { function preparePrivateKey(key) {
return prepareAsymmetricKey(key, false, allowKeyObject); return prepareAsymmetricKey(key, kConsumePrivate);
} }
function preparePublicOrPrivateKey(key, allowKeyObject) { function preparePublicOrPrivateKey(key) {
return prepareAsymmetricKey(key, undefined, allowKeyObject); return prepareAsymmetricKey(key, kConsumePublic);
} }
function prepareSecretKey(key, bufferOnly = false) { function prepareSecretKey(key, bufferOnly = false) {
@ -296,14 +307,15 @@ function createSecretKey(key) {
} }
function createPublicKey(key) { function createPublicKey(key) {
const { format, type, data } = preparePublicOrPrivateKey(key, false); const { format, type, data } = prepareAsymmetricKey(key, kCreatePublic);
const handle = new KeyObjectHandle(kKeyTypePublic); const handle = new KeyObjectHandle(kKeyTypePublic);
handle.init(data, format, type); handle.init(data, format, type);
return new PublicKeyObject(handle); return new PublicKeyObject(handle);
} }
function createPrivateKey(key) { function createPrivateKey(key) {
const { format, type, data, passphrase } = preparePrivateKey(key, false); const { format, type, data, passphrase } =
prepareAsymmetricKey(key, kCreatePrivate);
const handle = new KeyObjectHandle(kKeyTypePrivate); const handle = new KeyObjectHandle(kKeyTypePrivate);
handle.init(data, format, type, passphrase); handle.init(data, format, type, passphrase);
return new PrivateKeyObject(handle); return new PrivateKeyObject(handle);

View File

@ -3409,7 +3409,7 @@ void KeyObject::Init(const FunctionCallbackInfo<Value>& args) {
CHECK_EQ(args.Length(), 3); CHECK_EQ(args.Length(), 3);
offset = 0; offset = 0;
pkey = GetPublicOrPrivateKeyFromJs(args, &offset, false); pkey = GetPublicOrPrivateKeyFromJs(args, &offset, true);
if (!pkey) if (!pkey)
return; return;
key->InitPublic(pkey); key->InitPublic(pkey);

View File

@ -59,9 +59,27 @@ const privatePem = fixtures.readSync('test_rsa_privkey.pem', 'ascii');
} }
{ {
// Passing an existing key object should throw. // Passing an existing public key object to createPublicKey should throw.
const publicKey = createPublicKey(publicPem); const publicKey = createPublicKey(publicPem);
common.expectsError(() => createPublicKey(publicKey), { common.expectsError(() => createPublicKey(publicKey), {
type: TypeError,
code: 'ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE',
message: 'Invalid key object type public, expected private.'
});
// Constructing a private key from a public key should be impossible, even
// if the public key was derived from a private key.
common.expectsError(() => createPrivateKey(createPublicKey(privatePem)), {
type: TypeError,
code: 'ERR_INVALID_ARG_TYPE',
message: 'The "key" argument must be one of type string, Buffer, ' +
'TypedArray, or DataView. Received type object'
});
// Similarly, passing an existing private key object to createPrivateKey
// should throw.
const privateKey = createPrivateKey(privatePem);
common.expectsError(() => createPrivateKey(privateKey), {
type: TypeError, type: TypeError,
code: 'ERR_INVALID_ARG_TYPE', code: 'ERR_INVALID_ARG_TYPE',
message: 'The "key" argument must be one of type string, Buffer, ' + message: 'The "key" argument must be one of type string, Buffer, ' +
@ -80,6 +98,12 @@ const privatePem = fixtures.readSync('test_rsa_privkey.pem', 'ascii');
assert.strictEqual(privateKey.asymmetricKeyType, 'rsa'); assert.strictEqual(privateKey.asymmetricKeyType, 'rsa');
assert.strictEqual(privateKey.symmetricKeySize, undefined); assert.strictEqual(privateKey.symmetricKeySize, undefined);
// It should be possible to derive a public key from a private key.
const derivedPublicKey = createPublicKey(privateKey);
assert.strictEqual(derivedPublicKey.type, 'public');
assert.strictEqual(derivedPublicKey.asymmetricKeyType, 'rsa');
assert.strictEqual(derivedPublicKey.symmetricKeySize, undefined);
const publicDER = publicKey.export({ const publicDER = publicKey.export({
format: 'der', format: 'der',
type: 'pkcs1' type: 'pkcs1'
@ -95,8 +119,18 @@ const privatePem = fixtures.readSync('test_rsa_privkey.pem', 'ascii');
const plaintext = Buffer.from('Hello world', 'utf8'); const plaintext = Buffer.from('Hello world', 'utf8');
const ciphertexts = [ const ciphertexts = [
// Encrypt using the public key.
publicEncrypt(publicKey, plaintext), publicEncrypt(publicKey, plaintext),
publicEncrypt({ key: publicKey }, plaintext), publicEncrypt({ key: publicKey }, plaintext),
// Encrypt using the private key.
publicEncrypt(privateKey, plaintext),
publicEncrypt({ key: privateKey }, plaintext),
// Encrypt using a public key derived from the private key.
publicEncrypt(derivedPublicKey, plaintext),
publicEncrypt({ key: derivedPublicKey }, plaintext),
// Test distinguishing PKCS#1 public and private keys based on the // Test distinguishing PKCS#1 public and private keys based on the
// DER-encoded data only. // DER-encoded data only.
publicEncrypt({ format: 'der', type: 'pkcs1', key: publicDER }, plaintext), publicEncrypt({ format: 'der', type: 'pkcs1', key: publicDER }, plaintext),