@blamejs/pki 0.1.16 → 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.
@@ -93,39 +93,49 @@ function _assertArity(schema, kids, ctx) {
93
93
  // the OID / INTEGER / string reads). `decode(fn)` hands the whole node to fn,
94
94
  // which owns its try/catch and code mapping. `any()` yields the node itself.
95
95
 
96
- function oidLeaf() { return { kind: "leaf", read: asn1.read.oid }; }
97
- function integerLeaf() { return { kind: "leaf", read: asn1.read.integer }; }
98
- function boolean() { return { kind: "leaf", read: asn1.read.boolean }; }
99
- function octetString() { return { kind: "leaf", read: asn1.read.octetString }; }
100
- function bitString() { return { kind: "leaf", read: function (n) { var b = asn1.read.bitString(n); return { unusedBits: b.unusedBits, bytes: b.bytes }; } }; }
96
+ // Leaves carry BOTH directions: `read(node) -> value` and `write(value) -> DER TLV`,
97
+ // so a single leaf definition drives decode (walk) and encode (build). A [tag]
98
+ // IMPLICIT leaf's write emits the context-tagged TLV directly, mirroring its read.
99
+ function _implicitIntContent(v) { return asn1.decode(asn1.build.integer(v)).content; }
100
+ function oidLeaf() { return { kind: "leaf", read: asn1.read.oid, write: function (v) { return asn1.build.oid(v); } }; }
101
+ function integerLeaf() { return { kind: "leaf", read: asn1.read.integer, write: function (v) { return asn1.build.integer(v); } }; }
102
+ function boolean() { return { kind: "leaf", read: asn1.read.boolean, write: function (v) { return asn1.build.boolean(v); } }; }
103
+ function octetString() { return { kind: "leaf", read: asn1.read.octetString, write: function (v) { return asn1.build.octetString(v); } }; }
104
+ function bitString() { return { kind: "leaf", read: function (n) { var b = asn1.read.bitString(n); return { unusedBits: b.unusedBits, bytes: b.bytes }; }, write: function (v) { return asn1.build.bitString(v.bytes, v.unusedBits); } }; }
101
105
  // A [tag] IMPLICIT BIT STRING leaf (context-class primitive) — the primitive-leaf
102
106
  // counterpart to implicitSetOf, for e.g. the PKCS#8 publicKey [1] (RFC 5958 §2).
103
- function implicitBitString(tag) { return { kind: "leaf", read: function (n) { var b = asn1.read.bitStringImplicit(n, tag); return { unusedBits: b.unusedBits, bytes: b.bytes }; } }; }
107
+ function implicitBitString(tag) { return { kind: "leaf", read: function (n) { var b = asn1.read.bitStringImplicit(n, tag); return { unusedBits: b.unusedBits, bytes: b.bytes }; }, write: function (v) { return asn1.build.contextPrimitive(tag, Buffer.concat([Buffer.from([v.unusedBits]), v.bytes])); } }; }
104
108
  // A [tag] IMPLICIT OCTET STRING leaf (context-class primitive) — the sibling of
105
109
  // implicitBitString, for e.g. the CMS SignerIdentifier subjectKeyIdentifier [0]
106
110
  // (RFC 5652 §5.3). Yields the raw content bytes.
107
- function implicitOctetString(tag) { return { kind: "leaf", read: function (n) { return asn1.read.octetStringImplicit(n, tag); } }; }
111
+ function implicitOctetString(tag) { return { kind: "leaf", read: function (n) { return asn1.read.octetStringImplicit(n, tag); }, write: function (v) { return asn1.build.contextPrimitive(tag, v); } }; }
108
112
  // A [tag] IMPLICIT NULL leaf (context-class primitive, empty content) — the sibling
109
113
  // of implicitBitString/implicitOctetString, for e.g. the OCSP CertStatus good [0] /
110
114
  // unknown [2] arms (RFC 6960 §4.2.1). Yields null; rejects a constructed or
111
115
  // non-empty [tag] node fail-closed.
112
- function implicitNull(tag) { return { kind: "leaf", read: function (n) { return asn1.read.nullImplicit(n, tag); } }; }
116
+ function implicitNull(tag) { return { kind: "leaf", read: function (n) { return asn1.read.nullImplicit(n, tag); }, write: function () { return asn1.build.contextPrimitive(tag, Buffer.alloc(0)); } }; }
113
117
  // A [tag] IMPLICIT INTEGER leaf (context-class primitive) — the integer sibling of
114
118
  // implicitBitString, for the RFC 3161 Accuracy millis [0] / micros [1] fields
115
119
  // (context-tagged primitive INTEGERs). Yields a BigInt; a constructed or wrong-tag
116
120
  // node fails asn1/* at the codec.
117
- function implicitInteger(tag) { return { kind: "leaf", read: function (n) { return asn1.read.integerImplicit(n, tag); } }; }
121
+ function implicitInteger(tag) { return { kind: "leaf", read: function (n) { return asn1.read.integerImplicit(n, tag); }, write: function (v) { return asn1.build.contextPrimitive(tag, _implicitIntContent(v)); } }; }
118
122
  function any() { return { kind: "any" }; }
119
- function decode(fn) { return { kind: "decode", fn: fn }; }
123
+ // decode(fn, write?): a custom decode leaf; `write` is its optional paired encoder
124
+ // (value -> DER TLV) so the same leaf drives both directions.
125
+ function decode(fn, write) { return { kind: "decode", fn: fn, write: write }; }
120
126
 
121
127
  // time(ns): a UTCTime / GeneralizedTime value, asserting the tag before the
122
- // codec reads it (mirrors _parseValidityTime's x509/bad-time guard).
128
+ // codec reads it (mirrors _parseValidityTime's x509/bad-time guard). Encodes as
129
+ // UTCTime for years 1950..2049 and GeneralizedTime otherwise (RFC 5280 §4.1.2.5).
123
130
  function time(ns) {
124
131
  return decode(function (n, ctx) {
125
132
  if (n.tagClass !== "universal" || (n.tagNumber !== TAGS.UTC_TIME && n.tagNumber !== TAGS.GENERALIZED_TIME)) {
126
133
  _fail(ctx, ns.prefix + "/bad-time", "time must be UTCTime or GeneralizedTime");
127
134
  }
128
135
  return asn1.read.time(n);
136
+ }, function (date) {
137
+ var y = date.getUTCFullYear();
138
+ return (y >= 1950 && y < 2050) ? asn1.build.utcTime(date) : asn1.build.generalizedTime(date);
129
139
  });
130
140
  }
131
141
 
@@ -136,6 +146,10 @@ function optional(name, schema, opts) {
136
146
  opts = opts || {};
137
147
  // How the optional field is recognized at its position:
138
148
  // - default: a context [tag] (the certificate version [0] shape).
149
+ // - tags: a context tag in the set — an OPTIONAL field whose type is
150
+ // itself a CHOICE of several context alternatives, e.g. the CRMF CertReqMsg
151
+ // popo ProofOfPossession ([0]..[3]) sitting before the universal-SEQUENCE
152
+ // regInfo (RFC 4211 §3), where ANY of the CHOICE's tags marks it present.
139
153
  // - whenUniversal: the next element iff its UNIVERSAL tag is in the set —
140
154
  // the CRL TBSCertList shape (bare INTEGER version, Time nextUpdate,
141
155
  // SEQUENCE revokedCertificates), disambiguated by tag, not a context [n].
@@ -147,7 +161,9 @@ function optional(name, schema, opts) {
147
161
  ? function () { return true; }
148
162
  : opts.whenUniversal
149
163
  ? function (n) { return n.tagClass === "universal" && opts.whenUniversal.indexOf(n.tagNumber) !== -1; }
150
- : function (n) { return n.tagClass === "context" && n.tagNumber === opts.tag; };
164
+ : opts.tags
165
+ ? function (n) { return n.tagClass === "context" && opts.tags.indexOf(n.tagNumber) !== -1; }
166
+ : function (n) { return n.tagClass === "context" && n.tagNumber === opts.tag; };
151
167
  return { fkind: "optional", name: name, schema: schema, tag: opts.tag, match: match,
152
168
  explicit: !!opts.explicit, emptyCode: opts.emptyCode, hasDefault: ("default" in opts), def: opts.default };
153
169
  }
@@ -184,12 +200,14 @@ function choice(alts, opts) {
184
200
  function seqOf(item, opts) {
185
201
  opts = opts || {};
186
202
  return { kind: "repeat", item: item, assert: opts.assert || "sequence", code: opts.code, what: opts.what,
187
- min: opts.min, unique: opts.unique, dupCode: opts.dupCode, build: opts.build };
203
+ min: opts.min, max: opts.max, maxCode: opts.maxCode,
204
+ unique: opts.unique, dupCode: opts.dupCode, build: opts.build };
188
205
  }
189
206
  function setOf(item, opts) {
190
207
  opts = opts || {};
191
208
  return { kind: "repeat", item: item, assert: opts.assert || "set", derSetOrder: true, code: opts.code, what: opts.what,
192
- min: opts.min, unique: opts.unique, dupCode: opts.dupCode, build: opts.build };
209
+ min: opts.min, max: opts.max, maxCode: opts.maxCode,
210
+ unique: opts.unique, dupCode: opts.dupCode, build: opts.build };
193
211
  }
194
212
  function setOfUnique(item, keyFn, opts) {
195
213
  return setOf(item, Object.assign({ unique: keyFn }, opts || {}));
@@ -200,7 +218,8 @@ function setOfUnique(item, keyFn, opts) {
200
218
  function implicitSetOf(tag, item, opts) {
201
219
  opts = opts || {};
202
220
  return { kind: "repeat", item: item, assert: "implicit", implicitTag: tag, derSetOrder: true, code: opts.code, what: opts.what,
203
- min: opts.min, unique: opts.unique, dupCode: opts.dupCode, build: opts.build };
221
+ min: opts.min, max: opts.max, maxCode: opts.maxCode,
222
+ unique: opts.unique, dupCode: opts.dupCode, build: opts.build };
204
223
  }
205
224
  // [tag] IMPLICIT SEQUENCE OF item — the context tag REPLACES the universal SEQUENCE
206
225
  // tag, so the node is a context-class constructed [tag] whose direct children are the
@@ -209,7 +228,8 @@ function implicitSetOf(tag, item, opts) {
209
228
  function implicitSeqOf(tag, item, opts) {
210
229
  opts = opts || {};
211
230
  return { kind: "repeat", item: item, assert: "implicit", implicitTag: tag, code: opts.code, what: opts.what,
212
- min: opts.min, unique: opts.unique, dupCode: opts.dupCode, build: opts.build };
231
+ min: opts.min, max: opts.max, maxCode: opts.maxCode,
232
+ unique: opts.unique, dupCode: opts.dupCode, build: opts.build };
213
233
  }
214
234
 
215
235
  // ---- the walk engine -------------------------------------------------
@@ -218,7 +238,7 @@ function implicitSeqOf(tag, item, opts) {
218
238
  * @primitive pki.schema.engine.walk
219
239
  * @signature pki.schema.engine.walk(schema, node, ctx) -> value
220
240
  * @since 0.1.7
221
- * @status experimental
241
+ * @status stable
222
242
  * @spec X.690, X.680
223
243
  * @related pki.asn1.decode, pki.schema.x509.parse
224
244
  *
@@ -288,6 +308,12 @@ function _walkRepeat(schema, node, ctx) {
288
308
  if (schema.min != null && kids.length < schema.min) {
289
309
  _fail(ctx, schema.code, (schema.what || "value") + " must contain at least " + schema.min + " element(s)");
290
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
+ }
291
317
  // DER (X.690 §11.6) — the components of a SET OF appear in ascending order, the
292
318
  // encodings compared as octet strings. SEQUENCE OF is order-preserving and is
293
319
  // exempt (no derSetOrder). Compare full TLV byte strings, the same canonical
@@ -389,6 +415,178 @@ function _consumeTrailing(fld, kids, start, fields, ctx) {
389
415
  }
390
416
  }
391
417
 
418
+ // ---- the encode engine (the inverse of walk) -------------------------
419
+ // encode(schema, value) is the constructor direction of the SAME schema walk
420
+ // decodes: it interprets the schema against a plain structural value and emits
421
+ // canonical DER. A single schema definition therefore drives both directions, so
422
+ // context-tag (EXPLICIT/IMPLICIT) handling cannot diverge between them — the class
423
+ // of bug where a decoder reads a [tag] IMPLICIT field but an ad-hoc encoder emits
424
+ // the wrong tag is structurally retired. Encode errors are authoring faults (a bad
425
+ // value / an un-encodable schema), thrown as plain Errors, not parse verdicts.
426
+
427
+ function _encFail(message) { throw new Error("schema.encode: " + message); }
428
+
429
+ /**
430
+ * @primitive pki.schema.engine.encode
431
+ * @signature pki.schema.engine.encode(schema, value, ctx) -> Buffer
432
+ * @since 0.1.17
433
+ * @status experimental
434
+ * @spec X.690, X.680
435
+ * @related pki.schema.engine.walk
436
+ *
437
+ * Encode a structural value to canonical DER by interpreting the SAME schema
438
+ * `walk` decodes — the constructor direction. `value` mirrors the schema: a `seq`
439
+ * takes `{ fieldName: value }`, a leaf its natural JS value (an OID string, a
440
+ * BigInt, a `{ unusedBits, bytes }` BIT STRING, a `Date`), a `repeat` an array, a
441
+ * `choice` `{ arm, value }`. EXPLICIT wrappers and IMPLICIT `[tag]` retagging are
442
+ * applied by the engine, so `walk(schema, decode(encode(schema, v)))` round-trips.
443
+ *
444
+ * @example
445
+ * var S = pki.schema.engine;
446
+ * var der = S.encode(S.seq([S.field("n", S.integerLeaf())]), { n: 42n });
447
+ */
448
+ function encode(schema, value, ctx) {
449
+ switch (schema.kind) {
450
+ case "leaf": return schema.write(value);
451
+ case "any": return Buffer.isBuffer(value) ? value : value.bytes;
452
+ case "decode":
453
+ if (typeof schema.write !== "function") _encFail("this decode leaf has no paired encoder (decode(fn, write))");
454
+ return schema.write(value, ctx);
455
+ case "seq": return _encodeSeq(schema, value, ctx);
456
+ case "explicit": return asn1.build.explicit(schema.tag, encode(schema.schema, value, ctx));
457
+ case "repeat": return _encodeRepeat(schema, value, ctx);
458
+ case "choice": return _encodeChoice(schema, value, ctx);
459
+ default: _encFail("cannot encode schema kind " + JSON.stringify(schema.kind));
460
+ }
461
+ return undefined;
462
+ }
463
+
464
+ function _wrapConstructed(schema, parts) {
465
+ if (schema.implicitTag != null) return asn1.build.contextConstructed(schema.implicitTag, Buffer.concat(parts));
466
+ var mode = schema.assert || "sequence";
467
+ if (mode === "set") return asn1.build.set(parts);
468
+ // "constructed" is a DECODE-time leniency (accept any constructed shape); the
469
+ // canonical encoding is a universal SEQUENCE, same as "sequence".
470
+ if (mode === "sequence" || mode === "constructed") return asn1.build.sequence(parts);
471
+ _encFail("cannot encode a seq/repeat with assert " + JSON.stringify(mode));
472
+ return undefined;
473
+ }
474
+
475
+ function _present(v) { return v !== undefined && v !== null; }
476
+
477
+ function _encodeSeq(schema, value, ctx) {
478
+ value = value || {};
479
+ var parts = [];
480
+ for (var f = 0; f < schema.fields.length; f++) {
481
+ var fld = schema.fields[f];
482
+ if (fld.fkind === "required") {
483
+ parts.push(encode(fld.schema, value[fld.name], ctx));
484
+ } else if (fld.fkind === "optional") {
485
+ if (_present(value[fld.name])) {
486
+ var enc = encode(fld.schema, value[fld.name], ctx);
487
+ parts.push(fld.explicit ? asn1.build.explicit(fld.tag, enc) : enc);
488
+ }
489
+ } else if (fld.fkind === "trailing") {
490
+ // Emit present members in strictly-increasing tag order (the walk's rule).
491
+ var members = fld.members.slice().sort(function (a, b) { return a.tag - b.tag; });
492
+ for (var i = 0; i < members.length; i++) {
493
+ var mm = members[i];
494
+ if (_present(value[mm.name])) {
495
+ var menc = encode(mm.schema, value[mm.name], ctx);
496
+ parts.push(mm.explicit ? asn1.build.explicit(mm.tag, menc) : menc);
497
+ }
498
+ }
499
+ }
500
+ }
501
+ return _wrapConstructed(schema, parts);
502
+ }
503
+
504
+ function _encodeRepeat(schema, items, ctx) {
505
+ if (!Array.isArray(items)) _encFail("a repeat value must be an array");
506
+ // Enforce the SAME constraints walk enforces, so encode never emits DER its own
507
+ // decoder rejects (the walk(decode(encode)) invariant): the SIZE minimum, and
508
+ // semantic uniqueness (the key computed exactly as walk does, off the round-tripped
509
+ // item — a duplicate is an authoring fault, not a parse verdict).
510
+ if (schema.min != null && items.length < schema.min) {
511
+ _encFail("this repeat requires at least " + schema.min + " element(s) but got " + items.length);
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
+ }
516
+ var parts = items.map(function (it) { return encode(schema.item, it, ctx); });
517
+ if (schema.unique) {
518
+ var seen = new Set();
519
+ parts.forEach(function (tlv) {
520
+ var item = { node: asn1.decode(tlv) };
521
+ item.value = walk(schema.item, item.node, ctx);
522
+ var key = schema.unique(item);
523
+ if (seen.has(key)) _encFail("this repeat requires unique elements but got a duplicate key: " + key);
524
+ seen.add(key);
525
+ });
526
+ }
527
+ if (schema.derSetOrder) parts = parts.slice().sort(Buffer.compare);
528
+ if (schema.implicitTag != null) return asn1.build.contextConstructed(schema.implicitTag, Buffer.concat(parts));
529
+ return schema.assert === "set" ? asn1.build.set(parts) : asn1.build.sequence(parts);
530
+ }
531
+
532
+ function _encodeChoice(schema, value, ctx) {
533
+ if (!value || typeof value.arm !== "number" || value.arm < 0 || value.arm >= schema.alts.length) {
534
+ _encFail("a choice value must be { arm: <index>, value: <arm value> }");
535
+ }
536
+ return encode(schema.alts[value.arm].schema, value.value, ctx);
537
+ }
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
+
392
590
  module.exports = {
393
591
  // structural
394
592
  seq: seq, field: field, optional: optional, explicit: explicit, trailing: trailing,
@@ -399,5 +597,5 @@ module.exports = {
399
597
  implicitNull: implicitNull, implicitInteger: implicitInteger,
400
598
  any: any, decode: decode, time: time,
401
599
  // engine
402
- walk: walk,
600
+ walk: walk, encode: encode, embeddedDer: embeddedDer,
403
601
  };
@@ -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.walk(BASIC_OCSP_RESPONSE, inner, ctx).result,
301
+ basicResponse: schema.embeddedDer(BASIC_OCSP_RESPONSE, raw, ctx,
302
+ { code: "ocsp/bad-der", what: "BasicOCSPResponse" }).result,
305
303
  };
306
304
  },
307
305
  });
@@ -425,7 +423,7 @@ var OCSP_REQUEST = schema.seq([
425
423
  * @primitive pki.schema.ocsp.parseRequest
426
424
  * @signature pki.schema.ocsp.parseRequest(input) -> ocspRequest
427
425
  * @since 0.1.11
428
- * @status experimental
426
+ * @status stable
429
427
  * @spec RFC 6960
430
428
  * @related pki.schema.ocsp.parseResponse, pki.schema.parse
431
429
  *
@@ -447,7 +445,7 @@ var parseRequest = pkix.makeParser({ pemLabel: "OCSP REQUEST", PemError: PemErro
447
445
  * @primitive pki.schema.ocsp.parseResponse
448
446
  * @signature pki.schema.ocsp.parseResponse(input) -> ocspResponse
449
447
  * @since 0.1.11
450
- * @status experimental
448
+ * @status stable
451
449
  * @spec RFC 6960
452
450
  * @related pki.schema.ocsp.parseRequest, pki.schema.parse
453
451
  *
@@ -471,7 +469,7 @@ var parseResponse = pkix.makeParser({ pemLabel: "OCSP RESPONSE", PemError: PemEr
471
469
  * @primitive pki.schema.ocsp.pemDecode
472
470
  * @signature pki.schema.ocsp.pemDecode(text, label?) -> Buffer
473
471
  * @since 0.1.11
474
- * @status experimental
472
+ * @status stable
475
473
  * @spec RFC 7468, RFC 6960
476
474
  * @related pki.schema.ocsp.parseResponse
477
475
  *