@blamejs/pki 0.1.17 → 0.1.18
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 +23 -1
- package/README.md +8 -7
- package/lib/asn1-der.js +109 -22
- package/lib/constants.js +16 -0
- package/lib/framework-error.js +7 -0
- package/lib/oid.js +33 -2
- package/lib/path-validate.js +10 -2
- package/lib/schema-all.js +26 -7
- package/lib/schema-cms.js +10 -0
- package/lib/schema-crmf.js +1 -1
- package/lib/schema-engine.js +69 -5
- package/lib/schema-ocsp.js +2 -4
- package/lib/schema-pkcs12.js +615 -0
- package/lib/schema-pkcs8.js +10 -0
- package/lib/schema-pkix.js +13 -3
- package/lib/schema-tsp.js +1 -1
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -4,7 +4,29 @@ 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.1.
|
|
7
|
+
## v0.1.18 — 2026-07-08
|
|
8
|
+
|
|
9
|
+
An RFC 7292 PKCS#12 (PFX) store parser joins the pki.schema family.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- pki.schema.pkcs12.parse(input) — RFC 7292 PFX parsing. It decodes a DER / BER Buffer or PEM into { version, integrityMode, mac, macedBytes, authSafeSigned, safeBags, encryptedSafes }. Password-integrity stores surface { kind, hashOid, hashName, hashParameters, pbmac1, macValue, macSalt, iterations } plus macedBytes — the exact value octets the HMAC covers, excluding the octet-string header, so an external verifier hashes the correct region. The RFC 9579 PBMAC1 arm is validated, not just recognized: its parameters must be present, the key-derivation function must be PBKDF2 with a keyLength, and the decoded KDF (salt, iteration count, key length, PRF) and MAC scheme surface on pbmac1. The X.690 DEFAULT rule is enforced (an explicitly encoded iterations = 1 is non-canonical and rejects). Public-key-integrity stores surface the CMS SignedData and must carry at least one signer (the signature itself is verified externally). Each safeBag carries its type, friendlyName / localKeyId (decoded, single-value and single-instance rules enforced), and all attributes: keyBags delegate to pki.schema.pkcs8.parse, shrouded key bags to parseEncrypted (algorithm surfaced, ciphertext opaque), cert / CRL / secret values stay raw and byte-exact, and safeContentsBags recurse under a depth ceiling. Encrypted and enveloped safes are validated structurally by the CMS module with ciphertext kept raw, and must declare id-data (a SafeContents) as their encrypted content type. The version-3 rule, the contradictory MacData-alongside-SignedData combination, the closed bag-type and cert/CRL-type sets, and per-list element caps all fail closed with typed pkcs12/* codes; a MacData-less id-data store is legal syntax and parses as integrityMode "none".
|
|
14
|
+
- pki.schema.pkcs12.pemDecode(text, label?) / pemEncode(der, label?) — PEM handling for stores that transit text channels (default label PKCS12).
|
|
15
|
+
- pki.asn1.decode gains an opt-in ber option accepting exactly two shapes — an indefinite length on a constructed value and a constructed OCTET STRING, whose segments reassemble into one primitive content — for formats whose content is normatively BER. The default remains strict DER; minimal-length, minimal-integer, trailing-byte, and size / depth verdicts are unchanged in both modes, an indefinite length on a primitive value still rejects, a foreign-type segment inside a constructed string rejects, and constructed-string nesting is capped (each level re-copies its payload, so deep nesting is memory amplification, not data).
|
|
16
|
+
- pki.schema.engine.embeddedDer(schema, bytes, ctx, opts) — the named form of the re-decode idiom: decode a fresh DER / BER blob carried inside an already-decoded value and walk it against a schema, wrapping codec failures in the caller's typed code. A shared budget option bounds how many nested blobs one parse may unwrap, so a container that chains encodings across octet-string boundaries cannot restart the depth caps from zero. The timestamp, OCSP, and certificate-request parsers now route their embedded-structure decodes through it.
|
|
17
|
+
- SEQUENCE OF / SET OF schemas can declare an element-count ceiling (max), so a container listing a great many tiny elements fails typed instead of amplifying memory through per-element parse products; a single attribute's value list is now capped this way across every format.
|
|
18
|
+
- The OID registry gains the PKCS#12 bag types, the PKCS#12 password-based encryption identifiers, the PKCS#9 certTypes / crlTypes / friendlyName / localKeyId entries, PKCS#5 PBKDF2 / PBES2 / PBMAC1, the NIST AES content-encryption arc, and the HMAC-with-SHA identifiers, so a store's algorithms resolve to names.
|
|
19
|
+
- The error taxonomy gains Pkcs12Error, carrying a stable pkcs12/* code.
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
|
|
23
|
+
- The npm-publish workflow's vulnerability scan now scans the SBOM unconditionally and the vendored-bundle directory only when it holds a real bundle, so the scan is exact in both states instead of warning on the empty directory.
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
|
|
27
|
+
- Certification-path validation bounds the RSASSA-PSS saltLength and trailerField before numeric conversion, so an oversized value rejects with path/unsupported-algorithm instead of rounding silently on its way to the verifier — the same exact-or-rejected rule the PKCS#12 MAC parameters follow.
|
|
28
|
+
|
|
29
|
+
## v0.1.17 — 2026-07-06
|
|
8
30
|
|
|
9
31
|
An RFC 4211 certificate-request-message parser joins the pki.schema family.
|
|
10
32
|
|
package/README.md
CHANGED
|
@@ -200,10 +200,11 @@ is callable today; nothing below is a stub.
|
|
|
200
200
|
| `pki.schema.tsp` | Parse DER / PEM RFC 3161 timestamp responses and tokens — the TSTInfo payload (imprint, genTime with sub-second precision, serial, nonce, accuracy), the status-to-token coupling, and the token wrapper composed over CMS with the single-signer rule, fail-closed — `parse`, `parseTstInfo`, `parseToken`, `pemDecode` |
|
|
201
201
|
| `pki.schema.attrcert` | Parse DER / PEM RFC 5755 attribute certificates — the holder and issuer identities (validated GeneralNames), the validity window (real `Date`s), the privilege attributes (role / group / clearance) and extensions, with the raw signed region for a verifier; the obsolete v1 form is recognized and deferred, fail-closed — `parse`, `pemDecode` |
|
|
202
202
|
| `pki.schema.crmf` | Parse DER / PEM RFC 4211 certificate request messages (CertReqMessages — the CMP / EST enrollment body) — the requested-certificate template (subject, public key, validity, extensions), proof-of-possession, and registration controls, with the raw `CertRequest` region a POP verifier hashes; names dual-accepted (IMPLICIT and EXPLICIT), fail-closed — `parse`, `pemDecode` |
|
|
203
|
-
| `pki.schema.
|
|
203
|
+
| `pki.schema.pkcs12` | Parse DER / BER / PEM RFC 7292 PKCS#12 (PFX) stores — key bags via the PKCS#8 parser, shrouded keys (algorithm surfaced, ciphertext opaque), cert / CRL / secret bags raw and byte-exact, encrypted and enveloped safes structurally via CMS, `friendlyName` / `localKeyId` decoded, and the exact MAC byte range (`macedBytes`) plus RFC 9579 PBMAC1 recognition for external verification; BER accepted exactly where §4.1 requires it, fail-closed — `parse`, `pemDecode`, `pemEncode` |
|
|
204
|
+
| `pki.schema.engine` | The declarative ASN.1 structure-schema engine every format parser composes — `walk` / `encode` / `embeddedDer` plus the schema combinators |
|
|
204
205
|
| `pki.path` | RFC 5280 §6 certification-path validation — `validate` runs the §6.1 state machine (signature chaining, validity windows, name chaining, basic constraints and path length, key usage, name constraints, the certificate-policy tree) over an ordered path and a trust anchor, returning a structured verdict with per-check reason codes; `crlChecker` supplies CRL-based revocation. Pure and re-entrant, fail-closed — `validate`, `crlChecker` |
|
|
205
206
|
| `pki.C` / `pki.constants` | Version-stable constants — functional scale helpers (`C.TIME.*`, `C.BYTES.*`), codec `LIMITS`, `version` |
|
|
206
|
-
| `pki.errors` | The `PkiError` taxonomy — `defineClass` plus `ConstantsError` / `Asn1Error` / `OidError` / `PemError` / `CertificateError` / `CrlError` / `CsrError` / `Pkcs8Error` / `CmsError` / `OcspError` / `TspError` / `AttrCertError` / `CrmfError` / `PathError`, each carrying a stable `code` in `domain/reason` form |
|
|
207
|
+
| `pki.errors` | The `PkiError` taxonomy — `defineClass` plus `ConstantsError` / `Asn1Error` / `OidError` / `PemError` / `CertificateError` / `CrlError` / `CsrError` / `Pkcs8Error` / `CmsError` / `OcspError` / `TspError` / `AttrCertError` / `CrmfError` / `Pkcs12Error` / `PathError`, each carrying a stable `code` in `domain/reason` form |
|
|
207
208
|
| `pki` CLI | `pki version`, `pki oid <dotted\|name>`, `pki parse <cert>` |
|
|
208
209
|
|
|
209
210
|
### CLI
|
|
@@ -217,11 +218,11 @@ pki parse cert.pem # structured JSON summary of a certific
|
|
|
217
218
|
|
|
218
219
|
### What's coming
|
|
219
220
|
|
|
220
|
-
Certificate build/sign/verify,
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
221
|
+
Certificate build/sign/verify, CMS AuthenticatedData /
|
|
222
|
+
AuthEnvelopedData, PKCS#8 decryption (PBES2) / SPKI, PKCS#12 MAC verification
|
|
223
|
+
and bag decryption, and the post-quantum certificate and CMS surface (ML-DSA /
|
|
224
|
+
ML-KEM / SLH-DSA and hybrid composites) are on the roadmap and ride this same
|
|
225
|
+
core. See
|
|
225
226
|
[ROADMAP.md](ROADMAP.md) for the full plan and current status of each area, and
|
|
226
227
|
[CHANGELOG.md](CHANGELOG.md) for what has landed.
|
|
227
228
|
|
package/lib/asn1-der.js
CHANGED
|
@@ -127,10 +127,19 @@ function _asBuffer(input, who) {
|
|
|
127
127
|
* after the top-level value (unless `allowTrailing`), or exceeding the
|
|
128
128
|
* size / depth caps.
|
|
129
129
|
*
|
|
130
|
+
* `ber: true` is a scoped relaxation for formats whose content regions are
|
|
131
|
+
* normatively BER (RFC 7292 PKCS#12): it accepts an indefinite length on a
|
|
132
|
+
* constructed value and a constructed OCTET STRING, whose segments are
|
|
133
|
+
* reassembled into one primitive `content`. Nothing else is relaxed —
|
|
134
|
+
* definite lengths stay minimal, an indefinite length on a primitive value
|
|
135
|
+
* and a foreign-type segment still reject, and the size / depth caps hold.
|
|
136
|
+
*
|
|
130
137
|
* @opts
|
|
131
138
|
* maxBytes: number, // default: C.LIMITS.DER_MAX_BYTES (16 MiB)
|
|
132
139
|
* maxDepth: number, // default: C.LIMITS.DER_MAX_DEPTH (64)
|
|
133
140
|
* allowTrailing: boolean, // default: false — allow bytes after the top TLV
|
|
141
|
+
* ber: boolean, // default: false — accept indefinite lengths +
|
|
142
|
+
* // constructed OCTET STRINGs (BER content regions)
|
|
134
143
|
*
|
|
135
144
|
* @example
|
|
136
145
|
* var node = pki.asn1.decode(der);
|
|
@@ -144,14 +153,14 @@ function decode(input, opts) {
|
|
|
144
153
|
if (buf.length > maxBytes) {
|
|
145
154
|
throw new Asn1Error("asn1/too-large", "input " + buf.length + " bytes exceeds cap " + maxBytes);
|
|
146
155
|
}
|
|
147
|
-
var r = _decodeTLV(buf, 0, buf.length, 0, maxDepth);
|
|
156
|
+
var r = _decodeTLV(buf, 0, buf.length, 0, maxDepth, opts.ber === true);
|
|
148
157
|
if (!opts.allowTrailing && r.end !== buf.length) {
|
|
149
158
|
throw new Asn1Error("asn1/trailing-bytes", (buf.length - r.end) + " trailing byte(s) after the top-level value");
|
|
150
159
|
}
|
|
151
160
|
return r.node;
|
|
152
161
|
}
|
|
153
162
|
|
|
154
|
-
function _decodeTLV(buf, start, limit, depth, maxDepth) {
|
|
163
|
+
function _decodeTLV(buf, start, limit, depth, maxDepth, ber, strDepth) {
|
|
155
164
|
if (depth > maxDepth) {
|
|
156
165
|
throw new Asn1Error("asn1/too-deep", "nesting exceeds depth cap " + maxDepth);
|
|
157
166
|
}
|
|
@@ -195,8 +204,20 @@ function _decodeTLV(buf, start, limit, depth, maxDepth) {
|
|
|
195
204
|
// encoded constructed is not valid DER. Without this a constructed string
|
|
196
205
|
// tag decodes to a childless node that later paths (an X.509 DN attribute
|
|
197
206
|
// value) would hex-render or dereference instead of failing closed.
|
|
207
|
+
// The ber mode licenses exactly one constructed primitive-only type — the
|
|
208
|
+
// OCTET STRING (the streamed content carrier RFC 7292 §4.1 permits); its
|
|
209
|
+
// segments are reassembled below. Every other primitive-only type stays
|
|
210
|
+
// primitive even in BER. Reassembly re-copies each nesting level's payload,
|
|
211
|
+
// so nesting is capped on the way down: real producers segment one level
|
|
212
|
+
// deep, and a deep chain multiplies transient memory without carrying data.
|
|
198
213
|
if (tagClassBits === CLASS_UNIVERSAL && constructed && PRIMITIVE_ONLY_UNIVERSAL_TAGS[tagNumber]) {
|
|
199
|
-
|
|
214
|
+
if (!(ber && tagNumber === TAGS.OCTET_STRING)) {
|
|
215
|
+
throw new Asn1Error("asn1/constructed-primitive-type", "a universal primitive-only type must be encoded primitive in DER");
|
|
216
|
+
}
|
|
217
|
+
strDepth = (strDepth || 0) + 1;
|
|
218
|
+
if (strDepth > constants.LIMITS.BER_MAX_STRING_NESTING) {
|
|
219
|
+
throw new Asn1Error("asn1/bad-constructed-string", "constructed OCTET STRING nesting exceeds the cap " + constants.LIMITS.BER_MAX_STRING_NESTING);
|
|
220
|
+
}
|
|
200
221
|
}
|
|
201
222
|
|
|
202
223
|
if (p >= limit) throw new Asn1Error("asn1/truncated", "expected a length octet");
|
|
@@ -205,7 +226,12 @@ function _decodeTLV(buf, start, limit, depth, maxDepth) {
|
|
|
205
226
|
if (lenByte < 0x80) {
|
|
206
227
|
length = lenByte;
|
|
207
228
|
} else if (lenByte === 0x80) {
|
|
208
|
-
|
|
229
|
+
// X.690 §8.1.3.6 — indefinite length is only defined for a constructed
|
|
230
|
+
// encoding; the value runs to the end-of-contents octets.
|
|
231
|
+
if (!ber || !constructed) {
|
|
232
|
+
throw new Asn1Error("asn1/indefinite-length", "indefinite length is not valid DER");
|
|
233
|
+
}
|
|
234
|
+
length = -1;
|
|
209
235
|
} else {
|
|
210
236
|
var numLenBytes = lenByte & 0x7f;
|
|
211
237
|
if (numLenBytes > 6) throw new Asn1Error("asn1/length-too-large", "length uses more than 6 octets");
|
|
@@ -218,34 +244,79 @@ function _decodeTLV(buf, start, limit, depth, maxDepth) {
|
|
|
218
244
|
}
|
|
219
245
|
|
|
220
246
|
var contentStart = p;
|
|
221
|
-
var
|
|
222
|
-
|
|
247
|
+
var indefinite = length === -1;
|
|
248
|
+
var contentEnd = indefinite ? -1 : contentStart + length;
|
|
249
|
+
if (!indefinite && contentEnd > limit) throw new Asn1Error("asn1/truncated", "content length overruns the buffer");
|
|
250
|
+
|
|
251
|
+
var children = null;
|
|
252
|
+
var content = null;
|
|
253
|
+
var child;
|
|
254
|
+
var end;
|
|
255
|
+
if (constructed) {
|
|
256
|
+
children = [];
|
|
257
|
+
var cp = contentStart;
|
|
258
|
+
if (indefinite) {
|
|
259
|
+
for (;;) {
|
|
260
|
+
if (cp + 2 > limit) throw new Asn1Error("asn1/truncated", "indefinite-length value is missing its end-of-contents octets");
|
|
261
|
+
if (buf[cp] === 0x00 && buf[cp + 1] === 0x00) break;
|
|
262
|
+
child = _decodeTLV(buf, cp, limit, depth + 1, maxDepth, ber, strDepth);
|
|
263
|
+
children.push(child.node);
|
|
264
|
+
cp = child.end;
|
|
265
|
+
}
|
|
266
|
+
contentEnd = cp;
|
|
267
|
+
end = cp + 2;
|
|
268
|
+
} else {
|
|
269
|
+
while (cp < contentEnd) {
|
|
270
|
+
child = _decodeTLV(buf, cp, contentEnd, depth + 1, maxDepth, ber, strDepth);
|
|
271
|
+
children.push(child.node);
|
|
272
|
+
cp = child.end;
|
|
273
|
+
}
|
|
274
|
+
end = contentEnd;
|
|
275
|
+
}
|
|
276
|
+
} else {
|
|
277
|
+
content = buf.subarray(contentStart, contentEnd);
|
|
278
|
+
end = contentEnd;
|
|
279
|
+
}
|
|
223
280
|
|
|
224
281
|
var node = {
|
|
225
282
|
tagClass: _className(tagClassBits),
|
|
226
283
|
constructed: constructed,
|
|
227
284
|
tagNumber: tagNumber,
|
|
228
|
-
length:
|
|
285
|
+
length: contentEnd - contentStart,
|
|
229
286
|
header: { start: start, end: contentStart },
|
|
230
287
|
contentStart: contentStart,
|
|
231
288
|
contentEnd: contentEnd,
|
|
232
|
-
content:
|
|
233
|
-
children:
|
|
234
|
-
bytes: buf.subarray(start,
|
|
289
|
+
content: content,
|
|
290
|
+
children: children,
|
|
291
|
+
bytes: buf.subarray(start, end),
|
|
235
292
|
};
|
|
236
293
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
294
|
+
// BER reassembly: a constructed OCTET STRING's segments concatenate into
|
|
295
|
+
// one primitive content, so every downstream reader sees the value a
|
|
296
|
+
// single-segment encoding would carry; `bytes` keeps the original wire
|
|
297
|
+
// range for raw round-trip surfaces. Nested constructed segments arrive
|
|
298
|
+
// already reassembled by the recursion (their nesting depth is capped on
|
|
299
|
+
// the way down — see the strDepth check above).
|
|
300
|
+
if (ber && constructed && tagClassBits === CLASS_UNIVERSAL && tagNumber === TAGS.OCTET_STRING) {
|
|
301
|
+
var segments = [];
|
|
302
|
+
for (var s = 0; s < children.length; s++) {
|
|
303
|
+
if (children[s].tagClass !== "universal" || children[s].tagNumber !== TAGS.OCTET_STRING) {
|
|
304
|
+
throw new Asn1Error("asn1/bad-constructed-string", "a constructed OCTET STRING segment must itself be an OCTET STRING");
|
|
305
|
+
}
|
|
306
|
+
segments.push(children[s].content);
|
|
244
307
|
}
|
|
245
|
-
|
|
246
|
-
node.
|
|
308
|
+
node.content = Buffer.concat(segments);
|
|
309
|
+
node.constructed = false;
|
|
310
|
+
node.children = null;
|
|
247
311
|
}
|
|
248
|
-
|
|
312
|
+
// A context-tagged constructed node from a ber decode may be an IMPLICIT
|
|
313
|
+
// streamed string (the tag hides the underlying type), which only the
|
|
314
|
+
// schema-driven typed reader can identify — mark it so readers reassemble
|
|
315
|
+
// exactly those nodes and a strict decode's verdicts never change.
|
|
316
|
+
if (ber && constructed && tagClassBits === CLASS_CONTEXT && node.children) {
|
|
317
|
+
node.ber = true;
|
|
318
|
+
}
|
|
319
|
+
return { node: node, end: end };
|
|
249
320
|
}
|
|
250
321
|
|
|
251
322
|
// ---- Node assertions ------------------------------------------------
|
|
@@ -375,13 +446,29 @@ function readOctetString(node) {
|
|
|
375
446
|
// Read a [tag] IMPLICIT OCTET STRING — a context-class PRIMITIVE node whose
|
|
376
447
|
// content is the octet-string body (the IMPLICIT tag replaces the universal one,
|
|
377
448
|
// so there is no inner universal node). Primitive-only, like its universal
|
|
378
|
-
// counterpart, so
|
|
379
|
-
//
|
|
449
|
+
// counterpart, so a strict decode's constructed/streamed form is rejected.
|
|
450
|
+
// A node from a ber decode (the `ber` marker) may carry the streamed form —
|
|
451
|
+
// the context tag hides the underlying type from the decoder, so THIS reader
|
|
452
|
+
// is where a [tag] IMPLICIT OCTET STRING's segments reassemble (the shape CMS
|
|
453
|
+
// ciphertext streams as). Used for the CMS SignerIdentifier
|
|
454
|
+
// subjectKeyIdentifier [0] (RFC 5652 §5.3) and EncryptedContentInfo
|
|
455
|
+
// encryptedContent [0] (§6.1).
|
|
380
456
|
function readOctetStringImplicit(node, tag) {
|
|
381
457
|
if (node.tagClass !== "context" || node.tagNumber !== tag) {
|
|
382
458
|
throw new Asn1Error("asn1/unexpected-tag", "readOctetStringImplicit: expected context tag [" + tag +
|
|
383
459
|
"], got " + node.tagClass + "/" + node.tagNumber);
|
|
384
460
|
}
|
|
461
|
+
if (node.constructed && node.ber === true) {
|
|
462
|
+
var segments = [];
|
|
463
|
+
for (var s = 0; s < node.children.length; s++) {
|
|
464
|
+
var seg = node.children[s];
|
|
465
|
+
if (seg.tagClass !== "universal" || seg.tagNumber !== TAGS.OCTET_STRING || seg.constructed || !seg.content) {
|
|
466
|
+
throw new Asn1Error("asn1/bad-constructed-string", "a constructed OCTET STRING segment must itself be an OCTET STRING");
|
|
467
|
+
}
|
|
468
|
+
segments.push(seg.content);
|
|
469
|
+
}
|
|
470
|
+
return Buffer.concat(segments);
|
|
471
|
+
}
|
|
385
472
|
_expectPrimitive(node, "readOctetStringImplicit");
|
|
386
473
|
return node.content;
|
|
387
474
|
}
|
package/lib/constants.js
CHANGED
|
@@ -121,6 +121,22 @@ var LIMITS = {
|
|
|
121
121
|
// work on an untrusted certificate bundle (a real chain is well under this;
|
|
122
122
|
// the operator may override via opts.maxPathCerts).
|
|
123
123
|
PATH_MAX_CERTS: 100,
|
|
124
|
+
// PKCS#12 container ceilings. A PFX carries lists at three altitudes
|
|
125
|
+
// (ContentInfos per AuthenticatedSafe, SafeBags per SafeContents,
|
|
126
|
+
// attributes per bag) and can chain fresh DER blobs inside OCTET STRINGs,
|
|
127
|
+
// where every re-decode would otherwise restart the depth cap from zero.
|
|
128
|
+
// A real keystore holds a handful of keys and certificates; thousands of
|
|
129
|
+
// elements or dozens of chained re-decodes is hostile input, not a store.
|
|
130
|
+
PKCS12_MAX_ELEMENTS: 1024,
|
|
131
|
+
PKCS12_MAX_REDECODES: 64,
|
|
132
|
+
PKCS12_MAX_BAG_DEPTH: 16,
|
|
133
|
+
// BER constructed-string reassembly copies each nesting level's payload, so
|
|
134
|
+
// nesting multiplies transient memory. Real producers segment one level
|
|
135
|
+
// deep; nesting past this cap is amplification, not a store.
|
|
136
|
+
BER_MAX_STRING_NESTING: 8,
|
|
137
|
+
// A single attribute's value SET — no deployed attribute carries more than
|
|
138
|
+
// a handful of values; a list at this scale is amplification.
|
|
139
|
+
ATTRIBUTE_MAX_VALUES: 256,
|
|
124
140
|
};
|
|
125
141
|
|
|
126
142
|
// Single-sourced from the package manifest so the reported version can
|
package/lib/framework-error.js
CHANGED
|
@@ -160,6 +160,12 @@ var AttrCertError = defineClass("AttrCertError", { withCause: true });
|
|
|
160
160
|
// `.cause`.
|
|
161
161
|
var CrmfError = defineClass("CrmfError", { withCause: true });
|
|
162
162
|
|
|
163
|
+
// Pkcs12Error — a byte sequence that is not a well-formed RFC 7292 PFX
|
|
164
|
+
// (AuthenticatedSafe / SafeContents / SafeBag), or a PFX violating a §4
|
|
165
|
+
// coherence rule (version, integrity mode, bag dispatch, MacData). Carries
|
|
166
|
+
// the underlying leaf fault as `.cause`.
|
|
167
|
+
var Pkcs12Error = defineClass("Pkcs12Error", { withCause: true });
|
|
168
|
+
|
|
163
169
|
// PathError — a certification path that fails RFC 5280 §6 validation, or a
|
|
164
170
|
// malformed input handed to the validator (an extension value that does not
|
|
165
171
|
// decode, an empty path, an unsupported signature algorithm). Carries the
|
|
@@ -184,5 +190,6 @@ module.exports = {
|
|
|
184
190
|
TspError: TspError,
|
|
185
191
|
AttrCertError: AttrCertError,
|
|
186
192
|
CrmfError: CrmfError,
|
|
193
|
+
Pkcs12Error: Pkcs12Error,
|
|
187
194
|
PathError: PathError,
|
|
188
195
|
};
|
package/lib/oid.js
CHANGED
|
@@ -128,10 +128,35 @@ var FAMILIES = {
|
|
|
128
128
|
data: 1, signedData: 2, envelopedData: 3, signedAndEnvelopedData: 4,
|
|
129
129
|
digestedData: 5, encryptedData: 6 } },
|
|
130
130
|
|
|
131
|
-
// PKCS#9 attribute types — incl. the CMS signed-attribute OIDs (RFC 5652 §11)
|
|
131
|
+
// PKCS#9 attribute types — incl. the CMS signed-attribute OIDs (RFC 5652 §11)
|
|
132
|
+
// and the PKCS#12 bag attributes friendlyName / localKeyId (RFC 7292 §4.2).
|
|
132
133
|
pkcs9: { base: [1, 2, 840, 113549, 1, 9], of: {
|
|
133
134
|
emailAddress: 1, contentType: 3, messageDigest: 4, signingTime: 5,
|
|
134
|
-
challengePassword: 7, extensionRequest: 14 } },
|
|
135
|
+
challengePassword: 7, extensionRequest: 14, friendlyName: 20, localKeyId: 21 } },
|
|
136
|
+
|
|
137
|
+
// PKCS#9 CertBag / CRLBag value discriminators (RFC 7292 §4.2.3-§4.2.4).
|
|
138
|
+
pkcs9CertTypes: { base: [1, 2, 840, 113549, 1, 9, 22], of: { x509Certificate: 1, sdsiCertificate: 2 } },
|
|
139
|
+
pkcs9CrlTypes: { base: [1, 2, 840, 113549, 1, 9, 23], of: { x509CRL: 1 } },
|
|
140
|
+
|
|
141
|
+
// PKCS#5 password-based schemes (RFC 8018) + the PBMAC1 MacData arm (RFC 9579).
|
|
142
|
+
pkcs5: { base: [1, 2, 840, 113549, 1, 5], of: { pbkdf2: 12, pbes2: 13, pbmac1: 14 } },
|
|
143
|
+
|
|
144
|
+
// PKCS#12 bag types (RFC 7292 §4.2, Appendix D).
|
|
145
|
+
pkcs12BagTypes: { base: [1, 2, 840, 113549, 1, 12, 10, 1], of: {
|
|
146
|
+
keyBag: 1, pkcs8ShroudedKeyBag: 2, certBag: 3, crlBag: 4, secretBag: 5, safeContentsBag: 6 } },
|
|
147
|
+
|
|
148
|
+
// PKCS#12 password-based encryption schemes (RFC 7292 Appendix C) — legacy
|
|
149
|
+
// PBE identifiers still emitted by deployed exporters; recognized so a
|
|
150
|
+
// shrouded bag's algorithm resolves to a name, never decrypted here.
|
|
151
|
+
pkcs12Pbe: { base: [1, 2, 840, 113549, 1, 12, 1], of: {
|
|
152
|
+
pbeWithSHAAnd128BitRC4: 1, pbeWithSHAAnd40BitRC4: 2,
|
|
153
|
+
"pbeWithSHAAnd3-KeyTripleDES-CBC": 3, "pbeWithSHAAnd2-KeyTripleDES-CBC": 4,
|
|
154
|
+
"pbeWithSHAAnd128BitRC2-CBC": 5, "pbeWithSHAAnd40BitRC2-CBC": 6 } },
|
|
155
|
+
|
|
156
|
+
// RSADSI digest / HMAC algorithms (RFC 8018 §B.1) — the PBKDF2 / PBMAC1 PRFs.
|
|
157
|
+
rsadsiDigest: { base: [1, 2, 840, 113549, 2], of: {
|
|
158
|
+
hmacWithSHA1: 7, hmacWithSHA224: 8, hmacWithSHA256: 9,
|
|
159
|
+
hmacWithSHA384: 10, hmacWithSHA512: 11 } },
|
|
135
160
|
|
|
136
161
|
// S/MIME content types on the PKCS#9 smime arc (RFC 5652, RFC 3161): id-ct.
|
|
137
162
|
smimeCt: { base: [1, 2, 840, 113549, 1, 9, 16, 1], of: { authData: 2, tSTInfo: 4, encKeyWithID: 21 } },
|
|
@@ -157,6 +182,12 @@ var FAMILIES = {
|
|
|
157
182
|
// nistHash covers only SHA-256 and above.
|
|
158
183
|
oiwSecsig: { base: [1, 3, 14, 3, 2], of: { sha1: 26 } },
|
|
159
184
|
|
|
185
|
+
// NIST AES content-encryption algorithms (arc 2.16.840.1.101.3.4.1) — the
|
|
186
|
+
// modern PBES2 / CMS content ciphers a PKCS#12 or EnvelopedData names.
|
|
187
|
+
nistAes: { base: [2, 16, 840, 1, 101, 3, 4, 1], of: {
|
|
188
|
+
"aes128-CBC": 2, "aes128-GCM": 6, "aes192-CBC": 22, "aes192-GCM": 26,
|
|
189
|
+
"aes256-CBC": 42, "aes256-GCM": 46 } },
|
|
190
|
+
|
|
160
191
|
// NIST hash functions (SHA-2, SHA-3, SHAKE).
|
|
161
192
|
nistHash: { base: [2, 16, 840, 1, 101, 3, 4, 2], of: {
|
|
162
193
|
sha256: 1, sha384: 2, sha512: 3, "sha3-256": 8, "sha3-512": 10, shake256: 12 } },
|
package/lib/path-validate.js
CHANGED
|
@@ -193,10 +193,18 @@ function resolveRsaPss(paramsBytes) {
|
|
|
193
193
|
// A negative saltLength must be rejected: the OpenSSL-backed verify shim
|
|
194
194
|
// reads -1/-2/-3 as RSA_PSS_SALTLEN_DIGEST/AUTO/MAX, and AUTO (-2) accepts
|
|
195
195
|
// a signature of ANY salt length — defeating the salt-length binding.
|
|
196
|
-
|
|
196
|
+
// The upper bound keeps the value exact through Number conversion: the
|
|
197
|
+
// verifier binds to the salt length the certificate states, so a value
|
|
198
|
+
// that would round is not verifiable material (no real salt exceeds the
|
|
199
|
+
// modulus size, let alone this).
|
|
200
|
+
if (sl < 0n || sl > 2147483647n) throw E("path/unsupported-algorithm", "RSASSA-PSS saltLength must be a non-negative integer within the salt-length range");
|
|
197
201
|
saltLength = Number(sl);
|
|
198
202
|
} else if (f.tagNumber === 3) {
|
|
199
|
-
|
|
203
|
+
// Compared for equality with 1 below — bound before conversion so an
|
|
204
|
+
// oversized value cannot round on its way to the comparison.
|
|
205
|
+
var tf = asn1.read.integer(f.children[0]);
|
|
206
|
+
if (tf < 0n || tf > 2147483647n) throw E("path/unsupported-algorithm", "unsupported RSASSA-PSS trailerField " + tf.toString());
|
|
207
|
+
trailer = Number(tf);
|
|
200
208
|
}
|
|
201
209
|
});
|
|
202
210
|
if (hash === null) throw E("path/unsupported-algorithm", "RSASSA-PSS hashAlgorithm must be stated explicitly (the SHA-1 default is rejected)");
|
package/lib/schema-all.js
CHANGED
|
@@ -11,9 +11,9 @@
|
|
|
11
11
|
*
|
|
12
12
|
* @intro
|
|
13
13
|
* The schema family: a declarative ASN.1 structure-schema engine and the
|
|
14
|
-
* per-format parsers built on it. Every format
|
|
15
|
-
*
|
|
16
|
-
* member that COMPOSES the
|
|
14
|
+
* per-format parsers built on it. Every format — from X.509 certificates
|
|
15
|
+
* and CRLs through CMS, OCSP, timestamps, and PKCS#12 stores; `all()`
|
|
16
|
+
* enumerates the registered set — is a member that COMPOSES the
|
|
17
17
|
* shared engine and the shared PKIX sub-schemas (AlgorithmIdentifier, Name,
|
|
18
18
|
* Extension), so a structural rule — bounds-checked positional reads,
|
|
19
19
|
* optional / tagged field ordering, SET-OF uniqueness, fail-closed typed
|
|
@@ -36,6 +36,7 @@ var x509 = require("./schema-x509");
|
|
|
36
36
|
var crl = require("./schema-crl");
|
|
37
37
|
var csr = require("./schema-csr");
|
|
38
38
|
var pkcs8 = require("./schema-pkcs8");
|
|
39
|
+
var pkcs12 = require("./schema-pkcs12");
|
|
39
40
|
var cms = require("./schema-cms");
|
|
40
41
|
var ocsp = require("./schema-ocsp");
|
|
41
42
|
var tsp = require("./schema-tsp");
|
|
@@ -47,9 +48,12 @@ var SchemaError = frameworkError.SchemaError;
|
|
|
47
48
|
var PemError = frameworkError.PemError;
|
|
48
49
|
|
|
49
50
|
// The shared parse-entry opts for the orchestrator: label-agnostic PEM unwrap
|
|
50
|
-
// (any block type routes), the schema/* error family. Detection needs
|
|
51
|
-
// the coerced
|
|
52
|
-
|
|
51
|
+
// (any block type routes), the schema/* error family. Detection needs a decoded
|
|
52
|
+
// root, so the coerced bytes are what gets routed to the matched format. The
|
|
53
|
+
// ber mode lets a BER-encoded PFX (the dominant real-world .p12 shape) decode
|
|
54
|
+
// far enough to detect; a BER input matching a strict-DER-only format still
|
|
55
|
+
// fails typed inside that format's own parse.
|
|
56
|
+
var ENTRY = { pemLabel: null, PemError: PemError, ErrorClass: SchemaError, prefix: "schema", what: "input", ber: true };
|
|
53
57
|
|
|
54
58
|
// REGISTRY — each format is declarative data: a name, a `detect(root)` that
|
|
55
59
|
// recognizes the decoded DER root as this format's outer structure, and the
|
|
@@ -117,6 +121,20 @@ var FORMATS = [
|
|
|
117
121
|
detect: ocsp.matchesResponse,
|
|
118
122
|
parse: function (input) { return ocsp.parseResponse(input); },
|
|
119
123
|
},
|
|
124
|
+
{
|
|
125
|
+
// PKCS#12 PFX ::= SEQUENCE { version INTEGER, authSafe ContentInfo, macData
|
|
126
|
+
// OPTIONAL } — INTEGER-first like pkcs8, so the discriminators are deeper:
|
|
127
|
+
// children[1] is a ContentInfo (SEQUENCE of exactly 2: OID + [0] constructed —
|
|
128
|
+
// a shape a PrivateKeyInfo's AlgorithmIdentifier never presents) and
|
|
129
|
+
// children[2] is a MacData SEQUENCE or absent (pkcs8 requires an OCTET STRING
|
|
130
|
+
// there). Shape-disjoint from pkcs8, and registered ahead of it as the more
|
|
131
|
+
// specific probe (RFC 7292 §4). A BER-encoded PFX detects through the
|
|
132
|
+
// orchestrator's BER decode fallback and routes here.
|
|
133
|
+
name: "pkcs12",
|
|
134
|
+
module: pkcs12,
|
|
135
|
+
detect: pkcs12.matches,
|
|
136
|
+
parse: function (input) { return pkcs12.parse(input); },
|
|
137
|
+
},
|
|
120
138
|
{
|
|
121
139
|
// PKCS#8 PrivateKeyInfo / OneAsymmetricKey — SEQUENCE whose first child is an
|
|
122
140
|
// INTEGER (version) and third an OCTET STRING (privateKey); disjoint from the
|
|
@@ -193,7 +211,7 @@ var FORMATS = [
|
|
|
193
211
|
* The names of every registered format, in detection order.
|
|
194
212
|
*
|
|
195
213
|
* @example
|
|
196
|
-
* pki.schema.all(); // → ["cms", "tsp", "crmf", "ocsp-request", "ocsp-response", "pkcs8", "csr", "attrcert", "attrcert-v1", "crl", "x509"]
|
|
214
|
+
* pki.schema.all(); // → ["cms", "tsp", "crmf", "ocsp-request", "ocsp-response", "pkcs12", "pkcs8", "csr", "attrcert", "attrcert-v1", "crl", "x509"]
|
|
197
215
|
*/
|
|
198
216
|
function all() { return FORMATS.map(function (f) { return f.name; }); }
|
|
199
217
|
|
|
@@ -235,6 +253,7 @@ module.exports = {
|
|
|
235
253
|
crl: { parse: crl.parse, pemDecode: crl.pemDecode },
|
|
236
254
|
csr: { parse: csr.parse, pemDecode: csr.pemDecode, pemEncode: csr.pemEncode },
|
|
237
255
|
pkcs8: { parse: pkcs8.parse, parseEncrypted: pkcs8.parseEncrypted, pemDecode: pkcs8.pemDecode, pemEncode: pkcs8.pemEncode },
|
|
256
|
+
pkcs12: { parse: pkcs12.parse, pemDecode: pkcs12.pemDecode, pemEncode: pkcs12.pemEncode },
|
|
238
257
|
cms: { parse: cms.parse, pemDecode: cms.pemDecode, pemEncode: cms.pemEncode },
|
|
239
258
|
ocsp: { parseRequest: ocsp.parseRequest, parseResponse: ocsp.parseResponse, pemDecode: ocsp.pemDecode },
|
|
240
259
|
tsp: { parse: tsp.parse, parseTstInfo: tsp.parseTstInfo, parseToken: tsp.parseToken, pemDecode: tsp.pemDecode },
|
package/lib/schema-cms.js
CHANGED
|
@@ -692,10 +692,20 @@ function matches(root) {
|
|
|
692
692
|
// universal SEQUENCE (an IMPLICIT [n] EnvelopedData is retagged by the caller).
|
|
693
693
|
function walkEnvelopedData(node) { return schema.walk(ENVELOPED_DATA, node, NS).result; }
|
|
694
694
|
|
|
695
|
+
// Validate a bare SignedData / EncryptedData node the same way — for a composer
|
|
696
|
+
// that holds an already-decoded content node and must not re-decode its bytes
|
|
697
|
+
// (an RFC 7292 PFX authSafe or encrypted safe, whose wire encoding may be BER
|
|
698
|
+
// that the strict `parse` entry would refuse). Same contract as
|
|
699
|
+
// walkEnvelopedData: the node is the bare structure, typed cms/* on rejection.
|
|
700
|
+
function walkSignedData(node) { return schema.walk(SIGNED_DATA, node, NS).result; }
|
|
701
|
+
function walkEncryptedData(node) { return schema.walk(ENCRYPTED_DATA, node, NS).result; }
|
|
702
|
+
|
|
695
703
|
module.exports = {
|
|
696
704
|
parse: parse,
|
|
697
705
|
pemDecode: pemDecode,
|
|
698
706
|
pemEncode: pemEncode,
|
|
699
707
|
matches: matches,
|
|
700
708
|
walkEnvelopedData: walkEnvelopedData,
|
|
709
|
+
walkSignedData: walkSignedData,
|
|
710
|
+
walkEncryptedData: walkEncryptedData,
|
|
701
711
|
};
|
package/lib/schema-crmf.js
CHANGED
|
@@ -216,7 +216,7 @@ function popoPrivKey(type) {
|
|
|
216
216
|
}
|
|
217
217
|
}
|
|
218
218
|
if (inner.tagNumber === 3) {
|
|
219
|
-
try { schema.
|
|
219
|
+
try { schema.embeddedDer(PKMAC_VALUE, asn1.sequenceTlv(inner), NS, { code: "crmf/bad-popo", what: "agreeMAC [3] PKMACValue" }); }
|
|
220
220
|
catch (e) { throw ctx.E("crmf/bad-popo", "agreeMAC [3] must be a PKMACValue SEQUENCE { algId, BIT STRING } (RFC 4211 §4.2)", e); }
|
|
221
221
|
}
|
|
222
222
|
// encryptedKey [4] EnvelopedData — validate the CMS structure fail-closed by
|
package/lib/schema-engine.js
CHANGED
|
@@ -200,12 +200,14 @@ function choice(alts, opts) {
|
|
|
200
200
|
function seqOf(item, opts) {
|
|
201
201
|
opts = opts || {};
|
|
202
202
|
return { kind: "repeat", item: item, assert: opts.assert || "sequence", code: opts.code, what: opts.what,
|
|
203
|
-
min: opts.min,
|
|
203
|
+
min: opts.min, max: opts.max, maxCode: opts.maxCode,
|
|
204
|
+
unique: opts.unique, dupCode: opts.dupCode, build: opts.build };
|
|
204
205
|
}
|
|
205
206
|
function setOf(item, opts) {
|
|
206
207
|
opts = opts || {};
|
|
207
208
|
return { kind: "repeat", item: item, assert: opts.assert || "set", derSetOrder: true, code: opts.code, what: opts.what,
|
|
208
|
-
min: opts.min,
|
|
209
|
+
min: opts.min, max: opts.max, maxCode: opts.maxCode,
|
|
210
|
+
unique: opts.unique, dupCode: opts.dupCode, build: opts.build };
|
|
209
211
|
}
|
|
210
212
|
function setOfUnique(item, keyFn, opts) {
|
|
211
213
|
return setOf(item, Object.assign({ unique: keyFn }, opts || {}));
|
|
@@ -216,7 +218,8 @@ function setOfUnique(item, keyFn, opts) {
|
|
|
216
218
|
function implicitSetOf(tag, item, opts) {
|
|
217
219
|
opts = opts || {};
|
|
218
220
|
return { kind: "repeat", item: item, assert: "implicit", implicitTag: tag, derSetOrder: true, code: opts.code, what: opts.what,
|
|
219
|
-
min: opts.min,
|
|
221
|
+
min: opts.min, max: opts.max, maxCode: opts.maxCode,
|
|
222
|
+
unique: opts.unique, dupCode: opts.dupCode, build: opts.build };
|
|
220
223
|
}
|
|
221
224
|
// [tag] IMPLICIT SEQUENCE OF item — the context tag REPLACES the universal SEQUENCE
|
|
222
225
|
// tag, so the node is a context-class constructed [tag] whose direct children are the
|
|
@@ -225,7 +228,8 @@ function implicitSetOf(tag, item, opts) {
|
|
|
225
228
|
function implicitSeqOf(tag, item, opts) {
|
|
226
229
|
opts = opts || {};
|
|
227
230
|
return { kind: "repeat", item: item, assert: "implicit", implicitTag: tag, code: opts.code, what: opts.what,
|
|
228
|
-
min: opts.min,
|
|
231
|
+
min: opts.min, max: opts.max, maxCode: opts.maxCode,
|
|
232
|
+
unique: opts.unique, dupCode: opts.dupCode, build: opts.build };
|
|
229
233
|
}
|
|
230
234
|
|
|
231
235
|
// ---- the walk engine -------------------------------------------------
|
|
@@ -304,6 +308,12 @@ function _walkRepeat(schema, node, ctx) {
|
|
|
304
308
|
if (schema.min != null && kids.length < schema.min) {
|
|
305
309
|
_fail(ctx, schema.code, (schema.what || "value") + " must contain at least " + schema.min + " element(s)");
|
|
306
310
|
}
|
|
311
|
+
// The element-count ceiling: a container of a great many tiny elements
|
|
312
|
+
// amplifies memory through per-element walk products, so a schema that
|
|
313
|
+
// parses attacker-sized lists declares `max` and fails typed instead.
|
|
314
|
+
if (schema.max != null && kids.length > schema.max) {
|
|
315
|
+
_fail(ctx, schema.maxCode || schema.code, (schema.what || "value") + " exceeds the element cap " + schema.max);
|
|
316
|
+
}
|
|
307
317
|
// DER (X.690 §11.6) — the components of a SET OF appear in ascending order, the
|
|
308
318
|
// encodings compared as octet strings. SEQUENCE OF is order-preserving and is
|
|
309
319
|
// exempt (no derSetOrder). Compare full TLV byte strings, the same canonical
|
|
@@ -500,6 +510,9 @@ function _encodeRepeat(schema, items, ctx) {
|
|
|
500
510
|
if (schema.min != null && items.length < schema.min) {
|
|
501
511
|
_encFail("this repeat requires at least " + schema.min + " element(s) but got " + items.length);
|
|
502
512
|
}
|
|
513
|
+
if (schema.max != null && items.length > schema.max) {
|
|
514
|
+
_encFail("this repeat caps at " + schema.max + " element(s) but got " + items.length);
|
|
515
|
+
}
|
|
503
516
|
var parts = items.map(function (it) { return encode(schema.item, it, ctx); });
|
|
504
517
|
if (schema.unique) {
|
|
505
518
|
var seen = new Set();
|
|
@@ -523,6 +536,57 @@ function _encodeChoice(schema, value, ctx) {
|
|
|
523
536
|
return encode(schema.alts[value.arm].schema, value.value, ctx);
|
|
524
537
|
}
|
|
525
538
|
|
|
539
|
+
/**
|
|
540
|
+
* @primitive pki.schema.engine.embeddedDer
|
|
541
|
+
* @signature pki.schema.engine.embeddedDer(schema, bytes, ctx, opts?) -> value
|
|
542
|
+
* @since 0.1.18
|
|
543
|
+
* @status experimental
|
|
544
|
+
* @spec X.690
|
|
545
|
+
* @defends ASN.1-parser-DoS (CWE-400)
|
|
546
|
+
* @related pki.schema.engine.walk, pki.asn1.decode
|
|
547
|
+
*
|
|
548
|
+
* Decode a fresh DER (or, with `ber: true`, BER) blob carried inside an
|
|
549
|
+
* already-decoded value — an OCTET STRING whose content is itself an encoded
|
|
550
|
+
* structure — and walk it against a schema. A codec failure is wrapped in the
|
|
551
|
+
* caller's typed `code`; a schema rejection keeps its own code. This is the
|
|
552
|
+
* one named form of the re-decode idiom, so the caps that a fresh
|
|
553
|
+
* `pki.asn1.decode` would restart from zero can be carried across re-decode
|
|
554
|
+
* boundaries: a shared `budget` (`{ remaining: n }`) decrements on every call
|
|
555
|
+
* and fails with `budgetCode` at zero, bounding how many nested blobs one
|
|
556
|
+
* parse may unwrap however deeply a container chains them.
|
|
557
|
+
*
|
|
558
|
+
* @opts
|
|
559
|
+
* code: string, // typed code wrapping a codec failure (required)
|
|
560
|
+
* what: string, // human label for the wrapped message
|
|
561
|
+
* ber: boolean, // default false — BER content region (RFC 7292 §4.1)
|
|
562
|
+
* budget: object, // { remaining: n } shared across a parse's re-decodes
|
|
563
|
+
* budgetCode: string, // typed code when the budget is exhausted
|
|
564
|
+
*
|
|
565
|
+
* @example
|
|
566
|
+
* var S = pki.schema.engine;
|
|
567
|
+
* var INNER = S.seq([S.field("version", S.integerLeaf())], { code: "app/bad-inner" });
|
|
568
|
+
* var ns = { prefix: "app", E: MyError, oid: pki.oid };
|
|
569
|
+
* S.embeddedDer(INNER, pki.asn1.build.sequence([pki.asn1.build.integer(3n)]), ns,
|
|
570
|
+
* { code: "app/bad-der", what: "the embedded structure" });
|
|
571
|
+
*/
|
|
572
|
+
function embeddedDer(schema, bytes, ctx, opts) {
|
|
573
|
+
opts = opts || {};
|
|
574
|
+
if (opts.budget) {
|
|
575
|
+
if (!(opts.budget.remaining > 0)) {
|
|
576
|
+
throw ctx.E(opts.budgetCode || opts.code, (opts.what || "embedded DER") +
|
|
577
|
+
": the cross-decode budget is exhausted (nesting chained across too many re-decode boundaries)");
|
|
578
|
+
}
|
|
579
|
+
opts.budget.remaining -= 1;
|
|
580
|
+
}
|
|
581
|
+
var node;
|
|
582
|
+
try {
|
|
583
|
+
node = asn1.decode(bytes, opts.ber ? { ber: true } : undefined);
|
|
584
|
+
} catch (e) {
|
|
585
|
+
throw ctx.E(opts.code, (opts.what || "embedded DER") + " did not decode: " + e.message, e);
|
|
586
|
+
}
|
|
587
|
+
return walk(schema, node, ctx);
|
|
588
|
+
}
|
|
589
|
+
|
|
526
590
|
module.exports = {
|
|
527
591
|
// structural
|
|
528
592
|
seq: seq, field: field, optional: optional, explicit: explicit, trailing: trailing,
|
|
@@ -533,5 +597,5 @@ module.exports = {
|
|
|
533
597
|
implicitNull: implicitNull, implicitInteger: implicitInteger,
|
|
534
598
|
any: any, decode: decode, time: time,
|
|
535
599
|
// engine
|
|
536
|
-
walk: walk, encode: encode,
|
|
600
|
+
walk: walk, encode: encode, embeddedDer: embeddedDer,
|
|
537
601
|
};
|
package/lib/schema-ocsp.js
CHANGED
|
@@ -294,14 +294,12 @@ var RESPONSE_BYTES = schema.seq([
|
|
|
294
294
|
if (responseType !== OID_OCSP_BASIC) {
|
|
295
295
|
throw NS.E("ocsp/unsupported-response-type", (ctx.oid.name(responseType) || responseType) + " is not id-pkix-ocsp-basic (RFC 6960 §4.2.1)");
|
|
296
296
|
}
|
|
297
|
-
var inner;
|
|
298
|
-
try { inner = asn1.decode(raw); }
|
|
299
|
-
catch (e) { throw NS.E("ocsp/bad-der", "BasicOCSPResponse DER did not decode: " + ((e && e.message) || String(e)), e); }
|
|
300
297
|
return {
|
|
301
298
|
responseType: responseType,
|
|
302
299
|
responseTypeName: ctx.oid.name(responseType) || null,
|
|
303
300
|
response: raw,
|
|
304
|
-
basicResponse: schema.
|
|
301
|
+
basicResponse: schema.embeddedDer(BASIC_OCSP_RESPONSE, raw, ctx,
|
|
302
|
+
{ code: "ocsp/bad-der", what: "BasicOCSPResponse" }).result,
|
|
305
303
|
};
|
|
306
304
|
},
|
|
307
305
|
});
|