@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.
- package/CHANGELOG.md +45 -1
- package/README.md +9 -7
- package/lib/asn1-der.js +109 -22
- package/lib/constants.js +16 -0
- package/lib/framework-error.js +14 -0
- package/lib/oid.js +48 -2
- package/lib/path-validate.js +14 -2
- package/lib/schema-all.js +45 -8
- package/lib/schema-attrcert.js +6 -6
- package/lib/schema-cmp.js +928 -0
- package/lib/schema-cms.js +20 -13
- package/lib/schema-crl.js +2 -2
- package/lib/schema-crmf.js +63 -29
- package/lib/schema-csr.js +4 -4
- package/lib/schema-engine.js +114 -8
- package/lib/schema-ocsp.js +10 -14
- package/lib/schema-pkcs12.js +614 -0
- package/lib/schema-pkcs8.js +16 -7
- package/lib/schema-pkix.js +40 -15
- package/lib/schema-tsp.js +11 -18
- package/lib/schema-x509.js +3 -3
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/lib/schema-cms.js
CHANGED
|
@@ -132,12 +132,11 @@ function rawElement(item) {
|
|
|
132
132
|
// [3], a v2AttrCert [2], a v1AttrCert [1]; a RevocationInfoChoice `other` is [1]),
|
|
133
133
|
// the SignerInfo versions, and whether the content type is id-data.
|
|
134
134
|
function _expectedSignedDataVersion(certificates, crls, signerInfos, eContentType) {
|
|
135
|
-
function
|
|
136
|
-
var
|
|
137
|
-
var otherCrl = crls.some(function (c) { return ctx(c, 1); });
|
|
135
|
+
var otherCert = certificates.some(function (c) { return schema.isContext(c, 3); });
|
|
136
|
+
var otherCrl = crls.some(function (c) { return schema.isContext(c, 1); });
|
|
138
137
|
if (otherCert || otherCrl) return 5;
|
|
139
|
-
if (certificates.some(function (c) { return
|
|
140
|
-
if (certificates.some(function (c) { return
|
|
138
|
+
if (certificates.some(function (c) { return schema.isContext(c, 2); })) return 4; // v2AttrCert [2]
|
|
139
|
+
if (certificates.some(function (c) { return schema.isContext(c, 1); }) || // v1AttrCert [1]
|
|
141
140
|
signerInfos.some(function (si) { return si.version === 3; }) ||
|
|
142
141
|
eContentType !== OID_DATA) return 3;
|
|
143
142
|
return 1;
|
|
@@ -528,12 +527,11 @@ var ORIGINATOR_INFO = schema.seq([
|
|
|
528
527
|
// [2]; a crl `other` is [1]; a pwri or ori forces v3; all-ktri-IAS with no
|
|
529
528
|
// originatorInfo/unprotectedAttrs is v0; everything else v2).
|
|
530
529
|
function _expectedEnvelopedDataVersion(originatorInfo, recipientInfos, hasUnprotectedAttrs) {
|
|
531
|
-
function ctx(el, n) { return el.tagClass === "context" && el.tagNumber === n; }
|
|
532
530
|
var hasOrig = !!originatorInfo;
|
|
533
531
|
var certs = hasOrig ? originatorInfo.certs : [];
|
|
534
532
|
var crls = hasOrig ? originatorInfo.crls : [];
|
|
535
|
-
if (hasOrig && (certs.some(function (c) { return
|
|
536
|
-
if ((hasOrig && certs.some(function (c) { return
|
|
533
|
+
if (hasOrig && (certs.some(function (c) { return schema.isContext(c, 3); }) || crls.some(function (c) { return schema.isContext(c, 1); }))) return 4;
|
|
534
|
+
if ((hasOrig && certs.some(function (c) { return schema.isContext(c, 2); })) ||
|
|
537
535
|
recipientInfos.some(function (r) { return r.type === "pwri" || r.type === "ori"; })) return 3;
|
|
538
536
|
if (!hasOrig && !hasUnprotectedAttrs &&
|
|
539
537
|
recipientInfos.every(function (r) { return r.type === "ktri" && r.ridType === "issuerAndSerialNumber"; })) return 0;
|
|
@@ -676,11 +674,10 @@ function pemEncode(der, label) { return pkix.pemEncode(der, label || "CMS", PemE
|
|
|
676
674
|
// registry order.
|
|
677
675
|
function matches(root) {
|
|
678
676
|
var TAGS = asn1.TAGS;
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
if (!k
|
|
682
|
-
if (!(k[
|
|
683
|
-
if (!(k[1].tagClass === "context" && k[1].tagNumber === 0 && k[1].children)) return false;
|
|
677
|
+
var k = pkix.rootSequenceChildren(root, 2, 2);
|
|
678
|
+
if (!k) return false;
|
|
679
|
+
if (!schema.isUniversal(k[0], TAGS.OBJECT_IDENTIFIER)) return false;
|
|
680
|
+
if (!(schema.isContext(k[1], 0) && k[1].children)) return false;
|
|
684
681
|
return true;
|
|
685
682
|
}
|
|
686
683
|
|
|
@@ -692,10 +689,20 @@ function matches(root) {
|
|
|
692
689
|
// universal SEQUENCE (an IMPLICIT [n] EnvelopedData is retagged by the caller).
|
|
693
690
|
function walkEnvelopedData(node) { return schema.walk(ENVELOPED_DATA, node, NS).result; }
|
|
694
691
|
|
|
692
|
+
// Validate a bare SignedData / EncryptedData node the same way — for a composer
|
|
693
|
+
// that holds an already-decoded content node and must not re-decode its bytes
|
|
694
|
+
// (an RFC 7292 PFX authSafe or encrypted safe, whose wire encoding may be BER
|
|
695
|
+
// that the strict `parse` entry would refuse). Same contract as
|
|
696
|
+
// walkEnvelopedData: the node is the bare structure, typed cms/* on rejection.
|
|
697
|
+
function walkSignedData(node) { return schema.walk(SIGNED_DATA, node, NS).result; }
|
|
698
|
+
function walkEncryptedData(node) { return schema.walk(ENCRYPTED_DATA, node, NS).result; }
|
|
699
|
+
|
|
695
700
|
module.exports = {
|
|
696
701
|
parse: parse,
|
|
697
702
|
pemDecode: pemDecode,
|
|
698
703
|
pemEncode: pemEncode,
|
|
699
704
|
matches: matches,
|
|
700
705
|
walkEnvelopedData: walkEnvelopedData,
|
|
706
|
+
walkSignedData: walkSignedData,
|
|
707
|
+
walkEncryptedData: walkEncryptedData,
|
|
701
708
|
};
|
package/lib/schema-crl.js
CHANGED
|
@@ -238,10 +238,10 @@ function matches(root) {
|
|
|
238
238
|
// version, then signature (SEQUENCE) + issuer (SEQUENCE); the next element is
|
|
239
239
|
// thisUpdate (Time) for a CRL, Validity (SEQUENCE) for a certificate.
|
|
240
240
|
var i = 0;
|
|
241
|
-
if (
|
|
241
|
+
if (schema.isUniversal(tbs.children[i], TAGS.INTEGER)) i++;
|
|
242
242
|
i += 2; // signature + issuer
|
|
243
243
|
var pos = tbs.children[i];
|
|
244
|
-
return
|
|
244
|
+
return schema.isUniversalOneOf(pos, [TAGS.UTC_TIME, TAGS.GENERALIZED_TIME]);
|
|
245
245
|
}
|
|
246
246
|
|
|
247
247
|
module.exports = {
|
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
|
|
@@ -296,30 +296,27 @@ var CERT_TEMPLATE = schema.seq([
|
|
|
296
296
|
assert: "sequence", code: "crmf/bad-cert-template", what: "CertTemplate",
|
|
297
297
|
build: function (m, ctx) {
|
|
298
298
|
var f = m.fields;
|
|
299
|
-
//
|
|
300
|
-
//
|
|
301
|
-
// CA
|
|
302
|
-
//
|
|
303
|
-
//
|
|
304
|
-
//
|
|
305
|
-
//
|
|
306
|
-
var caAssigned = ["serialNumber", "signingAlg", "issuerUID", "subjectUID"];
|
|
307
|
-
for (var i = 0; i < caAssigned.length; i++) {
|
|
308
|
-
if (f[caAssigned[i]].present) {
|
|
309
|
-
throw ctx.E("crmf/bad-cert-template", "CertTemplate " + caAssigned[i] + " MUST be omitted — it is CA-assigned or deprecated (RFC 4211 §5)");
|
|
310
|
-
}
|
|
311
|
-
}
|
|
312
|
-
// version MUST be 2 (v3) when supplied; SHOULD be omitted (RFC 4211 §5).
|
|
299
|
+
// version MUST be 2 (v3) when supplied; SHOULD be omitted (RFC 4211 §5). This
|
|
300
|
+
// is a structural CertTemplate rule (it holds wherever the template appears);
|
|
301
|
+
// the CA-assigned-field omission rule is REQUEST-context-specific and lives in
|
|
302
|
+
// CERT_REQUEST.build, because the same CertTemplate structure identifies an
|
|
303
|
+
// existing certificate — serialNumber and issuer present — inside a CMP
|
|
304
|
+
// RevDetails (RFC 9810 §5.3.9). Every field is surfaced so both the request
|
|
305
|
+
// omission check and the revocation consumer read them off one result.
|
|
313
306
|
if (f.version.present && f.version.value !== 2n) {
|
|
314
307
|
throw ctx.E("crmf/bad-version", "CertTemplate version MUST be 2 (v3) if supplied (RFC 4211 §5)");
|
|
315
308
|
}
|
|
316
309
|
return {
|
|
317
|
-
version:
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
310
|
+
version: f.version.present ? f.version.value : null,
|
|
311
|
+
serialNumber: f.serialNumber.present ? f.serialNumber.value : null,
|
|
312
|
+
signingAlg: f.signingAlg.present ? f.signingAlg.value.result : null,
|
|
313
|
+
issuer: f.issuer.present ? f.issuer.value : null,
|
|
314
|
+
validity: f.validity.present ? f.validity.value.result : null,
|
|
315
|
+
subject: f.subject.present ? f.subject.value : null,
|
|
316
|
+
publicKey: f.publicKey.present ? f.publicKey.value.result : null,
|
|
317
|
+
issuerUID: f.issuerUID.present ? f.issuerUID.value : null,
|
|
318
|
+
subjectUID: f.subjectUID.present ? f.subjectUID.value : null,
|
|
319
|
+
extensions: f.extensions.present ? f.extensions.value.result : null,
|
|
323
320
|
};
|
|
324
321
|
},
|
|
325
322
|
});
|
|
@@ -334,11 +331,32 @@ var CERT_REQUEST = schema.seq([
|
|
|
334
331
|
schema.optional("controls", CONTROLS, { whenUniversal: [TAGS.SEQUENCE] }),
|
|
335
332
|
], {
|
|
336
333
|
assert: "sequence", code: "crmf/bad-cert-request", what: "CertRequest",
|
|
337
|
-
build: function (m) {
|
|
334
|
+
build: function (m, ctx) {
|
|
335
|
+
var tpl = m.fields.certTemplate.value.result;
|
|
336
|
+
// RFC 4211 §5 — serialNumber, signingAlg, issuerUID, and subjectUID MUST be
|
|
337
|
+
// omitted from a REQUEST's CertTemplate: serialNumber and signingAlg are
|
|
338
|
+
// assigned by the CA, and the UID pair is deprecated. A requester must not
|
|
339
|
+
// dictate a CA-assigned value (a requester-chosen serialNumber is a real
|
|
340
|
+
// hazard), so a request template that sets any of them is rejected
|
|
341
|
+
// fail-closed. The check lives HERE, not in CERT_TEMPLATE, because the same
|
|
342
|
+
// structure legitimately carries serialNumber/issuer in a CMP RevDetails.
|
|
343
|
+
// A cross-certification request (ccr) is the ONE exception: RFC 9810 Appendix
|
|
344
|
+
// D.6 profiles its template with signingAlg PRESENT (the requesting CA states
|
|
345
|
+
// the algorithm it wants the cross-certificate signed with), so the caller
|
|
346
|
+
// relaxes that single field via ctx.allowSigningAlg; serialNumber and the UID
|
|
347
|
+
// pair stay forbidden.
|
|
348
|
+
var caAssigned = ctx.allowSigningAlg
|
|
349
|
+
? ["serialNumber", "issuerUID", "subjectUID"]
|
|
350
|
+
: ["serialNumber", "signingAlg", "issuerUID", "subjectUID"];
|
|
351
|
+
for (var i = 0; i < caAssigned.length; i++) {
|
|
352
|
+
if (tpl[caAssigned[i]] !== null) {
|
|
353
|
+
throw ctx.E("crmf/bad-cert-template", "CertTemplate " + caAssigned[i] + " MUST be omitted — it is CA-assigned or deprecated (RFC 4211 §5)");
|
|
354
|
+
}
|
|
355
|
+
}
|
|
338
356
|
return {
|
|
339
357
|
certReqId: m.fields.certReqId.value,
|
|
340
358
|
certReqIdHex: m.fields.certReqId.node.content.toString("hex"),
|
|
341
|
-
certTemplate:
|
|
359
|
+
certTemplate: tpl,
|
|
342
360
|
controls: m.fields.controls.present ? m.fields.controls.value.result : null,
|
|
343
361
|
certReqBytes: m.node.bytes, // the exact CertRequest TLV the POP signature covers (§4.1)
|
|
344
362
|
};
|
|
@@ -466,20 +484,36 @@ function pemDecode(text, label) { return pkix.pemDecode(text, label || null, Pem
|
|
|
466
484
|
// ocsp-request: an OCSPRequest's tbsRequest never leads with the INTEGER-then-
|
|
467
485
|
// SEQUENCE pair (its children[0] is a context [0] version or a requestList).
|
|
468
486
|
function matches(root) {
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
487
|
+
var k = pkix.rootSequenceChildren(root, 1);
|
|
488
|
+
if (!k) return false;
|
|
489
|
+
var msg = k[0];
|
|
490
|
+
if (!(msg.children && schema.isUniversal(msg, TAGS.SEQUENCE) && msg.children.length >= 1)) return false;
|
|
472
491
|
var certReq = msg.children[0];
|
|
473
|
-
if (!certReq.children
|
|
492
|
+
if (!(certReq.children && schema.isUniversal(certReq, TAGS.SEQUENCE) && certReq.children.length >= 2)) return false;
|
|
474
493
|
var id = certReq.children[0], tpl = certReq.children[1];
|
|
475
|
-
return
|
|
476
|
-
|
|
494
|
+
return schema.isUniversal(id, TAGS.INTEGER) &&
|
|
495
|
+
schema.isUniversal(tpl, TAGS.SEQUENCE) && !!tpl.children;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// Validate a bare, already-decoded CertReqMessages / CertTemplate node — for a
|
|
499
|
+
// composer that carries these structures inside its own envelope (an RFC 9810
|
|
500
|
+
// CMP PKIBody's ir/cr/kur/krr/ccr arms; a RevDetails.certDetails). NS-bound to
|
|
501
|
+
// crmf so an interior failure throws CrmfError with a crmf/* code, never the
|
|
502
|
+
// composer's class wearing a foreign code. Same contract as the CMS walkers.
|
|
503
|
+
// opts.allowSigningAlg relaxes the request-template signingAlg-omission rule for
|
|
504
|
+
// the CMP cross-certification (ccr) arm only (RFC 9810 Appendix D.6).
|
|
505
|
+
function walkCertReqMessages(node, opts) {
|
|
506
|
+
var ctx = (opts && opts.allowSigningAlg) ? Object.assign({}, NS, { allowSigningAlg: true }) : NS;
|
|
507
|
+
return schema.walk(CERT_REQ_MESSAGES, node, ctx).result;
|
|
477
508
|
}
|
|
509
|
+
function walkCertTemplate(node) { return schema.walk(CERT_TEMPLATE, node, NS).result; }
|
|
478
510
|
|
|
479
511
|
module.exports = {
|
|
480
512
|
parse: parse,
|
|
481
513
|
pemDecode: pemDecode,
|
|
482
514
|
matches: matches,
|
|
515
|
+
walkCertReqMessages: walkCertReqMessages,
|
|
516
|
+
walkCertTemplate: walkCertTemplate,
|
|
483
517
|
// The top schema, exposed (not on the curated pki.schema.crmf surface) so a
|
|
484
518
|
// round-trip test can prove schema.encode(schema, v) -> decode -> parse recovers
|
|
485
519
|
// v — the invariant that structurally validates the IMPLICIT/EXPLICIT tag handling.
|
package/lib/schema-csr.js
CHANGED
|
@@ -171,10 +171,10 @@ function matches(root) {
|
|
|
171
171
|
if (!cri) return false;
|
|
172
172
|
var k = cri.children;
|
|
173
173
|
if (k.length !== 4) return false;
|
|
174
|
-
return
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
174
|
+
return schema.isUniversal(k[0], TAGS.INTEGER) &&
|
|
175
|
+
schema.isUniversal(k[1], TAGS.SEQUENCE) &&
|
|
176
|
+
schema.isUniversal(k[2], TAGS.SEQUENCE) &&
|
|
177
|
+
schema.isContext(k[3], 0);
|
|
178
178
|
}
|
|
179
179
|
|
|
180
180
|
module.exports = {
|
package/lib/schema-engine.js
CHANGED
|
@@ -160,10 +160,10 @@ function optional(name, schema, opts) {
|
|
|
160
160
|
var match = opts.whenAny
|
|
161
161
|
? function () { return true; }
|
|
162
162
|
: opts.whenUniversal
|
|
163
|
-
? function (n) { return n
|
|
163
|
+
? function (n) { return isUniversalOneOf(n, opts.whenUniversal); }
|
|
164
164
|
: opts.tags
|
|
165
|
-
? function (n) { return n
|
|
166
|
-
: function (n) { return n
|
|
165
|
+
? function (n) { return isContextOneOf(n, opts.tags); }
|
|
166
|
+
: function (n) { return isContext(n, opts.tag); };
|
|
167
167
|
return { fkind: "optional", name: name, schema: schema, tag: opts.tag, match: match,
|
|
168
168
|
explicit: !!opts.explicit, emptyCode: opts.emptyCode, hasDefault: ("default" in opts), def: opts.default };
|
|
169
169
|
}
|
|
@@ -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,96 @@ 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
|
+
|
|
590
|
+
// X.690 §11.2.2 — a NamedBitList BIT STRING (KeyUsage, PKIFailureInfo, and the
|
|
591
|
+
// like) MUST drop every trailing zero bit under DER, giving one canonical
|
|
592
|
+
// encoding per value: an empty value carries 0 unused bits; a non-empty value's
|
|
593
|
+
// last content octet is non-zero AND its lowest USED bit (at position
|
|
594
|
+
// `unusedBits`) is set. `fail(message)` throws the caller's typed domain error,
|
|
595
|
+
// so the reject code stays per-format. Centralized so no format re-derives — and
|
|
596
|
+
// silently diverges on — the minimal-encoding rule (a DER-canonicalization
|
|
597
|
+
// bypass class); see the named-bitlist-minimal-encoding-inlined gate.
|
|
598
|
+
// ASN.1 node tag predicates — the per-node `tagClass`/`tagNumber` test every
|
|
599
|
+
// format's matches() detector (and the engine's own optional/choice walkers)
|
|
600
|
+
// would otherwise re-inline. Centralized so a detector composes one predicate
|
|
601
|
+
// instead of hand-rolling the comparison (and drifting); see the
|
|
602
|
+
// detector-reinlines-root-tag-guard gate.
|
|
603
|
+
function isUniversal(node, tagNumber) {
|
|
604
|
+
return !!node && node.tagClass === "universal" && node.tagNumber === tagNumber;
|
|
605
|
+
}
|
|
606
|
+
function isContext(node, tagNumber) {
|
|
607
|
+
return !!node && node.tagClass === "context" && node.tagNumber === tagNumber;
|
|
608
|
+
}
|
|
609
|
+
function isUniversalOneOf(node, tagNumbers) {
|
|
610
|
+
return !!node && node.tagClass === "universal" && tagNumbers.indexOf(node.tagNumber) !== -1;
|
|
611
|
+
}
|
|
612
|
+
function isContextOneOf(node, tagNumbers) {
|
|
613
|
+
return !!node && node.tagClass === "context" && tagNumbers.indexOf(node.tagNumber) !== -1;
|
|
614
|
+
}
|
|
615
|
+
function isContextInRange(node, min, max) {
|
|
616
|
+
return !!node && node.tagClass === "context" && node.tagNumber >= min && node.tagNumber <= max;
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
function assertMinimalNamedBits(unusedBits, bytes, fail) {
|
|
620
|
+
if (bytes.length === 0) {
|
|
621
|
+
if (unusedBits !== 0) fail("an empty NamedBitList must encode with 0 unused bits (X.690 §11.2.2)");
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
var last = bytes[bytes.length - 1];
|
|
625
|
+
if (last === 0) fail("a NamedBitList must not carry a trailing all-zero octet (X.690 §11.2.2)");
|
|
626
|
+
if (((last >> unusedBits) & 1) !== 1) fail("a NamedBitList must drop all trailing zero bits (X.690 §11.2.2)");
|
|
627
|
+
}
|
|
628
|
+
|
|
526
629
|
module.exports = {
|
|
527
630
|
// structural
|
|
528
631
|
seq: seq, field: field, optional: optional, explicit: explicit, trailing: trailing,
|
|
@@ -533,5 +636,8 @@ module.exports = {
|
|
|
533
636
|
implicitNull: implicitNull, implicitInteger: implicitInteger,
|
|
534
637
|
any: any, decode: decode, time: time,
|
|
535
638
|
// engine
|
|
536
|
-
walk: walk, encode: encode,
|
|
639
|
+
walk: walk, encode: encode, embeddedDer: embeddedDer, assertMinimalNamedBits: assertMinimalNamedBits,
|
|
640
|
+
// node tag predicates
|
|
641
|
+
isUniversal: isUniversal, isContext: isContext,
|
|
642
|
+
isUniversalOneOf: isUniversalOneOf, isContextOneOf: isContextOneOf, isContextInRange: isContextInRange,
|
|
537
643
|
};
|
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
|
});
|
|
@@ -491,11 +489,10 @@ function pemDecode(text, label) { return pkix.pemDecode(text, label || "OCSP RES
|
|
|
491
489
|
// unconditionally exclusive.
|
|
492
490
|
function matchesResponse(root) {
|
|
493
491
|
var T = asn1.TAGS;
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
if (k
|
|
497
|
-
if (
|
|
498
|
-
if (k.length === 2 && !(k[1].tagClass === "context" && k[1].tagNumber === 0 && k[1].children)) return false;
|
|
492
|
+
var k = pkix.rootSequenceChildren(root, 1, 2);
|
|
493
|
+
if (!k) return false;
|
|
494
|
+
if (!schema.isUniversal(k[0], T.ENUMERATED)) return false;
|
|
495
|
+
if (k.length === 2 && !(schema.isContext(k[1], 0) && k[1].children)) return false;
|
|
499
496
|
return true;
|
|
500
497
|
}
|
|
501
498
|
|
|
@@ -505,11 +502,10 @@ function matchesResponse(root) {
|
|
|
505
502
|
// signed-envelope trio), while an OCSPRequest is 1-2 — so it is excluded by arity.
|
|
506
503
|
function matchesRequest(root) {
|
|
507
504
|
var T = asn1.TAGS;
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
if (k.
|
|
511
|
-
if (
|
|
512
|
-
if (k.length === 2 && !(k[1].tagClass === "context" && k[1].tagNumber === 0 && k[1].children)) return false;
|
|
505
|
+
var k = pkix.rootSequenceChildren(root, 1, 2);
|
|
506
|
+
if (!k) return false;
|
|
507
|
+
if (!(schema.isUniversal(k[0], T.SEQUENCE) && k[0].children)) return false;
|
|
508
|
+
if (k.length === 2 && !(schema.isContext(k[1], 0) && k[1].children)) return false;
|
|
513
509
|
return true;
|
|
514
510
|
}
|
|
515
511
|
|