Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

crypto: allow deriving public from private keys #26278

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 12 additions & 5 deletions doc/api/crypto.md
Original file line number Diff line number Diff line change
Expand Up @@ -1813,28 +1813,35 @@ 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
only if the `format` is `'der'`.
* 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
tniessen marked this conversation as resolved.
Show resolved Hide resolved
`'private'` is given, a new `KeyObject` with type `'public'` will be returned
sam-github marked this conversation as resolved.
Show resolved Hide resolved
and it will be impossible to extract the private key from the returned object.

### crypto.createSecretKey(key)
<!-- YAML
Expand Down
50 changes: 31 additions & 19 deletions lib/internal/crypto/keys.js
Original file line number Diff line number Diff line change
Expand Up @@ -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']])
Expand Down Expand Up @@ -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);
sam-github marked this conversation as resolved.
Show resolved Hide resolved
}

// Parses the private key encoding based on an object. keyType must be undefined
Expand All @@ -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 };
Expand All @@ -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) {
Expand Down Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion src/node_crypto.cc
Original file line number Diff line number Diff line change
Expand Up @@ -3414,7 +3414,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);
Expand Down
36 changes: 35 additions & 1 deletion test/parallel/test-crypto-key-objects.js
Original file line number Diff line number Diff line change
Expand Up @@ -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, ' +
Expand All @@ -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'
Expand All @@ -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),
Expand Down