crypto: add crypto.parsePKCS12() · nodejs/node@dbcba0a · GitHub
Skip to content

Commit dbcba0a

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 dbcba0a

12 files changed

Lines changed: 609 additions & 0 deletions

File tree

‎doc/api/crypto.md‎

Lines changed: 63 additions & 0 deletions

‎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: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
'use strict';
22

33
const {
4+
ArrayPrototypeMap,
45
ArrayPrototypeSlice,
56
ObjectDefineProperties,
67
ObjectPrototypeHasOwnProperty,
78
ObjectSetPrototypeOf,
89
StringPrototypeIncludes,
910
StringPrototypeStartsWith,
1011
SymbolToStringTag,
12+
TypedArrayPrototypeIncludes,
1113
Uint8Array,
1214
} = primordials;
1315

@@ -35,6 +37,7 @@ const {
3537
kKeyEncodingPKCS8,
3638
kKeyEncodingSPKI,
3739
kKeyEncodingSEC1,
40+
parsePKCS12: _parsePKCS12,
3841
} = internalBinding('crypto');
3942

4043
const {
@@ -63,6 +66,7 @@ const {
6366

6467
const {
6568
getArrayBufferOrView,
69+
getBufferSourceBytes,
6670
bigIntArrayToUnsignedBigInt,
6771
normalizeAlgorithm,
6872
hasAnyNotIn,
@@ -77,6 +81,8 @@ const {
7781
isArrayBufferView,
7882
} = require('internal/util/types');
7983

84+
const { Buffer } = require('buffer');
85+
8086
const {
8187
fileURLToPath,
8288
getURLHref,
@@ -757,6 +763,65 @@ function createPublicKey(key) {
757763
return new PublicKeyObject(handle);
758764
}
759765

766+
/**
767+
* Parses a PKCS#12 (.p12 / .pfx) bundle. Returns an object holding the first
768+
* private key as `privateKey`, the certificate associated with it as
769+
* `certificate`, and every other certificate in the bundle as an array in
770+
* `additionalCertificates`. `privateKey` and `certificate` are null when the
771+
* bundle contains none.
772+
* @param {ArrayBuffer|Buffer|TypedArray|DataView} bundle
773+
* @param {object} [options]
774+
* @returns {object}
775+
*/
776+
function parsePKCS12(bundle, options = kEmptyObject) {
777+
if (!isArrayBufferView(bundle) && !isAnyArrayBuffer(bundle)) {
778+
throw new ERR_INVALID_ARG_TYPE(
779+
'bundle',
780+
['ArrayBuffer', 'TypedArray', 'DataView', 'Buffer'],
781+
bundle);
782+
}
783+
784+
validateObject(options, 'options');
785+
const { passphrase } = options;
786+
787+
// `undefined` means no passphrase, '' a zero-length one. OpenSSL accepts
788+
// either PKCS#12 password encoding for both, so they behave the same.
789+
let passBuf;
790+
if (passphrase !== undefined) {
791+
passBuf = getArrayBufferOrView(passphrase, 'options.passphrase', 'utf8');
792+
// OpenSSL takes the passphrase as a NUL-terminated C string, so one
793+
// containing a NUL byte would be truncated there and the bundle opened
794+
// with only the bytes before it. A PKCS#12 password cannot represent an
795+
// embedded NUL anyway, so reject it outright.
796+
if (TypedArrayPrototypeIncludes(getBufferSourceBytes(passBuf), 0)) {
797+
throw new ERR_INVALID_ARG_VALUE(
798+
'options.passphrase', passphrase, 'must not contain null bytes');
799+
}
800+
// The binding reads the passphrase as a view; wrap a bare ArrayBuffer.
801+
if (isAnyArrayBuffer(passBuf)) passBuf = Buffer.from(passBuf);
802+
}
803+
804+
// Likewise, the binding reads the bundle as a view.
805+
const bundleBuf = isAnyArrayBuffer(bundle) ? Buffer.from(bundle) : bundle;
806+
807+
const {
808+
0: keyHandle,
809+
1: certHandle,
810+
2: otherHandles,
811+
} = _parsePKCS12(bundleBuf, passBuf);
812+
813+
// Required lazily: internal/crypto/x509 depends on this module.
814+
const { InternalX509Certificate } = require('internal/crypto/x509');
815+
816+
return {
817+
privateKey: keyHandle === null ? null : new PrivateKeyObject(keyHandle),
818+
certificate:
819+
certHandle === null ? null : new InternalX509Certificate(certHandle),
820+
additionalCertificates:
821+
ArrayPrototypeMap(otherHandles, (h) => new InternalX509Certificate(h)),
822+
};
823+
}
824+
760825
/**
761826
* Converts a secret KeyObjectHandle to a CryptoKey by dispatching to the
762827
* algorithm-specific Web Crypto import path.
@@ -1358,6 +1423,7 @@ module.exports = {
13581423
createSecretKey,
13591424
createPublicKey,
13601425
createPrivateKey,
1426+
parsePKCS12,
13611427
KeyObject,
13621428
CryptoKey,
13631429
InternalCryptoKey,

‎node.gyp‎

Lines changed: 2 additions & 0 deletions

0 commit comments

Comments
 (0)