@blamejs/pki 0.1.15 → 0.1.17
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 +29 -0
- package/README.md +3 -1
- package/index.js +4 -0
- package/lib/asn1-der.js +13 -0
- package/lib/constants.js +4 -0
- package/lib/framework-error.js +14 -0
- package/lib/oid.js +17 -4
- package/lib/path-validate.js +1393 -0
- package/lib/schema-all.js +16 -1
- package/lib/schema-cms.js +12 -3
- package/lib/schema-crl.js +2 -2
- package/lib/schema-crmf.js +487 -0
- package/lib/schema-csr.js +3 -3
- package/lib/schema-engine.js +148 -14
- package/lib/schema-ocsp.js +3 -3
- package/lib/schema-pkcs8.js +4 -4
- package/lib/schema-pkix.js +374 -37
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/lib/schema-engine.js
CHANGED
|
@@ -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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
function
|
|
100
|
-
function
|
|
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
|
-
|
|
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
|
-
:
|
|
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
|
}
|
|
@@ -218,7 +234,7 @@ function implicitSeqOf(tag, item, opts) {
|
|
|
218
234
|
* @primitive pki.schema.engine.walk
|
|
219
235
|
* @signature pki.schema.engine.walk(schema, node, ctx) -> value
|
|
220
236
|
* @since 0.1.7
|
|
221
|
-
* @status
|
|
237
|
+
* @status stable
|
|
222
238
|
* @spec X.690, X.680
|
|
223
239
|
* @related pki.asn1.decode, pki.schema.x509.parse
|
|
224
240
|
*
|
|
@@ -389,6 +405,124 @@ function _consumeTrailing(fld, kids, start, fields, ctx) {
|
|
|
389
405
|
}
|
|
390
406
|
}
|
|
391
407
|
|
|
408
|
+
// ---- the encode engine (the inverse of walk) -------------------------
|
|
409
|
+
// encode(schema, value) is the constructor direction of the SAME schema walk
|
|
410
|
+
// decodes: it interprets the schema against a plain structural value and emits
|
|
411
|
+
// canonical DER. A single schema definition therefore drives both directions, so
|
|
412
|
+
// context-tag (EXPLICIT/IMPLICIT) handling cannot diverge between them — the class
|
|
413
|
+
// of bug where a decoder reads a [tag] IMPLICIT field but an ad-hoc encoder emits
|
|
414
|
+
// the wrong tag is structurally retired. Encode errors are authoring faults (a bad
|
|
415
|
+
// value / an un-encodable schema), thrown as plain Errors, not parse verdicts.
|
|
416
|
+
|
|
417
|
+
function _encFail(message) { throw new Error("schema.encode: " + message); }
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* @primitive pki.schema.engine.encode
|
|
421
|
+
* @signature pki.schema.engine.encode(schema, value, ctx) -> Buffer
|
|
422
|
+
* @since 0.1.17
|
|
423
|
+
* @status experimental
|
|
424
|
+
* @spec X.690, X.680
|
|
425
|
+
* @related pki.schema.engine.walk
|
|
426
|
+
*
|
|
427
|
+
* Encode a structural value to canonical DER by interpreting the SAME schema
|
|
428
|
+
* `walk` decodes — the constructor direction. `value` mirrors the schema: a `seq`
|
|
429
|
+
* takes `{ fieldName: value }`, a leaf its natural JS value (an OID string, a
|
|
430
|
+
* BigInt, a `{ unusedBits, bytes }` BIT STRING, a `Date`), a `repeat` an array, a
|
|
431
|
+
* `choice` `{ arm, value }`. EXPLICIT wrappers and IMPLICIT `[tag]` retagging are
|
|
432
|
+
* applied by the engine, so `walk(schema, decode(encode(schema, v)))` round-trips.
|
|
433
|
+
*
|
|
434
|
+
* @example
|
|
435
|
+
* var S = pki.schema.engine;
|
|
436
|
+
* var der = S.encode(S.seq([S.field("n", S.integerLeaf())]), { n: 42n });
|
|
437
|
+
*/
|
|
438
|
+
function encode(schema, value, ctx) {
|
|
439
|
+
switch (schema.kind) {
|
|
440
|
+
case "leaf": return schema.write(value);
|
|
441
|
+
case "any": return Buffer.isBuffer(value) ? value : value.bytes;
|
|
442
|
+
case "decode":
|
|
443
|
+
if (typeof schema.write !== "function") _encFail("this decode leaf has no paired encoder (decode(fn, write))");
|
|
444
|
+
return schema.write(value, ctx);
|
|
445
|
+
case "seq": return _encodeSeq(schema, value, ctx);
|
|
446
|
+
case "explicit": return asn1.build.explicit(schema.tag, encode(schema.schema, value, ctx));
|
|
447
|
+
case "repeat": return _encodeRepeat(schema, value, ctx);
|
|
448
|
+
case "choice": return _encodeChoice(schema, value, ctx);
|
|
449
|
+
default: _encFail("cannot encode schema kind " + JSON.stringify(schema.kind));
|
|
450
|
+
}
|
|
451
|
+
return undefined;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function _wrapConstructed(schema, parts) {
|
|
455
|
+
if (schema.implicitTag != null) return asn1.build.contextConstructed(schema.implicitTag, Buffer.concat(parts));
|
|
456
|
+
var mode = schema.assert || "sequence";
|
|
457
|
+
if (mode === "set") return asn1.build.set(parts);
|
|
458
|
+
// "constructed" is a DECODE-time leniency (accept any constructed shape); the
|
|
459
|
+
// canonical encoding is a universal SEQUENCE, same as "sequence".
|
|
460
|
+
if (mode === "sequence" || mode === "constructed") return asn1.build.sequence(parts);
|
|
461
|
+
_encFail("cannot encode a seq/repeat with assert " + JSON.stringify(mode));
|
|
462
|
+
return undefined;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function _present(v) { return v !== undefined && v !== null; }
|
|
466
|
+
|
|
467
|
+
function _encodeSeq(schema, value, ctx) {
|
|
468
|
+
value = value || {};
|
|
469
|
+
var parts = [];
|
|
470
|
+
for (var f = 0; f < schema.fields.length; f++) {
|
|
471
|
+
var fld = schema.fields[f];
|
|
472
|
+
if (fld.fkind === "required") {
|
|
473
|
+
parts.push(encode(fld.schema, value[fld.name], ctx));
|
|
474
|
+
} else if (fld.fkind === "optional") {
|
|
475
|
+
if (_present(value[fld.name])) {
|
|
476
|
+
var enc = encode(fld.schema, value[fld.name], ctx);
|
|
477
|
+
parts.push(fld.explicit ? asn1.build.explicit(fld.tag, enc) : enc);
|
|
478
|
+
}
|
|
479
|
+
} else if (fld.fkind === "trailing") {
|
|
480
|
+
// Emit present members in strictly-increasing tag order (the walk's rule).
|
|
481
|
+
var members = fld.members.slice().sort(function (a, b) { return a.tag - b.tag; });
|
|
482
|
+
for (var i = 0; i < members.length; i++) {
|
|
483
|
+
var mm = members[i];
|
|
484
|
+
if (_present(value[mm.name])) {
|
|
485
|
+
var menc = encode(mm.schema, value[mm.name], ctx);
|
|
486
|
+
parts.push(mm.explicit ? asn1.build.explicit(mm.tag, menc) : menc);
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
return _wrapConstructed(schema, parts);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function _encodeRepeat(schema, items, ctx) {
|
|
495
|
+
if (!Array.isArray(items)) _encFail("a repeat value must be an array");
|
|
496
|
+
// Enforce the SAME constraints walk enforces, so encode never emits DER its own
|
|
497
|
+
// decoder rejects (the walk(decode(encode)) invariant): the SIZE minimum, and
|
|
498
|
+
// semantic uniqueness (the key computed exactly as walk does, off the round-tripped
|
|
499
|
+
// item — a duplicate is an authoring fault, not a parse verdict).
|
|
500
|
+
if (schema.min != null && items.length < schema.min) {
|
|
501
|
+
_encFail("this repeat requires at least " + schema.min + " element(s) but got " + items.length);
|
|
502
|
+
}
|
|
503
|
+
var parts = items.map(function (it) { return encode(schema.item, it, ctx); });
|
|
504
|
+
if (schema.unique) {
|
|
505
|
+
var seen = new Set();
|
|
506
|
+
parts.forEach(function (tlv) {
|
|
507
|
+
var item = { node: asn1.decode(tlv) };
|
|
508
|
+
item.value = walk(schema.item, item.node, ctx);
|
|
509
|
+
var key = schema.unique(item);
|
|
510
|
+
if (seen.has(key)) _encFail("this repeat requires unique elements but got a duplicate key: " + key);
|
|
511
|
+
seen.add(key);
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
if (schema.derSetOrder) parts = parts.slice().sort(Buffer.compare);
|
|
515
|
+
if (schema.implicitTag != null) return asn1.build.contextConstructed(schema.implicitTag, Buffer.concat(parts));
|
|
516
|
+
return schema.assert === "set" ? asn1.build.set(parts) : asn1.build.sequence(parts);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
function _encodeChoice(schema, value, ctx) {
|
|
520
|
+
if (!value || typeof value.arm !== "number" || value.arm < 0 || value.arm >= schema.alts.length) {
|
|
521
|
+
_encFail("a choice value must be { arm: <index>, value: <arm value> }");
|
|
522
|
+
}
|
|
523
|
+
return encode(schema.alts[value.arm].schema, value.value, ctx);
|
|
524
|
+
}
|
|
525
|
+
|
|
392
526
|
module.exports = {
|
|
393
527
|
// structural
|
|
394
528
|
seq: seq, field: field, optional: optional, explicit: explicit, trailing: trailing,
|
|
@@ -399,5 +533,5 @@ module.exports = {
|
|
|
399
533
|
implicitNull: implicitNull, implicitInteger: implicitInteger,
|
|
400
534
|
any: any, decode: decode, time: time,
|
|
401
535
|
// engine
|
|
402
|
-
walk: walk,
|
|
536
|
+
walk: walk, encode: encode,
|
|
403
537
|
};
|
package/lib/schema-ocsp.js
CHANGED
|
@@ -425,7 +425,7 @@ var OCSP_REQUEST = schema.seq([
|
|
|
425
425
|
* @primitive pki.schema.ocsp.parseRequest
|
|
426
426
|
* @signature pki.schema.ocsp.parseRequest(input) -> ocspRequest
|
|
427
427
|
* @since 0.1.11
|
|
428
|
-
* @status
|
|
428
|
+
* @status stable
|
|
429
429
|
* @spec RFC 6960
|
|
430
430
|
* @related pki.schema.ocsp.parseResponse, pki.schema.parse
|
|
431
431
|
*
|
|
@@ -447,7 +447,7 @@ var parseRequest = pkix.makeParser({ pemLabel: "OCSP REQUEST", PemError: PemErro
|
|
|
447
447
|
* @primitive pki.schema.ocsp.parseResponse
|
|
448
448
|
* @signature pki.schema.ocsp.parseResponse(input) -> ocspResponse
|
|
449
449
|
* @since 0.1.11
|
|
450
|
-
* @status
|
|
450
|
+
* @status stable
|
|
451
451
|
* @spec RFC 6960
|
|
452
452
|
* @related pki.schema.ocsp.parseRequest, pki.schema.parse
|
|
453
453
|
*
|
|
@@ -471,7 +471,7 @@ var parseResponse = pkix.makeParser({ pemLabel: "OCSP RESPONSE", PemError: PemEr
|
|
|
471
471
|
* @primitive pki.schema.ocsp.pemDecode
|
|
472
472
|
* @signature pki.schema.ocsp.pemDecode(text, label?) -> Buffer
|
|
473
473
|
* @since 0.1.11
|
|
474
|
-
* @status
|
|
474
|
+
* @status stable
|
|
475
475
|
* @spec RFC 7468, RFC 6960
|
|
476
476
|
* @related pki.schema.ocsp.parseResponse
|
|
477
477
|
*
|
package/lib/schema-pkcs8.js
CHANGED
|
@@ -102,7 +102,7 @@ var ENCRYPTED_PRIVATE_KEY_INFO = schema.seq([
|
|
|
102
102
|
* @primitive pki.schema.pkcs8.parse
|
|
103
103
|
* @signature pki.schema.pkcs8.parse(input) -> privateKey
|
|
104
104
|
* @since 0.1.9
|
|
105
|
-
* @status
|
|
105
|
+
* @status stable
|
|
106
106
|
* @spec RFC 5208, RFC 5958
|
|
107
107
|
* @related pki.schema.parse, pki.schema.x509.parse
|
|
108
108
|
*
|
|
@@ -124,7 +124,7 @@ var parse = pkix.makeParser({ pemLabel: "PRIVATE KEY", PemError: PemError, Error
|
|
|
124
124
|
* @primitive pki.schema.pkcs8.parseEncrypted
|
|
125
125
|
* @signature pki.schema.pkcs8.parseEncrypted(input) -> encrypted
|
|
126
126
|
* @since 0.1.9
|
|
127
|
-
* @status
|
|
127
|
+
* @status stable
|
|
128
128
|
* @spec RFC 5958, RFC 5208
|
|
129
129
|
* @related pki.schema.pkcs8.parse
|
|
130
130
|
*
|
|
@@ -143,7 +143,7 @@ var parseEncrypted = pkix.makeParser({ pemLabel: "ENCRYPTED PRIVATE KEY", PemErr
|
|
|
143
143
|
* @primitive pki.schema.pkcs8.pemDecode
|
|
144
144
|
* @signature pki.schema.pkcs8.pemDecode(text, label?) -> Buffer
|
|
145
145
|
* @since 0.1.9
|
|
146
|
-
* @status
|
|
146
|
+
* @status stable
|
|
147
147
|
* @spec RFC 7468, RFC 5958
|
|
148
148
|
* @related pki.schema.pkcs8.parse
|
|
149
149
|
*
|
|
@@ -160,7 +160,7 @@ function pemDecode(text, label) { return pkix.pemDecode(text, label || "PRIVATE
|
|
|
160
160
|
* @primitive pki.schema.pkcs8.pemEncode
|
|
161
161
|
* @signature pki.schema.pkcs8.pemEncode(der, label?) -> string
|
|
162
162
|
* @since 0.1.9
|
|
163
|
-
* @status
|
|
163
|
+
* @status stable
|
|
164
164
|
* @spec RFC 7468
|
|
165
165
|
* @related pki.schema.pkcs8.pemDecode
|
|
166
166
|
*
|