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: add support for PEM-level encryption #23151

Closed
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
4 changes: 3 additions & 1 deletion lib/internal/crypto/keygen.js
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,9 @@ function parseKeyEncoding(keyType, options) {
if (cipher != null) {
if (typeof cipher !== 'string')
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(
strPrivateType, 'does not support encryption');
}
Expand Down
32 changes: 20 additions & 12 deletions src/node_crypto.cc
Original file line number Diff line number Diff line change
Expand Up @@ -5060,20 +5060,24 @@ class GenerateKeyPairJob : public CryptoJob {

// Now do the same for the private key (which is a bit more difficult).
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_NULL(private_key_encoding_.cipher_);

RSAPointer rsa(EVP_PKEY_get1_RSA(pkey));
if (private_key_encoding_.format_ == PK_FORMAT_PEM) {
// Encode PKCS#1 as PEM.
if (PEM_write_bio_RSAPrivateKey(bio.get(), rsa.get(),
nullptr, nullptr, 0,
nullptr, nullptr) != 1)
char* pass = private_key_encoding_.passphrase_.get();
if (PEM_write_bio_RSAPrivateKey(
bio.get(), rsa.get(),
private_key_encoding_.cipher_,
reinterpret_cast<unsigned char*>(pass),
private_key_encoding_.passphrase_length_,
nullptr, nullptr) != 1)
return false;
} 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_NULL(private_key_encoding_.cipher_);
if (i2d_RSAPrivateKey_bio(bio.get(), rsa.get()) != 1)
return false;
}
Expand Down Expand Up @@ -5101,20 +5105,24 @@ class GenerateKeyPairJob : public CryptoJob {
} else {
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_NULL(private_key_encoding_.cipher_);

ECKeyPointer ec_key(EVP_PKEY_get1_EC_KEY(pkey));
if (private_key_encoding_.format_ == PK_FORMAT_PEM) {
// Encode SEC1 as PEM.
if (PEM_write_bio_ECPrivateKey(bio.get(), ec_key.get(),
nullptr, nullptr, 0,
nullptr, nullptr) != 1)
char* pass = private_key_encoding_.passphrase_.get();
if (PEM_write_bio_ECPrivateKey(
bio.get(), ec_key.get(),
private_key_encoding_.cipher_,
reinterpret_cast<unsigned char*>(pass),
private_key_encoding_.passphrase_length_,
nullptr, nullptr) != 1)
return false;
} else {
// Encode SEC1 as DER.
// Encode SEC1 as DER. This does not permit encryption.
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)
return false;
}
Expand Down
78 changes: 74 additions & 4 deletions test/parallel/test-crypto-keygen.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,19 +47,23 @@ function testSignVerify(publicKey, privateKey) {
}

// 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 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 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 pkcs1PrivExp = getRegExpForPEM('RSA PRIVATE KEY');
const pkcs1EncExp = (cipher) => getRegExpForPEM('RSA PRIVATE KEY', cipher);
const spkiExp = getRegExpForPEM('PUBLIC KEY');
const pkcs8Exp = getRegExpForPEM('PRIVATE KEY');
const pkcs8EncExp = getRegExpForPEM('ENCRYPTED 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
// for testing.
Expand Down Expand Up @@ -137,6 +141,42 @@ function convertDERToPEM(label, der) {
testEncryptDecrypt(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);
}));
}

{
Expand Down Expand Up @@ -203,6 +243,36 @@ function convertDERToPEM(label, der) {

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' });
}));
}

{
Expand Down Expand Up @@ -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']) {
common.expectsError(() => {
generateKeyPairSync(type === 'pkcs1' ? 'rsa' : 'ec', {
Expand All @@ -649,7 +719,7 @@ function convertDERToPEM(label, der) {
publicKeyEncoding: { type: 'spki', format: 'pem' },
privateKeyEncoding: {
type,
format: 'pem',
format: 'der',
cipher: 'aes-128-cbc',
passphrase: 'hello'
}
Expand Down