Skip to content

Commit 1bc0b49

Browse files
committed
crypto: add crypto.parsePKCS12()
Return the private key, end-entity certificate, and any other non-matching certificates from a PKCS#12 (.p12/.pfx) bundle as a KeyObject and X509Certificate instances. Node.js already parses PKCS#12 in SecureContext::LoadPKCS12, which backs tls's `pfx` option, but the results are consumed directly into an SSL_CTX and never reach JavaScript. Callers who need the key or the certificates for anything other than an immediate TLS connection have to shell out to `openssl pkcs12` or take a userland dependency. The binding wraps d2i_PKCS12_bio() and PKCS12_parse() and follows their semantics, matching the existing TLS path: the first private key is returned, the end-entity certificate is the one associated with that key, and any remaining certificates are returned through `additionalCertificates`. A bundle containing no private key reports `certificate` as null and returns its certificates through `additionalCertificates`. Absent and empty passphrases are kept distinct, since OpenSSL treats them differently. Bundles that require OpenSSL's legacy provider throw ERR_CRYPTO_UNSUPPORTED_OPERATION, reusing the error added for the TLS path. Signed-off-by: bmuenzenmeyer <brian.muenzenmeyer@gmail.com>
1 parent 9e168a3 commit 1bc0b49

10 files changed

Lines changed: 500 additions & 0 deletions

File tree

doc/api/crypto.md

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5516,6 +5516,64 @@ const derivedKey = hkdfSync('sha512', 'key', 'salt', 'info', 64);
55165516
console.log(Buffer.from(derivedKey).toString('hex')); // '24156e2...5391653'
55175517
```
55185518

5519+
### `crypto.parsePKCS12(bundle[, options])`
5520+
5521+
<!-- YAML
5522+
added: REPLACEME
5523+
-->
5524+
5525+
* `bundle` {ArrayBuffer|Buffer|TypedArray|DataView} The DER-encoded PKCS#12
5526+
bundle.
5527+
* `options` {Object}
5528+
* `passphrase` {string|ArrayBuffer|Buffer|TypedArray|DataView} The passphrase
5529+
protecting the bundle. Omit for bundles with no passphrase. Omitting this
5530+
option is **not** equivalent to passing an empty string; the two are
5531+
handled differently, and a bundle created with one will not open with the
5532+
other.
5533+
* Returns: {Object}
5534+
* `privateKey` {KeyObject|null} The private key, or `null` if the bundle
5535+
contains none.
5536+
* `certificate` {X509Certificate|null} The certificate associated with
5537+
`privateKey`, or `null` if the bundle contains none.
5538+
* `additionalCertificates` {X509Certificate\[]} Every other certificate in
5539+
the bundle. These are not necessarily certificate authorities; this is
5540+
whatever remains once `certificate` has been taken out. May be empty.
5541+
5542+
Parses a PKCS#12 bundle — commonly seen with the `.p12` or `.pfx` extension —
5543+
and returns its contents.
5544+
5545+
```mjs
5546+
import { parsePKCS12 } from 'node:crypto';
5547+
import { readFileSync } from 'node:fs';
5548+
5549+
const { privateKey, certificate } = parsePKCS12(
5550+
readFileSync('bundle.p12'),
5551+
{ passphrase: 'secret' },
5552+
);
5553+
5554+
console.log(certificate.subject);
5555+
console.log(privateKey.export({ type: 'pkcs8', format: 'pem' }));
5556+
```
5557+
5558+
A PKCS#12 bundle may technically contain more than one private key. This API
5559+
returns only the first, matching the behavior of OpenSSL's `PKCS12_parse()`.
5560+
5561+
`certificate` is identified by its association with the private key. A bundle
5562+
containing no private key therefore reports `certificate` as `null` and returns
5563+
all of its certificates through `additionalCertificates`, including any
5564+
end-entity certificate the bundle holds.
5565+
5566+
Bundles encrypted with older algorithms — notably RC2 and PBE-SHA1 variants
5567+
produced by legacy Windows tooling and older versions of `keytool` — require
5568+
OpenSSL's legacy provider. Reading these throws an error with the code
5569+
[`ERR_CRYPTO_UNSUPPORTED_OPERATION`][]; starting Node.js with
5570+
[`--openssl-legacy-provider`][] may allow them to be read, subject to the
5571+
security implications of enabling that provider.
5572+
5573+
To use a PKCS#12 bundle directly for a TLS connection, prefer the `pfx` option
5574+
of [`tls.createSecureContext()`][] rather than parsing and re-supplying the
5575+
parts.
5576+
55195577
### `crypto.pbkdf2(password, salt, iterations, keylen, digest, callback)`
55205578

55215579
<!-- YAML
@@ -7624,11 +7682,13 @@ See the [list of SSL OP Flags][] for details.
76247682
[`--enable-fips`]: cli.md#--enable-fips
76257683
[`--force-fips`]: cli.md#--force-fips
76267684
[`--openssl-config`]: cli.md#--openssl-configfile
7685+
[`--openssl-legacy-provider`]: cli.md#--openssl-legacy-provider
76277686
[`--openssl-shared-config`]: cli.md#--openssl-shared-config
76287687
[`BN_is_prime_ex`]: https://www.openssl.org/docs/man1.1.1/man3/BN_is_prime_ex.html
76297688
[`Buffer`]: buffer.md
76307689
[`DH_generate_key()`]: https://www.openssl.org/docs/man3.0/man3/DH_generate_key.html
76317690
[`DiffieHellmanGroup`]: #class-diffiehellmangroup
7691+
[`ERR_CRYPTO_UNSUPPORTED_OPERATION`]: errors.md#err_crypto_unsupported_operation
76327692
[`KeyObject`]: #class-keyobject
76337693
[`Sign`]: #class-sign
76347694
[`String.prototype.normalize()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/normalize
@@ -7690,6 +7750,7 @@ See the [list of SSL OP Flags][] for details.
76907750
[`stream.Transform`]: stream.md#class-streamtransform
76917751
[`stream.Writable` options]: stream.md#new-streamwritableoptions
76927752
[`stream.transform` options]: stream.md#new-streamtransformoptions
7753+
[`tls.createSecureContext()`]: tls.md#tlscreatesecurecontextoptions
76937754
[`util.promisify()`]: util.md#utilpromisifyoriginal
76947755
[`verify.update()`]: #verifyupdatedata-inputencoding
76957756
[`verify.verify()`]: #verifyverifykey-signature-signatureencoding

lib/crypto.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ const {
8484
createSecretKey,
8585
createPublicKey,
8686
createPrivateKey,
87+
parsePKCS12,
8788
KeyObject,
8889
} = require('internal/crypto/keys');
8990
const {
@@ -216,6 +217,7 @@ module.exports = {
216217
getMacs,
217218
hkdf,
218219
hkdfSync,
220+
parsePKCS12,
219221
pbkdf2,
220222
pbkdf2Sync,
221223
generateKeyPair,

lib/internal/crypto/keys.js

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
'use strict';
22

33
const {
4+
ArrayPrototypeMap,
45
ArrayPrototypeSlice,
56
ObjectDefineProperties,
67
ObjectPrototypeHasOwnProperty,
@@ -35,6 +36,7 @@ const {
3536
kKeyEncodingPKCS8,
3637
kKeyEncodingSPKI,
3738
kKeyEncodingSEC1,
39+
parsePKCS12: _parsePKCS12,
3840
} = internalBinding('crypto');
3941

4042
const {
@@ -77,6 +79,8 @@ const {
7779
isArrayBufferView,
7880
} = require('internal/util/types');
7981

82+
const { Buffer } = require('buffer');
83+
8084
const {
8185
fileURLToPath,
8286
getURLHref,
@@ -757,6 +761,58 @@ function createPublicKey(key) {
757761
return new PublicKeyObject(handle);
758762
}
759763

764+
/**
765+
* Parses a PKCS#12 (.p12 / .pfx) bundle. Returns an object holding the first
766+
* private key as `privateKey`, the certificate associated with it as
767+
* `certificate`, and every other certificate in the bundle as an array in
768+
* `additionalCertificates`. `privateKey` and `certificate` are null when the
769+
* bundle contains none.
770+
* @param {ArrayBuffer|Buffer|TypedArray|DataView} bundle
771+
* @param {object} [options]
772+
* @returns {object}
773+
*/
774+
function parsePKCS12(bundle, options = kEmptyObject) {
775+
if (!isArrayBufferView(bundle) && !isAnyArrayBuffer(bundle)) {
776+
throw new ERR_INVALID_ARG_TYPE(
777+
'bundle',
778+
['ArrayBuffer', 'TypedArray', 'DataView', 'Buffer'],
779+
bundle);
780+
}
781+
782+
validateObject(options, 'options');
783+
const { passphrase } = options;
784+
785+
// Absent and empty passphrases are distinct at the OpenSSL level and are
786+
// kept distinct here. `undefined` means no passphrase; '' means a
787+
// zero-length one.
788+
let passBuf;
789+
if (passphrase !== undefined) {
790+
passBuf = getArrayBufferOrView(passphrase, 'options.passphrase', 'utf8');
791+
// The binding reads the passphrase as a view; wrap a bare ArrayBuffer.
792+
if (isAnyArrayBuffer(passBuf)) passBuf = Buffer.from(passBuf);
793+
}
794+
795+
// Likewise, the binding reads the bundle as a view.
796+
const bundleBuf = isAnyArrayBuffer(bundle) ? Buffer.from(bundle) : bundle;
797+
798+
const {
799+
0: keyHandle,
800+
1: certHandle,
801+
2: otherHandles,
802+
} = _parsePKCS12(bundleBuf, passBuf);
803+
804+
// Required lazily: internal/crypto/x509 depends on this module.
805+
const { InternalX509Certificate } = require('internal/crypto/x509');
806+
807+
return {
808+
privateKey: keyHandle === null ? null : new PrivateKeyObject(keyHandle),
809+
certificate:
810+
certHandle === null ? null : new InternalX509Certificate(certHandle),
811+
additionalCertificates:
812+
ArrayPrototypeMap(otherHandles, (h) => new InternalX509Certificate(h)),
813+
};
814+
}
815+
760816
/**
761817
* Converts a secret KeyObjectHandle to a CryptoKey by dispatching to the
762818
* algorithm-specific Web Crypto import path.
@@ -1358,6 +1414,7 @@ module.exports = {
13581414
createSecretKey,
13591415
createPublicKey,
13601416
createPrivateKey,
1417+
parsePKCS12,
13611418
KeyObject,
13621419
CryptoKey,
13631420
InternalCryptoKey,

node.gyp

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -409,6 +409,7 @@
409409
'src/crypto/crypto_hash.cc',
410410
'src/crypto/crypto_keys.cc',
411411
'src/crypto/crypto_keygen.cc',
412+
'src/crypto/crypto_pkcs12.cc',
412413
'src/crypto/crypto_scrypt.cc',
413414
'src/crypto/crypto_tls.cc',
414415
'src/crypto/crypto_x509.cc',
@@ -429,6 +430,7 @@
429430
'src/crypto/crypto_hash.h',
430431
'src/crypto/crypto_keys.h',
431432
'src/crypto/crypto_keygen.h',
433+
'src/crypto/crypto_pkcs12.h',
432434
'src/crypto/crypto_scrypt.h',
433435
'src/crypto/crypto_tls.h',
434436
'src/crypto/crypto_context.h',

0 commit comments

Comments
 (0)