Skip to content

Commit 0a1c650

Browse files
committed
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: #23151 Refs: #22660 Reviewed-By: Ben Noordhuis <[email protected]> Reviewed-By: James M Snell <[email protected]>
1 parent 6117af3 commit 0a1c650

File tree

3 files changed

+97
-17
lines changed

3 files changed

+97
-17
lines changed

lib/internal/crypto/keygen.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,9 @@ function parseKeyEncoding(keyType, options) {
140140
if (cipher != null) {
141141
if (typeof cipher !== 'string')
142142
throw new ERR_INVALID_OPT_VALUE('privateKeyEncoding.cipher', cipher);
143-
if (privateType !== PK_ENCODING_PKCS8) {
143+
if (privateFormat === PK_FORMAT_DER &&
144+
(privateType === PK_ENCODING_PKCS1 ||
145+
privateType === PK_ENCODING_SEC1)) {
144146
throw new ERR_CRYPTO_INCOMPATIBLE_KEY_OPTIONS(
145147
strPrivateType, 'does not support encryption');
146148
}

src/node_crypto.cc

Lines changed: 20 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5060,20 +5060,24 @@ class GenerateKeyPairJob : public CryptoJob {
50605060

50615061
// Now do the same for the private key (which is a bit more difficult).
50625062
if (private_key_encoding_.type_ == PK_ENCODING_PKCS1) {
5063-
// PKCS#1 is only permitted for RSA keys and without encryption.
5063+
// PKCS#1 is only permitted for RSA keys.
50645064
CHECK_EQ(EVP_PKEY_id(pkey), EVP_PKEY_RSA);
5065-
CHECK_NULL(private_key_encoding_.cipher_);
50665065

50675066
RSAPointer rsa(EVP_PKEY_get1_RSA(pkey));
50685067
if (private_key_encoding_.format_ == PK_FORMAT_PEM) {
50695068
// Encode PKCS#1 as PEM.
5070-
if (PEM_write_bio_RSAPrivateKey(bio.get(), rsa.get(),
5071-
nullptr, nullptr, 0,
5072-
nullptr, nullptr) != 1)
5069+
char* pass = private_key_encoding_.passphrase_.get();
5070+
if (PEM_write_bio_RSAPrivateKey(
5071+
bio.get(), rsa.get(),
5072+
private_key_encoding_.cipher_,
5073+
reinterpret_cast<unsigned char*>(pass),
5074+
private_key_encoding_.passphrase_length_,
5075+
nullptr, nullptr) != 1)
50735076
return false;
50745077
} else {
5075-
// Encode PKCS#1 as DER.
5078+
// Encode PKCS#1 as DER. This does not permit encryption.
50765079
CHECK_EQ(private_key_encoding_.format_, PK_FORMAT_DER);
5080+
CHECK_NULL(private_key_encoding_.cipher_);
50775081
if (i2d_RSAPrivateKey_bio(bio.get(), rsa.get()) != 1)
50785082
return false;
50795083
}
@@ -5101,20 +5105,24 @@ class GenerateKeyPairJob : public CryptoJob {
51015105
} else {
51025106
CHECK_EQ(private_key_encoding_.type_, PK_ENCODING_SEC1);
51035107

5104-
// SEC1 is only permitted for EC keys and without encryption.
5108+
// SEC1 is only permitted for EC keys.
51055109
CHECK_EQ(EVP_PKEY_id(pkey), EVP_PKEY_EC);
5106-
CHECK_NULL(private_key_encoding_.cipher_);
51075110

51085111
ECKeyPointer ec_key(EVP_PKEY_get1_EC_KEY(pkey));
51095112
if (private_key_encoding_.format_ == PK_FORMAT_PEM) {
51105113
// Encode SEC1 as PEM.
5111-
if (PEM_write_bio_ECPrivateKey(bio.get(), ec_key.get(),
5112-
nullptr, nullptr, 0,
5113-
nullptr, nullptr) != 1)
5114+
char* pass = private_key_encoding_.passphrase_.get();
5115+
if (PEM_write_bio_ECPrivateKey(
5116+
bio.get(), ec_key.get(),
5117+
private_key_encoding_.cipher_,
5118+
reinterpret_cast<unsigned char*>(pass),
5119+
private_key_encoding_.passphrase_length_,
5120+
nullptr, nullptr) != 1)
51145121
return false;
51155122
} else {
5116-
// Encode SEC1 as DER.
5123+
// Encode SEC1 as DER. This does not permit encryption.
51175124
CHECK_EQ(private_key_encoding_.format_, PK_FORMAT_DER);
5125+
CHECK_NULL(private_key_encoding_.cipher_);
51185126
if (i2d_ECPrivateKey_bio(bio.get(), ec_key.get()) != 1)
51195127
return false;
51205128
}

test/parallel/test-crypto-keygen.js

Lines changed: 74 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,19 +47,23 @@ function testSignVerify(publicKey, privateKey) {
4747
}
4848

4949
// Constructs a regular expression for a PEM-encoded key with the given label.
50-
function getRegExpForPEM(label) {
50+
function getRegExpForPEM(label, cipher) {
5151
const head = `\\-\\-\\-\\-\\-BEGIN ${label}\\-\\-\\-\\-\\-`;
52+
const rfc1421Header = cipher == null ? '' :
53+
`\nProc-Type: 4,ENCRYPTED\nDEK-Info: ${cipher},[^\n]+\n`;
5254
const body = '([a-zA-Z0-9\\+/=]{64}\n)*[a-zA-Z0-9\\+/=]{1,64}';
5355
const end = `\\-\\-\\-\\-\\-END ${label}\\-\\-\\-\\-\\-`;
54-
return new RegExp(`^${head}\n${body}\n${end}\n$`);
56+
return new RegExp(`^${head}${rfc1421Header}\n${body}\n${end}\n$`);
5557
}
5658

5759
const pkcs1PubExp = getRegExpForPEM('RSA PUBLIC KEY');
5860
const pkcs1PrivExp = getRegExpForPEM('RSA PRIVATE KEY');
61+
const pkcs1EncExp = (cipher) => getRegExpForPEM('RSA PRIVATE KEY', cipher);
5962
const spkiExp = getRegExpForPEM('PUBLIC KEY');
6063
const pkcs8Exp = getRegExpForPEM('PRIVATE KEY');
6164
const pkcs8EncExp = getRegExpForPEM('ENCRYPTED PRIVATE KEY');
6265
const sec1Exp = getRegExpForPEM('EC PRIVATE KEY');
66+
const sec1EncExp = (cipher) => getRegExpForPEM('EC PRIVATE KEY', cipher);
6367

6468
// Since our own APIs only accept PEM, not DER, we need to convert DER to PEM
6569
// for testing.
@@ -137,6 +141,42 @@ function convertDERToPEM(label, der) {
137141
testEncryptDecrypt(publicKey, privateKey);
138142
testSignVerify(publicKey, privateKey);
139143
}));
144+
145+
// Now do the same with an encrypted private key.
146+
generateKeyPair('rsa', {
147+
publicExponent: 0x10001,
148+
modulusLength: 4096,
149+
publicKeyEncoding: {
150+
type: 'pkcs1',
151+
format: 'der'
152+
},
153+
privateKeyEncoding: {
154+
type: 'pkcs1',
155+
format: 'pem',
156+
cipher: 'aes-256-cbc',
157+
passphrase: 'secret'
158+
}
159+
}, common.mustCall((err, publicKeyDER, privateKey) => {
160+
assert.ifError(err);
161+
162+
// The public key is encoded as DER (which is binary) instead of PEM. We
163+
// will still need to convert it to PEM for testing.
164+
assert(Buffer.isBuffer(publicKeyDER));
165+
const publicKey = convertDERToPEM('RSA PUBLIC KEY', publicKeyDER);
166+
assertApproximateSize(publicKey, 720);
167+
168+
assert.strictEqual(typeof privateKey, 'string');
169+
assert(pkcs1EncExp('AES-256-CBC').test(privateKey));
170+
171+
// Since the private key is encrypted, signing shouldn't work anymore.
172+
assert.throws(() => {
173+
testSignVerify(publicKey, privateKey);
174+
}, /bad decrypt|asn1 encoding routines/);
175+
176+
const key = { key: privateKey, passphrase: 'secret' };
177+
testEncryptDecrypt(publicKey, key);
178+
testSignVerify(publicKey, key);
179+
}));
140180
}
141181

142182
{
@@ -203,6 +243,36 @@ function convertDERToPEM(label, der) {
203243

204244
testSignVerify(publicKey, privateKey);
205245
}));
246+
247+
// Do the same with an encrypted private key.
248+
generateKeyPair('ec', {
249+
namedCurve: 'prime256v1',
250+
paramEncoding: 'named',
251+
publicKeyEncoding: {
252+
type: 'spki',
253+
format: 'pem'
254+
},
255+
privateKeyEncoding: {
256+
type: 'sec1',
257+
format: 'pem',
258+
cipher: 'aes-128-cbc',
259+
passphrase: 'secret'
260+
}
261+
}, common.mustCall((err, publicKey, privateKey) => {
262+
assert.ifError(err);
263+
264+
assert.strictEqual(typeof publicKey, 'string');
265+
assert(spkiExp.test(publicKey));
266+
assert.strictEqual(typeof privateKey, 'string');
267+
assert(sec1EncExp('AES-128-CBC').test(privateKey));
268+
269+
// Since the private key is encrypted, signing shouldn't work anymore.
270+
assert.throws(() => {
271+
testSignVerify(publicKey, privateKey);
272+
}, /bad decrypt|asn1 encoding routines/);
273+
274+
testSignVerify(publicKey, { key: privateKey, passphrase: 'secret' });
275+
}));
206276
}
207277

208278
{
@@ -640,7 +710,7 @@ function convertDERToPEM(label, der) {
640710
});
641711
}
642712

643-
// Attempting to encrypt a non-PKCS#8 key.
713+
// Attempting to encrypt a DER-encoded, non-PKCS#8 key.
644714
for (const type of ['pkcs1', 'sec1']) {
645715
common.expectsError(() => {
646716
generateKeyPairSync(type === 'pkcs1' ? 'rsa' : 'ec', {
@@ -649,7 +719,7 @@ function convertDERToPEM(label, der) {
649719
publicKeyEncoding: { type: 'spki', format: 'pem' },
650720
privateKeyEncoding: {
651721
type,
652-
format: 'pem',
722+
format: 'der',
653723
cipher: 'aes-128-cbc',
654724
passphrase: 'hello'
655725
}

0 commit comments

Comments
 (0)