crypto: add support for PEM-level encryption
This adds support for PEM-level encryption as defined in RFC 1421. PEM-level encryption is intentionally unsupported for PKCS#8 private keys since PKCS#8 defines a newer encryption format. PR-URL: https://github.com/nodejs/node/pull/23151 Refs: https://github.com/nodejs/node/pull/22660 Reviewed-By: Ben Noordhuis <info@bnoordhuis.nl> Reviewed-By: James M Snell <jasnell@gmail.com>
This commit is contained in:
parent
6117af3490
commit
0a1c65079a
@ -140,7 +140,9 @@ function parseKeyEncoding(keyType, options) {
|
|||||||
if (cipher != null) {
|
if (cipher != null) {
|
||||||
if (typeof cipher !== 'string')
|
if (typeof cipher !== 'string')
|
||||||
throw new ERR_INVALID_OPT_VALUE('privateKeyEncoding.cipher', cipher);
|
throw new ERR_INVALID_OPT_VALUE('privateKeyEncoding.cipher', cipher);
|
||||||
if (privateType !== PK_ENCODING_PKCS8) {
|
if (privateFormat === PK_FORMAT_DER &&
|
||||||
|
(privateType === PK_ENCODING_PKCS1 ||
|
||||||
|
privateType === PK_ENCODING_SEC1)) {
|
||||||
throw new ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS(
|
throw new ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS(
|
||||||
strPrivateType, 'does not support encryption');
|
strPrivateType, 'does not support encryption');
|
||||||
}
|
}
|
||||||
|
@ -5060,20 +5060,24 @@ class GenerateKeyPairJob : public CryptoJob {
|
|||||||
|
|
||||||
// Now do the same for the private key (which is a bit more difficult).
|
// Now do the same for the private key (which is a bit more difficult).
|
||||||
if (private_key_encoding_.type_ == PK_ENCODING_PKCS1) {
|
if (private_key_encoding_.type_ == PK_ENCODING_PKCS1) {
|
||||||
// PKCS#1 is only permitted for RSA keys and without encryption.
|
// PKCS#1 is only permitted for RSA keys.
|
||||||
CHECK_EQ(EVP_PKEY_id(pkey), EVP_PKEY_RSA);
|
CHECK_EQ(EVP_PKEY_id(pkey), EVP_PKEY_RSA);
|
||||||
CHECK_NULL(private_key_encoding_.cipher_);
|
|
||||||
|
|
||||||
RSAPointer rsa(EVP_PKEY_get1_RSA(pkey));
|
RSAPointer rsa(EVP_PKEY_get1_RSA(pkey));
|
||||||
if (private_key_encoding_.format_ == PK_FORMAT_PEM) {
|
if (private_key_encoding_.format_ == PK_FORMAT_PEM) {
|
||||||
// Encode PKCS#1 as PEM.
|
// Encode PKCS#1 as PEM.
|
||||||
if (PEM_write_bio_RSAPrivateKey(bio.get(), rsa.get(),
|
char* pass = private_key_encoding_.passphrase_.get();
|
||||||
nullptr, nullptr, 0,
|
if (PEM_write_bio_RSAPrivateKey(
|
||||||
nullptr, nullptr) != 1)
|
bio.get(), rsa.get(),
|
||||||
|
private_key_encoding_.cipher_,
|
||||||
|
reinterpret_cast<unsigned char*>(pass),
|
||||||
|
private_key_encoding_.passphrase_length_,
|
||||||
|
nullptr, nullptr) != 1)
|
||||||
return false;
|
return false;
|
||||||
} else {
|
} else {
|
||||||
// Encode PKCS#1 as DER.
|
// Encode PKCS#1 as DER. This does not permit encryption.
|
||||||
CHECK_EQ(private_key_encoding_.format_, PK_FORMAT_DER);
|
CHECK_EQ(private_key_encoding_.format_, PK_FORMAT_DER);
|
||||||
|
CHECK_NULL(private_key_encoding_.cipher_);
|
||||||
if (i2d_RSAPrivateKey_bio(bio.get(), rsa.get()) != 1)
|
if (i2d_RSAPrivateKey_bio(bio.get(), rsa.get()) != 1)
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -5101,20 +5105,24 @@ class GenerateKeyPairJob : public CryptoJob {
|
|||||||
} else {
|
} else {
|
||||||
CHECK_EQ(private_key_encoding_.type_, PK_ENCODING_SEC1);
|
CHECK_EQ(private_key_encoding_.type_, PK_ENCODING_SEC1);
|
||||||
|
|
||||||
// SEC1 is only permitted for EC keys and without encryption.
|
// SEC1 is only permitted for EC keys.
|
||||||
CHECK_EQ(EVP_PKEY_id(pkey), EVP_PKEY_EC);
|
CHECK_EQ(EVP_PKEY_id(pkey), EVP_PKEY_EC);
|
||||||
CHECK_NULL(private_key_encoding_.cipher_);
|
|
||||||
|
|
||||||
ECKeyPointer ec_key(EVP_PKEY_get1_EC_KEY(pkey));
|
ECKeyPointer ec_key(EVP_PKEY_get1_EC_KEY(pkey));
|
||||||
if (private_key_encoding_.format_ == PK_FORMAT_PEM) {
|
if (private_key_encoding_.format_ == PK_FORMAT_PEM) {
|
||||||
// Encode SEC1 as PEM.
|
// Encode SEC1 as PEM.
|
||||||
if (PEM_write_bio_ECPrivateKey(bio.get(), ec_key.get(),
|
char* pass = private_key_encoding_.passphrase_.get();
|
||||||
nullptr, nullptr, 0,
|
if (PEM_write_bio_ECPrivateKey(
|
||||||
nullptr, nullptr) != 1)
|
bio.get(), ec_key.get(),
|
||||||
|
private_key_encoding_.cipher_,
|
||||||
|
reinterpret_cast<unsigned char*>(pass),
|
||||||
|
private_key_encoding_.passphrase_length_,
|
||||||
|
nullptr, nullptr) != 1)
|
||||||
return false;
|
return false;
|
||||||
} else {
|
} else {
|
||||||
// Encode SEC1 as DER.
|
// Encode SEC1 as DER. This does not permit encryption.
|
||||||
CHECK_EQ(private_key_encoding_.format_, PK_FORMAT_DER);
|
CHECK_EQ(private_key_encoding_.format_, PK_FORMAT_DER);
|
||||||
|
CHECK_NULL(private_key_encoding_.cipher_);
|
||||||
if (i2d_ECPrivateKey_bio(bio.get(), ec_key.get()) != 1)
|
if (i2d_ECPrivateKey_bio(bio.get(), ec_key.get()) != 1)
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -47,19 +47,23 @@ function testSignVerify(publicKey, privateKey) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Constructs a regular expression for a PEM-encoded key with the given label.
|
// Constructs a regular expression for a PEM-encoded key with the given label.
|
||||||
function getRegExpForPEM(label) {
|
function getRegExpForPEM(label, cipher) {
|
||||||
const head = `\\-\\-\\-\\-\\-BEGIN ${label}\\-\\-\\-\\-\\-`;
|
const head = `\\-\\-\\-\\-\\-BEGIN ${label}\\-\\-\\-\\-\\-`;
|
||||||
|
const rfc1421Header = cipher == null ? '' :
|
||||||
|
`\nProc-Type: 4,ENCRYPTED\nDEK-Info: ${cipher},[^\n]+\n`;
|
||||||
const body = '([a-zA-Z0-9\\+/=]{64}\n)*[a-zA-Z0-9\\+/=]{1,64}';
|
const body = '([a-zA-Z0-9\\+/=]{64}\n)*[a-zA-Z0-9\\+/=]{1,64}';
|
||||||
const end = `\\-\\-\\-\\-\\-END ${label}\\-\\-\\-\\-\\-`;
|
const end = `\\-\\-\\-\\-\\-END ${label}\\-\\-\\-\\-\\-`;
|
||||||
return new RegExp(`^${head}\n${body}\n${end}\n$`);
|
return new RegExp(`^${head}${rfc1421Header}\n${body}\n${end}\n$`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const pkcs1PubExp = getRegExpForPEM('RSA PUBLIC KEY');
|
const pkcs1PubExp = getRegExpForPEM('RSA PUBLIC KEY');
|
||||||
const pkcs1PrivExp = getRegExpForPEM('RSA PRIVATE KEY');
|
const pkcs1PrivExp = getRegExpForPEM('RSA PRIVATE KEY');
|
||||||
|
const pkcs1EncExp = (cipher) => getRegExpForPEM('RSA PRIVATE KEY', cipher);
|
||||||
const spkiExp = getRegExpForPEM('PUBLIC KEY');
|
const spkiExp = getRegExpForPEM('PUBLIC KEY');
|
||||||
const pkcs8Exp = getRegExpForPEM('PRIVATE KEY');
|
const pkcs8Exp = getRegExpForPEM('PRIVATE KEY');
|
||||||
const pkcs8EncExp = getRegExpForPEM('ENCRYPTED PRIVATE KEY');
|
const pkcs8EncExp = getRegExpForPEM('ENCRYPTED PRIVATE KEY');
|
||||||
const sec1Exp = getRegExpForPEM('EC PRIVATE KEY');
|
const sec1Exp = getRegExpForPEM('EC PRIVATE KEY');
|
||||||
|
const sec1EncExp = (cipher) => getRegExpForPEM('EC PRIVATE KEY', cipher);
|
||||||
|
|
||||||
// Since our own APIs only accept PEM, not DER, we need to convert DER to PEM
|
// Since our own APIs only accept PEM, not DER, we need to convert DER to PEM
|
||||||
// for testing.
|
// for testing.
|
||||||
@ -137,6 +141,42 @@ function convertDERToPEM(label, der) {
|
|||||||
testEncryptDecrypt(publicKey, privateKey);
|
testEncryptDecrypt(publicKey, privateKey);
|
||||||
testSignVerify(publicKey, privateKey);
|
testSignVerify(publicKey, privateKey);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
// Now do the same with an encrypted private key.
|
||||||
|
generateKeyPair('rsa', {
|
||||||
|
publicExponent: 0x10001,
|
||||||
|
modulusLength: 4096,
|
||||||
|
publicKeyEncoding: {
|
||||||
|
type: 'pkcs1',
|
||||||
|
format: 'der'
|
||||||
|
},
|
||||||
|
privateKeyEncoding: {
|
||||||
|
type: 'pkcs1',
|
||||||
|
format: 'pem',
|
||||||
|
cipher: 'aes-256-cbc',
|
||||||
|
passphrase: 'secret'
|
||||||
|
}
|
||||||
|
}, common.mustCall((err, publicKeyDER, privateKey) => {
|
||||||
|
assert.ifError(err);
|
||||||
|
|
||||||
|
// The public key is encoded as DER (which is binary) instead of PEM. We
|
||||||
|
// will still need to convert it to PEM for testing.
|
||||||
|
assert(Buffer.isBuffer(publicKeyDER));
|
||||||
|
const publicKey = convertDERToPEM('RSA PUBLIC KEY', publicKeyDER);
|
||||||
|
assertApproximateSize(publicKey, 720);
|
||||||
|
|
||||||
|
assert.strictEqual(typeof privateKey, 'string');
|
||||||
|
assert(pkcs1EncExp('AES-256-CBC').test(privateKey));
|
||||||
|
|
||||||
|
// Since the private key is encrypted, signing shouldn't work anymore.
|
||||||
|
assert.throws(() => {
|
||||||
|
testSignVerify(publicKey, privateKey);
|
||||||
|
}, /bad decrypt|asn1 encoding routines/);
|
||||||
|
|
||||||
|
const key = { key: privateKey, passphrase: 'secret' };
|
||||||
|
testEncryptDecrypt(publicKey, key);
|
||||||
|
testSignVerify(publicKey, key);
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
@ -203,6 +243,36 @@ function convertDERToPEM(label, der) {
|
|||||||
|
|
||||||
testSignVerify(publicKey, privateKey);
|
testSignVerify(publicKey, privateKey);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
// Do the same with an encrypted private key.
|
||||||
|
generateKeyPair('ec', {
|
||||||
|
namedCurve: 'prime256v1',
|
||||||
|
paramEncoding: 'named',
|
||||||
|
publicKeyEncoding: {
|
||||||
|
type: 'spki',
|
||||||
|
format: 'pem'
|
||||||
|
},
|
||||||
|
privateKeyEncoding: {
|
||||||
|
type: 'sec1',
|
||||||
|
format: 'pem',
|
||||||
|
cipher: 'aes-128-cbc',
|
||||||
|
passphrase: 'secret'
|
||||||
|
}
|
||||||
|
}, common.mustCall((err, publicKey, privateKey) => {
|
||||||
|
assert.ifError(err);
|
||||||
|
|
||||||
|
assert.strictEqual(typeof publicKey, 'string');
|
||||||
|
assert(spkiExp.test(publicKey));
|
||||||
|
assert.strictEqual(typeof privateKey, 'string');
|
||||||
|
assert(sec1EncExp('AES-128-CBC').test(privateKey));
|
||||||
|
|
||||||
|
// Since the private key is encrypted, signing shouldn't work anymore.
|
||||||
|
assert.throws(() => {
|
||||||
|
testSignVerify(publicKey, privateKey);
|
||||||
|
}, /bad decrypt|asn1 encoding routines/);
|
||||||
|
|
||||||
|
testSignVerify(publicKey, { key: privateKey, passphrase: 'secret' });
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
@ -640,7 +710,7 @@ function convertDERToPEM(label, der) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Attempting to encrypt a non-PKCS#8 key.
|
// Attempting to encrypt a DER-encoded, non-PKCS#8 key.
|
||||||
for (const type of ['pkcs1', 'sec1']) {
|
for (const type of ['pkcs1', 'sec1']) {
|
||||||
common.expectsError(() => {
|
common.expectsError(() => {
|
||||||
generateKeyPairSync(type === 'pkcs1' ? 'rsa' : 'ec', {
|
generateKeyPairSync(type === 'pkcs1' ? 'rsa' : 'ec', {
|
||||||
@ -649,7 +719,7 @@ function convertDERToPEM(label, der) {
|
|||||||
publicKeyEncoding: { type: 'spki', format: 'pem' },
|
publicKeyEncoding: { type: 'spki', format: 'pem' },
|
||||||
privateKeyEncoding: {
|
privateKeyEncoding: {
|
||||||
type,
|
type,
|
||||||
format: 'pem',
|
format: 'der',
|
||||||
cipher: 'aes-128-cbc',
|
cipher: 'aes-128-cbc',
|
||||||
passphrase: 'hello'
|
passphrase: 'hello'
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user