@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.
- package/CHANGELOG.md +41 -0
- package/README.md +13 -9
- package/bin/pki.js +2 -1
- package/index.js +8 -7
- package/lib/asn1-der.js +29 -11
- package/lib/constants.js +1 -1
- package/lib/framework-error.js +15 -0
- package/lib/oid.js +8 -3
- package/lib/schema-all.js +139 -0
- package/lib/schema-crl.js +251 -0
- package/lib/schema-csr.js +203 -0
- package/lib/{asn1-schema.js → schema-engine.js} +49 -15
- package/lib/schema-pkix.js +320 -0
- package/lib/schema-x509.js +243 -0
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
- package/lib/x509.js +0 -393
|
@@ -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.
|
|
6
|
-
* @nav
|
|
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
|
|
113
|
-
// [tag] (the certificate version [0] shape).
|
|
114
|
-
//
|
|
115
|
-
//
|
|
116
|
-
//
|
|
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
|
-
:
|
|
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.
|
|
171
|
-
* @signature pki.
|
|
172
|
-
* @since 0.1.
|
|
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.
|
|
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,
|