@blamejs/pki 0.1.17 → 0.1.19

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.
@@ -0,0 +1,614 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (c) blamejs contributors
3
+ "use strict";
4
+ /**
5
+ * @module pki.schema.pkcs12
6
+ * @nav Schema
7
+ * @title PKCS#12
8
+ * @order 190
9
+ * @slug pkcs12
10
+ *
11
+ * @intro
12
+ * PKCS#12 (PFX) key-and-certificate store handling per RFC 7292. `parse`
13
+ * decodes a `.p12` / `.pfx` container — the personal-information-exchange
14
+ * format OpenSSL, Windows CAPI, macOS Keychain, and NSS emit — into its
15
+ * bags: private keys (delegated to the PKCS#8 parser), shrouded keys
16
+ * (algorithm surfaced, ciphertext kept opaque), certificates, CRLs,
17
+ * secrets, and nested safe contents, each with its `friendlyName` /
18
+ * `localKeyId` attributes decoded.
19
+ *
20
+ * A PFX is a nested container: the authenticated safe is a CMS ContentInfo
21
+ * whose OCTET STRING content re-decodes to a list of ContentInfos, each
22
+ * carrying plaintext safe contents or a CMS EncryptedData / EnvelopedData
23
+ * privacy wrapper (surfaced structurally via the CMS module; ciphertext is
24
+ * never parsed). Integrity is surfaced, not verified: password mode yields
25
+ * the MAC parameters plus the exact byte range the HMAC covers
26
+ * (`macedBytes`), public-key mode the SignedData with its signers, and a
27
+ * MAC-less store surfaces `integrityMode: "none"` for the caller's policy
28
+ * to judge. RFC 7292
29
+ * §4.1 encodes content in BER, so parsing this one format accepts exactly
30
+ * two BER shapes — indefinite lengths and constructed OCTET STRINGs —
31
+ * anywhere in the store; every other strictness verdict, and every other
32
+ * format, stays strict DER, fail-closed.
33
+ *
34
+ * @card
35
+ * Parse DER / BER / PEM RFC 7292 PKCS#12 (PFX) stores into key / cert /
36
+ * CRL / secret bags with their attributes — keys via the PKCS#8 parser,
37
+ * encrypted safes via CMS, MAC inputs surfaced raw for external
38
+ * verification, fail-closed.
39
+ */
40
+
41
+ var C = require("./constants.js");
42
+ var asn1 = require("./asn1-der.js");
43
+ var oid = require("./oid.js");
44
+ var schema = require("./schema-engine.js");
45
+ var pkix = require("./schema-pkix.js");
46
+ var cms = require("./schema-cms.js");
47
+ var pkcs8 = require("./schema-pkcs8.js");
48
+ var frameworkError = require("./framework-error.js");
49
+
50
+ var Pkcs12Error = frameworkError.Pkcs12Error;
51
+ var PemError = frameworkError.PemError;
52
+ var NS = pkix.makeNS("pkcs12", Pkcs12Error, oid);
53
+ var TAGS = asn1.TAGS;
54
+
55
+ // Content types (RFC 5652 §4 / RFC 7292 §4.1).
56
+ var OID_DATA = oid.byName("data");
57
+ var OID_SIGNED_DATA = oid.byName("signedData");
58
+ var OID_ENVELOPED_DATA = oid.byName("envelopedData");
59
+ var OID_ENCRYPTED_DATA = oid.byName("encryptedData");
60
+ // Bag types (RFC 7292 §4.2).
61
+ var OID_KEY_BAG = oid.byName("keyBag");
62
+ var OID_SHROUDED_KEY_BAG = oid.byName("pkcs8ShroudedKeyBag");
63
+ var OID_CERT_BAG = oid.byName("certBag");
64
+ var OID_CRL_BAG = oid.byName("crlBag");
65
+ var OID_SECRET_BAG = oid.byName("secretBag");
66
+ var OID_SAFE_CONTENTS_BAG = oid.byName("safeContentsBag");
67
+ // CertBag / CRLBag discriminators (§4.2.3-§4.2.4).
68
+ var OID_X509_CERTIFICATE = oid.byName("x509Certificate");
69
+ var OID_SDSI_CERTIFICATE = oid.byName("sdsiCertificate");
70
+ var OID_X509_CRL = oid.byName("x509CRL");
71
+ // Bag attributes (§4.2) + the RFC 9579 MacData arm.
72
+ var OID_FRIENDLY_NAME = oid.byName("friendlyName");
73
+ var OID_LOCAL_KEY_ID = oid.byName("localKeyId");
74
+ var OID_PBMAC1 = oid.byName("pbmac1");
75
+
76
+ // DigestInfo ::= SEQUENCE { digestAlgorithm AlgorithmIdentifier, digest OCTET STRING }.
77
+ var DIGEST_INFO = schema.seq([
78
+ schema.field("digestAlgorithm", pkix.algorithmIdentifier(NS)),
79
+ schema.field("digest", schema.octetString()),
80
+ ], {
81
+ assert: "sequence", arity: { exact: 2 }, code: "pkcs12/bad-mac-data", what: "DigestInfo",
82
+ build: function (m) {
83
+ return { digestAlgorithm: m.fields.digestAlgorithm.value.result, digest: m.fields.digest.value };
84
+ },
85
+ });
86
+
87
+ // PBKDF2-params (RFC 8018 §5.2), constrained to the RFC 9579 PBMAC1 profile:
88
+ // the salt MUST use the specified (OCTET STRING) choice and keyLength MUST be
89
+ // present (§4.b / §5 — a MacData consumer cannot infer the MAC key size).
90
+ var PBKDF2_PARAMS = schema.seq([
91
+ schema.field("salt", schema.octetString()),
92
+ schema.field("iterationCount", schema.integerLeaf()),
93
+ schema.optional("keyLength", schema.integerLeaf(), { whenUniversal: [TAGS.INTEGER] }),
94
+ schema.optional("prf", pkix.algorithmIdentifier(NS), { whenUniversal: [TAGS.SEQUENCE] }),
95
+ ], {
96
+ assert: "sequence", code: "pkcs12/bad-mac-data", what: "PBKDF2-params",
97
+ build: function (m, ctx) {
98
+ if (!m.fields.keyLength.present) {
99
+ throw ctx.E("pkcs12/bad-mac-data", "PBMAC1 PBKDF2-params must carry keyLength (RFC 9579 §5)");
100
+ }
101
+ // Both counters surface as exact JS numbers — a value past the bound
102
+ // would round silently on conversion and hand a verifier wrong inputs.
103
+ var iterations = m.fields.iterationCount.value;
104
+ if (iterations < 1n || iterations > 2147483647n) {
105
+ throw ctx.E("pkcs12/bad-mac-data", "PBKDF2 iterationCount must be a positive integer within the iteration-count range");
106
+ }
107
+ var keyLength = m.fields.keyLength.value;
108
+ if (keyLength < 1n || keyLength > 2147483647n) {
109
+ throw ctx.E("pkcs12/bad-mac-data", "PBKDF2 keyLength must be a positive integer within the key-length range");
110
+ }
111
+ var prf = m.fields.prf.present ? m.fields.prf.value.result : null;
112
+ return {
113
+ salt: m.fields.salt.value,
114
+ iterationCount: Number(iterations),
115
+ keyLength: Number(keyLength),
116
+ prfOid: prf ? prf.oid : oid.byName("hmacWithSHA1"),
117
+ prfName: prf ? prf.name : "hmacWithSHA1",
118
+ };
119
+ },
120
+ });
121
+
122
+ // PBMAC1-params ::= SEQUENCE { keyDerivationFunc AlgorithmIdentifier{PBKDF2},
123
+ // messageAuthScheme AlgorithmIdentifier } (RFC 8018 §A.5 / RFC 9579 §4).
124
+ var PBMAC1_PARAMS = schema.seq([
125
+ schema.field("keyDerivationFunc", schema.seq([
126
+ schema.field("algorithm", schema.oidLeaf()),
127
+ schema.field("parameters", PBKDF2_PARAMS),
128
+ ], { assert: "sequence", arity: { exact: 2 }, code: "pkcs12/bad-mac-data", what: "PBMAC1 keyDerivationFunc" })),
129
+ schema.field("messageAuthScheme", pkix.algorithmIdentifier(NS)),
130
+ ], {
131
+ assert: "sequence", arity: { exact: 2 }, code: "pkcs12/bad-mac-data", what: "PBMAC1-params",
132
+ build: function (m, ctx) {
133
+ var kdf = m.fields.keyDerivationFunc.value;
134
+ if (kdf.fields.algorithm.value !== oid.byName("pbkdf2")) {
135
+ throw ctx.E("pkcs12/bad-mac-data", "PBMAC1 keyDerivationFunc must be PBKDF2 (RFC 9579 §4)");
136
+ }
137
+ var scheme = m.fields.messageAuthScheme.value.result;
138
+ return {
139
+ kdf: kdf.fields.parameters.value.result,
140
+ schemeOid: scheme.oid,
141
+ schemeName: scheme.name,
142
+ };
143
+ },
144
+ });
145
+
146
+ // MacData ::= SEQUENCE { mac DigestInfo, macSalt OCTET STRING,
147
+ // iterations INTEGER DEFAULT 1 }.
148
+ var MAC_DATA = schema.seq([
149
+ schema.field("mac", DIGEST_INFO),
150
+ schema.field("macSalt", schema.octetString()),
151
+ schema.optional("iterations", schema.integerLeaf(), { whenUniversal: [TAGS.INTEGER], default: 1 }),
152
+ ], {
153
+ assert: "sequence", code: "pkcs12/bad-mac-data", what: "MacData",
154
+ build: function (m, ctx) {
155
+ // X.690 §11.5 — a DEFAULT-valued component must be omitted from a DER
156
+ // encoding, so an explicitly-encoded iterations = 1 is non-canonical.
157
+ var it = m.fields.iterations;
158
+ var iterations = 1;
159
+ if (it.present) {
160
+ var v = it.value;
161
+ if (v === 1n) throw ctx.E("pkcs12/bad-mac-iterations", "iterations equal to its DEFAULT 1 must be omitted (X.690 §11.5)");
162
+ if (v < 1n || v > 2147483647n) throw ctx.E("pkcs12/bad-mac-iterations", "iterations must be a positive integer within the iteration-count range");
163
+ iterations = Number(v);
164
+ }
165
+ var di = m.fields.mac.value.result;
166
+ var pbmac1 = null;
167
+ if (di.digestAlgorithm.oid === OID_PBMAC1) {
168
+ // RFC 9579 §4 — the PBMAC1 algorithm identifier MUST carry PBMAC1-params
169
+ // (the KDF and MAC scheme a verifier needs live there, not in MacData).
170
+ if (di.digestAlgorithm.parameters === null) {
171
+ throw ctx.E("pkcs12/bad-mac-data", "a PBMAC1 MacData must carry PBMAC1-params (RFC 9579 §4)");
172
+ }
173
+ pbmac1 = schema.embeddedDer(PBMAC1_PARAMS, di.digestAlgorithm.parameters, ctx,
174
+ { code: "pkcs12/bad-mac-data", what: "PBMAC1-params", ber: true }).result;
175
+ }
176
+ return {
177
+ kind: pbmac1 ? "pbmac1" : "hmac",
178
+ hashOid: di.digestAlgorithm.oid,
179
+ hashName: di.digestAlgorithm.name,
180
+ hashParameters: di.digestAlgorithm.parameters,
181
+ pbmac1: pbmac1,
182
+ macValue: di.digest,
183
+ macSalt: m.fields.macSalt.value,
184
+ iterations: iterations,
185
+ };
186
+ },
187
+ });
188
+
189
+ // CertBag ::= SEQUENCE { certId OID, certValue [0] EXPLICIT ANY } (§4.2.3):
190
+ // x509Certificate wraps the DER certificate in an OCTET STRING; sdsiCertificate
191
+ // is an IA5String. Both arms of the closed set are accepted; the value is
192
+ // surfaced raw (byte-exact), never re-encoded or recursively parsed.
193
+ var CERT_BAG = schema.seq([
194
+ schema.field("certId", schema.oidLeaf()),
195
+ schema.field("certValue", schema.explicit(0, schema.any(), { code: "pkcs12/bad-bag-value", what: "certValue" })),
196
+ ], {
197
+ assert: "sequence", arity: { exact: 2 }, code: "pkcs12/bad-bag-value", what: "CertBag",
198
+ build: function (m, ctx) {
199
+ var certId = m.fields.certId.value;
200
+ var node = m.fields.certValue.value;
201
+ if (certId === OID_X509_CERTIFICATE) {
202
+ return { certType: "x509Certificate", certId: certId, certValue: _octetContent(node, ctx, "an x509Certificate certValue") };
203
+ }
204
+ if (certId === OID_SDSI_CERTIFICATE) {
205
+ if (!(node.tagClass === "universal" && node.tagNumber === TAGS.IA5_STRING && node.content)) {
206
+ throw ctx.E("pkcs12/bad-bag-value", "an sdsiCertificate certValue must be an IA5String (RFC 7292 §4.2.3)");
207
+ }
208
+ return { certType: "sdsiCertificate", certId: certId, certValue: node.content };
209
+ }
210
+ throw ctx.E("pkcs12/bad-cert-type", (ctx.oid.name(certId) || certId) + " is not a recognized CertBag certId (RFC 7292 §4.2.3)");
211
+ },
212
+ });
213
+
214
+ // CRLBag ::= SEQUENCE { crlId OID, crlValue [0] EXPLICIT ANY } (§4.2.4).
215
+ var CRL_BAG = schema.seq([
216
+ schema.field("crlId", schema.oidLeaf()),
217
+ schema.field("crlValue", schema.explicit(0, schema.any(), { code: "pkcs12/bad-bag-value", what: "crlValue" })),
218
+ ], {
219
+ assert: "sequence", arity: { exact: 2 }, code: "pkcs12/bad-bag-value", what: "CRLBag",
220
+ build: function (m, ctx) {
221
+ var crlId = m.fields.crlId.value;
222
+ if (crlId !== OID_X509_CRL) {
223
+ throw ctx.E("pkcs12/bad-crl-type", (ctx.oid.name(crlId) || crlId) + " is not a recognized CRLBag crlId (RFC 7292 §4.2.4)");
224
+ }
225
+ return { crlType: "x509CRL", crlId: crlId, crlValue: _octetContent(m.fields.crlValue.value, ctx, "an x509CRL crlValue") };
226
+ },
227
+ });
228
+
229
+ // SecretBag ::= SEQUENCE { secretTypeId OID, secretValue [0] EXPLICIT ANY }
230
+ // (§4.2.5). SecretTypes is an open set, so the type is surfaced by OID with
231
+ // the value raw — an unrecognized secret is representable, not a fault.
232
+ var SECRET_BAG = schema.seq([
233
+ schema.field("secretTypeId", schema.oidLeaf()),
234
+ schema.field("secretValue", schema.explicit(0, schema.any(), { code: "pkcs12/bad-bag-value", what: "secretValue" })),
235
+ ], {
236
+ assert: "sequence", arity: { exact: 2 }, code: "pkcs12/bad-bag-value", what: "SecretBag",
237
+ build: function (m, ctx) {
238
+ var t = m.fields.secretTypeId.value;
239
+ return { secretTypeId: t, secretTypeName: ctx.oid.name(t) || null, secretValue: m.fields.secretValue.value.bytes };
240
+ },
241
+ });
242
+
243
+ // SafeBag ::= SEQUENCE { bagId OID, bagValue [0] EXPLICIT ANY DEFINED BY bagId,
244
+ // bagAttributes SET OF PKCS12Attribute OPTIONAL } (§4.2).
245
+ // The build surfaces the structural record (bagId + the inner value node +
246
+ // decoded attributes); the bagId dispatch happens in _buildBag, where the
247
+ // per-parse recursion state lives.
248
+ var SAFE_BAG = schema.seq([
249
+ schema.field("bagId", schema.oidLeaf()),
250
+ schema.field("bagValue", schema.explicit(0, schema.any(), { code: "pkcs12/bad-bag-value", what: "bagValue" })),
251
+ schema.optional("bagAttributes", schema.setOf(pkix.attribute(NS), {
252
+ code: "pkcs12/bad-attributes", what: "bagAttributes",
253
+ max: C.LIMITS.PKCS12_MAX_ELEMENTS, maxCode: "pkcs12/too-many-elements",
254
+ }), { whenUniversal: [TAGS.SET] }),
255
+ ], {
256
+ assert: "sequence", code: "pkcs12/bad-safe-contents", what: "SafeBag",
257
+ build: function (m, ctx) {
258
+ var attributes = [];
259
+ var friendlyName = null;
260
+ var localKeyId = null;
261
+ var seen = {};
262
+ if (m.fields.bagAttributes.present) {
263
+ var items = m.fields.bagAttributes.value.items;
264
+ for (var i = 0; i < items.length; i++) {
265
+ var a = items[i].value.result;
266
+ attributes.push(a);
267
+ // friendlyName / localKeyId are SINGLE VALUE TRUE (PKCS#9 / RFC 2985):
268
+ // exactly one value, at most one instance of each attribute.
269
+ if (a.type === OID_FRIENDLY_NAME) {
270
+ if (seen[a.type] || a.values.length !== 1) throw ctx.E("pkcs12/bad-friendly-name", "friendlyName carries exactly one BMPString value (PKCS#9 SINGLE VALUE)");
271
+ friendlyName = _bmpValue(a.values[0], ctx);
272
+ }
273
+ if (a.type === OID_LOCAL_KEY_ID) {
274
+ if (seen[a.type] || a.values.length !== 1) throw ctx.E("pkcs12/bad-local-key-id", "localKeyId carries exactly one OCTET STRING value (PKCS#9 SINGLE VALUE)");
275
+ // The value bytes are a raw wire slice out of a store whose content
276
+ // is normatively BER, so the re-decode follows the same BER rules.
277
+ localKeyId = _octetContent(asn1.decode(a.values[0], { ber: true }), ctx, "a localKeyId value", "pkcs12/bad-local-key-id");
278
+ }
279
+ seen[a.type] = true;
280
+ }
281
+ }
282
+ return {
283
+ bagId: m.fields.bagId.value,
284
+ valueNode: m.fields.bagValue.value,
285
+ attributes: attributes,
286
+ friendlyName: friendlyName,
287
+ localKeyId: localKeyId,
288
+ };
289
+ },
290
+ });
291
+
292
+ // SafeContents ::= SEQUENCE OF SafeBag (§4.2). Strictly a SEQUENCE — a SET OF
293
+ // here is a known producer divergence and rejects.
294
+ var SAFE_CONTENTS = schema.seqOf(SAFE_BAG, {
295
+ code: "pkcs12/bad-safe-contents", what: "SafeContents",
296
+ max: C.LIMITS.PKCS12_MAX_ELEMENTS, maxCode: "pkcs12/too-many-elements",
297
+ });
298
+
299
+ // AuthenticatedSafe ::= SEQUENCE OF ContentInfo (§4.1) — each element parsed
300
+ // structurally here and dispatched by contentType in _dispatchSafes.
301
+ var AS_ELEMENT = schema.seq([
302
+ schema.field("contentType", schema.oidLeaf()),
303
+ schema.field("content", schema.explicit(0, schema.any(), { code: "pkcs12/bad-safe-contentinfo", what: "safe ContentInfo content" })),
304
+ ], {
305
+ assert: "sequence", arity: { exact: 2 }, code: "pkcs12/bad-safe-contentinfo", what: "safe ContentInfo",
306
+ build: function (m) {
307
+ return { contentType: m.fields.contentType.value, innerNode: m.fields.content.value };
308
+ },
309
+ });
310
+ var AUTHENTICATED_SAFE = schema.seqOf(AS_ELEMENT, {
311
+ code: "pkcs12/bad-authenticated-safe", what: "AuthenticatedSafe",
312
+ max: C.LIMITS.PKCS12_MAX_ELEMENTS, maxCode: "pkcs12/too-many-elements",
313
+ });
314
+
315
+ // The authSafe ContentInfo (§4): contentType is data (password integrity) or
316
+ // signedData (public-key integrity) and nothing else.
317
+ var AUTH_SAFE = schema.seq([
318
+ schema.field("contentType", schema.oidLeaf()),
319
+ schema.field("content", schema.explicit(0, schema.any(), { code: "pkcs12/bad-authsafe", what: "authSafe content" })),
320
+ ], {
321
+ assert: "sequence", arity: { exact: 2 }, code: "pkcs12/bad-authsafe", what: "authSafe ContentInfo",
322
+ build: function (m) {
323
+ return { contentType: m.fields.contentType.value, innerNode: m.fields.content.value };
324
+ },
325
+ });
326
+
327
+ // PFX ::= SEQUENCE { version INTEGER {v3(3)}, authSafe ContentInfo,
328
+ // macData MacData OPTIONAL } (§4).
329
+ var PFX = schema.seq([
330
+ schema.field("version", pkix.versionReader(NS, { "3": 3 })),
331
+ schema.field("authSafe", AUTH_SAFE),
332
+ schema.optional("macData", MAC_DATA, { whenUniversal: [TAGS.SEQUENCE] }),
333
+ ], {
334
+ assert: "sequence", code: "pkcs12/not-a-pfx", what: "PFX",
335
+ build: function (m, ctx) {
336
+ var version = m.fields.version.value;
337
+ var authSafe = m.fields.authSafe.value.result;
338
+ var macPresent = m.fields.macData.present;
339
+ var mac = macPresent ? m.fields.macData.value.result : null;
340
+ // Every re-decode below shares one budget, so nesting chained across
341
+ // OCTET-STRING (and delegation) boundaries cannot restart the caps.
342
+ var state = { budget: { remaining: C.LIMITS.PKCS12_MAX_REDECODES }, bagDepth: 0 };
343
+
344
+ if (authSafe.contentType === OID_DATA) {
345
+ // §5.1 step 5B — the MAC covers the value octets of the id-data OCTET
346
+ // STRING (the AuthenticatedSafe encoding), excluding the TLV header.
347
+ // MacData itself is OPTIONAL in the PFX syntax: a store without it
348
+ // (OpenSSL `pkcs12 -export -nomac`) carries no integrity protection at
349
+ // all, which is a policy concern for the caller — surfaced as
350
+ // integrityMode "none", never rejected by a parser that surfaces
351
+ // integrity inputs rather than verifying them.
352
+ var macedBytes = _octetContent(authSafe.innerNode, ctx, "an id-data authSafe content");
353
+ var dispatched = _dispatchSafes(_walkAuthenticatedSafe(macedBytes, state, ctx), state, ctx);
354
+ return {
355
+ version: version, integrityMode: macPresent ? "password" : "none", mac: mac, macedBytes: macedBytes,
356
+ authSafeSigned: null, safeBags: dispatched.safeBags, encryptedSafes: dispatched.encryptedSafes,
357
+ };
358
+ }
359
+ if (authSafe.contentType === OID_SIGNED_DATA) {
360
+ // §4 integrity coherence: public-key integrity has no MacData.
361
+ if (macPresent) throw ctx.E("pkcs12/bad-integrity-mode", "an id-signedData authSafe (public-key integrity) must omit macData (RFC 7292 §4)");
362
+ var signed = cms.walkSignedData(authSafe.innerNode);
363
+ if (signed.encapContentInfo.eContentType !== OID_DATA) {
364
+ throw ctx.E("pkcs12/bad-authsafe", "a public-key-integrity authSafe must encapsulate id-data carrying the AuthenticatedSafe (RFC 7292 §4.1)");
365
+ }
366
+ if (signed.encapContentInfo.eContent === null) {
367
+ throw ctx.E("pkcs12/bad-authsafe", "a public-key-integrity authSafe must carry attached eContent (RFC 7292 §4.1)");
368
+ }
369
+ // A signer-less SignedData carries no integrity at all — the one thing
370
+ // public-key-integrity mode exists to provide.
371
+ if (signed.signerInfos.length < 1) {
372
+ throw ctx.E("pkcs12/bad-authsafe", "a public-key-integrity authSafe must carry at least one SignerInfo (RFC 7292 §4)");
373
+ }
374
+ var dispatchedSigned = _dispatchSafes(_walkAuthenticatedSafe(signed.encapContentInfo.eContent, state, ctx), state, ctx);
375
+ return {
376
+ version: version, integrityMode: "public-key", mac: null, macedBytes: null,
377
+ authSafeSigned: signed, safeBags: dispatchedSigned.safeBags, encryptedSafes: dispatchedSigned.encryptedSafes,
378
+ };
379
+ }
380
+ throw ctx.E("pkcs12/bad-authsafe-type", "authSafe contentType must be id-data or id-signedData (RFC 7292 §4), got " +
381
+ (ctx.oid.name(authSafe.contentType) || authSafe.contentType));
382
+ },
383
+ });
384
+
385
+ // The value octets of a universal OCTET STRING node (a BER constructed string
386
+ // arrives already reassembled to primitive content by the codec's ber mode).
387
+ function _octetContent(node, ctx, what, code) {
388
+ if (!(node && node.tagClass === "universal" && node.tagNumber === TAGS.OCTET_STRING && node.content)) {
389
+ throw ctx.E(code || "pkcs12/bad-bag-value", what + " must be an OCTET STRING");
390
+ }
391
+ return node.content;
392
+ }
393
+
394
+ // RFC 7292 §4.1 — a privacy-mode safe wraps a SafeContents, so the encrypted
395
+ // content's declared type must be id-data whichever CMS privacy structure
396
+ // carries it, and the ciphertext must be attached: CMS permits a detached
397
+ // EncryptedContentInfo, but here the ciphertext IS the safe's contents — a
398
+ // safe without it holds nothing a passphrase could ever recover.
399
+ function _privacySafe(content, ctx) {
400
+ if (content.encryptedContentInfo.contentType !== OID_DATA) {
401
+ throw ctx.E("pkcs12/bad-safe-contentinfo-type",
402
+ "an encrypted safe must declare id-data (SafeContents) as its encrypted content type (RFC 7292 §4.1)");
403
+ }
404
+ if (content.encryptedContentInfo.encryptedContent === null) {
405
+ throw ctx.E("pkcs12/bad-safe-contentinfo",
406
+ "an encrypted safe must carry attached ciphertext — its encryptedContent is the SafeContents (RFC 7292 §4.1)");
407
+ }
408
+ return content;
409
+ }
410
+
411
+ // A PKCS#9 SINGLE-VALUE BMPString attribute value (friendlyName), decoded
412
+ // UTF-16BE by the codec's string reader (even length + surrogate rules there).
413
+ function _bmpValue(bytes, ctx) {
414
+ var node = asn1.decode(bytes, { ber: true });
415
+ if (!(node.tagClass === "universal" && node.tagNumber === TAGS.BMP_STRING)) {
416
+ throw ctx.E("pkcs12/bad-friendly-name", "a friendlyName value must be a BMPString (RFC 7292 §4.2)");
417
+ }
418
+ return asn1.read.string(node);
419
+ }
420
+
421
+ // Re-decode the AuthenticatedSafe blob (BER content region, budgeted).
422
+ function _walkAuthenticatedSafe(bytes, state, ctx) {
423
+ return schema.embeddedDer(AUTHENTICATED_SAFE, bytes, ctx, {
424
+ code: "pkcs12/bad-der", what: "the AuthenticatedSafe", ber: true,
425
+ budget: state.budget, budgetCode: "pkcs12/too-deep",
426
+ });
427
+ }
428
+
429
+ // Dispatch each AuthenticatedSafe element by contentType (§4.1): id-data
430
+ // carries plaintext SafeContents (re-decoded here); the two privacy modes are
431
+ // full CMS structures validated by the CMS module on the already-decoded node
432
+ // — ciphertext stays opaque inside the CMS surface.
433
+ function _dispatchSafes(asMatch, state, ctx) {
434
+ var safeBags = [];
435
+ var encryptedSafes = [];
436
+ for (var i = 0; i < asMatch.items.length; i++) {
437
+ var el = asMatch.items[i].value.result;
438
+ if (el.contentType === OID_DATA) {
439
+ var contents = schema.embeddedDer(SAFE_CONTENTS, _octetContent(el.innerNode, ctx, "an id-data safe content"), ctx, {
440
+ code: "pkcs12/bad-der", what: "a SafeContents", ber: true,
441
+ budget: state.budget, budgetCode: "pkcs12/too-deep",
442
+ });
443
+ for (var j = 0; j < contents.items.length; j++) {
444
+ safeBags.push(_buildBag(contents.items[j].value.result, state, ctx));
445
+ }
446
+ } else if (el.contentType === OID_ENCRYPTED_DATA) {
447
+ encryptedSafes.push({ type: "encryptedData", content: _privacySafe(cms.walkEncryptedData(el.innerNode), ctx) });
448
+ } else if (el.contentType === OID_ENVELOPED_DATA) {
449
+ encryptedSafes.push({ type: "envelopedData", content: _privacySafe(cms.walkEnvelopedData(el.innerNode), ctx) });
450
+ } else {
451
+ throw ctx.E("pkcs12/bad-safe-contentinfo-type", "an AuthenticatedSafe element must be id-data, id-encryptedData, or id-envelopedData (RFC 7292 §4.1), got " +
452
+ (ctx.oid.name(el.contentType) || el.contentType));
453
+ }
454
+ }
455
+ return { safeBags: safeBags, encryptedSafes: encryptedSafes };
456
+ }
457
+
458
+ // bagId dispatch (§4.2, Appendix D). The bag-type set is closed: an
459
+ // unrecognized bagId rejects. Key material delegates to the exported PKCS#8
460
+ // parsers; cert / CRL / secret values are surfaced raw.
461
+ function _buildBag(rec, state, ctx) {
462
+ var bag = {
463
+ type: null, bagId: rec.bagId,
464
+ friendlyName: rec.friendlyName, localKeyId: rec.localKeyId, attributes: rec.attributes,
465
+ };
466
+ // Key bags are walked from the DECODED node — their wire bytes may carry
467
+ // BER shapes the strict pkcs8 parse entry refuses, and the node already
468
+ // exists (no re-decode).
469
+ if (rec.bagId === OID_KEY_BAG) {
470
+ bag.type = "keyBag";
471
+ bag.key = pkcs8.walkPrivateKeyInfo(rec.valueNode);
472
+ return bag;
473
+ }
474
+ if (rec.bagId === OID_SHROUDED_KEY_BAG) {
475
+ bag.type = "pkcs8ShroudedKeyBag";
476
+ bag.encrypted = pkcs8.walkEncryptedPrivateKeyInfo(rec.valueNode);
477
+ return bag;
478
+ }
479
+ if (rec.bagId === OID_CERT_BAG) {
480
+ bag.type = "certBag";
481
+ var cert = schema.walk(CERT_BAG, rec.valueNode, ctx).result;
482
+ bag.certType = cert.certType; bag.certId = cert.certId; bag.certValue = cert.certValue;
483
+ return bag;
484
+ }
485
+ if (rec.bagId === OID_CRL_BAG) {
486
+ bag.type = "crlBag";
487
+ var crl = schema.walk(CRL_BAG, rec.valueNode, ctx).result;
488
+ bag.crlType = crl.crlType; bag.crlId = crl.crlId; bag.crlValue = crl.crlValue;
489
+ return bag;
490
+ }
491
+ if (rec.bagId === OID_SECRET_BAG) {
492
+ bag.type = "secretBag";
493
+ var secret = schema.walk(SECRET_BAG, rec.valueNode, ctx).result;
494
+ bag.secretTypeId = secret.secretTypeId; bag.secretTypeName = secret.secretTypeName; bag.secretValue = secret.secretValue;
495
+ return bag;
496
+ }
497
+ if (rec.bagId === OID_SAFE_CONTENTS_BAG) {
498
+ // §4.2.6 recursion — bounded independently of the codec depth cap so a
499
+ // chain across re-decode boundaries cannot restart it.
500
+ if (state.bagDepth + 1 > C.LIMITS.PKCS12_MAX_BAG_DEPTH) {
501
+ throw ctx.E("pkcs12/too-deep", "safeContentsBag nesting exceeds the depth cap " + C.LIMITS.PKCS12_MAX_BAG_DEPTH);
502
+ }
503
+ var nestedState = { budget: state.budget, bagDepth: state.bagDepth + 1 };
504
+ bag.type = "safeContentsBag";
505
+ bag.nested = [];
506
+ var nestedMatch = schema.walk(SAFE_CONTENTS, rec.valueNode, ctx);
507
+ for (var i = 0; i < nestedMatch.items.length; i++) {
508
+ bag.nested.push(_buildBag(nestedMatch.items[i].value.result, nestedState, ctx));
509
+ }
510
+ return bag;
511
+ }
512
+ throw ctx.E("pkcs12/bad-bag-type", (ctx.oid.name(rec.bagId) || rec.bagId) + " is not a recognized SafeBag bagId (RFC 7292 §4.2)");
513
+ }
514
+
515
+ /**
516
+ * @primitive pki.schema.pkcs12.parse
517
+ * @signature pki.schema.pkcs12.parse(input) -> pfx
518
+ * @since 0.1.18
519
+ * @status experimental
520
+ * @spec RFC 7292, RFC 9579
521
+ * @defends ASN.1-parser-DoS (CWE-400)
522
+ * @related pki.schema.parse, pki.schema.pkcs8.parse, pki.schema.cms.parse
523
+ *
524
+ * Parse a DER / BER `Buffer` or a PEM string into a structured PFX:
525
+ * `{ version, integrityMode, mac, macedBytes, authSafeSigned, safeBags,
526
+ * encryptedSafes }`. `integrityMode` is `"password"` (id-data authSafe with
527
+ * MacData — `mac` carries `{ kind, hashOid, hashName, hashParameters,
528
+ * pbmac1, macValue, macSalt, iterations }`, where `kind` distinguishes the
529
+ * RFC 9579 PBMAC1 arm; for PBMAC1 the required parameters are validated and
530
+ * decoded onto `pbmac1` (`{ kdf: { salt, iterationCount, keyLength, prfOid,
531
+ * prfName }, schemeOid, schemeName }`) with `hashParameters` keeping the raw
532
+ * bytes, and `macedBytes` is the exact byte range the HMAC covers),
533
+ * `"none"` (id-data authSafe without MacData — the shape
534
+ * `openssl pkcs12 -export -nomac` emits; `mac` is `null` and the store
535
+ * carries no integrity protection, a policy decision left to the caller),
536
+ * or `"public-key"` (id-signedData authSafe — the CMS SignedData surfaced
537
+ * on `authSafeSigned`, signature not verified here). Each of `safeBags` is
538
+ * `{ type, bagId, friendlyName, localKeyId, attributes }` plus its arm:
539
+ * a `keyBag` carries `key` (the PKCS#8 parse), a `pkcs8ShroudedKeyBag`
540
+ * carries `encrypted` (algorithm surfaced, ciphertext opaque), `certBag` /
541
+ * `crlBag` / `secretBag` carry their values raw and byte-exact, and a
542
+ * `safeContentsBag` carries `nested` bags. Encrypted / enveloped safes are
543
+ * surfaced structurally on `encryptedSafes` via the CMS module — recipient
544
+ * infos and algorithms decoded, ciphertext never parsed. MAC verification
545
+ * and bag decryption are passphrase operations for an external layer; the
546
+ * inputs are surfaced exactly.
547
+ *
548
+ * Throws `Pkcs12Error` when the bytes are not a well-formed PFX, and
549
+ * `Asn1Error` when the underlying encoding is malformed.
550
+ *
551
+ * @example
552
+ * var store = pki.schema.pkcs12.parse(der);
553
+ * store.safeBags.map(function (b) { return b.type; });
554
+ */
555
+ var parse = pkix.makeParser({
556
+ pemLabel: "PKCS12", PemError: PemError, ErrorClass: Pkcs12Error,
557
+ prefix: "pkcs12", what: "PFX", topSchema: PFX, ns: NS, ber: true,
558
+ });
559
+
560
+ /**
561
+ * @primitive pki.schema.pkcs12.pemDecode
562
+ * @signature pki.schema.pkcs12.pemDecode(text, label?) -> Buffer
563
+ * @since 0.1.18
564
+ * @status experimental
565
+ * @spec RFC 7468, RFC 7292
566
+ * @related pki.schema.pkcs12.parse
567
+ *
568
+ * Extract the DER bytes from a PEM block (default label `PKCS12`). A
569
+ * `.p12` / `.pfx` file is almost always binary — the PEM path is a
570
+ * convenience for stores that transit text channels.
571
+ *
572
+ * @example
573
+ * var der = pki.schema.pkcs12.pemDecode(pemText);
574
+ */
575
+ function pemDecode(text, label) { return pkix.pemDecode(text, label || "PKCS12", PemError); }
576
+
577
+ /**
578
+ * @primitive pki.schema.pkcs12.pemEncode
579
+ * @signature pki.schema.pkcs12.pemEncode(der, label?) -> string
580
+ * @since 0.1.18
581
+ * @status experimental
582
+ * @spec RFC 7468
583
+ * @related pki.schema.pkcs12.pemDecode
584
+ *
585
+ * Wrap DER bytes in a PEM envelope (default label `PKCS12`).
586
+ *
587
+ * @example
588
+ * var pem = pki.schema.pkcs12.pemEncode(der);
589
+ */
590
+ function pemEncode(der, label) { return pkix.pemEncode(der, label || "PKCS12", PemError); }
591
+
592
+ // A PFX root leads with a universal INTEGER (version), colliding with the
593
+ // PKCS#8 detector on the first child — so the discriminators are children[1]
594
+ // (a ContentInfo: SEQUENCE of exactly 2, OID first, [0] constructed second —
595
+ // a shape a PrivateKeyInfo's AlgorithmIdentifier never presents) and
596
+ // children[2] (a SEQUENCE MacData or absent, never PKCS#8's OCTET STRING).
597
+ function matches(root) {
598
+ var k = pkix.rootSequenceChildren(root, 2, 3);
599
+ if (!k) return false;
600
+ if (!schema.isUniversal(k[0], TAGS.INTEGER)) return false;
601
+ var ci = k[1];
602
+ if (!(ci.children && schema.isUniversal(ci, TAGS.SEQUENCE) && ci.children.length === 2)) return false;
603
+ if (!schema.isUniversal(ci.children[0], TAGS.OBJECT_IDENTIFIER)) return false;
604
+ if (!(schema.isContext(ci.children[1], 0) && ci.children[1].children)) return false;
605
+ if (k.length === 3 && !(schema.isUniversal(k[2], TAGS.SEQUENCE) && k[2].children)) return false;
606
+ return true;
607
+ }
608
+
609
+ module.exports = {
610
+ parse: parse,
611
+ pemDecode: pemDecode,
612
+ pemEncode: pemEncode,
613
+ matches: matches,
614
+ };
@@ -179,15 +179,14 @@ function pemEncode(der, label) { return pkix.pemEncode(der, label || "PRIVATE KE
179
179
  // order.
180
180
  function matches(root) {
181
181
  var TAGS = asn1.TAGS;
182
- if (!root || root.tagClass !== "universal" || root.tagNumber !== TAGS.SEQUENCE) return false;
183
- var k = root.children;
184
- if (!k || k.length < 3 || k.length > 5) return false;
185
- if (!(k[0].tagClass === "universal" && k[0].tagNumber === TAGS.INTEGER)) return false;
186
- if (!(k[1].tagClass === "universal" && k[1].tagNumber === TAGS.SEQUENCE)) return false;
187
- if (!(k[2].tagClass === "universal" && k[2].tagNumber === TAGS.OCTET_STRING)) return false;
182
+ var k = pkix.rootSequenceChildren(root, 3, 5);
183
+ if (!k) return false;
184
+ if (!schema.isUniversal(k[0], TAGS.INTEGER)) return false;
185
+ if (!schema.isUniversal(k[1], TAGS.SEQUENCE)) return false;
186
+ if (!schema.isUniversal(k[2], TAGS.OCTET_STRING)) return false;
188
187
  var last = -1;
189
188
  for (var i = 3; i < k.length; i++) {
190
- if (k[i].tagClass !== "context" || k[i].tagNumber <= last || k[i].tagNumber > 1) return false;
189
+ if (!schema.isContextInRange(k[i], 0, 1) || k[i].tagNumber <= last) return false;
191
190
  last = k[i].tagNumber;
192
191
  }
193
192
  return true;
@@ -202,10 +201,20 @@ function matches(root) {
202
201
  // auto-route can return once a validated encryption-algorithm discriminator (the
203
202
  // PBES layer) exists.
204
203
 
204
+ // Validate a bare, already-decoded PrivateKeyInfo / EncryptedPrivateKeyInfo
205
+ // node — for a composer that holds a decoded node and must not re-parse its
206
+ // bytes (an RFC 7292 PFX key bag, whose wire encoding may be BER that the
207
+ // strict `parse` entry would refuse). Same contract as the CMS walkers: the
208
+ // node is the bare structure, typed pkcs8/* on rejection.
209
+ function walkPrivateKeyInfo(node) { return schema.walk(PRIVATE_KEY_INFO, node, NS).result; }
210
+ function walkEncryptedPrivateKeyInfo(node) { return schema.walk(ENCRYPTED_PRIVATE_KEY_INFO, node, NS).result; }
211
+
205
212
  module.exports = {
206
213
  parse: parse,
207
214
  parseEncrypted: parseEncrypted,
208
215
  pemDecode: pemDecode,
209
216
  pemEncode: pemEncode,
210
217
  matches: matches,
218
+ walkPrivateKeyInfo: walkPrivateKeyInfo,
219
+ walkEncryptedPrivateKeyInfo: walkEncryptedPrivateKeyInfo,
211
220
  };