@blamejs/pki 0.3.10 → 0.3.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +10 -0
- package/README.md +1 -0
- package/index.js +6 -0
- package/lib/asn1-der.js +14 -0
- package/lib/cmp-build.js +3 -28
- package/lib/pbes2.js +49 -1
- package/lib/pkcs12-build.js +434 -0
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,16 @@ All notable changes to `@blamejs/pki` are documented here. The format
|
|
|
4
4
|
follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this
|
|
5
5
|
project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## v0.3.11 — 2026-07-23
|
|
8
|
+
|
|
9
|
+
pki.pkcs12 builds and MAC-verifies password-integrity PKCS#12 (.p12/.pfx) stores (RFC 7292, RFC 9579).
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- pki.pkcs12.build(spec, opts) builds a password-integrity PKCS#12 store. spec is { key, cert, ca?, friendlyName?, localKeyId? } (one PBES2-encrypted cert safe plus one shrouded-key safe) or { safeContents: [...] }, where each element is a plaintext or PBES2-encrypted SafeContents of key / shroudedKey / cert / crl / secret / nested safeContents bags; keys and certs are validated before wrapping. opts.mac selects a classic Appendix B HMAC (default) or an RFC 9579 PBMAC1 over SHA-256/384/512, or false for a MAC-less store; opts.password is the shared privacy + integrity password; opts.pem returns a PEM PKCS12 string. Shrouded keys and cert safes are encrypted under RFC 8018 PBES2 (AES-128/192/256-CBC). Passwords are BMPString+NULL encoded for the classic MAC and UTF-8 for the PBES2 bags and PBMAC1, so the output opens in OpenSSL and NSS; the MAC covers the exact AuthenticatedSafe byte range, a DEFAULT-1 MacData iterations and a <=160-bit PBMAC1 digest are rejected, and the store is re-parsed before return. RFC 7292, RFC 9579, RFC 8018.
|
|
14
|
+
- pki.pkcs12.verifyMac(pfx, password, opts) verifies a password-integrity store's MAC. pfx is a pki.schema.pkcs12.parse result, a DER Buffer, or a PEM string; the password is BMPString+NULL (classic) or UTF-8 (PBMAC1) encoded, the MAC is recomputed over the store's exact AuthenticatedSafe byte range with the store's own MAC parameters, and constant-time-compared to the stored value. Returns true / false for the password match; throws Pkcs12Error on a MAC-less or public-key-integrity store, or an unsupported MAC algorithm. RFC 7292 sec. 5.1, RFC 9579.
|
|
15
|
+
- pki.asn1.build.bmpString(str) encodes a JS string as a universal BMPString (UTF-16BE) TLV, rejecting an unpaired surrogate code point.
|
|
16
|
+
|
|
7
17
|
## v0.3.10 — 2026-07-23
|
|
8
18
|
|
|
9
19
|
pki.key exports, imports, and PBES2-encrypts private keys (RFC 5958, RFC 8018) over every WebCrypto algorithm.
|
package/README.md
CHANGED
|
@@ -229,6 +229,7 @@ is callable today; nothing below is a stub.
|
|
|
229
229
|
| `pki.cmp` | RFC 9810 Certificate Management Protocol message building — `build(message, opts)` assembles a protected `PKIMessage`. `message.header` carries the `sender` / `recipient` GeneralNames (including the anonymous NULL-DN) plus optional transaction metadata (`transactionID`, `senderNonce` / `recipNonce`, `messageTime` as a GeneralizedTime, `senderKID` / `recipKID`, `freeText`, `generalInfo`); `message.body` is a single-key object naming the arm. Request-side: `ir` / `cr` / `kur` (a `CertReqMessages` spec delegated to `pki.crmf.build`), `p10cr` (a PKCS#10 `CertificationRequest`), `certConf`, `pollReq`, `genm`, `rr`. CA/responder-side: `ip` / `cp` / `kup` / `ccp` (a `CertRepMessage` — `caPubs` and `response` entries carrying a `PKIStatusInfo` and, under a granting status, a `certifiedKeyPair`), `rp` (revocation response), `genp`, `error`, `pollRep`, `krp` (key-recovery response), `pkiconf`. Protection is exactly one of `opts.{ key, cert }` — a signature over the message under the sender key, the algorithm resolved from the signer certificate so RSA (PKCS#1 v1.5 / PSS), ECDSA, EdDSA, ML-DSA, SLH-DSA, and the composite arms all sign without a per-algorithm branch — or `opts.mac` — a PBMAC1 shared-secret HMAC (RFC 9481 / 9579, PBKDF2-derived). The protection covers the exact DER of the virtual `ProtectedPart` (the header and body) and is self-verified before the message is returned; the `protectionAlg` is derived, never caller-set, so the message the parser accepts is coherent by construction. Returns DER, or a PEM `CMP` block with `opts.pem`; malformed input throws a typed `CmpError`. Parsing stays at `pki.schema.cmp.parse` — `build` |
|
|
230
230
|
| `pki.crl` | RFC 5280 §5 certificate revocation list issuance — `sign(spec, issuer, opts)` builds and signs a `CertificateList`: a `spec` of `thisUpdate` / `nextUpdate`, an optional `crlNumber`, a `revoked` array (each entry a `serialNumber` + `revocationDate` with an optional `reason` or `invalidityDate`), and an optional `extensions` object (authority key identifier, issuing distribution point, delta-CRL indicator, freshest CRL, authority information access) or an array of pre-encoded Extension DER; an `issuer` of `{ cert, key }` or `{ name, publicKey, key }`. The signature algorithm is resolved from the issuer key, so RSA (PKCS#1 v1.5 / PSS via `opts.pss`), ECDSA, EdDSA, ML-DSA, SLH-DSA, and the composite arms all sign without a per-algorithm branch. The version is derived from the extension set (v2 when any CRL or entry extension is present, else v1), the outer `signatureAlgorithm` matches `tbsCertList.signature`, an empty revocation list omits the field rather than emitting an empty SEQUENCE, `reasonCode` is an ENUMERATED and `invalidityDate` is always GeneralizedTime, per-extension criticality is fixed by the RFC, and the produced signature is verified under the issuer key before return. `verify(crl, issuer)` checks a CRL signature through the one path-validation signature engine (algorithm-confusion and EdDSA low-order gates included), and `isRevoked(crl, serialNumber)` looks a serial up in the revocation list. Returns DER, or a PEM `X509 CRL` with `opts.pem`; malformed input throws a typed `CrlError`. Parsing stays at `pki.schema.crl.parse` — `sign` / `verify` / `isRevoked` |
|
|
231
231
|
| `pki.key` | RFC 5958 / RFC 8018 key-material lifecycle — `encrypt(privateKey, password, opts)` wraps a PKCS#8 private key (DER, PEM, or an extractable `CryptoKey`) into an `EncryptedPrivateKeyInfo` under PBES2 (PBKDF2 + AES-CBC-Pad): `opts` selects the `cipher` (`aes-256-cbc` default, `aes-192-cbc`, `aes-128-cbc`), the `prf` (`hmacWithSHA256` default, SHA-384/512, SHA-1), the `iterations` (default 600000), and the `salt`; the plaintext is validated as PKCS#8 before encryption, a default `prf` and `keyLength` are omitted so the parameters are byte-exact with OpenSSL, and the output is re-parsed before return. `decrypt(encrypted, password, opts)` recovers the inner `PrivateKeyInfo` (re-validated through `pki.schema.pkcs8.parse`) — only PBES2/PBKDF2/AES-CBC is accepted (PBES1, PBMAC1, scrypt refused), the salt and iteration count are bounded before any derivation (`opts.maxIterations` lowers the cap), a malformed parameter set or wrong-length IV is a distinct typed error, and — because a MAC-less PBES2-CBC decrypt must not be a padding oracle (RFC 8018 §8) — a wrong password and a valid-pad-but-not-a-key both surface the one uniform `key/decrypt-failed`. `export(key, opts)` / `import(input, opts)` move a private key as PKCS#8 or a public key as SubjectPublicKeyInfo, delegating the encoding to WebCrypto so RSA carries an explicit NULL, EC a named curve, and Ed25519/Ed448/X25519/X448 omit parameters (an ambiguous RSA/EC import requires `opts.algorithm`). `generate(algorithm, opts)` produces a key pair over RSA, ECDSA/ECDH, the Edwards/Montgomery curves, and the FIPS post-quantum ML-DSA / ML-KEM, and `publicFromPrivate(privateKey)` derives the public key. Returns DER or PEM; fail-closed with typed `KeyError`. Parsing stays at `pki.schema.pkcs8.parse` — `encrypt` / `decrypt` / `export` / `import` / `generate` / `publicFromPrivate` |
|
|
232
|
+
| `pki.pkcs12` | RFC 7292 / RFC 9579 PKCS#12 (.p12/.pfx) issuance — `build(spec, opts)` assembles a password-integrity store. `spec` is the OpenSSL-style `{ key, cert, ca?, friendlyName?, localKeyId? }` or the full `{ safeContents: [...] }`, where each element is a plaintext or PBES2-encrypted `SafeContents` of key / shroudedKey / cert / crl / secret / nested `safeContents` bags. Keys and certs are validated before wrapping; `friendlyName` (BMPString) and `localKeyId` attributes are single-value. The store is protected by a classic Appendix B HMAC (default, max interop) or an RFC 9579 PBMAC1 (`opts.mac.algorithm`), over SHA-256/384/512, with the shrouded keys and cert safes encrypted under RFC 8018 PBES2 (AES-128/192/256-CBC). Every password is encoded the PKCS#12 way — BMPString+NULL for the classic MAC, UTF-8 for the PBES2 bags and PBMAC1 (what OpenSSL and NSS consume) — so a file it emits opens in OpenSSL and NSS, cross-checked bidirectionally. The MAC is computed over the exact AuthenticatedSafe byte range, a DEFAULT-1 `MacData.iterations` is rejected up front, and the store is re-parsed before return. `verifyMac(pfx, password, opts)` recomputes a store's classic or PBMAC1 MAC over `macedBytes` and constant-time-compares it, throwing on a MAC-less or public-key-integrity store. Public-key integrity and bag decryption are not yet built. Returns DER or a PEM `PKCS12`; fail-closed with typed `Pkcs12Error`. Parsing stays at `pki.schema.pkcs12.parse` — `build` / `verifyMac` |
|
|
232
233
|
| `pki.cms` | RFC 5652 §5 CMS SignedData signing + signature verification — `sign(content, signers, opts)` produces a SignedData (attached or detached, one or many signers, RSA / RSASSA-PSS / ECDSA / EdDSA, the post-quantum ML-DSA-44/65/87 (RFC 9882) and SLH-DSA (all twelve FIPS 205 sets, RFC 9814), and composite ML-DSA (pairing ML-DSA with a traditional RSA / ECDSA / EdDSA — accepted only when **both** components verify — draft-ietf-lamps-cms-composite-sigs)); it builds the signed attributes (content-type, message-digest, signing-time) as canonical DER, signs the exact §5.4 preimage, and emits a DER `Buffer` or PEM. `verify(input, opts)` parses a SignedData over the strict `pki.schema.cms` codec, locates each SignerInfo's signer certificate by its issuerAndSerialNumber or subjectKeyIdentifier, and checks the signature over the exact §5.4 preimage: when signed attributes are present it confirms the message-digest attribute equals the content digest and verifies over the DER re-encoding of the SignedAttributes (the on-wire `[0]` tag replaced by a universal SET OF), otherwise directly over the content. It returns a per-signer verdict with the matched signer certificate; it does not chain that certificate to a trust anchor — that is the caller's step through `pki.path.validate`. **Content encryption** (RFC 5652/5083/5084/9629): `encrypt(content, recipients, opts)` produces an EnvelopedData, AuthEnvelopedData (AES-GCM, the authenticated default), or EncryptedData — recipients auto-dispatch off the certificate key to key-transport (RSAES-OAEP; v1.5 never emitted), key-agreement (ephemeral-static ECDH over P-256/384/521 with the X9.63 KDF, and X25519/X448 with HKDF), symmetric key-wrap, password (PBKDF2 + RFC 3211 PWRI-KEK), or the post-quantum ML-KEM KEMRecipientInfo (RFC 9629/9936) — one fresh content key wrapped for every recipient. `decrypt(input, keyMaterial, opts)` recovers the content through the matching arm and returns it with an `authenticated` flag; every secret-dependent failure collapses to one uniform `cms/decrypt-failed` verdict (Bleichenbacher / EFAIL / password-oracle freedom), and PKCS#1 v1.5 is decrypt-only under the RFC 3218 implicit-rejection countermeasure. **Compression** (RFC 3274): `compress(content, opts)` / `decompress(input, opts)` produce and consume a CompressedData (ZLIB, version 0, id-alg-zlibCompress); decompress bounds the uncompressed output at 16 MiB and stops before it is materialized, so a decompression bomb fails closed as `cms/decompress-too-large` — a size transform with no integrity/confidentiality (RFC 8551 §2.4.5). Fail-closed with typed `cms/*` errors — `sign`, `verify`, `encrypt`, `decrypt`, `compress`, `decompress` |
|
|
233
234
|
| `pki.smime` | RFC 8551 S/MIME message assembly, verification, encryption, and compression over the CMS layer — `sign(content, signers, opts)` wraps a MIME entity as a signed S/MIME message in either form: `multipart/signed` (clear-signed — the content stays readable in any MUA, a detached CMS SignedData rides alongside as `application/pkcs7-signature` with a matching `micalg`) or `application/pkcs7-mime; smime-type=signed-data` (opaque — the whole entity is a base64 CMS SignedData). The signed bytes are the entity's RFC 8551 §3.1.1 canonical form (CRLF line endings); `verify(message, opts)` unwraps both forms and recomputes over the same canonicalizer, so a transport that re-wraps line endings still verifies and a tampered part fails. `encrypt(content, recipients, opts)` envelopes a MIME entity as an opaque `application/pkcs7-mime` message and `decrypt(message, keyMaterial, opts)` opens one — `smime-type=authEnveloped-data` (AES-GCM, confidentiality and integrity, the default) or `smime-type=enveloped-data` (AES-CBC, confidentiality only, so `decrypt` reports `authenticated: false`, the §3.3 no-integrity caveat); the `smime-type` is derived from the CMS body, not the header, and decryption is fail-closed and oracle-free. The crypto is entirely `pki.cms.sign` / `verify` / `encrypt` / `decrypt` — any RSA / RSASSA-PSS / ECDSA / EdDSA / ML-DSA / SLH-DSA signer and any RSA-OAEP / ECDH / X25519 / X448 / AES-KW / PBKDF2 / ML-KEM recipient carries through (algorithm-agnostic). Like `cms.verify`, `verify` returns the per-signer cryptographic verdict plus the recovered content; chaining a signer to a trust anchor is the caller's `pki.path.validate` step. `compress(content, opts)` / `decompress(message, opts)` add the opaque `application/pkcs7-mime; smime-type=compressed-data; name=smime.p7z` frame (RFC 8551 §3.6, RFC 3274) — a size transform with no integrity/confidentiality (§2.4.5), decompress bounded against a bomb; the recovered content, which may itself be signed or enveloped, is returned for the caller to re-verify. Bidirectionally interoperable with `openssl smime` / `openssl cms`. Fail-closed with typed `smime/*` errors — `sign`, `verify`, `encrypt`, `decrypt`, `compress`, `decompress` |
|
|
234
235
|
| `pki.tsp` | RFC 3161 Time-Stamp Protocol — `sign(messageImprint, tsa, opts)` produces a TimeStampToken: a CMS SignedData (over `pki.cms.sign`) whose content is a `TSTInfo` carrying the timestamped message imprint, the TSA policy, a serial number, and `genTime` (with optional accuracy / nonce / ordering), plus the RFC 3161 §2.4.2 signing-certificate attribute binding the token to the TSA certificate (SHA-2 imprints, any `pki.cms.sign` TSA key). `request` / `parseRequest` build and parse the TimeStampReq a client sends (imprint, requested policy, nonce, certReq), `response` / `parseResponse` the TimeStampResp a TSA returns — a granted status wrapping a token, or a rejection with PKIStatus and failure info, the §2.4.2 status↔token coupling enforced in both directions. `verify(token, data, opts)` verifies a token fail-closed: the CMS signature over the exact signed bytes, the message imprint recomputed from the data, the TSTInfo content type, the ESSCertID(V2) binding to the TSA certificate, the §2.3 critical timeStamping-only extendedKeyUsage, the request nonce when used, and — with a trust anchor supplied — full certification-path validation of the TSA certificate at the token's `genTime`, returning `{ valid, genTime, serialNumber, tstInfo, … }` — `sign`, `request`, `parseRequest`, `response`, `parseResponse`, `verify` |
|
package/index.js
CHANGED
|
@@ -47,6 +47,7 @@ var crmf = require("./lib/crmf-sign");
|
|
|
47
47
|
var cmp = require("./lib/cmp-build");
|
|
48
48
|
var crl = require("./lib/crl-sign");
|
|
49
49
|
var key = require("./lib/key");
|
|
50
|
+
var pkcs12 = require("./lib/pkcs12-build");
|
|
50
51
|
var merkle = require("./lib/merkle");
|
|
51
52
|
var shbs = require("./lib/shbs");
|
|
52
53
|
var hpke = require("./lib/hpke");
|
|
@@ -118,6 +119,11 @@ module.exports = {
|
|
|
118
119
|
// PBES2 (EncryptedPrivateKeyInfo), pki.key.export / import a PKCS#8 private or SPKI public key, and
|
|
119
120
|
// pki.key.generate / publicFromPrivate over every WebCrypto algorithm. Parsing lives at pki.schema.pkcs8.
|
|
120
121
|
key: key,
|
|
122
|
+
// `pkcs12` is the RFC 7292 / RFC 9579 PKCS#12 (.p12/.pfx) producing side -- pki.pkcs12.build assembles a
|
|
123
|
+
// password-integrity store (key/cert/crl/secret bags in an AuthenticatedSafe, shrouded keys + cert safes
|
|
124
|
+
// under PBES2, a classic HMAC or PBMAC1 MAC), and pki.pkcs12.verifyMac checks a store's MAC. Parsing lives
|
|
125
|
+
// at pki.schema.pkcs12.parse.
|
|
126
|
+
pkcs12: pkcs12,
|
|
121
127
|
// `merkle` is the RFC 6962 / RFC 9162 Merkle-tree proof-verification core --
|
|
122
128
|
// pki.merkle.leafHash / nodeHash / emptyRootHash build the domain-separated
|
|
123
129
|
// (0x00 leaf / 0x01 node) SHA-256 tree hashes; pki.merkle.verifyInclusion and
|
package/lib/asn1-der.js
CHANGED
|
@@ -971,6 +971,20 @@ var build = {
|
|
|
971
971
|
if (!PRINTABLE_RE.test(s)) throw new Asn1Error("asn1/bad-printable-string", "value has characters outside the PrintableString set");
|
|
972
972
|
return _universal(TAGS.PRINTABLE_STRING, false, Buffer.from(s, "latin1"));
|
|
973
973
|
},
|
|
974
|
+
bmpString: function (s) {
|
|
975
|
+
// BMPString (UCS-2, X.680) encodes each character as a 2-byte big-endian BMP scalar value -- the exact
|
|
976
|
+
// inverse of _decodeUtf16be. A surrogate code unit (U+D800..U+DFFF), whether lone or half of a >U+FFFF
|
|
977
|
+
// pair, cannot be a BMPString character, so it is rejected rather than silently emitted.
|
|
978
|
+
s = String(s);
|
|
979
|
+
var out = Buffer.alloc(s.length * 2);
|
|
980
|
+
for (var i = 0; i < s.length; i++) {
|
|
981
|
+
var u = s.charCodeAt(i);
|
|
982
|
+
if (u >= 0xD800 && u <= 0xDFFF) throw new Asn1Error("asn1/bad-bmp-string", "BMPString cannot encode a surrogate code point (non-BMP characters are unsupported)");
|
|
983
|
+
out[i * 2] = (u >> 8) & 0xFF;
|
|
984
|
+
out[i * 2 + 1] = u & 0xFF;
|
|
985
|
+
}
|
|
986
|
+
return _universal(TAGS.BMP_STRING, false, out);
|
|
987
|
+
},
|
|
974
988
|
utcTime: function (date) { return _universal(TAGS.UTC_TIME, false, Buffer.from(_utcTimeString(date), "latin1")); },
|
|
975
989
|
generalizedTime: function (date) { return _universal(TAGS.GENERALIZED_TIME, false, Buffer.from(_generalizedTimeString(date), "latin1")); },
|
|
976
990
|
sequence: function (children) { return _universal(TAGS.SEQUENCE, true, Buffer.concat(_asBufferArray(children, "build.sequence"))); },
|
package/lib/cmp-build.js
CHANGED
|
@@ -38,6 +38,7 @@ var signScheme = require("./sign-scheme");
|
|
|
38
38
|
var pkix = require("./schema-pkix");
|
|
39
39
|
var pkiBuild = require("./pki-build");
|
|
40
40
|
var webcrypto = require("./webcrypto");
|
|
41
|
+
var pbes2 = require("./pbes2");
|
|
41
42
|
var constants = require("./constants");
|
|
42
43
|
var guard = require("./guard-all");
|
|
43
44
|
var frameworkError = require("./framework-error");
|
|
@@ -148,9 +149,6 @@ function _encodeGeneralInfo(itavs, code, what) {
|
|
|
148
149
|
|
|
149
150
|
// An AlgorithmIdentifier { algorithm OID, parameters ANY OPTIONAL } for a bare/absent-params digest.
|
|
150
151
|
function _algIdNoParams(name) { return b.sequence([b.oid(O(name))]); }
|
|
151
|
-
// An HMAC AlgorithmIdentifier carries NULL parameters (RFC 8018 App. B.1.1 / RFC 4231), unlike a bare SHA-2
|
|
152
|
-
// digest algId (ABSENT parameters, RFC 5754 sec. 2). Used for the PBMAC1 prf + messageAuthScheme.
|
|
153
|
-
function _hmacAlgId(name) { return b.sequence([b.oid(O(name)), b.nullValue()]); }
|
|
154
152
|
|
|
155
153
|
// PKIHeader ::= SEQUENCE { pvno INTEGER, sender GeneralName, recipient GeneralName, EXPLICIT [0..8] }.
|
|
156
154
|
// protectionAlgDer is the DERIVED AlgorithmIdentifier ([1]), present iff the message is protected. Returns
|
|
@@ -473,17 +471,6 @@ function _encodeBody(bodySpec, key, opts) {
|
|
|
473
471
|
|
|
474
472
|
// ---- protection ----
|
|
475
473
|
|
|
476
|
-
// PBMAC1 protectionAlg: id-PBMAC1 with PBMAC1-params { keyDerivationFunc PBKDF2{salt,iter,keyLength,prf},
|
|
477
|
-
// messageAuthScheme HMAC } -- the byte-exact inverse of schema-pkcs12.js PBMAC1_PARAMS / PBKDF2_PARAMS.
|
|
478
|
-
function _encodePbmac1AlgId(pd) {
|
|
479
|
-
var pbkdf2Params = [b.octetString(pd.salt), b.integer(BigInt(pd.iterationCount)), b.integer(BigInt(pd.keyLength))];
|
|
480
|
-
// prf: omit iff the DEFAULT algid-hmacWithSHA1; else the prf AlgorithmIdentifier with ABSENT params.
|
|
481
|
-
// nosemgrep: pki-non-constant-time-secret-compare -- prfName is a PRF algorithm name, not a secret
|
|
482
|
-
if (pd.prfName !== "hmacWithSHA1") pbkdf2Params.push(_hmacAlgId(pd.prfName));
|
|
483
|
-
var pbkdf2AlgId = b.sequence([b.oid(O("pbkdf2")), b.sequence(pbkdf2Params)]);
|
|
484
|
-
var pbmac1Params = b.sequence([pbkdf2AlgId, _hmacAlgId(pd.macName)]);
|
|
485
|
-
return b.sequence([b.oid(O("pbmac1")), pbmac1Params]);
|
|
486
|
-
}
|
|
487
474
|
|
|
488
475
|
// Resolve the protection selector to { protectionAlgDer, computeBits(protectedPartDer)->Promise<Buffer> },
|
|
489
476
|
// senderSpki, senderScheme } BEFORE the header is built (protectionAlg is opts-derived, not header-derived).
|
|
@@ -538,28 +525,16 @@ function _resolveProtection(opts) {
|
|
|
538
525
|
var macDesc = { salt: salt, iterationCount: iterationCount, keyLength: keyLength, prfName: PBMAC1_PRF[prf], macName: PBMAC1_MAC_OID[prf] };
|
|
539
526
|
|
|
540
527
|
return {
|
|
541
|
-
protectionAlgDer:
|
|
528
|
+
protectionAlgDer: pbes2.pbmac1AlgId(macDesc),
|
|
542
529
|
certDer: null,
|
|
543
530
|
computeBits: function (protectedPartDer) {
|
|
544
|
-
return
|
|
531
|
+
return pbes2.pbmac1(secretBuf, salt, iterationCount, keyLength, prf, prf, protectedPartDer).then(function (mac) {
|
|
545
532
|
return b.bitString(mac, 0);
|
|
546
533
|
});
|
|
547
534
|
},
|
|
548
535
|
};
|
|
549
536
|
}
|
|
550
537
|
|
|
551
|
-
// PBKDF2-derive an HMAC key from the shared secret, then HMAC the ProtectedPart (both via the engine).
|
|
552
|
-
function _pbmac1(secretBuf, salt, iterationCount, keyLength, prf, message) {
|
|
553
|
-
var subtle = webcrypto.webcrypto.subtle;
|
|
554
|
-
return subtle.importKey("raw", secretBuf, { name: "PBKDF2" }, false, ["deriveBits"]).then(function (baseKey) {
|
|
555
|
-
return subtle.deriveBits({ name: "PBKDF2", salt: salt, iterations: iterationCount, hash: prf }, baseKey, keyLength * 8);
|
|
556
|
-
}).then(function (bits) {
|
|
557
|
-
return subtle.importKey("raw", Buffer.from(bits), { name: "HMAC", hash: prf }, false, ["sign"]).then(function (hmacKey) {
|
|
558
|
-
return subtle.sign({ name: "HMAC" }, hmacKey, message);
|
|
559
|
-
});
|
|
560
|
-
}).then(function (sig) { return Buffer.from(sig); });
|
|
561
|
-
}
|
|
562
|
-
|
|
563
538
|
// ---- orchestrator ----
|
|
564
539
|
|
|
565
540
|
function build(message, opts) {
|
package/lib/pbes2.js
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
var nodeCrypto = require("crypto");
|
|
14
14
|
var asn1 = require("./asn1-der");
|
|
15
15
|
var oid = require("./oid");
|
|
16
|
+
var webcrypto = require("./webcrypto");
|
|
16
17
|
var guard = require("./guard-all");
|
|
17
18
|
var C = require("./constants");
|
|
18
19
|
|
|
@@ -127,10 +128,57 @@ function cbcDecrypt(key, iv, ct, keyBits) {
|
|
|
127
128
|
return Buffer.concat([d.update(ct), d.final()]);
|
|
128
129
|
}
|
|
129
130
|
|
|
131
|
+
// Higher-level PBES2 AES-CBC encrypt: derive an AES key from PRE-FORMATTED password bytes (the caller owns
|
|
132
|
+
// the encoding -- CMS UTF-8 vs PKCS#12 App. B.1) and encrypt the plaintext, returning the PBES2
|
|
133
|
+
// AlgorithmIdentifier + ciphertext. `cipherName` is an AES-CBC registry name (aes128/192/256-CBC).
|
|
134
|
+
function pbes2Encrypt(pwBytes, plaintext, opts, E, prefix) {
|
|
135
|
+
opts = opts || {};
|
|
136
|
+
var cipherName = opts.cipher || "aes256-CBC";
|
|
137
|
+
var keyBits = CONTENT_KEYBITS[O(cipherName)];
|
|
138
|
+
if (!keyBits || !/CBC/.test(cipherName)) throw E(prefix + "/bad-input", "unsupported PBES2 cipher " + cipherName + " (AES-CBC only)");
|
|
139
|
+
var prf = opts.prf || "hmacWithSHA256";
|
|
140
|
+
var prfNode = prfNodeByName(prf, E, prefix);
|
|
141
|
+
var iterations = assertIterations(opts.iterations == null ? 2048 : opts.iterations, E, prefix);
|
|
142
|
+
var salt = opts.salt != null ? assertSalt(opts.salt, E, prefix) : nodeCrypto.randomBytes(16);
|
|
143
|
+
var iv = opts.iv != null ? opts.iv : nodeCrypto.randomBytes(16);
|
|
144
|
+
var key = nodeCrypto.pbkdf2Sync(pwBytes, salt, iterations, keyBits / 8, prfNode);
|
|
145
|
+
return { algId: pbes2AlgId(salt, iterations, prf, cipherName, iv), ct: cbcEncrypt(key, iv, plaintext, keyBits) };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ---- PBMAC1 (RFC 9579 / RFC 8018 App. A.5) : PBKDF2 -> HMAC over a message --
|
|
149
|
+
// An hmacWithSHA_n AlgorithmIdentifier carries NULL parameters (RFC 8018 B.1.1).
|
|
150
|
+
function _hmacAlgId(name) { return b.sequence([b.oid(O(name)), b.nullValue()]); }
|
|
151
|
+
|
|
152
|
+
// The id-PBMAC1 AlgorithmIdentifier: id-PBMAC1 + PBMAC1-params { keyDerivationFunc PBKDF2, messageAuthScheme
|
|
153
|
+
// HMAC } -- the byte-exact inverse of schema-pkcs12 / schema-cmp's PBMAC1_PARAMS. `desc` = { salt,
|
|
154
|
+
// iterationCount, keyLength, prfName, macName }. The prf is omitted iff it equals the default hmacWithSHA1.
|
|
155
|
+
function pbmac1AlgId(desc) {
|
|
156
|
+
var pbkdf2Params = [b.octetString(desc.salt), b.integer(BigInt(desc.iterationCount)), b.integer(BigInt(desc.keyLength))];
|
|
157
|
+
if (desc.prfName !== "hmacWithSHA1") pbkdf2Params.push(_hmacAlgId(desc.prfName));
|
|
158
|
+
var pbkdf2AlgId = b.sequence([b.oid(O("pbkdf2")), b.sequence(pbkdf2Params)]);
|
|
159
|
+
return b.sequence([b.oid(O("pbmac1")), b.sequence([pbkdf2AlgId, _hmacAlgId(desc.macName)])]);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Compute PBMAC1 = HMAC_macHash(PBKDF2(pwBytes, salt, iter, keyLength, prfHash), message). The PBKDF2 prf
|
|
163
|
+
// and the HMAC messageAuthScheme are INDEPENDENT (RFC 9579 sec. 4 / RFC 8018 App. A.5) -- e.g. a SHA-512 PRF
|
|
164
|
+
// with a SHA-256 HMAC -- so both are passed explicitly. `prfHash` / `macHash` are WebCrypto hash names
|
|
165
|
+
// ("SHA-256"); pwBytes is pre-formatted (the caller owns the encoding).
|
|
166
|
+
function pbmac1(pwBytes, salt, iterationCount, keyLength, prfHash, macHash, message) {
|
|
167
|
+
var subtle = webcrypto.webcrypto.subtle;
|
|
168
|
+
return subtle.importKey("raw", pwBytes, { name: "PBKDF2" }, false, ["deriveBits"]).then(function (baseKey) {
|
|
169
|
+
return subtle.deriveBits({ name: "PBKDF2", salt: salt, iterations: iterationCount, hash: prfHash }, baseKey, keyLength * 8);
|
|
170
|
+
}).then(function (bits) {
|
|
171
|
+
return subtle.importKey("raw", Buffer.from(bits), { name: "HMAC", hash: macHash }, false, ["sign"]).then(function (hmacKey) {
|
|
172
|
+
return subtle.sign({ name: "HMAC" }, hmacKey, message);
|
|
173
|
+
});
|
|
174
|
+
}).then(function (sig) { return Buffer.from(sig); });
|
|
175
|
+
}
|
|
176
|
+
|
|
130
177
|
module.exports = {
|
|
131
178
|
passwordBytes: passwordBytes, assertIterations: assertIterations, assertSalt: assertSalt,
|
|
132
179
|
prfNodeByName: prfNodeByName, prfNodeByOid: prfNodeByOid,
|
|
133
180
|
pbkdf2ParamsSeq: pbkdf2ParamsSeq, pbes2AlgId: pbes2AlgId, parsePbkdf2Params: parsePbkdf2Params,
|
|
134
181
|
requireChildren: requireChildren, seqChildren: seqChildren,
|
|
135
|
-
cbcEncrypt: cbcEncrypt, cbcDecrypt: cbcDecrypt, CONTENT_KEYBITS: CONTENT_KEYBITS,
|
|
182
|
+
cbcEncrypt: cbcEncrypt, cbcDecrypt: cbcDecrypt, pbes2Encrypt: pbes2Encrypt, CONTENT_KEYBITS: CONTENT_KEYBITS,
|
|
183
|
+
pbmac1AlgId: pbmac1AlgId, pbmac1: pbmac1,
|
|
136
184
|
};
|
|
@@ -0,0 +1,434 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (c) blamejs contributors
|
|
3
|
+
"use strict";
|
|
4
|
+
/**
|
|
5
|
+
* @module pki.pkcs12
|
|
6
|
+
* @nav Signing
|
|
7
|
+
* @title PKCS#12
|
|
8
|
+
* @intro The PKCS#12 (.p12 / .pfx) producing side (RFC 7292, RFC 9579). `pki.pkcs12.build` assembles a
|
|
9
|
+
* password-integrity PFX -- key, certificate, CRL, and secret bags (shrouded keys and cert safes encrypted
|
|
10
|
+
* under RFC 8018 PBES2) wrapped in an AuthenticatedSafe, protected by either a classic Appendix B HMAC or
|
|
11
|
+
* an RFC 9579 PBMAC1 (PBKDF2 + HMAC) MAC, over AES-128/192/256-CBC and SHA-256/384/512. Every password is
|
|
12
|
+
* encoded the PKCS#12 way (BMPString + NULL, Appendix B.1), so a file it emits opens in OpenSSL and NSS.
|
|
13
|
+
* `pki.pkcs12.verifyMac` recomputes a store's MAC over the exact AuthenticatedSafe byte range and
|
|
14
|
+
* constant-time-compares it. Parsing lives at `pki.schema.pkcs12.parse`.
|
|
15
|
+
* @spec RFC 7292, RFC 9579, RFC 8018
|
|
16
|
+
* @card Build a password-integrity PKCS#12 store (RFC 7292 / RFC 9579) and verify its MAC.
|
|
17
|
+
*/
|
|
18
|
+
//
|
|
19
|
+
// The PBES2 bag encryption and the PBMAC1 MAC compose the shared lib/pbes2.js home (the same PBKDF2 + AES-CBC
|
|
20
|
+
// + HMAC primitives pki.cms and pki.key use), fed PKCS#12 App. B.1-formatted password bytes -- NOT the CMS
|
|
21
|
+
// UTF-8 encoding, the single most common PKCS#12 interop failure. The classic Appendix B.2 KDF (ID=3, the MAC
|
|
22
|
+
// integrity purpose) is bespoke PKCS#12 crypto with no in-tree equivalent, built here as one primitive and
|
|
23
|
+
// cross-checked against OpenSSL. The MAC is computed over the CONTENT octets of the id-data authSafe OCTET
|
|
24
|
+
// STRING (excluding its TLV header) -- exactly the parser's `macedBytes`, the canonical off-by-the-header
|
|
25
|
+
// trap designed out. Public-key integrity (an id-signedData authSafe) and bag/store DECRYPTION are deferred:
|
|
26
|
+
// build produces password-integrity stores, and a `pki.pkcs12.open` reader is the re-open condition.
|
|
27
|
+
|
|
28
|
+
var nodeCrypto = require("crypto");
|
|
29
|
+
var asn1 = require("./asn1-der");
|
|
30
|
+
var oid = require("./oid");
|
|
31
|
+
var pbes2 = require("./pbes2");
|
|
32
|
+
var pkcs8 = require("./schema-pkcs8");
|
|
33
|
+
var x509 = require("./schema-x509");
|
|
34
|
+
var schemaCrl = require("./schema-crl");
|
|
35
|
+
var schemaPkcs12 = require("./schema-pkcs12");
|
|
36
|
+
var pkix = require("./schema-pkix");
|
|
37
|
+
var guard = require("./guard-all");
|
|
38
|
+
var frameworkError = require("./framework-error");
|
|
39
|
+
var C = require("./constants");
|
|
40
|
+
|
|
41
|
+
var b = asn1.build;
|
|
42
|
+
var Pkcs12Error = frameworkError.Pkcs12Error;
|
|
43
|
+
var PemError = frameworkError.PemError;
|
|
44
|
+
function O(n) { return oid.byName(n); }
|
|
45
|
+
function _err(code, msg, cause) { return new Pkcs12Error(code, msg, cause); }
|
|
46
|
+
|
|
47
|
+
// PKCS#12 integrity/privacy defaults (named, not call-site literals -- rule 12).
|
|
48
|
+
var DEFAULT_MAC_ITER = 2048;
|
|
49
|
+
var DEFAULT_PBMAC1_ITER = 2048;
|
|
50
|
+
var MAC_SALT_BYTES = 8;
|
|
51
|
+
var MAX_PBMAC1_KEYLEN = 1024; // an HMAC key beyond a hash block is pointless -- bound the derived length
|
|
52
|
+
// The classic Appendix B KDF is a SYNCHRONOUS JS hash loop (unlike native/async PBKDF2), so its iteration
|
|
53
|
+
// count is capped far lower than the PBKDF2 limit -- a store just under the PBKDF2 cap would block the event
|
|
54
|
+
// loop for seconds. 1e6 is ~500x the OpenSSL default (2048) yet bounds the loop to a fraction of a second.
|
|
55
|
+
var CLASSIC_MAC_MAX_ITERATIONS = 1000000;
|
|
56
|
+
|
|
57
|
+
// The classic App. B.2 KDF (u = hash output bytes, v = compression block bytes) per RFC 7292 App. B.2.
|
|
58
|
+
var P12_KDF_UV = {
|
|
59
|
+
sha1: { u: 20, v: 64 }, sha224: { u: 28, v: 64 }, sha256: { u: 32, v: 64 },
|
|
60
|
+
sha384: { u: 48, v: 128 }, sha512: { u: 64, v: 128 },
|
|
61
|
+
};
|
|
62
|
+
// PBMAC1 hash -> { prf AlgorithmIdentifier name, WebCrypto hash, HMAC key length }. RFC 9579 sec. 5/7 forbids
|
|
63
|
+
// a <= 160-bit digest, so SHA-1 is absent.
|
|
64
|
+
var PBMAC1_PRF = {
|
|
65
|
+
sha256: { prfName: "hmacWithSHA256", wc: "SHA-256", keyLen: 32 },
|
|
66
|
+
sha384: { prfName: "hmacWithSHA384", wc: "SHA-384", keyLen: 48 },
|
|
67
|
+
sha512: { prfName: "hmacWithSHA512", wc: "SHA-512", keyLen: 64 },
|
|
68
|
+
};
|
|
69
|
+
// PBMAC1 prf AlgorithmIdentifier name -> WebCrypto hash (verify direction).
|
|
70
|
+
var PRF_WC = { hmacWithSHA1: "SHA-1", hmacWithSHA256: "SHA-256", hmacWithSHA384: "SHA-384", hmacWithSHA512: "SHA-512" };
|
|
71
|
+
// operator cipher name -> AES-CBC registry name the PBES2 home speaks.
|
|
72
|
+
var CIPHER_NAME = { "aes-128-cbc": "aes128-CBC", "aes-192-cbc": "aes192-CBC", "aes-256-cbc": "aes256-CBC" };
|
|
73
|
+
// classic-MAC hash node name -> the registered digest OID name (identical spelling here).
|
|
74
|
+
var DIGEST_NAME = { sha1: "sha1", sha256: "sha256", sha384: "sha384", sha512: "sha512" };
|
|
75
|
+
|
|
76
|
+
// ---- password + KDF primitives ---------------------------------------------
|
|
77
|
+
|
|
78
|
+
// RFC 7292 App. B.1: a PKCS#12 password is the BMPString (UTF-16BE) encoding of the string plus a trailing
|
|
79
|
+
// 2-byte NULL terminator. "Beavis" -> 14 bytes. A non-BMP scalar (surrogate) is rejected. A Buffer/Uint8Array
|
|
80
|
+
// is taken verbatim as already-formatted bytes (an escape hatch for a caller that pre-encodes).
|
|
81
|
+
function _p12Password(pw) {
|
|
82
|
+
if (pw == null) pw = "";
|
|
83
|
+
if (Buffer.isBuffer(pw)) return pw;
|
|
84
|
+
if (pw instanceof Uint8Array) return Buffer.from(pw);
|
|
85
|
+
if (typeof pw !== "string") throw _err("pkcs12/bad-input", "a password must be a string, Buffer, or Uint8Array");
|
|
86
|
+
var out = Buffer.alloc(pw.length * 2 + 2); // + the 2-byte NULL terminator
|
|
87
|
+
for (var i = 0; i < pw.length; i++) {
|
|
88
|
+
var u = pw.charCodeAt(i);
|
|
89
|
+
if (u >= 0xD800 && u <= 0xDFFF) throw _err("pkcs12/bad-input", "the password contains a non-BMP character (a surrogate code point cannot be BMPString-encoded)");
|
|
90
|
+
out[i * 2] = (u >> 8) & 0xFF;
|
|
91
|
+
out[i * 2 + 1] = u & 0xFF;
|
|
92
|
+
}
|
|
93
|
+
return out; // the final two bytes are already 0x00 0x00
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// The password bytes for the RFC 8018 PBES2 bag ciphers and the RFC 9579 PBMAC1 MAC. Although RFC 9579 sec. 6
|
|
97
|
+
// specifies the BMPString encoding here too, OpenSSL and NSS -- and thus the interoperable ecosystem -- feed
|
|
98
|
+
// PBKDF2 the raw UTF-8 password for these modern schemes (confirmed byte-for-byte against `openssl pkcs12`),
|
|
99
|
+
// reserving the BMPString+NULL form for the bespoke Appendix B KDF only. A file we emit must open in OpenSSL,
|
|
100
|
+
// so the modern schemes use UTF-8 here; only the classic Appendix B MAC uses `_p12Password`.
|
|
101
|
+
function _pbePassword(pw) {
|
|
102
|
+
if (pw == null) pw = "";
|
|
103
|
+
if (Buffer.isBuffer(pw)) return pw;
|
|
104
|
+
if (pw instanceof Uint8Array) return Buffer.from(pw);
|
|
105
|
+
if (typeof pw !== "string") throw _err("pkcs12/bad-input", "a password must be a string, Buffer, or Uint8Array");
|
|
106
|
+
return Buffer.from(pw, "utf8");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Concatenate copies of `src` to the smallest positive multiple of `blockSize` (>= src length), truncating
|
|
110
|
+
// the final copy. An empty source yields an empty buffer (RFC 7292 App. B.2 steps 2/3).
|
|
111
|
+
function _blockFill(src, blockSize) {
|
|
112
|
+
if (src.length === 0) return Buffer.alloc(0);
|
|
113
|
+
var total = blockSize * Math.ceil(src.length / blockSize);
|
|
114
|
+
var out = Buffer.alloc(total);
|
|
115
|
+
for (var i = 0; i < total; i++) out[i] = src[i % src.length];
|
|
116
|
+
return out;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// RFC 7292 Appendix B.2 KDF: derive `nBytes` of key material for purpose `id` (3 = MAC integrity) from the
|
|
120
|
+
// App. B.1 password bytes + salt over `iterations` rounds of `hashName`. NOT PBKDF2. The only consumer is the
|
|
121
|
+
// classic MAC, which always requests exactly the hash output length u, so the App. B.2 block count
|
|
122
|
+
// c = ceil(nBytes/u) is 1 -- one diversified, salted, iterated hash (D || S || P, hashed r times). The
|
|
123
|
+
// multi-block feedback (the c > 1 case, App. B.2 step 6C) has no consumer here; a future >u-byte derivation
|
|
124
|
+
// (e.g. a classic-PBE bag path) reintroduces it with its own known-answer test rather than shipping it
|
|
125
|
+
// untested. `nBytes` over u is rejected. The single-block path is proven bidirectionally against OpenSSL.
|
|
126
|
+
function _p12Kdf(hashName, id, pwBytes, salt, iterations, nBytes) {
|
|
127
|
+
var uv = P12_KDF_UV[hashName];
|
|
128
|
+
if (nBytes > uv.u) throw _err("pkcs12/unsupported-algorithm", "the App. B.2 KDF here derives at most the hash output length");
|
|
129
|
+
var v = uv.v;
|
|
130
|
+
var A = Buffer.concat([Buffer.alloc(v, id), _blockFill(salt, v), _blockFill(pwBytes, v)]); // D || S || P
|
|
131
|
+
for (var r = 0; r < iterations; r++) A = nodeCrypto.createHash(hashName).update(A).digest(); // H^r(D||I)
|
|
132
|
+
return A.subarray(0, nBytes);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ---- input coercion --------------------------------------------------------
|
|
136
|
+
|
|
137
|
+
function _coerceDer(input, what) {
|
|
138
|
+
return pkix.coerceToDer(input, { pemLabel: null, PemError: PemError, ErrorClass: Pkcs12Error, prefix: "pkcs12" });
|
|
139
|
+
}
|
|
140
|
+
function _bytes(input, label) {
|
|
141
|
+
return guard.bytes.view(input, Pkcs12Error, "pkcs12/bad-input", label);
|
|
142
|
+
}
|
|
143
|
+
function _assertMacIter(n, cap) {
|
|
144
|
+
if (typeof n !== "number" || !isFinite(n) || n < 1 || Math.floor(n) !== n) throw _err("pkcs12/bad-input", "MAC iterations must be a positive integer");
|
|
145
|
+
if (n === 1) throw _err("pkcs12/bad-input", "MAC iterations must be greater than 1 (a DEFAULT-1 MacData iterations cannot be DER-encoded, X.690 sec. 11.5)");
|
|
146
|
+
if (n > cap) throw _err("pkcs12/bad-input", "MAC iterations exceeds the cap " + cap);
|
|
147
|
+
return n;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ---- bag builders ----------------------------------------------------------
|
|
151
|
+
|
|
152
|
+
// PKCS12Attribute SET OF: friendlyName (a single BMPString) + localKeyId (a single OCTET STRING). Each
|
|
153
|
+
// attribute's attrValues is a single-value SET OF (RFC 2985 SINGLE VALUE).
|
|
154
|
+
function _bagAttributes(bag) {
|
|
155
|
+
var attrs = [];
|
|
156
|
+
if (bag.friendlyName != null) attrs.push(b.sequence([b.oid(O("friendlyName")), b.set([b.bmpString(String(bag.friendlyName))])]));
|
|
157
|
+
if (bag.localKeyId != null) {
|
|
158
|
+
if (bag.localKeyId === "ski") throw _err("pkcs12/bad-input", "localKeyId 'ski' auto-derivation is not yet supported -- supply an explicit Buffer");
|
|
159
|
+
attrs.push(b.sequence([b.oid(O("localKeyId")), b.set([b.octetString(_bytes(bag.localKeyId, "localKeyId"))])]));
|
|
160
|
+
}
|
|
161
|
+
return attrs.length ? b.set(attrs) : null;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// A secretBag secretTypeId is a caller-chosen content type: a registered OID name, or an arbitrary
|
|
165
|
+
// dotted-decimal OID string preserved verbatim (a name -> its OID; an already-dotted OID passes through).
|
|
166
|
+
function _secretTypeOid(id) {
|
|
167
|
+
try { return b.oid(O(id) || id); } // O() (oid.byName) throws OidError on a non-string -- keep it inside the domain wrap
|
|
168
|
+
catch (e) { throw _err("pkcs12/bad-input", "secretTypeId must be a registered name or a dotted-decimal OID string", e); }
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// A SafeBag ::= SEQUENCE { bagId, bagValue [0] EXPLICIT DEFINED BY bagId, bagAttributes SET OF OPTIONAL }.
|
|
172
|
+
function _safeBag(bagName, bagValueDer, bag) {
|
|
173
|
+
var children = [b.oid(O(bagName)), b.explicit(0, bagValueDer)];
|
|
174
|
+
var attrs = _bagAttributes(bag);
|
|
175
|
+
if (attrs) children.push(attrs);
|
|
176
|
+
return b.sequence(children);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function _buildBag(bag, opts, depth) {
|
|
180
|
+
if (!bag || typeof bag !== "object") throw _err("pkcs12/bad-input", "each bag must be an object with a type");
|
|
181
|
+
switch (bag.type) {
|
|
182
|
+
case "key": {
|
|
183
|
+
var keyDer = _coerceDer(bag.key, "keyBag key");
|
|
184
|
+
try { pkcs8.parse(keyDer); } catch (e) { throw _err("pkcs12/bad-input", "keyBag key is not a well-formed PKCS#8 PrivateKeyInfo", e); }
|
|
185
|
+
return _safeBag("keyBag", keyDer, bag);
|
|
186
|
+
}
|
|
187
|
+
case "shroudedKey": {
|
|
188
|
+
var kDer = _coerceDer(bag.key, "shroudedKey key");
|
|
189
|
+
try { pkcs8.parse(kDer); } catch (e2) { throw _err("pkcs12/bad-input", "shroudedKey key is not a well-formed PKCS#8 PrivateKeyInfo", e2); }
|
|
190
|
+
var enc = bag.encrypt || {};
|
|
191
|
+
var pw = _pbePassword(enc.password != null ? enc.password : opts.password);
|
|
192
|
+
var r = pbes2.pbes2Encrypt(pw, kDer, _pbeOpts(enc), _err, "pkcs12");
|
|
193
|
+
return _safeBag("pkcs8ShroudedKeyBag", b.sequence([r.algId, b.octetString(r.ct)]), bag); // EncryptedPrivateKeyInfo
|
|
194
|
+
}
|
|
195
|
+
case "cert": {
|
|
196
|
+
var certDer = _coerceDer(bag.cert, "certBag cert");
|
|
197
|
+
try { x509.parse(certDer); } catch (e3) { throw _err("pkcs12/bad-input", "certBag cert is not a well-formed X.509 certificate", e3); }
|
|
198
|
+
return _safeBag("certBag", b.sequence([b.oid(O("x509Certificate")), b.explicit(0, b.octetString(certDer))]), bag);
|
|
199
|
+
}
|
|
200
|
+
case "crl": {
|
|
201
|
+
var crlDer = _coerceDer(bag.crl, "crlBag crl");
|
|
202
|
+
try { schemaCrl.parse(crlDer); } catch (e4) { throw _err("pkcs12/bad-input", "crlBag crl is not a well-formed X.509 CRL", e4); }
|
|
203
|
+
return _safeBag("crlBag", b.sequence([b.oid(O("x509CRL")), b.explicit(0, b.octetString(crlDer))]), bag);
|
|
204
|
+
}
|
|
205
|
+
case "secret": {
|
|
206
|
+
if (bag.secretTypeId == null) throw _err("pkcs12/bad-input", "a secret bag needs a secretTypeId");
|
|
207
|
+
var secretValueDer = _reqDer(bag.secretValue, "secretValue");
|
|
208
|
+
return _safeBag("secretBag", b.sequence([_secretTypeOid(bag.secretTypeId), b.explicit(0, secretValueDer)]), bag);
|
|
209
|
+
}
|
|
210
|
+
case "safeContents": {
|
|
211
|
+
if (depth + 1 > C.LIMITS.PKCS12_MAX_BAG_DEPTH) throw _err("pkcs12/bad-input", "safeContents bag nesting exceeds the depth cap " + C.LIMITS.PKCS12_MAX_BAG_DEPTH);
|
|
212
|
+
return _safeBag("safeContentsBag", _buildSafeContents(bag.nested || [], opts, depth + 1), bag);
|
|
213
|
+
}
|
|
214
|
+
default:
|
|
215
|
+
throw _err("pkcs12/bad-input", "unknown bag type " + JSON.stringify(bag.type) + " (key / shroudedKey / cert / crl / secret / safeContents)");
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// A pre-encoded DER value (one well-formed TLV, no trailing bytes) supplied verbatim (secretValue).
|
|
220
|
+
function _reqDer(input, label) {
|
|
221
|
+
if (input == null) throw _err("pkcs12/bad-input", label + " is required");
|
|
222
|
+
var der = _bytes(input, label);
|
|
223
|
+
var node;
|
|
224
|
+
try { node = asn1.decode(der); } catch (e) { throw _err("pkcs12/bad-input", label + " must be one well-formed DER value", e); }
|
|
225
|
+
if (node.bytes.length !== der.length) throw _err("pkcs12/bad-input", label + " must be exactly one DER value with no trailing bytes");
|
|
226
|
+
return der;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function _pbeOpts(enc) {
|
|
230
|
+
var cipher = CIPHER_NAME[enc.cipher || "aes-256-cbc"];
|
|
231
|
+
if (!cipher) throw _err("pkcs12/bad-input", "unsupported PBES2 cipher " + JSON.stringify(enc.cipher) + " (aes-128-cbc / aes-192-cbc / aes-256-cbc)");
|
|
232
|
+
return {
|
|
233
|
+
cipher: cipher,
|
|
234
|
+
salt: enc.salt != null ? _bytes(enc.salt, "encrypt salt") : undefined,
|
|
235
|
+
iterations: enc.iterations,
|
|
236
|
+
prf: enc.prf,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// SafeContents ::= SEQUENCE OF SafeBag (ordered, NOT a SET OF).
|
|
241
|
+
function _buildSafeContents(bags, opts, depth) {
|
|
242
|
+
if (!Array.isArray(bags)) throw _err("pkcs12/bad-input", "bags must be an array");
|
|
243
|
+
if (bags.length > C.LIMITS.PKCS12_MAX_ELEMENTS) throw _err("pkcs12/bad-input", "a SafeContents exceeds the element cap " + C.LIMITS.PKCS12_MAX_ELEMENTS);
|
|
244
|
+
return b.sequence(bags.map(function (bag) { return _buildBag(bag, opts, depth); }));
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// One AuthenticatedSafe element: a plaintext id-data ContentInfo, or an id-encryptedData ContentInfo whose
|
|
248
|
+
// EncryptedData is the PBES2 encryption of the SafeContents (RFC 7292 sec. 5.1 step 2A/2B).
|
|
249
|
+
function _buildAuthSafeElement(sc, opts) {
|
|
250
|
+
if (!sc || typeof sc !== "object") throw _err("pkcs12/bad-input", "each safeContents entry must be an object");
|
|
251
|
+
var safeContentsDer = _buildSafeContents(sc.bags || [], opts, 0);
|
|
252
|
+
if (!sc.encrypt) {
|
|
253
|
+
return b.sequence([b.oid(O("data")), b.explicit(0, b.octetString(safeContentsDer))]);
|
|
254
|
+
}
|
|
255
|
+
var pw = _pbePassword(sc.encrypt.password != null ? sc.encrypt.password : opts.password);
|
|
256
|
+
var r = pbes2.pbes2Encrypt(pw, safeContentsDer, _pbeOpts(sc.encrypt), _err, "pkcs12");
|
|
257
|
+
var eci = b.sequence([b.oid(O("data")), r.algId, b.contextPrimitive(0, r.ct)]); // EncryptedContentInfo, [0] IMPLICIT ct
|
|
258
|
+
var encData = b.sequence([b.integer(0n), eci]); // EncryptedData { version 0, eci }
|
|
259
|
+
return b.sequence([b.oid(O("encryptedData")), b.explicit(0, encData)]);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// ---- MacData ---------------------------------------------------------------
|
|
263
|
+
|
|
264
|
+
// MacData over the AuthenticatedSafe DER (= the parser's macedBytes). Classic App. B HMAC (default, max
|
|
265
|
+
// interop) or RFC 9579 PBMAC1. Returns the MacData DER.
|
|
266
|
+
async function _buildMacData(macOpts, sharedPassword, authSafeDer) {
|
|
267
|
+
var password = macOpts.password != null ? macOpts.password : sharedPassword;
|
|
268
|
+
var algorithm = macOpts.algorithm || "hmac";
|
|
269
|
+
var hash = macOpts.hash || "sha256";
|
|
270
|
+
var salt = macOpts.salt != null ? _bytes(macOpts.salt, "mac salt") : nodeCrypto.randomBytes(MAC_SALT_BYTES);
|
|
271
|
+
if (salt.length === 0) throw _err("pkcs12/bad-input", "the MAC salt must be non-empty (RFC 9579 sec. 4c)");
|
|
272
|
+
if (salt.length > C.LIMITS.PBKDF2_MAX_SALT) throw _err("pkcs12/bad-input", "the MAC salt exceeds the " + C.LIMITS.PBKDF2_MAX_SALT + "-octet cap");
|
|
273
|
+
|
|
274
|
+
if (algorithm === "hmac") {
|
|
275
|
+
var node = DIGEST_NAME[hash];
|
|
276
|
+
if (!node || !P12_KDF_UV[node]) throw _err("pkcs12/unsupported-algorithm", "unsupported classic MAC hash " + JSON.stringify(hash) + " (sha1 / sha256 / sha384 / sha512)");
|
|
277
|
+
var iter = _assertMacIter(macOpts.iterations == null ? DEFAULT_MAC_ITER : macOpts.iterations, CLASSIC_MAC_MAX_ITERATIONS);
|
|
278
|
+
var macKey = _p12Kdf(node, 3, _p12Password(password), salt, iter, P12_KDF_UV[node].u); // classic KDF -> BMPString
|
|
279
|
+
var digest = nodeCrypto.createHmac(node, macKey).update(authSafeDer).digest();
|
|
280
|
+
var digestInfo = b.sequence([b.sequence([b.oid(O(node)), b.nullValue()]), b.octetString(digest)]);
|
|
281
|
+
return b.sequence([digestInfo, b.octetString(salt), b.integer(BigInt(iter))]);
|
|
282
|
+
}
|
|
283
|
+
if (algorithm === "pbmac1") {
|
|
284
|
+
var prf = PBMAC1_PRF[hash];
|
|
285
|
+
if (!prf) throw _err("pkcs12/unsupported-algorithm", "PBMAC1 requires a SHA-256/384/512 digest (RFC 9579 sec. 5/7 forbids a <= 160-bit digest, e.g. SHA-1)");
|
|
286
|
+
var iter2 = _assertMacIter(macOpts.iterations == null ? DEFAULT_PBMAC1_ITER : macOpts.iterations, C.LIMITS.PBKDF2_MAX_ITERATIONS);
|
|
287
|
+
var keyLen = macOpts.keyLength != null ? macOpts.keyLength : prf.keyLen;
|
|
288
|
+
if (typeof keyLen !== "number" || !Number.isInteger(keyLen) || keyLen < 20 || keyLen > MAX_PBMAC1_KEYLEN) throw _err("pkcs12/bad-input", "PBMAC1 keyLength must be an integer in [20, " + MAX_PBMAC1_KEYLEN + "] (RFC 9579 sec. 9)");
|
|
289
|
+
var mac = await pbes2.pbmac1(_pbePassword(password), salt, iter2, keyLen, prf.wc, prf.wc, authSafeDer); // PBKDF2 -> UTF-8; prf == messageAuthScheme on build
|
|
290
|
+
var desc = { salt: salt, iterationCount: iter2, keyLength: keyLen, prfName: prf.prfName, macName: prf.prfName };
|
|
291
|
+
var digestInfo2 = b.sequence([pbes2.pbmac1AlgId(desc), b.octetString(mac)]);
|
|
292
|
+
// MacData.macSalt + iterations are ignored on a PBMAC1 verify but MUST be present + non-1 (RFC 9579 4c/4d).
|
|
293
|
+
return b.sequence([digestInfo2, b.octetString(salt), b.integer(BigInt(iter2))]);
|
|
294
|
+
}
|
|
295
|
+
throw _err("pkcs12/bad-input", "opts.mac.algorithm must be 'hmac' or 'pbmac1', got " + JSON.stringify(algorithm));
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// ---- spec normalization ----------------------------------------------------
|
|
299
|
+
|
|
300
|
+
// Accept the OpenSSL-style convenience form ({ key, cert, ca, friendlyName, localKeyId }) or the full
|
|
301
|
+
// inverse-of-parse form ({ safeContents: SafeContentsSpec[] }). The convenience form -> one privacy safe of
|
|
302
|
+
// PBES2 cert bags + one shrouded-key safe.
|
|
303
|
+
function _normalizeSpec(spec, opts) {
|
|
304
|
+
if (spec && Array.isArray(spec.safeContents)) return spec.safeContents;
|
|
305
|
+
if (spec && (spec.key != null || spec.cert != null)) {
|
|
306
|
+
var certBags = [];
|
|
307
|
+
if (spec.cert != null) certBags.push({ type: "cert", cert: spec.cert, friendlyName: spec.friendlyName, localKeyId: spec.localKeyId });
|
|
308
|
+
if (spec.ca != null && !Array.isArray(spec.ca)) throw _err("pkcs12/bad-input", "spec.ca must be an array of certificates");
|
|
309
|
+
(spec.ca || []).forEach(function (ca) { certBags.push({ type: "cert", cert: ca }); });
|
|
310
|
+
var sc = [];
|
|
311
|
+
if (certBags.length) sc.push({ encrypt: { password: opts.password }, bags: certBags });
|
|
312
|
+
if (spec.key != null) sc.push({ bags: [{ type: "shroudedKey", key: spec.key, encrypt: { password: opts.password }, friendlyName: spec.friendlyName, localKeyId: spec.localKeyId }] });
|
|
313
|
+
return sc;
|
|
314
|
+
}
|
|
315
|
+
throw _err("pkcs12/bad-input", "spec must be { safeContents: [...] } or { key, cert, ca? }");
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// ---- public API ------------------------------------------------------------
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* @primitive pki.pkcs12.build
|
|
322
|
+
* @signature pki.pkcs12.build(spec, opts?) -> Promise<Buffer|string>
|
|
323
|
+
* @since 0.3.11
|
|
324
|
+
* @status experimental
|
|
325
|
+
* @spec RFC 7292, RFC 9579, RFC 8018
|
|
326
|
+
* @related pki.schema.pkcs12.parse, pki.pkcs12.verifyMac
|
|
327
|
+
*
|
|
328
|
+
* Build a password-integrity PKCS#12 (.p12 / .pfx) store. `spec` is either the OpenSSL-style convenience
|
|
329
|
+
* form `{ key, cert, ca?, friendlyName?, localKeyId? }` (one PBES2-encrypted cert safe plus one shrouded-key
|
|
330
|
+
* safe) or the full form `{ safeContents: [...] }`, where each element is a plaintext or PBES2-encrypted
|
|
331
|
+
* `SafeContents` of key / shroudedKey / cert / crl / secret / nested safeContents bags. Keys and certs are
|
|
332
|
+
* validated before wrapping; every password is BMPString+NULL encoded (RFC 7292 App. B.1). The store is
|
|
333
|
+
* MACed with a classic Appendix B HMAC (default) or an RFC 9579 PBMAC1, and re-parsed before return.
|
|
334
|
+
*
|
|
335
|
+
* @opts
|
|
336
|
+
* - `password` -- the shared privacy + integrity password (string / Buffer / Uint8Array).
|
|
337
|
+
* - `mac` -- `false` for a MAC-less store, or `{ algorithm: 'hmac'(default)|'pbmac1', hash: 'sha256'(default)|'sha1'|'sha384'|'sha512', salt?, iterations?, keyLength? }`.
|
|
338
|
+
* - `pem` (boolean) -- return a PEM `PKCS12` string instead of DER.
|
|
339
|
+
* @example
|
|
340
|
+
* var p12 = await pki.pkcs12.build({ safeContents: [{ bags: [
|
|
341
|
+
* { type: 'cert', cert: signerCertDer },
|
|
342
|
+
* { type: 'shroudedKey', key: signerKeyPkcs8, encrypt: { password: 'changeit' } } ] }] },
|
|
343
|
+
* { password: 'changeit', mac: { algorithm: 'hmac', hash: 'sha256' } });
|
|
344
|
+
*/
|
|
345
|
+
async function build(spec, opts) {
|
|
346
|
+
opts = opts || {};
|
|
347
|
+
if (opts.integrity && opts.integrity.mode === "public-key") {
|
|
348
|
+
throw _err("pkcs12/unsupported-algorithm", "public-key integrity (an id-signedData authSafe) is not yet supported -- use password integrity");
|
|
349
|
+
}
|
|
350
|
+
var safeContentsSpecs = _normalizeSpec(spec, opts);
|
|
351
|
+
if (!Array.isArray(safeContentsSpecs) || !safeContentsSpecs.length) throw _err("pkcs12/bad-input", "the store has no safe contents");
|
|
352
|
+
var elements = [];
|
|
353
|
+
for (var i = 0; i < safeContentsSpecs.length; i++) elements.push(_buildAuthSafeElement(safeContentsSpecs[i], opts));
|
|
354
|
+
var authSafeDer = b.sequence(elements); // AuthenticatedSafe ::= SEQUENCE OF ContentInfo
|
|
355
|
+
|
|
356
|
+
var pfxChildren = [b.integer(3n), b.sequence([b.oid(O("data")), b.explicit(0, b.octetString(authSafeDer))])];
|
|
357
|
+
if (opts.mac != null && opts.mac !== false && typeof opts.mac !== "object") throw _err("pkcs12/bad-input", "opts.mac must be false or a { algorithm, hash, salt, iterations, keyLength } object");
|
|
358
|
+
if (opts.mac !== false) pfxChildren.push(await _buildMacData(opts.mac || {}, opts.password, authSafeDer));
|
|
359
|
+
var pfx = b.sequence(pfxChildren);
|
|
360
|
+
|
|
361
|
+
// Self-check: the emitted store round-trips through the strict parser (structure + MAC coherence).
|
|
362
|
+
try { schemaPkcs12.parse(pfx); } catch (e) { throw _err("pkcs12/bad-input", "the produced PKCS#12 store did not re-parse (build bug)", e); }
|
|
363
|
+
return opts.pem ? schemaPkcs12.pemEncode(pfx, "PKCS12") : pfx;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/**
|
|
367
|
+
* @primitive pki.pkcs12.verifyMac
|
|
368
|
+
* @signature pki.pkcs12.verifyMac(pfx, password, opts?) -> Promise<boolean>
|
|
369
|
+
* @since 0.3.11
|
|
370
|
+
* @status experimental
|
|
371
|
+
* @spec RFC 7292 sec. 5.1, RFC 9579
|
|
372
|
+
* @defends pkcs12-mac-forgery (CWE-347)
|
|
373
|
+
* @related pki.pkcs12.build, pki.schema.pkcs12.parse
|
|
374
|
+
*
|
|
375
|
+
* Verify a password-integrity PKCS#12 store's MAC. `pfx` is a `pki.schema.pkcs12.parse` result, a DER
|
|
376
|
+
* `Buffer`, or a PEM string. The password is BMPString+NULL encoded (RFC 7292 App. B.1), the MAC is
|
|
377
|
+
* recomputed over the store's exact AuthenticatedSafe byte range (`macedBytes`) using the store's own MAC
|
|
378
|
+
* parameters -- the classic Appendix B (ID=3) HMAC or the RFC 9579 PBMAC1 -- and constant-time-compared to
|
|
379
|
+
* the stored MAC value. Returns `true` / `false` for the password match; throws `Pkcs12Error` on a MAC-less
|
|
380
|
+
* or public-key-integrity store, or an unsupported MAC algorithm (never a falsy verdict standing in for an error).
|
|
381
|
+
*
|
|
382
|
+
* @example
|
|
383
|
+
* var p12 = await pki.pkcs12.build({ key: signerKeyPkcs8, cert: signerCertDer }, { password: 'changeit' });
|
|
384
|
+
* var ok = await pki.pkcs12.verifyMac(p12, 'changeit');
|
|
385
|
+
*/
|
|
386
|
+
async function verifyMac(pfx, password, opts) {
|
|
387
|
+
opts = opts || {};
|
|
388
|
+
var m = (pfx && pfx.integrityMode !== undefined && pfx.mac !== undefined) ? pfx : schemaPkcs12.parse(_coerceDer(pfx, "pfx"));
|
|
389
|
+
if (m.integrityMode !== "password" || !m.mac) throw _err("pkcs12/bad-input", "the store carries no password MAC (integrityMode " + m.integrityMode + ")");
|
|
390
|
+
var expected = m.mac.macValue;
|
|
391
|
+
var computed;
|
|
392
|
+
if (m.mac.kind === "hmac") {
|
|
393
|
+
var node = (m.mac.hashName || "").toLowerCase();
|
|
394
|
+
if (!P12_KDF_UV[node]) throw _err("pkcs12/unsupported-algorithm", "unsupported classic MAC hash " + m.mac.hashName);
|
|
395
|
+
// The iteration count and salt are attacker-controlled work factors: bound them BEFORE the KDF runs.
|
|
396
|
+
_capWork(m.mac.iterations, m.mac.macSalt, opts, undefined, CLASSIC_MAC_MAX_ITERATIONS);
|
|
397
|
+
var macKey = _p12Kdf(node, 3, _p12Password(password), m.mac.macSalt, m.mac.iterations, P12_KDF_UV[node].u); // classic KDF -> BMPString
|
|
398
|
+
computed = nodeCrypto.createHmac(node, macKey).update(m.macedBytes).digest();
|
|
399
|
+
} else {
|
|
400
|
+
var kdf = m.mac.pbmac1.kdf;
|
|
401
|
+
var prfWc = PRF_WC[kdf.prfName];
|
|
402
|
+
// RFC 9579: HMAC under the messageAuthScheme, keyed by PBKDF2 under the (independent) prf.
|
|
403
|
+
var macWc = PRF_WC[m.mac.pbmac1.schemeName];
|
|
404
|
+
if (!prfWc) throw _err("pkcs12/unsupported-algorithm", "unsupported PBMAC1 prf " + kdf.prfName);
|
|
405
|
+
if (!macWc) throw _err("pkcs12/unsupported-algorithm", "unsupported PBMAC1 messageAuthScheme " + m.mac.pbmac1.schemeName);
|
|
406
|
+
// RFC 9579 sec. 5/7: a <= 160-bit digest (SHA-1) MUST NOT be used for PBMAC1 -- refuse it on verify, so a
|
|
407
|
+
// downgraded store cannot pass under a weak MAC even though the algorithm identifiers parse.
|
|
408
|
+
if (prfWc === "SHA-1" || macWc === "SHA-1") throw _err("pkcs12/unsupported-algorithm", "PBMAC1 with a <= 160-bit digest (SHA-1) is refused (RFC 9579 sec. 5/7)");
|
|
409
|
+
_capWork(kdf.iterationCount, kdf.salt, opts, kdf.keyLength, C.LIMITS.PBKDF2_MAX_ITERATIONS);
|
|
410
|
+
computed = await pbes2.pbmac1(_pbePassword(password), kdf.salt, kdf.iterationCount, kdf.keyLength, prfWc, macWc, m.macedBytes); // PBKDF2 -> UTF-8
|
|
411
|
+
}
|
|
412
|
+
return computed.length === expected.length && guard.crypto.constantTimeEqual(computed, expected);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// Bound the attacker-controlled MAC work factors before a hostile store can force an expensive derivation:
|
|
416
|
+
// the iteration count (<= PBKDF2_MAX_ITERATIONS, downward-overridable via opts.maxIterations), the salt, and
|
|
417
|
+
// the PBMAC1 keyLength. A store that exceeds any cap is a typed reject, never a multi-second CPU burn.
|
|
418
|
+
function _capWork(iterations, salt, opts, keyLength, hardCap) {
|
|
419
|
+
var cap = hardCap;
|
|
420
|
+
if (opts.maxIterations != null) {
|
|
421
|
+
if (typeof opts.maxIterations !== "number" || !isFinite(opts.maxIterations) || opts.maxIterations < 1 || Math.floor(opts.maxIterations) !== opts.maxIterations) throw _err("pkcs12/bad-input", "maxIterations must be a positive integer");
|
|
422
|
+
cap = Math.min(opts.maxIterations, cap);
|
|
423
|
+
}
|
|
424
|
+
if (iterations > cap) throw _err("pkcs12/iteration-limit", "the MAC iteration count " + iterations + " exceeds the cap " + cap);
|
|
425
|
+
if (salt && salt.length > C.LIMITS.PBKDF2_MAX_SALT) throw _err("pkcs12/bad-input", "the MAC salt exceeds the " + C.LIMITS.PBKDF2_MAX_SALT + "-octet cap");
|
|
426
|
+
// RFC 9579 sec. 9: the PBMAC1 keyLength floor (>= 20) is enforced on verify too, so a downgraded store with
|
|
427
|
+
// a short derived key is refused rather than accepted under a weak MAC.
|
|
428
|
+
if (keyLength != null && (keyLength < 20 || keyLength > MAX_PBMAC1_KEYLEN)) throw _err("pkcs12/bad-input", "the PBMAC1 keyLength must be in [20, " + MAX_PBMAC1_KEYLEN + "] (RFC 9579 sec. 9)");
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
module.exports = {
|
|
432
|
+
build: build,
|
|
433
|
+
verifyMac: verifyMac,
|
|
434
|
+
};
|
package/package.json
CHANGED
package/sbom.cdx.json
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
"$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
|
|
3
3
|
"bomFormat": "CycloneDX",
|
|
4
4
|
"specVersion": "1.5",
|
|
5
|
-
"serialNumber": "urn:uuid:
|
|
5
|
+
"serialNumber": "urn:uuid:253a6908-56c1-4a56-bdf4-33cb2035a161",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-07-
|
|
8
|
+
"timestamp": "2026-07-23T22:26:59.213Z",
|
|
9
9
|
"lifecycles": [
|
|
10
10
|
{
|
|
11
11
|
"phase": "build"
|
|
@@ -19,14 +19,14 @@
|
|
|
19
19
|
}
|
|
20
20
|
],
|
|
21
21
|
"component": {
|
|
22
|
-
"bom-ref": "@blamejs/pki@0.3.
|
|
22
|
+
"bom-ref": "@blamejs/pki@0.3.11",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "pki",
|
|
25
|
-
"version": "0.3.
|
|
25
|
+
"version": "0.3.11",
|
|
26
26
|
"scope": "required",
|
|
27
27
|
"author": "blamejs contributors",
|
|
28
28
|
"description": "Pure-JavaScript PKI toolkit that owns its stack — X.509, ASN.1/DER, CMS, PQC-first.",
|
|
29
|
-
"purl": "pkg:npm/%40blamejs/pki@0.3.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/pki@0.3.11",
|
|
30
30
|
"properties": [],
|
|
31
31
|
"externalReferences": [
|
|
32
32
|
{
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"components": [],
|
|
55
55
|
"dependencies": [
|
|
56
56
|
{
|
|
57
|
-
"ref": "@blamejs/pki@0.3.
|
|
57
|
+
"ref": "@blamejs/pki@0.3.11",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|