@blamejs/pki 0.1.6 → 0.1.8

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.
@@ -0,0 +1,251 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (c) blamejs contributors
3
+ "use strict";
4
+ /**
5
+ * @module pki.schema.crl
6
+ * @nav Schema
7
+ * @title CRL
8
+ * @order 120
9
+ * @slug crl
10
+ *
11
+ * @intro
12
+ * X.509 Certificate Revocation List handling per RFC 5280 §5. `parse` turns a
13
+ * DER or PEM CRL into a structured, fully-decoded object: version, issuer
14
+ * distinguished name, this/next update as real `Date`s, the ordered list of
15
+ * revoked certificates (serial + revocation date + entry extensions), and the
16
+ * CRL extensions. It composes the same schema engine and shared PKIX
17
+ * sub-schemas (AlgorithmIdentifier, Name, Extension) the certificate parser
18
+ * uses, so the CertificateList inherits the identical fail-closed structural
19
+ * rules, and the raw `tbsCertList` bytes are returned for signature checking.
20
+ *
21
+ * @card
22
+ * Parse DER / PEM X.509 CRLs into structured, validated fields — revoked
23
+ * serials with real-`Date` revocation times, named extensions, fail-closed.
24
+ */
25
+
26
+ var asn1 = require("./asn1-der");
27
+ var schema = require("./schema-engine");
28
+ var pkix = require("./schema-pkix");
29
+ var oid = require("./oid");
30
+ var frameworkError = require("./framework-error");
31
+
32
+ var CrlError = frameworkError.CrlError;
33
+ var PemError = frameworkError.PemError;
34
+ var TAGS = asn1.TAGS;
35
+
36
+
37
+ // CRLReason ::= ENUMERATED (RFC 5280 §5.3.1) — value 7 is unused/reserved.
38
+ var CRL_REASONS = [0n, 1n, 2n, 3n, 4n, 5n, 6n, 8n, 9n, 10n];
39
+
40
+ // Extension-value decoding is keyed off the STABLE dotted OID (resolved once at
41
+ // load from the canonical name), not the mutable display name — a caller's
42
+ // pki.oid.register() display override must not change parse behaviour.
43
+ var OID_CRL_NUMBER = oid.byName("cRLNumber");
44
+ var OID_REASON_CODE = oid.byName("reasonCode");
45
+ var OID_INVALIDITY_DATE = oid.byName("invalidityDate");
46
+
47
+ // The crl error namespace the schema engine walks under. Shared PKIX sub-schemas
48
+ // are instantiated here under crl/* so a structural fault reports a crl code.
49
+ var NS = { prefix: "crl", E: function (code, message, cause) { return new CrlError(code, message, cause); }, oid: oid };
50
+
51
+ var ALGORITHM_IDENTIFIER = pkix.algorithmIdentifier(NS);
52
+ var NAME = pkix.name(NS);
53
+ var EXTENSIONS = pkix.extensions(NS);
54
+ var TIME = schema.time(NS);
55
+
56
+ // CRL Version ::= INTEGER { v1(0), v2(1) } — a BARE INTEGER (not [0] EXPLICIT).
57
+ // A CRL is at most v2; reject an explicit v1 (DER forbids the default) and any
58
+ // value >= 2. Do NOT reuse the certificate readVersion (it maps 2 -> v3).
59
+ var CRL_VERSION = schema.decode(function (n) {
60
+ var v = asn1.read.integer(n);
61
+ if (v === 1n) return 2;
62
+ if (v === 0n) throw NS.E("crl/bad-version", "DER forbids explicitly encoding the default version v1");
63
+ throw NS.E("crl/bad-version", "unsupported CRL version " + v.toString() + " (a CRL is at most v2)");
64
+ });
65
+
66
+ // The three cheap, high-value CRL extension values are decoded from their raw
67
+ // extnValue octets (RFC 5280 §5.2/§5.3); GeneralNames-based extensions
68
+ // (issuingDistributionPoint, certificateIssuer, authorityKeyIdentifier, …) stay
69
+ // raw with their bytes reachable. A malformed decoded value fails closed.
70
+ function decodeExt(ext) {
71
+ var value = ext.value;
72
+ try {
73
+ if (ext.oid === OID_CRL_NUMBER) { // cRLNumber ::= INTEGER (0..MAX)
74
+ value = asn1.read.integer(asn1.decode(ext.value));
75
+ if (value < 0n) throw new Error("cRLNumber must be non-negative (INTEGER 0..MAX)");
76
+ } else if (ext.oid === OID_REASON_CODE) { // reasonCode ::= ENUMERATED (CRLReason)
77
+ // read.enumerated asserts the ENUMERATED tag (a bare INTEGER is rejected) —
78
+ // the codec owns the tag check, so this reader need not repeat it.
79
+ var reason = asn1.read.enumerated(asn1.decode(ext.value));
80
+ if (CRL_REASONS.indexOf(reason) === -1) throw new Error("undefined CRLReason " + reason.toString());
81
+ value = Number(reason);
82
+ } else if (ext.oid === OID_INVALIDITY_DATE) { // invalidityDate ::= GeneralizedTime
83
+ var n = asn1.decode(ext.value);
84
+ if (n.tagClass !== "universal" || n.tagNumber !== TAGS.GENERALIZED_TIME) {
85
+ throw new Error("invalidityDate must be a GeneralizedTime");
86
+ }
87
+ value = asn1.read.time(n);
88
+ }
89
+ } catch (e) {
90
+ throw NS.E("crl/bad-extension-value", "malformed " + (ext.name || ext.oid) + " extension value: " + ((e && e.message) || String(e)), e);
91
+ }
92
+ return { oid: ext.oid, name: ext.name, critical: ext.critical, value: value };
93
+ }
94
+
95
+ // RevokedCertificate ::= SEQUENCE { userCertificate CertificateSerialNumber,
96
+ // revocationDate Time, crlEntryExtensions Extensions OPTIONAL }
97
+ var REVOKED_ENTRY = schema.seq([
98
+ schema.field("userCertificate", schema.integerLeaf()),
99
+ schema.field("revocationDate", TIME),
100
+ schema.optional("crlEntryExtensions", EXTENSIONS, { whenUniversal: [TAGS.SEQUENCE] }),
101
+ ], {
102
+ assert: "sequence", arity: { min: 2 }, code: "crl/bad-revoked-entry", what: "RevokedCertificate",
103
+ build: function (m) {
104
+ return {
105
+ serialNumber: m.fields.userCertificate.value,
106
+ // Raw INTEGER content bytes (not a reserialized BigInt) so the hex matches
107
+ // the X.509 parser's serialNumberHex and preserves DER sign padding
108
+ // (a positive 0x80 serial encodes as content 00 80).
109
+ serialNumberHex: m.fields.userCertificate.node.content.toString("hex"),
110
+ revocationDate: m.fields.revocationDate.value,
111
+ crlEntryExtensions: m.fields.crlEntryExtensions.present ? m.fields.crlEntryExtensions.value.result.map(decodeExt) : [],
112
+ };
113
+ },
114
+ });
115
+
116
+ var REVOKED_LIST = schema.seqOf(REVOKED_ENTRY, {
117
+ assert: "sequence", min: 1, code: "crl/bad-revoked-certificates", what: "revokedCertificates",
118
+ build: function (m) { return m.items.map(function (it) { return it.value.result; }); },
119
+ });
120
+
121
+ // TBSCertList ::= SEQUENCE { version Version OPTIONAL, signature AlgorithmIdentifier,
122
+ // issuer Name, thisUpdate Time, nextUpdate Time OPTIONAL,
123
+ // revokedCertificates SEQUENCE OF SEQUENCE {...} OPTIONAL,
124
+ // crlExtensions [0] EXPLICIT Extensions OPTIONAL }.
125
+ // The three OPTIONAL universal-tagged fields (version=INTEGER, nextUpdate=Time,
126
+ // revokedCertificates=SEQUENCE) are disambiguated by their universal tag;
127
+ // crlExtensions is modeled as a trailing [0]..[0] so a stray non-[0] trailing
128
+ // context tag is REJECTED (crl/bad-tbs), not silently ignored.
129
+ var TBS_CERTLIST = schema.seq([
130
+ schema.optional("version", CRL_VERSION, { whenUniversal: [TAGS.INTEGER] }),
131
+ schema.field("signature", ALGORITHM_IDENTIFIER),
132
+ schema.field("issuer", NAME),
133
+ schema.field("thisUpdate", TIME),
134
+ schema.optional("nextUpdate", TIME, { whenUniversal: [TAGS.UTC_TIME, TAGS.GENERALIZED_TIME] }),
135
+ schema.optional("revokedCertificates", REVOKED_LIST, { whenUniversal: [TAGS.SEQUENCE] }),
136
+ schema.trailing([{ tag: 0, name: "crlExtensions", schema: EXTENSIONS, explicit: true, emptyCode: "crl/bad-extensions" }],
137
+ { minTag: 0, maxTag: 0, unexpectedCode: "crl/bad-tbs", orderCode: "crl/bad-tbs" }),
138
+ ], {
139
+ assert: "sequence", code: "crl/bad-tbs", what: "tbsCertList",
140
+ build: function (m) {
141
+ return {
142
+ version: m.fields.version.present ? m.fields.version.value : 1,
143
+ issuer: m.fields.issuer.value.result, // Name is a seqOf → field.value is the match; .result is the {rdns, dn} build
144
+ thisUpdate: m.fields.thisUpdate.value,
145
+ nextUpdate: m.fields.nextUpdate.present ? m.fields.nextUpdate.value : null,
146
+ revokedCertificates: m.fields.revokedCertificates.present ? m.fields.revokedCertificates.value.result : [],
147
+ crlExtensions: m.fields.crlExtensions.present ? m.fields.crlExtensions.value.result.map(decodeExt) : [],
148
+ crlExtensionsPresent: m.fields.crlExtensions.present,
149
+ };
150
+ },
151
+ });
152
+
153
+ // CertificateList ::= SEQUENCE { tbsCertList, signatureAlgorithm, signatureValue }
154
+ // — the shared SIGNED envelope. The CRL-specific invariants (outer==inner
155
+ // signatureAlgorithm agreement, non-empty issuer, v2-only extensions) live in the
156
+ // build; the envelope owns the SEQUENCE-of-3 shape and signature extraction.
157
+ var CERTIFICATE_LIST = pkix.signedEnvelope(NS, TBS_CERTLIST, {
158
+ code: "crl/not-a-crl", what: "CertificateList",
159
+ build: function (e) {
160
+ var tbs = e.tbsMatch.result;
161
+ // RFC 5280 §5.1.1.2 — the outer signatureAlgorithm MUST equal tbsCertList.signature.
162
+ if (!e.outerSignatureAlgorithmBytes.equals(e.tbsMatch.fields.signature.node.bytes)) {
163
+ throw NS.E("crl/bad-signature-algorithm", "signatureAlgorithm must match tbsCertList.signature (RFC 5280 §5.1.1.2)");
164
+ }
165
+ // RFC 5280 §5.1.2.3 — the issuer MUST be a non-empty distinguished name.
166
+ if (!tbs.issuer.rdns.length) {
167
+ throw NS.E("crl/bad-issuer", "issuer must be a non-empty distinguished name");
168
+ }
169
+ // RFC 5280 §5.1.2.1 — crlExtensions / crlEntryExtensions appear only in a v2 CRL.
170
+ var hasExtensions = tbs.crlExtensionsPresent ||
171
+ tbs.revokedCertificates.some(function (r) { return r.crlEntryExtensions.length > 0; });
172
+ if (hasExtensions && tbs.version !== 2) {
173
+ throw NS.E("crl/bad-version", "crlExtensions / crlEntryExtensions are only permitted in a v2 CRL");
174
+ }
175
+ return {
176
+ version: tbs.version,
177
+ issuer: tbs.issuer,
178
+ thisUpdate: tbs.thisUpdate,
179
+ nextUpdate: tbs.nextUpdate,
180
+ revokedCertificates: tbs.revokedCertificates,
181
+ crlExtensions: tbs.crlExtensions,
182
+ tbsBytes: e.tbsBytes,
183
+ signatureAlgorithm: e.signatureAlgorithm,
184
+ signatureValue: e.signatureValue,
185
+ };
186
+ },
187
+ });
188
+
189
+ /**
190
+ * @primitive pki.schema.crl.parse
191
+ * @signature pki.schema.crl.parse(input) -> crl
192
+ * @since 0.1.7
193
+ * @status experimental
194
+ * @spec RFC 5280
195
+ * @related pki.schema.x509.parse, pki.schema.parse
196
+ *
197
+ * Parse a DER `Buffer` or a PEM (`X509 CRL`) string into a structured CRL:
198
+ * `{ version, issuer, thisUpdate, nextUpdate, revokedCertificates,
199
+ * crlExtensions, tbsBytes, signatureAlgorithm, signatureValue }`. Every field is
200
+ * validated on the way in; a malformed CertificateList / TBSCertList throws a
201
+ * typed `CrlError` (`crl/*`) and a leaf-level codec fault surfaces as `asn1/*`.
202
+ *
203
+ * @example
204
+ * var crl = pki.schema.crl.parse(der);
205
+ * crl.revokedCertificates[0].serialNumberHex; // → "0a3f…"
206
+ */
207
+ var parse = pkix.makeParser({ pemLabel: "X509 CRL", PemError: PemError, ErrorClass: CrlError, prefix: "crl", what: "CRL", topSchema: CERTIFICATE_LIST, ns: NS });
208
+
209
+ /**
210
+ * @primitive pki.schema.crl.pemDecode
211
+ * @signature pki.schema.crl.pemDecode(text, label?) -> Buffer
212
+ * @since 0.1.7
213
+ * @status experimental
214
+ * @spec RFC 7468, RFC 5280
215
+ * @related pki.schema.crl.parse
216
+ *
217
+ * Extract the DER bytes from a PEM CRL block (default label `X509 CRL`). Throws
218
+ * `PemError` on a missing / mismatched envelope or a non-base64 body.
219
+ *
220
+ * @example
221
+ * var der = pki.schema.crl.pemDecode(pemText);
222
+ */
223
+ function pemDecode(text, label) { return pkix.pemDecode(text, label || "X509 CRL", PemError); }
224
+
225
+ // Certificate and CertificateList share the outer SEQUENCE-of-3 shape; a CRL is
226
+ // distinguished by its tbsCertList — the first tbs element is a bare INTEGER
227
+ // (version) or an AlgorithmIdentifier (signature) SEQUENCE, and crucially the
228
+ // field at the certificate's Validity position is a bare Time (thisUpdate). The
229
+ // orchestrator uses `matches` to route; a cert's tbs leads with a [0] EXPLICIT
230
+ // version or an INTEGER serial FOLLOWED by an AlgorithmIdentifier and a Name,
231
+ // then a Validity SEQUENCE, never a bare Time at that depth.
232
+ function matches(root) {
233
+ var tbs = pkix.signedEnvelopeTbs(root);
234
+ if (!tbs) return false;
235
+ // A certificate's tbs leads with [0] EXPLICIT version — a CRL never does.
236
+ if (tbs.children[0] && tbs.children[0].tagClass === "context") return false;
237
+ // Walk to the thisUpdate / validity position: skip an optional bare INTEGER
238
+ // version, then signature (SEQUENCE) + issuer (SEQUENCE); the next element is
239
+ // thisUpdate (Time) for a CRL, Validity (SEQUENCE) for a certificate.
240
+ var i = 0;
241
+ if (tbs.children[i] && tbs.children[i].tagClass === "universal" && tbs.children[i].tagNumber === TAGS.INTEGER) i++;
242
+ i += 2; // signature + issuer
243
+ var pos = tbs.children[i];
244
+ return !!pos && pos.tagClass === "universal" && (pos.tagNumber === TAGS.UTC_TIME || pos.tagNumber === TAGS.GENERALIZED_TIME);
245
+ }
246
+
247
+ module.exports = {
248
+ parse: parse,
249
+ pemDecode: pemDecode,
250
+ matches: matches,
251
+ };
@@ -0,0 +1,203 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (c) blamejs contributors
3
+ "use strict";
4
+ /**
5
+ * @module pki.schema.csr
6
+ * @nav Schema
7
+ * @title CSR
8
+ * @order 130
9
+ * @slug csr
10
+ *
11
+ * @intro
12
+ * PKCS#10 certification request handling per RFC 2986. `parse` turns a DER or
13
+ * PEM CSR into a structured object: version, subject distinguished name,
14
+ * subject public-key info, the requested attributes (each with its raw values),
15
+ * and the signature over the CertificationRequestInfo. It composes the same
16
+ * schema engine and shared PKIX sub-schemas (AlgorithmIdentifier, Name,
17
+ * SubjectPublicKeyInfo) the certificate parser uses, so the request inherits the
18
+ * identical fail-closed structural rules, and the raw
19
+ * `certificationRequestInfoBytes` are returned for signature checking.
20
+ *
21
+ * A CSR is self-signed only in the sense that the requester proves possession of
22
+ * the private key; unlike a certificate or CRL there is no inner signature
23
+ * algorithm to agree with, the subject MAY be empty, and the attribute set has
24
+ * no uniqueness constraint — the parser deliberately omits those three
25
+ * certificate/CRL guards.
26
+ *
27
+ * @card
28
+ * Parse DER / PEM PKCS#10 CSRs into structured, validated fields — subject DN,
29
+ * public key, requested attributes, signature, fail-closed.
30
+ */
31
+
32
+ var asn1 = require("./asn1-der");
33
+ var schema = require("./schema-engine");
34
+ var pkix = require("./schema-pkix");
35
+ var oid = require("./oid");
36
+ var frameworkError = require("./framework-error");
37
+
38
+ var CsrError = frameworkError.CsrError;
39
+ var PemError = frameworkError.PemError;
40
+
41
+ // The csr error namespace the schema engine walks under. Shared PKIX sub-schemas
42
+ // are instantiated here under csr/* so a structural fault reports a csr code.
43
+ var NS = { prefix: "csr", E: function (code, message, cause) { return new CsrError(code, message, cause); }, oid: oid };
44
+
45
+ var NAME = pkix.name(NS);
46
+ var SPKI = pkix.spki(NS);
47
+
48
+ // CertificationRequestInfo version ::= INTEGER { v1(0) } (RFC 2986 §4.1). A BARE
49
+ // mandatory INTEGER whose ONLY legal value is 0 — the inverse of the cert/CRL
50
+ // readers, which reject 0 as the DER-forbidden default. Surface v1 as 1. A
51
+ // cert-shaped [0] EXPLICIT wrapper at this position makes asn1.read.integer throw
52
+ // asn1/* (fail-closed).
53
+ var CSR_VERSION = schema.decode(function (n) {
54
+ var v = asn1.read.integer(n);
55
+ if (v === 0n) return 1;
56
+ throw NS.E("csr/bad-version", "unsupported CSR version " + v.toString() + " (PKCS#10 requires v1)");
57
+ });
58
+
59
+ // Attribute ::= SEQUENCE { type OBJECT IDENTIFIER, values SET OF AttributeValue }.
60
+ // AttributeValue is ANY, kept as raw DER (node.bytes) — an unrecognized attribute
61
+ // type never fails the parse. values is SET SIZE (1..MAX): an empty SET is
62
+ // rejected. There is NO SET-OF uniqueness on attributes (unlike cert extensions).
63
+ var ATTRIBUTE = schema.seq([
64
+ schema.field("type", schema.oidLeaf()),
65
+ schema.field("values", schema.setOf(schema.any(), { assert: "set", min: 1, code: "csr/bad-attribute-values", what: "attribute values" })),
66
+ ], {
67
+ assert: "sequence", arity: { exact: 2 }, code: "csr/bad-attribute", what: "Attribute",
68
+ build: function (m, ctx) {
69
+ var t = m.fields.type.value;
70
+ return {
71
+ type: t,
72
+ name: ctx.oid.name(t) || null,
73
+ values: m.fields.values.value.items.map(function (it) { return it.node.bytes; }),
74
+ };
75
+ },
76
+ });
77
+
78
+ // CertificationRequestInfo ::= SEQUENCE { version, subject Name,
79
+ // subjectPKInfo SubjectPublicKeyInfo, attributes [0] IMPLICIT SET OF Attribute }.
80
+ // attributes is a REQUIRED field (a CRI omitting [0] is a missing-required-field
81
+ // fault, not silently accepted) modeled as an IMPLICIT [0] SET OF with min:0 (an
82
+ // empty attributes SET is legal). subject MAY be empty — no non-empty-DN guard.
83
+ var CERTIFICATION_REQUEST_INFO = schema.seq([
84
+ schema.field("version", CSR_VERSION),
85
+ schema.field("subject", NAME),
86
+ schema.field("subjectPKInfo", SPKI),
87
+ schema.field("attributes", schema.implicitSetOf(0, ATTRIBUTE, { min: 0, code: "csr/bad-attributes", what: "attributes" })),
88
+ ], {
89
+ assert: "sequence", code: "csr/bad-cri", what: "certificationRequestInfo",
90
+ build: function (m) {
91
+ return {
92
+ version: m.fields.version.value,
93
+ subject: m.fields.subject.value.result, // Name is a seqOf → field.value is the match; .result is the {rdns, dn} build
94
+ subjectPublicKeyInfo: m.fields.subjectPKInfo.value.result,
95
+ attributes: m.fields.attributes.value.items.map(function (it) { return it.value.result; }),
96
+ };
97
+ },
98
+ });
99
+
100
+ // CertificationRequest ::= SEQUENCE { certificationRequestInfo, signatureAlgorithm,
101
+ // signature BIT STRING } — the shared SIGNED envelope. DIVERGENCE from the
102
+ // cert/CRL builders: OMIT the outer-vs-inner signatureAlgorithm agreement check
103
+ // (the CRI has no inner signature AlgorithmIdentifier) and the non-empty-subject
104
+ // guard (a CSR subject MAY be empty). The omission is structural — this build
105
+ // simply never references the agreement bytes.
106
+ var CERTIFICATION_REQUEST = pkix.signedEnvelope(NS, CERTIFICATION_REQUEST_INFO, {
107
+ code: "csr/not-a-certification-request", what: "CertificationRequest",
108
+ build: function (e) {
109
+ var cri = e.tbsMatch.result;
110
+ return {
111
+ version: cri.version,
112
+ subject: cri.subject,
113
+ subjectPublicKeyInfo: cri.subjectPublicKeyInfo,
114
+ attributes: cri.attributes,
115
+ certificationRequestInfoBytes: e.tbsBytes,
116
+ tbsBytes: e.tbsBytes,
117
+ signatureAlgorithm: e.signatureAlgorithm,
118
+ signatureValue: e.signatureValue,
119
+ };
120
+ },
121
+ });
122
+
123
+ /**
124
+ * @primitive pki.schema.csr.parse
125
+ * @signature pki.schema.csr.parse(input) -> csr
126
+ * @since 0.1.8
127
+ * @status experimental
128
+ * @spec RFC 2986
129
+ * @related pki.schema.x509.parse, pki.schema.parse
130
+ *
131
+ * Parse a DER `Buffer` or a PEM (`CERTIFICATE REQUEST`) string into a structured
132
+ * PKCS#10 request: `{ version, subject, subjectPublicKeyInfo, attributes,
133
+ * certificationRequestInfoBytes, tbsBytes, signatureAlgorithm, signatureValue }`.
134
+ * Every field is validated on the way in; a malformed CertificationRequest /
135
+ * CertificationRequestInfo throws a typed `CsrError` (`csr/*`) and a leaf-level
136
+ * codec fault surfaces as `asn1/*`. Attribute values are returned as raw DER
137
+ * buffers so an unrecognized attribute type never fails the parse.
138
+ *
139
+ * @example
140
+ * var csr = pki.schema.csr.parse(der);
141
+ * csr.subject.dn; // → "CN=req.example"
142
+ * csr.attributes[0].type; // → "1.2.840.113549.1.9.14"
143
+ */
144
+ var parse = pkix.makeParser({ pemLabel: "CERTIFICATE REQUEST", PemError: PemError, ErrorClass: CsrError, prefix: "csr", what: "certification request", topSchema: CERTIFICATION_REQUEST, ns: NS });
145
+
146
+ /**
147
+ * @primitive pki.schema.csr.pemDecode
148
+ * @signature pki.schema.csr.pemDecode(text, label?) -> Buffer
149
+ * @since 0.1.8
150
+ * @status experimental
151
+ * @spec RFC 7468, RFC 2986
152
+ * @related pki.schema.csr.parse
153
+ *
154
+ * Extract the DER bytes from a PEM CSR block (default label `CERTIFICATE
155
+ * REQUEST`). Throws `PemError` on a missing / mismatched envelope or a non-base64
156
+ * body.
157
+ *
158
+ * @example
159
+ * var der = pki.schema.csr.pemDecode(pemText);
160
+ */
161
+ function pemDecode(text, label) { return pkix.pemDecode(text, label || "CERTIFICATE REQUEST", PemError); }
162
+
163
+ /**
164
+ * @primitive pki.schema.csr.pemEncode
165
+ * @signature pki.schema.csr.pemEncode(der, label?) -> string
166
+ * @since 0.1.8
167
+ * @status experimental
168
+ * @spec RFC 7468
169
+ * @related pki.schema.csr.pemDecode
170
+ *
171
+ * Wrap DER bytes in a PEM CSR envelope (default label `CERTIFICATE REQUEST`).
172
+ *
173
+ * @example
174
+ * var pem = pki.schema.csr.pemEncode(der);
175
+ */
176
+ function pemEncode(der, label) { return pkix.pemEncode(der, label || "CERTIFICATE REQUEST", PemError); }
177
+
178
+ // A CertificationRequest shares the outer SEQUENCE-of-3 shape with a certificate
179
+ // and a CRL; a CSR is distinguished by its CertificationRequestInfo — a SEQUENCE
180
+ // of EXACTLY four children {version INTEGER, subject SEQUENCE, subjectPKInfo
181
+ // SEQUENCE, attributes [0]}. The mandatory trailing context-[0] attributes
182
+ // element is what a cert (whose tbs leads with [0] EXPLICIT version or an INTEGER
183
+ // serial and never ends the leading run this way) and a CRL (whose tbs reaches a
184
+ // bare Time at the validity position) never present, so the three detectors are
185
+ // mutually exclusive.
186
+ function matches(root) {
187
+ var TAGS = asn1.TAGS;
188
+ var cri = pkix.signedEnvelopeTbs(root);
189
+ if (!cri) return false;
190
+ var k = cri.children;
191
+ if (k.length !== 4) return false;
192
+ return k[0].tagClass === "universal" && k[0].tagNumber === TAGS.INTEGER &&
193
+ k[1].tagClass === "universal" && k[1].tagNumber === TAGS.SEQUENCE &&
194
+ k[2].tagClass === "universal" && k[2].tagNumber === TAGS.SEQUENCE &&
195
+ k[3].tagClass === "context" && k[3].tagNumber === 0;
196
+ }
197
+
198
+ module.exports = {
199
+ parse: parse,
200
+ pemDecode: pemDecode,
201
+ pemEncode: pemEncode,
202
+ matches: matches,
203
+ };
@@ -2,8 +2,8 @@
2
2
  // Copyright (c) blamejs contributors
3
3
  "use strict";
4
4
  /**
5
- * @module pki.asn1.schema
6
- * @nav ASN.1
5
+ * @module pki.schema.engine
6
+ * @nav Schema
7
7
  * @title Schema engine
8
8
  * @order 60
9
9
  *
@@ -62,6 +62,13 @@ function _assertShape(schema, node, ctx) {
62
62
  if (!node.children) {
63
63
  _fail(ctx, schema.code, (schema.what || "value") + " must be a constructed value");
64
64
  }
65
+ } else if (mode === "implicit") {
66
+ // IMPLICIT [tag] SET/SEQUENCE OF: the context tag replaces the universal
67
+ // tag, so the node is a context-class constructed [tag] and its direct
68
+ // children are the items (no inner universal SET, no EXPLICIT unwrap).
69
+ if (node.tagClass !== "context" || node.tagNumber !== schema.implicitTag || !node.children) {
70
+ _fail(ctx, schema.code, (schema.what || "value") + " must be an IMPLICIT [" + schema.implicitTag + "] SET OF");
71
+ }
65
72
  } else {
66
73
  _fail(ctx, schema.code, "unknown assert mode " + JSON.stringify(mode));
67
74
  }
@@ -109,14 +116,20 @@ function time(ns) {
109
116
  function field(name, schema) { return { fkind: "required", name: name, schema: schema }; }
110
117
  function optional(name, schema, opts) {
111
118
  opts = opts || {};
112
- // How the optional field is recognized at its position. Default: a context
113
- // [tag] (the certificate version [0] shape). `whenAny`: the next element
114
- // whatever its tag an OPTIONAL ANY like AlgorithmIdentifier.parameters. The
115
- // recognizer lets _walkSeq CONSUME the element so a closed sequence can reject
116
- // whatever is left over (without it, a trailing ANY looks unconsumed).
119
+ // How the optional field is recognized at its position:
120
+ // - default: a context [tag] (the certificate version [0] shape).
121
+ // - whenUniversal: the next element iff its UNIVERSAL tag is in the set
122
+ // the CRL TBSCertList shape (bare INTEGER version, Time nextUpdate,
123
+ // SEQUENCE revokedCertificates), disambiguated by tag, not a context [n].
124
+ // - whenAny: the next element whatever its tag — an OPTIONAL ANY like
125
+ // AlgorithmIdentifier.parameters.
126
+ // The recognizer lets _walkSeq CONSUME the element so a closed sequence can
127
+ // reject whatever is left over (without it, a trailing ANY looks unconsumed).
117
128
  var match = opts.whenAny
118
129
  ? function () { return true; }
119
- : function (n) { return n.tagClass === "context" && n.tagNumber === opts.tag; };
130
+ : opts.whenUniversal
131
+ ? function (n) { return n.tagClass === "universal" && opts.whenUniversal.indexOf(n.tagNumber) !== -1; }
132
+ : function (n) { return n.tagClass === "context" && n.tagNumber === opts.tag; };
120
133
  return { fkind: "optional", name: name, schema: schema, tag: opts.tag, match: match,
121
134
  explicit: !!opts.explicit, emptyCode: opts.emptyCode, hasDefault: ("default" in opts), def: opts.default };
122
135
  }
@@ -157,22 +170,30 @@ function seqOf(item, opts) {
157
170
  }
158
171
  function setOf(item, opts) {
159
172
  opts = opts || {};
160
- return { kind: "repeat", item: item, assert: opts.assert || "set", code: opts.code, what: opts.what,
173
+ return { kind: "repeat", item: item, assert: opts.assert || "set", derSetOrder: true, code: opts.code, what: opts.what,
161
174
  min: opts.min, unique: opts.unique, dupCode: opts.dupCode, build: opts.build };
162
175
  }
163
176
  function setOfUnique(item, keyFn, opts) {
164
177
  return setOf(item, Object.assign({ unique: keyFn }, opts || {}));
165
178
  }
179
+ // [tag] IMPLICIT SET OF item — the context tag REPLACES the universal SET tag,
180
+ // so the node is a context-class constructed [tag] whose direct children are the
181
+ // items (RFC 2986 §4.1 CSR attributes). No inner SET, no EXPLICIT unwrap.
182
+ function implicitSetOf(tag, item, opts) {
183
+ opts = opts || {};
184
+ return { kind: "repeat", item: item, assert: "implicit", implicitTag: tag, derSetOrder: true, code: opts.code, what: opts.what,
185
+ min: opts.min, unique: opts.unique, dupCode: opts.dupCode, build: opts.build };
186
+ }
166
187
 
167
188
  // ---- the walk engine -------------------------------------------------
168
189
 
169
190
  /**
170
- * @primitive pki.asn1.schema.walk
171
- * @signature pki.asn1.schema.walk(schema, node, ctx) -> value
172
- * @since 0.1.6
191
+ * @primitive pki.schema.engine.walk
192
+ * @signature pki.schema.engine.walk(schema, node, ctx) -> value
193
+ * @since 0.1.7
173
194
  * @status experimental
174
195
  * @spec X.690, X.680
175
- * @related pki.asn1.decode, pki.x509.parse
196
+ * @related pki.asn1.decode, pki.schema.x509.parse
176
197
  *
177
198
  * Interpret a declarative schema against a decoded DER node, enforcing the
178
199
  * schema's structural rules (shape assertion, arity, optional / context-tagged
@@ -188,7 +209,7 @@ function setOfUnique(item, keyFn, opts) {
188
209
  * `octetString` / `bitString` / `any` / `decode` / `time`).
189
210
  *
190
211
  * @example
191
- * var S = pki.asn1.schema;
212
+ * var S = pki.schema.engine;
192
213
  * var ALGID = S.seq([S.field("algorithm", S.oidLeaf())],
193
214
  * { assert: "sequence", arity: { min: 1 }, code: "app/bad-alg" });
194
215
  * S.walk(ALGID, pki.asn1.decode(der), { prefix: "app", E: MyError, oid: pki.oid });
@@ -240,6 +261,19 @@ function _walkRepeat(schema, node, ctx) {
240
261
  if (schema.min != null && kids.length < schema.min) {
241
262
  _fail(ctx, schema.code, (schema.what || "value") + " must contain at least " + schema.min + " element(s)");
242
263
  }
264
+ // DER (X.690 §11.6) — the components of a SET OF appear in ascending order, the
265
+ // encodings compared as octet strings. SEQUENCE OF is order-preserving and is
266
+ // exempt (no derSetOrder). Compare full TLV byte strings, the same canonical
267
+ // order build.set emits, so a decode round-trips a build. Equal-adjacent
268
+ // encodings are permitted here (semantic uniqueness is the `unique` option's
269
+ // job); only a descending pair is non-canonical.
270
+ if (schema.derSetOrder) {
271
+ for (var s = 1; s < kids.length; s++) {
272
+ if (Buffer.compare(kids[s - 1].bytes, kids[s].bytes) > 0) {
273
+ _fail(ctx, schema.code, (schema.what || "value") + " components must be in ascending DER order (X.690 §11.6)");
274
+ }
275
+ }
276
+ }
243
277
  var items = [];
244
278
  var seen = schema.unique ? new Set() : null;
245
279
  for (var i = 0; i < kids.length; i++) {
@@ -331,7 +365,7 @@ function _consumeTrailing(fld, kids, start, fields, ctx) {
331
365
  module.exports = {
332
366
  // structural
333
367
  seq: seq, field: field, optional: optional, explicit: explicit, trailing: trailing,
334
- seqOf: seqOf, setOf: setOf, setOfUnique: setOfUnique, choice: choice,
368
+ seqOf: seqOf, setOf: setOf, setOfUnique: setOfUnique, implicitSetOf: implicitSetOf, choice: choice,
335
369
  // leaves
336
370
  oidLeaf: oidLeaf, integerLeaf: integerLeaf, boolean: boolean, octetString: octetString,
337
371
  bitString: bitString, any: any, decode: decode, time: time,