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

478 lines
15 KiB
JavaScript

'use strict';
const common = require('../common');
if (!common.hasCrypto)
common.skip('missing crypto');
const assert = require('assert');
const {
createCipheriv,
createDecipheriv,
createSign,
createVerify,
createSecretKey,
createPublicKey,
createPrivateKey,
KeyObject,
randomBytes,
publicDecrypt,
publicEncrypt,
privateDecrypt,
privateEncrypt
} = require('crypto');
const fixtures = require('../common/fixtures');
const publicPem = fixtures.readKey('rsa_public.pem', 'ascii');
const privatePem = fixtures.readKey('rsa_private.pem', 'ascii');
const publicDsa = fixtures.readKey('dsa_public_1025.pem', 'ascii');
const privateDsa = fixtures.readKey('dsa_private_encrypted_1025.pem',
'ascii');
{
// Attempting to create an empty key should throw.
assert.throws(() => {
createSecretKey(Buffer.alloc(0));
}, {
name: 'RangeError',
code: 'ERR_OUT_OF_RANGE',
message: 'The value of "key.byteLength" is out of range. ' +
'It must be > 0. Received 0'
});
}
{
// Attempting to create a key of a wrong type should throw
const TYPE = 'wrong_type';
assert.throws(() => new KeyObject(TYPE), {
name: 'TypeError',
code: 'ERR_INVALID_ARG_VALUE',
message: `The argument 'type' is invalid. Received '${TYPE}'`
});
}
{
// Attempting to create a key with non-object handle should throw
assert.throws(() => new KeyObject('secret', ''), {
name: 'TypeError',
code: 'ERR_INVALID_ARG_TYPE',
message:
'The "handle" argument must be of type object. Received type ' +
"string ('')"
});
}
{
const keybuf = randomBytes(32);
const key = createSecretKey(keybuf);
assert.strictEqual(key.type, 'secret');
assert.strictEqual(key.symmetricKeySize, 32);
assert.strictEqual(key.asymmetricKeyType, undefined);
const exportedKey = key.export();
assert(keybuf.equals(exportedKey));
const plaintext = Buffer.from('Hello world', 'utf8');
const cipher = createCipheriv('aes-256-ecb', key, null);
const ciphertext = Buffer.concat([
cipher.update(plaintext), cipher.final()
]);
const decipher = createDecipheriv('aes-256-ecb', key, null);
const deciphered = Buffer.concat([
decipher.update(ciphertext), decipher.final()
]);
assert(plaintext.equals(deciphered));
}
{
// Passing an existing public key object to createPublicKey should throw.
const publicKey = createPublicKey(publicPem);
assert.throws(() => createPublicKey(publicKey), {
name: '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.
assert.throws(() => createPrivateKey(createPublicKey(privatePem)), {
name: 'TypeError',
code: 'ERR_INVALID_ARG_TYPE',
message: 'The "key" argument must be of type string or an instance of ' +
'Buffer, TypedArray, or DataView. Received an instance of ' +
'PublicKeyObject'
});
// Similarly, passing an existing private key object to createPrivateKey
// should throw.
const privateKey = createPrivateKey(privatePem);
assert.throws(() => createPrivateKey(privateKey), {
name: 'TypeError',
code: 'ERR_INVALID_ARG_TYPE',
message: 'The "key" argument must be of type string or an instance of ' +
'Buffer, TypedArray, or DataView. Received an instance of ' +
'PrivateKeyObject'
});
}
{
const publicKey = createPublicKey(publicPem);
assert.strictEqual(publicKey.type, 'public');
assert.strictEqual(publicKey.asymmetricKeyType, 'rsa');
assert.strictEqual(publicKey.symmetricKeySize, undefined);
const privateKey = createPrivateKey(privatePem);
assert.strictEqual(privateKey.type, 'private');
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);
// Test exporting with an invalid options object, this should throw.
for (const opt of [undefined, null, 'foo', 0, NaN]) {
assert.throws(() => publicKey.export(opt), {
name: 'TypeError',
code: 'ERR_INVALID_ARG_TYPE',
message: /^The "options" argument must be of type object/
});
}
const publicDER = publicKey.export({
format: 'der',
type: 'pkcs1'
});
const privateDER = privateKey.export({
format: 'der',
type: 'pkcs1'
});
assert(Buffer.isBuffer(publicDER));
assert(Buffer.isBuffer(privateDER));
const plaintext = Buffer.from('Hello world', 'utf8');
const testDecryption = (fn, ciphertexts, decryptionKeys) => {
for (const ciphertext of ciphertexts) {
for (const key of decryptionKeys) {
const deciphered = fn(key, ciphertext);
assert.deepStrictEqual(deciphered, plaintext);
}
}
};
testDecryption(privateDecrypt, [
// 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),
publicEncrypt({ format: 'der', type: 'pkcs1', key: privateDER }, plaintext)
], [
privateKey,
{ format: 'pem', key: privatePem },
{ format: 'der', type: 'pkcs1', key: privateDER }
]);
testDecryption(publicDecrypt, [
privateEncrypt(privateKey, plaintext)
], [
// Decrypt using the public key.
publicKey,
{ format: 'pem', key: publicPem },
{ format: 'der', type: 'pkcs1', key: publicDER },
// Decrypt using the private key.
privateKey,
{ format: 'pem', key: privatePem },
{ format: 'der', type: 'pkcs1', key: privateDER }
]);
}
{
// This should not cause a crash: https://github.com/nodejs/node/issues/25247
assert.throws(() => {
createPrivateKey({ key: '' });
}, {
message: 'error:2007E073:BIO routines:BIO_new_mem_buf:null parameter',
code: 'ERR_OSSL_BIO_NULL_PARAMETER',
reason: 'null parameter',
library: 'BIO routines',
function: 'BIO_new_mem_buf',
});
// This should not abort either: https://github.com/nodejs/node/issues/29904
assert.throws(() => {
createPrivateKey({ key: Buffer.alloc(0), format: 'der', type: 'spki' });
}, {
code: 'ERR_INVALID_OPT_VALUE',
message: 'The value "spki" is invalid for option "type"'
});
// Unlike SPKI, PKCS#1 is a valid encoding for private keys (and public keys),
// so it should be accepted by createPrivateKey, but OpenSSL won't parse it.
assert.throws(() => {
const key = createPublicKey(publicPem).export({
format: 'der',
type: 'pkcs1'
});
createPrivateKey({ key, format: 'der', type: 'pkcs1' });
}, {
message: /asn1 encoding/,
library: 'asn1 encoding routines'
});
}
[
{ private: fixtures.readKey('ed25519_private.pem', 'ascii'),
public: fixtures.readKey('ed25519_public.pem', 'ascii'),
keyType: 'ed25519' },
{ private: fixtures.readKey('ed448_private.pem', 'ascii'),
public: fixtures.readKey('ed448_public.pem', 'ascii'),
keyType: 'ed448' },
{ private: fixtures.readKey('x25519_private.pem', 'ascii'),
public: fixtures.readKey('x25519_public.pem', 'ascii'),
keyType: 'x25519' },
{ private: fixtures.readKey('x448_private.pem', 'ascii'),
public: fixtures.readKey('x448_public.pem', 'ascii'),
keyType: 'x448' },
].forEach((info) => {
const keyType = info.keyType;
{
const exportOptions = { type: 'pkcs8', format: 'pem' };
const key = createPrivateKey(info.private);
assert.strictEqual(key.type, 'private');
assert.strictEqual(key.asymmetricKeyType, keyType);
assert.strictEqual(key.symmetricKeySize, undefined);
assert.strictEqual(key.export(exportOptions), info.private);
}
{
const exportOptions = { type: 'spki', format: 'pem' };
[info.private, info.public].forEach((pem) => {
const key = createPublicKey(pem);
assert.strictEqual(key.type, 'public');
assert.strictEqual(key.asymmetricKeyType, keyType);
assert.strictEqual(key.symmetricKeySize, undefined);
assert.strictEqual(key.export(exportOptions), info.public);
});
}
});
{
// Reading an encrypted key without a passphrase should fail.
assert.throws(() => createPrivateKey(privateDsa), {
name: 'TypeError',
code: 'ERR_MISSING_PASSPHRASE',
message: 'Passphrase required for encrypted key'
});
// Reading an encrypted key with a passphrase that exceeds OpenSSL's buffer
// size limit should fail with an appropriate error code.
assert.throws(() => createPrivateKey({
key: privateDsa,
format: 'pem',
passphrase: Buffer.alloc(1025, 'a')
}), {
code: 'ERR_OSSL_PEM_BAD_PASSWORD_READ',
name: 'Error'
});
// The buffer has a size of 1024 bytes, so this passphrase should be permitted
// (but will fail decryption).
assert.throws(() => createPrivateKey({
key: privateDsa,
format: 'pem',
passphrase: Buffer.alloc(1024, 'a')
}), {
message: /bad decrypt/
});
const publicKey = createPublicKey(publicDsa);
assert.strictEqual(publicKey.type, 'public');
assert.strictEqual(publicKey.asymmetricKeyType, 'dsa');
assert.strictEqual(publicKey.symmetricKeySize, undefined);
const privateKey = createPrivateKey({
key: privateDsa,
format: 'pem',
passphrase: 'secret'
});
assert.strictEqual(privateKey.type, 'private');
assert.strictEqual(privateKey.asymmetricKeyType, 'dsa');
assert.strictEqual(privateKey.symmetricKeySize, undefined);
}
{
// Test RSA-PSS.
{
// This key pair does not restrict the message digest algorithm or salt
// length.
const publicPem = fixtures.readKey('rsa_pss_public_2048.pem');
const privatePem = fixtures.readKey('rsa_pss_private_2048.pem');
const publicKey = createPublicKey(publicPem);
const privateKey = createPrivateKey(privatePem);
assert.strictEqual(publicKey.type, 'public');
assert.strictEqual(publicKey.asymmetricKeyType, 'rsa-pss');
assert.strictEqual(privateKey.type, 'private');
assert.strictEqual(privateKey.asymmetricKeyType, 'rsa-pss');
for (const key of [privatePem, privateKey]) {
// Any algorithm should work.
for (const algo of ['sha1', 'sha256']) {
// Any salt length should work.
for (const saltLength of [undefined, 8, 10, 12, 16, 18, 20]) {
const signature = createSign(algo)
.update('foo')
.sign({ key, saltLength });
for (const pkey of [key, publicKey, publicPem]) {
const okay = createVerify(algo)
.update('foo')
.verify({ key: pkey, saltLength }, signature);
assert.ok(okay);
}
}
}
}
// Exporting the key using PKCS#1 should not work since this would discard
// any algorithm restrictions.
assert.throws(() => {
publicKey.export({ format: 'pem', type: 'pkcs1' });
}, {
code: 'ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS'
});
}
{
// This key pair enforces sha256 as the message digest and the MGF1
// message digest and a salt length of at least 16 bytes.
const publicPem =
fixtures.readKey('rsa_pss_public_2048_sha256_sha256_16.pem');
const privatePem =
fixtures.readKey('rsa_pss_private_2048_sha256_sha256_16.pem');
const publicKey = createPublicKey(publicPem);
const privateKey = createPrivateKey(privatePem);
assert.strictEqual(publicKey.type, 'public');
assert.strictEqual(publicKey.asymmetricKeyType, 'rsa-pss');
assert.strictEqual(privateKey.type, 'private');
assert.strictEqual(privateKey.asymmetricKeyType, 'rsa-pss');
for (const key of [privatePem, privateKey]) {
// Signing with anything other than sha256 should fail.
assert.throws(() => {
createSign('sha1').sign(key);
}, /digest not allowed/);
// Signing with salt lengths less than 16 bytes should fail.
for (const saltLength of [8, 10, 12]) {
assert.throws(() => {
createSign('sha1').sign({ key, saltLength });
}, /pss saltlen too small/);
}
// Signing with sha256 and appropriate salt lengths should work.
for (const saltLength of [undefined, 16, 18, 20]) {
const signature = createSign('sha256')
.update('foo')
.sign({ key, saltLength });
for (const pkey of [key, publicKey, publicPem]) {
const okay = createVerify('sha256')
.update('foo')
.verify({ key: pkey, saltLength }, signature);
assert.ok(okay);
}
}
}
}
{
// This key enforces sha512 as the message digest and sha256 as the MGF1
// message digest.
const publicPem =
fixtures.readKey('rsa_pss_public_2048_sha512_sha256_20.pem');
const privatePem =
fixtures.readKey('rsa_pss_private_2048_sha512_sha256_20.pem');
const publicKey = createPublicKey(publicPem);
const privateKey = createPrivateKey(privatePem);
assert.strictEqual(publicKey.type, 'public');
assert.strictEqual(publicKey.asymmetricKeyType, 'rsa-pss');
assert.strictEqual(privateKey.type, 'private');
assert.strictEqual(privateKey.asymmetricKeyType, 'rsa-pss');
// Node.js usually uses the same hash function for the message and for MGF1.
// However, when a different MGF1 message digest algorithm has been
// specified as part of the key, it should automatically switch to that.
// This behavior is required by sections 3.1 and 3.3 of RFC4055.
for (const key of [privatePem, privateKey]) {
// sha256 matches the MGF1 hash function and should be used internally,
// but it should not be permitted as the main message digest algorithm.
for (const algo of ['sha1', 'sha256']) {
assert.throws(() => {
createSign(algo).sign(key);
}, /digest not allowed/);
}
// sha512 should produce a valid signature.
const signature = createSign('sha512')
.update('foo')
.sign(key);
for (const pkey of [key, publicKey, publicPem]) {
const okay = createVerify('sha512')
.update('foo')
.verify(pkey, signature);
assert.ok(okay);
}
}
}
}
{
// Exporting an encrypted private key requires a cipher
const privateKey = createPrivateKey(privatePem);
assert.throws(() => {
privateKey.export({
format: 'pem', type: 'pkcs8', passphrase: 'super-secret'
});
}, {
name: 'TypeError',
code: 'ERR_INVALID_OPT_VALUE',
message: 'The value "undefined" is invalid for option "cipher"'
});
}