@blamejs/pki 0.1.22 → 0.1.23
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 +35 -0
- package/README.md +16 -6
- package/index.js +16 -12
- package/lib/asn1-der.js +126 -68
- package/lib/constants.js +46 -21
- package/lib/ct.js +54 -39
- package/lib/framework-error.js +57 -34
- package/lib/oid.js +110 -59
- package/lib/path-validate.js +302 -135
- package/lib/schema-all.js +49 -41
- package/lib/schema-attrcert.js +101 -64
- package/lib/schema-cmp.js +152 -131
- package/lib/schema-cms.js +621 -163
- package/lib/schema-crl.js +43 -21
- package/lib/schema-crmf.js +136 -79
- package/lib/schema-csr.js +63 -14
- package/lib/schema-engine.js +92 -43
- package/lib/schema-ocsp.js +101 -55
- package/lib/schema-pkcs12.js +80 -64
- package/lib/schema-pkcs8.js +12 -12
- package/lib/schema-pkix.js +283 -100
- package/lib/schema-smime.js +39 -39
- package/lib/schema-tsp.js +108 -59
- package/lib/schema-x509.js +36 -25
- package/lib/webcrypto.js +156 -22
- package/package.json +3 -2
- package/sbom.cdx.json +6 -6
package/lib/schema-csr.js
CHANGED
|
@@ -21,11 +21,11 @@
|
|
|
21
21
|
* A CSR is self-signed only in the sense that the requester proves possession of
|
|
22
22
|
* the private key; unlike a certificate or CRL there is no inner signature
|
|
23
23
|
* algorithm to agree with, the subject MAY be empty, and the attribute set has
|
|
24
|
-
* no uniqueness constraint
|
|
24
|
+
* no uniqueness constraint -- the parser deliberately omits those three
|
|
25
25
|
* certificate/CRL guards.
|
|
26
26
|
*
|
|
27
27
|
* @card
|
|
28
|
-
* Parse DER / PEM PKCS#10 CSRs into structured, validated fields
|
|
28
|
+
* Parse DER / PEM PKCS#10 CSRs into structured, validated fields -- subject DN,
|
|
29
29
|
* public key, requested attributes, signature, fail-closed.
|
|
30
30
|
*/
|
|
31
31
|
|
|
@@ -45,23 +45,59 @@ var NS = pkix.makeNS("csr", CsrError, oid);
|
|
|
45
45
|
var NAME = pkix.name(NS);
|
|
46
46
|
var SPKI = pkix.spki(NS);
|
|
47
47
|
|
|
48
|
-
// CertificationRequestInfo version ::= INTEGER { v1(0) } (RFC 2986
|
|
49
|
-
// mandatory INTEGER whose ONLY legal value is 0
|
|
48
|
+
// CertificationRequestInfo version ::= INTEGER { v1(0) } (RFC 2986 sec. 4.1). A BARE
|
|
49
|
+
// mandatory INTEGER whose ONLY legal value is 0 -- the inverse of the cert/CRL
|
|
50
50
|
// readers, which reject 0 as the DER-forbidden default. Surface v1 as 1. A
|
|
51
51
|
// cert-shaped [0] EXPLICIT wrapper at this position makes asn1.read.integer throw
|
|
52
52
|
// asn1/* (fail-closed).
|
|
53
53
|
var CSR_VERSION = pkix.versionReader(NS, { "0": 1 });
|
|
54
54
|
|
|
55
|
-
// Attribute ::= SEQUENCE { type OID, values SET OF ANY }
|
|
55
|
+
// Attribute ::= SEQUENCE { type OID, values SET OF ANY } -- the shared pkix factory
|
|
56
56
|
// under the csr NS (values raw DER, SET SIZE(1..MAX), no uniqueness). The same
|
|
57
57
|
// factory serves the PKCS#8 private-key attributes.
|
|
58
58
|
var ATTRIBUTE = pkix.attribute(NS);
|
|
59
59
|
|
|
60
|
+
// The two RECOGNIZED PKCS#9 request attributes get their value syntax enforced
|
|
61
|
+
// (RFC 2985 sec. 5.4.1 challengePassword / sec. 5.4.2 extensionRequest): both
|
|
62
|
+
// are SINGLE VALUE TRUE, extensionRequest carries one Extensions value
|
|
63
|
+
// (RFC 5280 sec. 4.1, SIZE(1..MAX)) and challengePassword one DirectoryString
|
|
64
|
+
// of 1..255 characters. A recognized type with a malformed, multi-valued, or
|
|
65
|
+
// wrong-syntax value fails closed (csr/bad-attribute-value) -- a CA pipeline
|
|
66
|
+
// reading requested extensions off csr.attributes must never receive garbage
|
|
67
|
+
// under the extensionRequest OID. An UNRECOGNIZED attribute type stays opaque
|
|
68
|
+
// raw DER and never fails the parse, and duplicate attribute TYPES stay legal
|
|
69
|
+
// (RFC 2986 puts no uniqueness on the attributes SET). Registry rows keyed by
|
|
70
|
+
// the stable dotted OID, not a switch; values are still surfaced raw.
|
|
71
|
+
var EXTENSIONS = pkix.extensions(NS);
|
|
72
|
+
var DIRECTORY_STRING_TAGS = [
|
|
73
|
+
asn1.TAGS.UTF8_STRING, asn1.TAGS.PRINTABLE_STRING, asn1.TAGS.TELETEX_STRING,
|
|
74
|
+
asn1.TAGS.UNIVERSAL_STRING, asn1.TAGS.BMP_STRING,
|
|
75
|
+
];
|
|
76
|
+
var RECOGNIZED_ATTRIBUTE_VALUE = {};
|
|
77
|
+
RECOGNIZED_ATTRIBUTE_VALUE[oid.byName("extensionRequest")] = function (node, ctx) {
|
|
78
|
+
try { schema.walk(EXTENSIONS, node, ctx); }
|
|
79
|
+
catch (e) {
|
|
80
|
+
throw ctx.E("csr/bad-attribute-value",
|
|
81
|
+
"extensionRequest value must be a well-formed Extensions SEQUENCE (RFC 2985 sec. 5.4.2): " + ((e && e.message) || String(e)), e);
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
RECOGNIZED_ATTRIBUTE_VALUE[oid.byName("challengePassword")] = function (node, ctx) {
|
|
85
|
+
if (node.tagClass !== "universal" || DIRECTORY_STRING_TAGS.indexOf(node.tagNumber) === -1) {
|
|
86
|
+
throw ctx.E("csr/bad-attribute-value", "challengePassword must be a DirectoryString (RFC 2985 sec. 5.4.1)");
|
|
87
|
+
}
|
|
88
|
+
var s;
|
|
89
|
+
try { s = asn1.read.string(node); }
|
|
90
|
+
catch (e) { throw ctx.E("csr/bad-attribute-value", "challengePassword must be a well-formed DirectoryString (RFC 2985 sec. 5.4.1)", e); }
|
|
91
|
+
if (s.length < 1 || s.length > 255) {
|
|
92
|
+
throw ctx.E("csr/bad-attribute-value", "challengePassword must be 1..255 characters (RFC 2985 sec. 5.4.1)");
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
|
|
60
96
|
// CertificationRequestInfo ::= SEQUENCE { version, subject Name,
|
|
61
97
|
// subjectPKInfo SubjectPublicKeyInfo, attributes [0] IMPLICIT SET OF Attribute }.
|
|
62
98
|
// attributes is a REQUIRED field (a CRI omitting [0] is a missing-required-field
|
|
63
99
|
// fault, not silently accepted) modeled as an IMPLICIT [0] SET OF with min:0 (an
|
|
64
|
-
// empty attributes SET is legal). subject MAY be empty
|
|
100
|
+
// empty attributes SET is legal). subject MAY be empty -- no non-empty-DN guard.
|
|
65
101
|
var CERTIFICATION_REQUEST_INFO = schema.seq([
|
|
66
102
|
schema.field("version", CSR_VERSION),
|
|
67
103
|
schema.field("subject", NAME),
|
|
@@ -69,21 +105,34 @@ var CERTIFICATION_REQUEST_INFO = schema.seq([
|
|
|
69
105
|
schema.field("attributes", schema.implicitSetOf(0, ATTRIBUTE, { min: 0, code: "csr/bad-attributes", what: "attributes" })),
|
|
70
106
|
], {
|
|
71
107
|
assert: "sequence", code: "csr/bad-cri", what: "certificationRequestInfo",
|
|
72
|
-
build: function (m) {
|
|
108
|
+
build: function (m, ctx) {
|
|
109
|
+
var attributes = m.fields.attributes.value.items.map(function (it) {
|
|
110
|
+
var a = it.value.result;
|
|
111
|
+
var checkValue = RECOGNIZED_ATTRIBUTE_VALUE[a.type];
|
|
112
|
+
if (checkValue) {
|
|
113
|
+
var valueItems = it.value.fields.values.value.items;
|
|
114
|
+
// Both recognized PKCS#9 types are SINGLE VALUE TRUE (RFC 2985 sec. 5.4).
|
|
115
|
+
if (valueItems.length !== 1) {
|
|
116
|
+
throw ctx.E("csr/bad-attribute-value", (a.name || a.type) + " is a SINGLE VALUE attribute (RFC 2985 sec. 5.4)");
|
|
117
|
+
}
|
|
118
|
+
checkValue(valueItems[0].node, ctx);
|
|
119
|
+
}
|
|
120
|
+
return a;
|
|
121
|
+
});
|
|
73
122
|
return {
|
|
74
123
|
version: m.fields.version.value,
|
|
75
|
-
subject: m.fields.subject.value.result, // Name is a seqOf
|
|
124
|
+
subject: m.fields.subject.value.result, // Name is a seqOf -> field.value is the match; .result is the {rdns, dn} build
|
|
76
125
|
subjectPublicKeyInfo: m.fields.subjectPKInfo.value.result,
|
|
77
|
-
attributes:
|
|
126
|
+
attributes: attributes,
|
|
78
127
|
};
|
|
79
128
|
},
|
|
80
129
|
});
|
|
81
130
|
|
|
82
131
|
// CertificationRequest ::= SEQUENCE { certificationRequestInfo, signatureAlgorithm,
|
|
83
|
-
// signature BIT STRING }
|
|
132
|
+
// signature BIT STRING } -- the shared SIGNED envelope. DIVERGENCE from the
|
|
84
133
|
// cert/CRL builders: OMIT the outer-vs-inner signatureAlgorithm agreement check
|
|
85
134
|
// (the CRI has no inner signature AlgorithmIdentifier) and the non-empty-subject
|
|
86
|
-
// guard (a CSR subject MAY be empty). The omission is structural
|
|
135
|
+
// guard (a CSR subject MAY be empty). The omission is structural -- this build
|
|
87
136
|
// simply never references the agreement bytes.
|
|
88
137
|
var CERTIFICATION_REQUEST = pkix.signedEnvelope(NS, CERTIFICATION_REQUEST_INFO, {
|
|
89
138
|
code: "csr/not-a-certification-request", what: "CertificationRequest",
|
|
@@ -120,8 +169,8 @@ var CERTIFICATION_REQUEST = pkix.signedEnvelope(NS, CERTIFICATION_REQUEST_INFO,
|
|
|
120
169
|
*
|
|
121
170
|
* @example
|
|
122
171
|
* var csr = pki.schema.csr.parse(der);
|
|
123
|
-
* csr.subject.dn; //
|
|
124
|
-
* csr.attributes[0].type; //
|
|
172
|
+
* csr.subject.dn; // -> "CN=req.example"
|
|
173
|
+
* csr.attributes[0].type; // -> "1.2.840.113549.1.9.14"
|
|
125
174
|
*/
|
|
126
175
|
var parse = pkix.makeParser({ pemLabel: "CERTIFICATE REQUEST", PemError: PemError, ErrorClass: CsrError, prefix: "csr", what: "certification request", topSchema: CERTIFICATION_REQUEST, ns: NS });
|
|
127
176
|
|
|
@@ -158,7 +207,7 @@ function pemDecode(text, label) { return pkix.pemDecode(text, label || "CERTIFIC
|
|
|
158
207
|
function pemEncode(der, label) { return pkix.pemEncode(der, label || "CERTIFICATE REQUEST", PemError); }
|
|
159
208
|
|
|
160
209
|
// A CertificationRequest shares the outer SEQUENCE-of-3 shape with a certificate
|
|
161
|
-
// and a CRL; a CSR is distinguished by its CertificationRequestInfo
|
|
210
|
+
// and a CRL; a CSR is distinguished by its CertificationRequestInfo -- a SEQUENCE
|
|
162
211
|
// of EXACTLY four children {version INTEGER, subject SEQUENCE, subjectPKInfo
|
|
163
212
|
// SEQUENCE, attributes [0]}. The mandatory trailing context-[0] attributes
|
|
164
213
|
// element is what a cert (whose tbs leads with [0] EXPLICIT version or an INTEGER
|
package/lib/schema-engine.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* @order 60
|
|
9
9
|
*
|
|
10
10
|
* @intro
|
|
11
|
-
* L2 of the ASN.1 stack
|
|
11
|
+
* L2 of the ASN.1 stack -- a declarative structure-schema engine. A schema is
|
|
12
12
|
* plain data (`{kind, ...}` descriptors built by the combinators here) and
|
|
13
13
|
* `walk(schema, node, ctx)` interprets it against a decoded DER node. The
|
|
14
14
|
* engine is where every cross-cutting structural rule lives ONCE: the shape
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* base the certificate parser (and, later, CRL / CMS) composes.
|
|
22
22
|
*
|
|
23
23
|
* @card
|
|
24
|
-
* Declarative ASN.1 structure schemas + one walk engine
|
|
24
|
+
* Declarative ASN.1 structure schemas + one walk engine -- the shared base the
|
|
25
25
|
* certificate / CRL / CMS parsers compose instead of hand-writing.
|
|
26
26
|
*/
|
|
27
27
|
|
|
@@ -85,6 +85,9 @@ function _assertArity(schema, kids, ctx) {
|
|
|
85
85
|
if (a.min != null && kids.length < a.min) {
|
|
86
86
|
_fail(ctx, schema.code, (schema.what || "value") + " must have at least " + a.min + " elements");
|
|
87
87
|
}
|
|
88
|
+
if (a.max != null && kids.length > a.max) {
|
|
89
|
+
_fail(ctx, schema.code, (schema.what || "value") + " must have at most " + a.max + " elements");
|
|
90
|
+
}
|
|
88
91
|
}
|
|
89
92
|
|
|
90
93
|
// ---- leaf / value combinators ---------------------------------------
|
|
@@ -102,23 +105,26 @@ function integerLeaf() { return { kind: "leaf", read: asn1.read.integer, writ
|
|
|
102
105
|
function boolean() { return { kind: "leaf", read: asn1.read.boolean, write: function (v) { return asn1.build.boolean(v); } }; }
|
|
103
106
|
function octetString() { return { kind: "leaf", read: asn1.read.octetString, write: function (v) { return asn1.build.octetString(v); } }; }
|
|
104
107
|
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); } }; }
|
|
105
|
-
// A [tag] IMPLICIT BIT STRING leaf (context-class primitive)
|
|
106
|
-
// counterpart to implicitSetOf, for e.g. the PKCS#8 publicKey [1] (RFC 5958
|
|
108
|
+
// A [tag] IMPLICIT BIT STRING leaf (context-class primitive) -- the primitive-leaf
|
|
109
|
+
// counterpart to implicitSetOf, for e.g. the PKCS#8 publicKey [1] (RFC 5958 sec. 2).
|
|
107
110
|
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])); } }; }
|
|
108
|
-
// A [tag] IMPLICIT OCTET STRING leaf (context-class primitive)
|
|
111
|
+
// A [tag] IMPLICIT OCTET STRING leaf (context-class primitive) -- the sibling of
|
|
109
112
|
// implicitBitString, for e.g. the CMS SignerIdentifier subjectKeyIdentifier [0]
|
|
110
|
-
// (RFC 5652
|
|
113
|
+
// (RFC 5652 sec. 5.3). Yields the raw content bytes.
|
|
111
114
|
function implicitOctetString(tag) { return { kind: "leaf", read: function (n) { return asn1.read.octetStringImplicit(n, tag); }, write: function (v) { return asn1.build.contextPrimitive(tag, v); } }; }
|
|
112
|
-
// A [tag] IMPLICIT NULL leaf (context-class primitive, empty content)
|
|
115
|
+
// A [tag] IMPLICIT NULL leaf (context-class primitive, empty content) -- the sibling
|
|
113
116
|
// of implicitBitString/implicitOctetString, for e.g. the OCSP CertStatus good [0] /
|
|
114
|
-
// unknown [2] arms (RFC 6960
|
|
117
|
+
// unknown [2] arms (RFC 6960 sec. 4.2.1). Yields null; rejects a constructed or
|
|
115
118
|
// non-empty [tag] node fail-closed.
|
|
116
119
|
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)); } }; }
|
|
117
|
-
// A [tag] IMPLICIT INTEGER leaf (context-class primitive)
|
|
120
|
+
// A [tag] IMPLICIT INTEGER leaf (context-class primitive) -- the integer sibling of
|
|
118
121
|
// implicitBitString, for the RFC 3161 Accuracy millis [0] / micros [1] fields
|
|
119
122
|
// (context-tagged primitive INTEGERs). Yields a BigInt; a constructed or wrong-tag
|
|
120
123
|
// node fails asn1/* at the codec.
|
|
121
124
|
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)); } }; }
|
|
125
|
+
// implicitBoolean, for the RFC 5280 sec. 5.2.5 IssuingDistributionPoint scope flags
|
|
126
|
+
// (context-tagged primitive BOOLEANs). DER value rules enforced (0x00/0xFF only).
|
|
127
|
+
function implicitBoolean(tag) { return { kind: "leaf", read: function (n) { return asn1.read.booleanImplicit(n, tag); }, write: function (v) { return asn1.build.contextPrimitive(tag, Buffer.from([v ? 0xff : 0x00])); } }; }
|
|
122
128
|
function any() { return { kind: "any" }; }
|
|
123
129
|
// decode(fn, write?): a custom decode leaf; `write` is its optional paired encoder
|
|
124
130
|
// (value -> DER TLV) so the same leaf drives both directions.
|
|
@@ -126,7 +132,7 @@ function decode(fn, write) { return { kind: "decode", fn: fn, write: write }; }
|
|
|
126
132
|
|
|
127
133
|
// time(ns): a UTCTime / GeneralizedTime value, asserting the tag before the
|
|
128
134
|
// codec reads it (mirrors _parseValidityTime's x509/bad-time guard). Encodes as
|
|
129
|
-
// UTCTime for years 1950..2049 and GeneralizedTime otherwise (RFC 5280
|
|
135
|
+
// UTCTime for years 1950..2049 and GeneralizedTime otherwise (RFC 5280 sec. 4.1.2.5).
|
|
130
136
|
function time(ns) {
|
|
131
137
|
return decode(function (n, ctx) {
|
|
132
138
|
if (n.tagClass !== "universal" || (n.tagNumber !== TAGS.UTC_TIME && n.tagNumber !== TAGS.GENERALIZED_TIME)) {
|
|
@@ -146,14 +152,14 @@ function optional(name, schema, opts) {
|
|
|
146
152
|
opts = opts || {};
|
|
147
153
|
// How the optional field is recognized at its position:
|
|
148
154
|
// - default: a context [tag] (the certificate version [0] shape).
|
|
149
|
-
// - tags: a context tag in the set
|
|
155
|
+
// - tags: a context tag in the set -- an OPTIONAL field whose type is
|
|
150
156
|
// itself a CHOICE of several context alternatives, e.g. the CRMF CertReqMsg
|
|
151
157
|
// popo ProofOfPossession ([0]..[3]) sitting before the universal-SEQUENCE
|
|
152
|
-
// regInfo (RFC 4211
|
|
153
|
-
// - whenUniversal: the next element iff its UNIVERSAL tag is in the set
|
|
158
|
+
// regInfo (RFC 4211 sec. 3), where ANY of the CHOICE's tags marks it present.
|
|
159
|
+
// - whenUniversal: the next element iff its UNIVERSAL tag is in the set --
|
|
154
160
|
// the CRL TBSCertList shape (bare INTEGER version, Time nextUpdate,
|
|
155
161
|
// SEQUENCE revokedCertificates), disambiguated by tag, not a context [n].
|
|
156
|
-
// - whenAny: the next element whatever its tag
|
|
162
|
+
// - whenAny: the next element whatever its tag -- an OPTIONAL ANY like
|
|
157
163
|
// AlgorithmIdentifier.parameters.
|
|
158
164
|
// The recognizer lets _walkSeq CONSUME the element so a closed sequence can
|
|
159
165
|
// reject whatever is left over (without it, a trailing ANY looks unconsumed).
|
|
@@ -164,8 +170,14 @@ function optional(name, schema, opts) {
|
|
|
164
170
|
: opts.tags
|
|
165
171
|
? function (n) { return isContextOneOf(n, opts.tags); }
|
|
166
172
|
: function (n) { return isContext(n, opts.tag); };
|
|
173
|
+
// default/defaultCode: `default` binds the DEFAULT value when the field is
|
|
174
|
+
// absent. `defaultCode` opts the field into the X.690 sec. 11.5 walk-side reject
|
|
175
|
+
// (DER: a component equal to its DEFAULT shall not be included) -- a present
|
|
176
|
+
// value equal to `default` (strict equality; Buffers by bytes) fails with
|
|
177
|
+
// that code, so each format keeps its own reject-code family.
|
|
167
178
|
return { fkind: "optional", name: name, schema: schema, tag: opts.tag, match: match,
|
|
168
|
-
explicit: !!opts.explicit, emptyCode: opts.emptyCode, hasDefault: ("default" in opts), def: opts.default
|
|
179
|
+
explicit: !!opts.explicit, emptyCode: opts.emptyCode, hasDefault: ("default" in opts), def: opts.default,
|
|
180
|
+
defaultCode: opts.defaultCode };
|
|
169
181
|
}
|
|
170
182
|
// trailing: the [minTag..maxTag] optional context fields, each at most once in
|
|
171
183
|
// strictly-increasing tag order (the tbs issuerUniqueID[1]/subjectUniqueID[2]/
|
|
@@ -212,16 +224,16 @@ function setOf(item, opts) {
|
|
|
212
224
|
function setOfUnique(item, keyFn, opts) {
|
|
213
225
|
return setOf(item, Object.assign({ unique: keyFn }, opts || {}));
|
|
214
226
|
}
|
|
215
|
-
// [tag] IMPLICIT SET OF item
|
|
227
|
+
// [tag] IMPLICIT SET OF item -- the context tag REPLACES the universal SET tag,
|
|
216
228
|
// so the node is a context-class constructed [tag] whose direct children are the
|
|
217
|
-
// items (RFC 2986
|
|
229
|
+
// items (RFC 2986 sec. 4.1 CSR attributes). No inner SET, no EXPLICIT unwrap.
|
|
218
230
|
function implicitSetOf(tag, item, opts) {
|
|
219
231
|
opts = opts || {};
|
|
220
232
|
return { kind: "repeat", item: item, assert: "implicit", implicitTag: tag, derSetOrder: true, code: opts.code, what: opts.what,
|
|
221
233
|
min: opts.min, max: opts.max, maxCode: opts.maxCode,
|
|
222
234
|
unique: opts.unique, dupCode: opts.dupCode, build: opts.build };
|
|
223
235
|
}
|
|
224
|
-
// [tag] IMPLICIT SEQUENCE OF item
|
|
236
|
+
// [tag] IMPLICIT SEQUENCE OF item -- the context tag REPLACES the universal SEQUENCE
|
|
225
237
|
// tag, so the node is a context-class constructed [tag] whose direct children are the
|
|
226
238
|
// items. Order-preserving: the SEQUENCE-OF sibling of implicitSetOf WITHOUT the SET
|
|
227
239
|
// ascending-DER-order rule (RFC 3161 extensions [1] IMPLICIT Extensions).
|
|
@@ -245,15 +257,17 @@ function implicitSeqOf(tag, item, opts) {
|
|
|
245
257
|
* Interpret a declarative schema against a decoded DER node, enforcing the
|
|
246
258
|
* schema's structural rules (shape assertion, arity, optional / context-tagged
|
|
247
259
|
* fields in increasing tag order, SET-OF uniqueness) and returning the built
|
|
248
|
-
* value
|
|
260
|
+
* value -- or the match tree (`{ node, fields | items }`, with the build output
|
|
249
261
|
* on `.result`) for a structure with no build fn. `ctx = { E, prefix, oid }`
|
|
250
262
|
* supplies the typed-error constructor, the error-code family prefix, and the
|
|
251
263
|
* OID registry a build fn resolves names through.
|
|
252
264
|
*
|
|
253
|
-
* The schema is assembled from the combinators this module exports
|
|
265
|
+
* The schema is assembled from the combinators this module exports -- structural
|
|
254
266
|
* (`seq` / `field` / `optional` / `explicit` / `trailing` / `seqOf` / `setOf` /
|
|
255
|
-
* `setOfUnique` / `
|
|
256
|
-
* `
|
|
267
|
+
* `setOfUnique` / `implicitSeqOf` / `implicitSetOf` / `choice`) and value
|
|
268
|
+
* (`oidLeaf` / `integerLeaf` / `boolean` / `octetString` / `bitString` /
|
|
269
|
+
* `implicitBitString` / `implicitOctetString` / `implicitNull` /
|
|
270
|
+
* `implicitInteger` / `any` / `decode` / `time`).
|
|
257
271
|
*
|
|
258
272
|
* @example
|
|
259
273
|
* var S = pki.schema.engine;
|
|
@@ -314,7 +328,7 @@ function _walkRepeat(schema, node, ctx) {
|
|
|
314
328
|
if (schema.max != null && kids.length > schema.max) {
|
|
315
329
|
_fail(ctx, schema.maxCode || schema.code, (schema.what || "value") + " exceeds the element cap " + schema.max);
|
|
316
330
|
}
|
|
317
|
-
// DER (X.690
|
|
331
|
+
// DER (X.690 sec. 11.6) -- the components of a SET OF appear in ascending order, the
|
|
318
332
|
// encodings compared as octet strings. SEQUENCE OF is order-preserving and is
|
|
319
333
|
// exempt (no derSetOrder). Compare full TLV byte strings, the same canonical
|
|
320
334
|
// order build.set emits, so a decode round-trips a build. Equal-adjacent
|
|
@@ -323,7 +337,7 @@ function _walkRepeat(schema, node, ctx) {
|
|
|
323
337
|
if (schema.derSetOrder) {
|
|
324
338
|
for (var s = 1; s < kids.length; s++) {
|
|
325
339
|
if (Buffer.compare(kids[s - 1].bytes, kids[s].bytes) > 0) {
|
|
326
|
-
_fail(ctx, schema.code, (schema.what || "value") + " components must be in ascending DER order (X.690
|
|
340
|
+
_fail(ctx, schema.code, (schema.what || "value") + " components must be in ascending DER order (X.690 sec. 11.6)");
|
|
327
341
|
}
|
|
328
342
|
}
|
|
329
343
|
}
|
|
@@ -345,6 +359,15 @@ function _walkRepeat(schema, node, ctx) {
|
|
|
345
359
|
return match;
|
|
346
360
|
}
|
|
347
361
|
|
|
362
|
+
// The walked-value <-> DEFAULT comparison behind the defaultCode reject: strict
|
|
363
|
+
// equality for the leaf value domains (BigInt, number, string, boolean), byte
|
|
364
|
+
// equality for Buffers. A format opting in declares its `default` in the same
|
|
365
|
+
// domain its leaf's read yields.
|
|
366
|
+
function _equalsDefault(value, def) {
|
|
367
|
+
if (Buffer.isBuffer(value) && Buffer.isBuffer(def)) return value.equals(def);
|
|
368
|
+
return value === def;
|
|
369
|
+
}
|
|
370
|
+
|
|
348
371
|
function _walkSeq(schema, node, ctx) {
|
|
349
372
|
var kids = _assertShape(schema, node, ctx);
|
|
350
373
|
_assertArity(schema, kids, ctx);
|
|
@@ -365,18 +388,31 @@ function _walkSeq(schema, node, ctx) {
|
|
|
365
388
|
if (fld.explicit) {
|
|
366
389
|
inner = _explicitInner(next, fld.tag, ctx, fld.emptyCode || schema.code);
|
|
367
390
|
}
|
|
368
|
-
|
|
391
|
+
var val = walk(fld.schema, inner, ctx);
|
|
392
|
+
// X.690 sec. 11.5 -- DER: a component equal to its DEFAULT value shall not
|
|
393
|
+
// be included in the encoding. Opt-in via defaultCode so the reject
|
|
394
|
+
// stays in the format's own error-code family.
|
|
395
|
+
if (fld.defaultCode != null && fld.hasDefault && _equalsDefault(val, fld.def)) {
|
|
396
|
+
_fail(ctx, fld.defaultCode, "field " + JSON.stringify(fld.name) + " explicitly encodes its DEFAULT value (X.690 sec. 11.5)");
|
|
397
|
+
}
|
|
398
|
+
fields[fld.name] = { node: next, present: true, value: val };
|
|
369
399
|
} else {
|
|
370
400
|
fields[fld.name] = { present: false, value: fld.hasDefault ? fld.def : undefined };
|
|
371
401
|
}
|
|
372
402
|
} else if (fld.fkind === "trailing") {
|
|
373
403
|
_consumeTrailing(fld, kids, idx, fields, ctx);
|
|
374
404
|
idx = kids.length;
|
|
405
|
+
} else {
|
|
406
|
+
// A malformed field descriptor is the same authoring fault as an
|
|
407
|
+
// unknown schema.kind (walk's default case): fail loud instead of
|
|
408
|
+
// silently skipping the descriptor -- a skipped field never binds and
|
|
409
|
+
// under-specifies the sequence.
|
|
410
|
+
_fail(ctx, (ctx.prefix || "schema") + "/bad-schema", "unknown field kind " + JSON.stringify(fld && fld.fkind));
|
|
375
411
|
}
|
|
376
412
|
}
|
|
377
413
|
|
|
378
414
|
// Every child must be consumed by a field. A leftover element (no trailing
|
|
379
|
-
// field ran to absorb it) is malformed
|
|
415
|
+
// field ran to absorb it) is malformed -- dropping it silently would let a
|
|
380
416
|
// closed sequence of optional fields accept a duplicate/extra element.
|
|
381
417
|
if (idx < kids.length) {
|
|
382
418
|
_fail(ctx, schema.code, (schema.what || "value") + " has an unexpected element after its last field");
|
|
@@ -393,7 +429,7 @@ function _consumeTrailing(fld, kids, start, fields, ctx) {
|
|
|
393
429
|
for (var m = 0; m < fld.members.length; m++) byTag[fld.members[m].tag] = fld.members[m];
|
|
394
430
|
// The monotonic-order sentinel must start below the lowest accepted tag.
|
|
395
431
|
// Context tag numbers are non-negative, so -1 is below any member tag when
|
|
396
|
-
// minTag is absent
|
|
432
|
+
// minTag is absent -- otherwise a trailing block whose first member is [0]
|
|
397
433
|
// would reject that field (0 <= last==0) as repeated/out-of-order.
|
|
398
434
|
var last = fld.minTag != null ? fld.minTag - 1 : -1;
|
|
399
435
|
for (var i = start; i < kids.length; i++) {
|
|
@@ -419,7 +455,7 @@ function _consumeTrailing(fld, kids, start, fields, ctx) {
|
|
|
419
455
|
// encode(schema, value) is the constructor direction of the SAME schema walk
|
|
420
456
|
// decodes: it interprets the schema against a plain structural value and emits
|
|
421
457
|
// canonical DER. A single schema definition therefore drives both directions, so
|
|
422
|
-
// context-tag (EXPLICIT/IMPLICIT) handling cannot diverge between them
|
|
458
|
+
// context-tag (EXPLICIT/IMPLICIT) handling cannot diverge between them -- the class
|
|
423
459
|
// of bug where a decoder reads a [tag] IMPLICIT field but an ad-hoc encoder emits
|
|
424
460
|
// the wrong tag is structurally retired. Encode errors are authoring faults (a bad
|
|
425
461
|
// value / an un-encodable schema), thrown as plain Errors, not parse verdicts.
|
|
@@ -435,7 +471,7 @@ function _encFail(message) { throw new Error("schema.encode: " + message); }
|
|
|
435
471
|
* @related pki.schema.engine.walk
|
|
436
472
|
*
|
|
437
473
|
* Encode a structural value to canonical DER by interpreting the SAME schema
|
|
438
|
-
* `walk` decodes
|
|
474
|
+
* `walk` decodes -- the constructor direction. `value` mirrors the schema: a `seq`
|
|
439
475
|
* takes `{ fieldName: value }`, a leaf its natural JS value (an OID string, a
|
|
440
476
|
* BigInt, a `{ unusedBits, bytes }` BIT STRING, a `Date`), a `repeat` an array, a
|
|
441
477
|
* `choice` `{ arm, value }`. EXPLICIT wrappers and IMPLICIT `[tag]` retagging are
|
|
@@ -484,7 +520,13 @@ function _encodeSeq(schema, value, ctx) {
|
|
|
484
520
|
} else if (fld.fkind === "optional") {
|
|
485
521
|
if (_present(value[fld.name])) {
|
|
486
522
|
var enc = encode(fld.schema, value[fld.name], ctx);
|
|
487
|
-
|
|
523
|
+
// X.690 sec. 11.5 -- DER: a component equal to its DEFAULT value shall not
|
|
524
|
+
// be included in the encoding. Compare canonical field encodings, so
|
|
525
|
+
// encode never emits the non-canonical explicit-default form (which a
|
|
526
|
+
// walk-side defaultCode reject would refuse to round-trip).
|
|
527
|
+
var isDefault = fld.hasDefault && fld.def !== undefined &&
|
|
528
|
+
enc.equals(encode(fld.schema, fld.def, ctx));
|
|
529
|
+
if (!isDefault) parts.push(fld.explicit ? asn1.build.explicit(fld.tag, enc) : enc);
|
|
488
530
|
}
|
|
489
531
|
} else if (fld.fkind === "trailing") {
|
|
490
532
|
// Emit present members in strictly-increasing tag order (the walk's rule).
|
|
@@ -496,6 +538,10 @@ function _encodeSeq(schema, value, ctx) {
|
|
|
496
538
|
parts.push(mm.explicit ? asn1.build.explicit(mm.tag, menc) : menc);
|
|
497
539
|
}
|
|
498
540
|
}
|
|
541
|
+
} else {
|
|
542
|
+
// The mirror of _walkSeq's unknown-field-kind reject: a malformed
|
|
543
|
+
// descriptor is an authoring fault on the encode direction too.
|
|
544
|
+
_encFail("unknown field kind " + JSON.stringify(fld && fld.fkind));
|
|
499
545
|
}
|
|
500
546
|
}
|
|
501
547
|
return _wrapConstructed(schema, parts);
|
|
@@ -506,7 +552,7 @@ function _encodeRepeat(schema, items, ctx) {
|
|
|
506
552
|
// Enforce the SAME constraints walk enforces, so encode never emits DER its own
|
|
507
553
|
// decoder rejects (the walk(decode(encode)) invariant): the SIZE minimum, and
|
|
508
554
|
// semantic uniqueness (the key computed exactly as walk does, off the round-tripped
|
|
509
|
-
// item
|
|
555
|
+
// item -- a duplicate is an authoring fault, not a parse verdict).
|
|
510
556
|
if (schema.min != null && items.length < schema.min) {
|
|
511
557
|
_encFail("this repeat requires at least " + schema.min + " element(s) but got " + items.length);
|
|
512
558
|
}
|
|
@@ -530,7 +576,10 @@ function _encodeRepeat(schema, items, ctx) {
|
|
|
530
576
|
}
|
|
531
577
|
|
|
532
578
|
function _encodeChoice(schema, value, ctx) {
|
|
533
|
-
|
|
579
|
+
// Number.isInteger: a NaN / fractional arm passes a `< 0 || >= length` range
|
|
580
|
+
// check (both compares are false for NaN) into an undefined alts lookup --
|
|
581
|
+
// the same authoring fault as an out-of-range arm, failed the same way.
|
|
582
|
+
if (!value || !Number.isInteger(value.arm) || value.arm < 0 || value.arm >= schema.alts.length) {
|
|
534
583
|
_encFail("a choice value must be { arm: <index>, value: <arm value> }");
|
|
535
584
|
}
|
|
536
585
|
return encode(schema.alts[value.arm].schema, value.value, ctx);
|
|
@@ -546,8 +595,8 @@ function _encodeChoice(schema, value, ctx) {
|
|
|
546
595
|
* @related pki.schema.engine.walk, pki.asn1.decode
|
|
547
596
|
*
|
|
548
597
|
* Decode a fresh DER (or, with `ber: true`, BER) blob carried inside an
|
|
549
|
-
* already-decoded value
|
|
550
|
-
* structure
|
|
598
|
+
* already-decoded value -- an OCTET STRING whose content is itself an encoded
|
|
599
|
+
* structure -- and walk it against a schema. A codec failure is wrapped in the
|
|
551
600
|
* caller's typed `code`; a schema rejection keeps its own code. This is the
|
|
552
601
|
* one named form of the re-decode idiom, so the caps that a fresh
|
|
553
602
|
* `pki.asn1.decode` would restart from zero can be carried across re-decode
|
|
@@ -558,7 +607,7 @@ function _encodeChoice(schema, value, ctx) {
|
|
|
558
607
|
* @opts
|
|
559
608
|
* code: string, // typed code wrapping a codec failure (required)
|
|
560
609
|
* what: string, // human label for the wrapped message
|
|
561
|
-
* ber: boolean, // default false
|
|
610
|
+
* ber: boolean, // default false -- BER content region (RFC 7292 sec. 4.1)
|
|
562
611
|
* budget: object, // { remaining: n } shared across a parse's re-decodes
|
|
563
612
|
* budgetCode: string, // typed code when the budget is exhausted
|
|
564
613
|
*
|
|
@@ -587,15 +636,15 @@ function embeddedDer(schema, bytes, ctx, opts) {
|
|
|
587
636
|
return walk(schema, node, ctx);
|
|
588
637
|
}
|
|
589
638
|
|
|
590
|
-
// X.690
|
|
639
|
+
// X.690 sec. 11.2.2 -- a NamedBitList BIT STRING (KeyUsage, PKIFailureInfo, and the
|
|
591
640
|
// like) MUST drop every trailing zero bit under DER, giving one canonical
|
|
592
641
|
// encoding per value: an empty value carries 0 unused bits; a non-empty value's
|
|
593
642
|
// last content octet is non-zero AND its lowest USED bit (at position
|
|
594
643
|
// `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
|
|
596
|
-
// silently diverges on
|
|
644
|
+
// so the reject code stays per-format. Centralized so no format re-derives -- and
|
|
645
|
+
// silently diverges on -- the minimal-encoding rule (a DER-canonicalization
|
|
597
646
|
// bypass class); see the named-bitlist-minimal-encoding-inlined gate.
|
|
598
|
-
// ASN.1 node tag predicates
|
|
647
|
+
// ASN.1 node tag predicates -- the per-node `tagClass`/`tagNumber` test every
|
|
599
648
|
// format's matches() detector (and the engine's own optional/choice walkers)
|
|
600
649
|
// would otherwise re-inline. Centralized so a detector composes one predicate
|
|
601
650
|
// instead of hand-rolling the comparison (and drifting); see the
|
|
@@ -618,12 +667,12 @@ function isContextInRange(node, min, max) {
|
|
|
618
667
|
|
|
619
668
|
function assertMinimalNamedBits(unusedBits, bytes, fail) {
|
|
620
669
|
if (bytes.length === 0) {
|
|
621
|
-
if (unusedBits !== 0) fail("an empty NamedBitList must encode with 0 unused bits (X.690
|
|
670
|
+
if (unusedBits !== 0) fail("an empty NamedBitList must encode with 0 unused bits (X.690 sec. 11.2.2)");
|
|
622
671
|
return;
|
|
623
672
|
}
|
|
624
673
|
var last = bytes[bytes.length - 1];
|
|
625
|
-
if (last === 0) fail("a NamedBitList must not carry a trailing all-zero octet (X.690
|
|
626
|
-
if (((last >> unusedBits) & 1) !== 1) fail("a NamedBitList must drop all trailing zero bits (X.690
|
|
674
|
+
if (last === 0) fail("a NamedBitList must not carry a trailing all-zero octet (X.690 sec. 11.2.2)");
|
|
675
|
+
if (((last >> unusedBits) & 1) !== 1) fail("a NamedBitList must drop all trailing zero bits (X.690 sec. 11.2.2)");
|
|
627
676
|
}
|
|
628
677
|
|
|
629
678
|
module.exports = {
|
|
@@ -633,7 +682,7 @@ module.exports = {
|
|
|
633
682
|
// leaves
|
|
634
683
|
oidLeaf: oidLeaf, integerLeaf: integerLeaf, boolean: boolean, octetString: octetString,
|
|
635
684
|
bitString: bitString, implicitBitString: implicitBitString, implicitOctetString: implicitOctetString,
|
|
636
|
-
implicitNull: implicitNull, implicitInteger: implicitInteger,
|
|
685
|
+
implicitNull: implicitNull, implicitInteger: implicitInteger, implicitBoolean: implicitBoolean,
|
|
637
686
|
any: any, decode: decode, time: time,
|
|
638
687
|
// engine
|
|
639
688
|
walk: walk, encode: encode, embeddedDer: embeddedDer, assertMinimalNamedBits: assertMinimalNamedBits,
|