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:
parent
84ebaaa339
commit
fe7162915e
@ -1813,11 +1813,15 @@ must be an object with the properties described above.
|
||||
<!-- YAML
|
||||
added: v11.6.0
|
||||
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
|
||||
pr-url: https://github.com/nodejs/node/pull/25217
|
||||
description: The `key` argument can now be a private key.
|
||||
-->
|
||||
* `key` {Object | string | Buffer}
|
||||
* `key` {Object | string | Buffer | KeyObject}
|
||||
- `key`: {string | Buffer}
|
||||
- `format`: {string} Must be `'pem'` or `'der'`. **Default:** `'pem'`.
|
||||
- `type`: {string} Must be `'pkcs1'` or `'spki'`. This option is required
|
||||
@ -1825,16 +1829,19 @@ changes:
|
||||
* Returns: {KeyObject}
|
||||
|
||||
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`
|
||||
must be an object with the properties described above.
|
||||
string or `Buffer`, `format` is assumed to be `'pem'`; if `key` is a `KeyObject`
|
||||
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.
|
||||
|
||||
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
|
||||
[`crypto.createPrivateKey()`][] had been called, except that the type of the
|
||||
returned `KeyObject` will be `public` and that the private key cannot be
|
||||
extracted from the returned `KeyObject`.
|
||||
returned `KeyObject` will be `'public'` and that the private key cannot be
|
||||
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)
|
||||
<!-- YAML
|
||||
|
@ -26,6 +26,12 @@ const { isArrayBufferView } = require('internal/util/types');
|
||||
|
||||
const kKeyType = Symbol('kKeyType');
|
||||
|
||||
// Key input contexts.
|
||||
const kConsumePublic = 0;
|
||||
const kConsumePrivate = 1;
|
||||
const kCreatePublic = 2;
|
||||
const kCreatePrivate = 3;
|
||||
|
||||
const encodingNames = [];
|
||||
for (const m of [[kKeyEncodingPKCS1, 'pkcs1'], [kKeyEncodingPKCS8, 'pkcs8'],
|
||||
[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
|
||||
// used to parse an output encoding.
|
||||
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
|
||||
@ -213,26 +219,31 @@ function parsePrivateKeyEncoding(enc, keyType, objName) {
|
||||
return parseKeyEncoding(enc, keyType, false, objName);
|
||||
}
|
||||
|
||||
function getKeyObjectHandle(key, isPublic, allowKeyObject) {
|
||||
if (!allowKeyObject) {
|
||||
function getKeyObjectHandle(key, ctx) {
|
||||
if (ctx === kCreatePrivate) {
|
||||
throw new ERR_INVALID_ARG_TYPE(
|
||||
'key',
|
||||
['string', 'Buffer', 'TypedArray', 'DataView'],
|
||||
key
|
||||
);
|
||||
}
|
||||
if (isPublic != null) {
|
||||
const expectedType = isPublic ? 'public' : 'private';
|
||||
if (key.type !== expectedType)
|
||||
throw new ERR_CRYPTO_INVALID_KEY_OBJECT_TYPE(key.type, expectedType);
|
||||
|
||||
if (key.type !== 'private') {
|
||||
if (ctx === kConsumePrivate || ctx === kCreatePublic)
|
||||
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];
|
||||
}
|
||||
|
||||
function prepareAsymmetricKey(key, isPublic, allowKeyObject = true) {
|
||||
function prepareAsymmetricKey(key, ctx) {
|
||||
if (isKeyObject(key)) {
|
||||
// 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)) {
|
||||
// Expect PEM by default, mostly for backward compatibility.
|
||||
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
|
||||
// additional options such as padding along with the key.
|
||||
if (isKeyObject(data))
|
||||
return { data: getKeyObjectHandle(data, isPublic, allowKeyObject) };
|
||||
return { data: getKeyObjectHandle(data, ctx) };
|
||||
// Either PEM or DER using PKCS#1 or SPKI.
|
||||
if (!isStringOrBuffer(data)) {
|
||||
throw new ERR_INVALID_ARG_TYPE(
|
||||
'key',
|
||||
['string', 'Buffer', 'TypedArray', 'DataView',
|
||||
...(allowKeyObject ? ['KeyObject'] : [])],
|
||||
...(ctx !== kCreatePrivate ? ['KeyObject'] : [])],
|
||||
key);
|
||||
}
|
||||
return { data, ...parseKeyEncoding(key, undefined, isPublic) };
|
||||
return { data, ...parseKeyEncoding(key, undefined) };
|
||||
} else {
|
||||
throw new ERR_INVALID_ARG_TYPE(
|
||||
'key',
|
||||
['string', 'Buffer', 'TypedArray', 'DataView',
|
||||
...(allowKeyObject ? ['KeyObject'] : [])],
|
||||
...(ctx !== kCreatePrivate ? ['KeyObject'] : [])],
|
||||
key
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function preparePrivateKey(key, allowKeyObject) {
|
||||
return prepareAsymmetricKey(key, false, allowKeyObject);
|
||||
function preparePrivateKey(key) {
|
||||
return prepareAsymmetricKey(key, kConsumePrivate);
|
||||
}
|
||||
|
||||
function preparePublicOrPrivateKey(key, allowKeyObject) {
|
||||
return prepareAsymmetricKey(key, undefined, allowKeyObject);
|
||||
function preparePublicOrPrivateKey(key) {
|
||||
return prepareAsymmetricKey(key, kConsumePublic);
|
||||
}
|
||||
|
||||
function prepareSecretKey(key, bufferOnly = false) {
|
||||
@ -296,14 +307,15 @@ function createSecretKey(key) {
|
||||
}
|
||||
|
||||
function createPublicKey(key) {
|
||||
const { format, type, data } = preparePublicOrPrivateKey(key, false);
|
||||
const { format, type, data } = prepareAsymmetricKey(key, kCreatePublic);
|
||||
const handle = new KeyObjectHandle(kKeyTypePublic);
|
||||
handle.init(data, format, type);
|
||||
return new PublicKeyObject(handle);
|
||||
}
|
||||
|
||||
function createPrivateKey(key) {
|
||||
const { format, type, data, passphrase } = preparePrivateKey(key, false);
|
||||
const { format, type, data, passphrase } =
|
||||
prepareAsymmetricKey(key, kCreatePrivate);
|
||||
const handle = new KeyObjectHandle(kKeyTypePrivate);
|
||||
handle.init(data, format, type, passphrase);
|
||||
return new PrivateKeyObject(handle);
|
||||
|
@ -3409,7 +3409,7 @@ void KeyObject::Init(const FunctionCallbackInfo<Value>& args) {
|
||||
CHECK_EQ(args.Length(), 3);
|
||||
|
||||
offset = 0;
|
||||
pkey = GetPublicOrPrivateKeyFromJs(args, &offset, false);
|
||||
pkey = GetPublicOrPrivateKeyFromJs(args, &offset, true);
|
||||
if (!pkey)
|
||||
return;
|
||||
key->InitPublic(pkey);
|
||||
|
@ -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);
|
||||
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,
|
||||
code: 'ERR_INVALID_ARG_TYPE',
|
||||
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.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({
|
||||
format: 'der',
|
||||
type: 'pkcs1'
|
||||
@ -95,8 +119,18 @@ const privatePem = fixtures.readSync('test_rsa_privkey.pem', 'ascii');
|
||||
|
||||
const plaintext = Buffer.from('Hello world', 'utf8');
|
||||
const ciphertexts = [
|
||||
// Encrypt using the public key.
|
||||
publicEncrypt(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
|
||||
// DER-encoded data only.
|
||||
publicEncrypt({ format: 'der', type: 'pkcs1', key: publicDER }, plaintext),
|
||||
|
Loading…
x
Reference in New Issue
Block a user