Skip to content

Commit 9c96573

Browse files
tniessentargos
authored andcommitted
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 25bf1f5 commit 9c96573

File tree

3 files changed

+97
-17
lines changed

3 files changed

+97
-17
lines changed

lib/internal/crypto/keygen.js

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

src/node_crypto.cc

+20-12
Original file line numberDiff line numberDiff line change
@@ -5066,20 +5066,24 @@ class GenerateKeyPairJob : public CryptoJob {
50665066

50675067
// Now do the same for the private key (which is a bit more difficult).
50685068
if (private_key_encoding_.type_ == PK_ENCODING_PKCS1) {
5069-
// PKCS#1 is only permitted for RSA keys and without encryption.
5069+
// PKCS#1 is only permitted for RSA keys.
50705070
CHECK_EQ(EVP_PKEY_id(pkey), EVP_PKEY_RSA);
5071-
CHECK_NULL(private_key_encoding_.cipher_);
50725071

50735072
RSAPointer rsa(EVP_PKEY_get1_RSA(pkey));
50745073
if (private_key_encoding_.format_ == PK_FORMAT_PEM) {
50755074
// Encode PKCS#1 as PEM.
5076-
if (PEM_write_bio_RSAPrivateKey(bio.get(), rsa.get(),
5077-
nullptr, nullptr, 0,
5078-
nullptr, nullptr) != 1)
5075+
char* pass = private_key_encoding_.passphrase_.get();
5076+
if (PEM_write_bio_RSAPrivateKey(
5077+
bio.get(), rsa.get(),
5078+
private_key_encoding_.cipher_,
5079+
reinterpret_cast<unsigned char*>(pass),
5080+
private_key_encoding_.passphrase_length_,
5081+
nullptr, nullptr) != 1)
50795082
return false;
50805083
} else {
5081-
// Encode PKCS#1 as DER.
5084+
// Encode PKCS#1 as DER. This does not permit encryption.
50825085
CHECK_EQ(private_key_encoding_.format_, PK_FORMAT_DER);
5086+
CHECK_NULL(private_key_encoding_.cipher_);
50835087
if (i2d_RSAPrivateKey_bio(bio.get(), rsa.get()) != 1)
50845088
return false;
50855089
}
@@ -5107,20 +5111,24 @@ class GenerateKeyPairJob : public CryptoJob {
51075111
} else {
51085112
CHECK_EQ(private_key_encoding_.type_, PK_ENCODING_SEC1);
51095113

5110-
// SEC1 is only permitted for EC keys and without encryption.
5114+
// SEC1 is only permitted for EC keys.
51115115
CHECK_EQ(EVP_PKEY_id(pkey), EVP_PKEY_EC);
5112-
CHECK_NULL(private_key_encoding_.cipher_);
51135116

51145117
ECKeyPointer ec_key(EVP_PKEY_get1_EC_KEY(pkey));
51155118
if (private_key_encoding_.format_ == PK_FORMAT_PEM) {
51165119
// Encode SEC1 as PEM.
5117-
if (PEM_write_bio_ECPrivateKey(bio.get(), ec_key.get(),
5118-
nullptr, nullptr, 0,
5119-
nullptr, nullptr) != 1)
5120+
char* pass = private_key_encoding_.passphrase_.get();
5121+
if (PEM_write_bio_ECPrivateKey(
5122+
bio.get(), ec_key.get(),
5123+
private_key_encoding_.cipher_,
5124+
reinterpret_cast<unsigned char*>(pass),
5125+
private_key_encoding_.passphrase_length_,
5126+
nullptr, nullptr) != 1)
51205127
return false;
51215128
} else {
5122-
// Encode SEC1 as DER.
5129+
// Encode SEC1 as DER. This does not permit encryption.
51235130
CHECK_EQ(private_key_encoding_.format_, PK_FORMAT_DER);
5131+
CHECK_NULL(private_key_encoding_.cipher_);
51245132
if (i2d_ECPrivateKey_bio(bio.get(), ec_key.get()) != 1)
51255133
return false;
51265134
}

test/parallel/test-crypto-keygen.js

+74-4
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)