@blamejs/pki 0.1.21 → 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 +47 -0
- package/README.md +17 -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 +64 -32
- package/lib/oid.js +114 -58
- package/lib/path-validate.js +302 -135
- package/lib/schema-all.js +54 -40
- 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 +306 -111
- package/lib/schema-smime.js +363 -0
- 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-tsp.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*
|
|
11
11
|
* @intro
|
|
12
12
|
* RFC 3161 Time-Stamp Protocol handling. A `TimeStampResp` is what a client
|
|
13
|
-
* receives from a TSA
|
|
13
|
+
* receives from a TSA -- a `PKIStatusInfo` plus, on success, a `TimeStampToken`;
|
|
14
14
|
* `parse` decodes it and enforces the status-to-token coupling (a granted
|
|
15
15
|
* response carries a token, a rejection does not). A `TimeStampToken` is itself a
|
|
16
16
|
* CMS SignedData whose encapsulated content is a `TSTInfo`, so `parseToken`
|
|
@@ -21,13 +21,14 @@
|
|
|
21
21
|
* The parser surfaces everything a verifier needs and interprets nothing it
|
|
22
22
|
* cannot: `messageImprint.hashAlgorithm` and the raw `hashedMessage`, the
|
|
23
23
|
* `genTime` (with sub-second precision), the `serialNumber` and `nonce` (lossless,
|
|
24
|
-
* as BigInt + hex), and the `policy`. The
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
24
|
+
* as BigInt + hex), and the `policy`. The PRESENCE of the ESS
|
|
25
|
+
* SigningCertificate(V2) attribute is asserted at parse (a structural token
|
|
26
|
+
* property); the imprint-to-request and nonce-to-request round-trips, the ESS
|
|
27
|
+
* hash-vs-certificate binding, the timestamping EKU, and the signature are
|
|
28
|
+
* verification-layer concerns above parse altitude. DER-only, fail-closed.
|
|
28
29
|
*
|
|
29
30
|
* @card
|
|
30
|
-
* Parse DER / PEM RFC 3161 timestamp responses and tokens
|
|
31
|
+
* Parse DER / PEM RFC 3161 timestamp responses and tokens -- per-response status,
|
|
31
32
|
* the TSTInfo payload (imprint, genTime, serial, nonce, accuracy), raw verifier
|
|
32
33
|
* inputs, single-signer token composition over CMS, fail-closed.
|
|
33
34
|
*/
|
|
@@ -47,16 +48,22 @@ var NS = pkix.makeNS("tsp", TspError, oid);
|
|
|
47
48
|
var ALGORITHM_IDENTIFIER = pkix.algorithmIdentifier(NS);
|
|
48
49
|
var EXTENSION = pkix.extension(NS);
|
|
49
50
|
|
|
50
|
-
// TSTInfo.version is INTEGER { v1(1) }
|
|
51
|
+
// TSTInfo.version is INTEGER { v1(1) } -- the only legal value is 1.
|
|
51
52
|
var VERSION = pkix.versionReader(NS, { "1": 1 });
|
|
52
53
|
|
|
53
54
|
// id-ct-TSTInfo is the eContentType that identifies a timestamp token (RFC 3161
|
|
54
|
-
//
|
|
55
|
+
// sec. 2.4.2); resolved from the registry, never a dotted literal.
|
|
55
56
|
var OID_TST_INFO = oid.byName("tSTInfo");
|
|
56
57
|
|
|
58
|
+
// The ESS certificate-identifier attributes: RFC 3161 sec. 2.4.2 requires the TSA
|
|
59
|
+
// certificate identifier as a SigningCertificate signed attribute; RFC 5816
|
|
60
|
+
// additionally permits SigningCertificateV2.
|
|
61
|
+
var OID_SIGNING_CERT = oid.byName("signingCertificate");
|
|
62
|
+
var OID_SIGNING_CERT_V2 = oid.byName("signingCertificateV2");
|
|
63
|
+
|
|
57
64
|
var TAGS = asn1.TAGS;
|
|
58
65
|
|
|
59
|
-
// PKIFailureInfo ::= BIT STRING NamedBitList (RFC 3161
|
|
66
|
+
// PKIFailureInfo ::= BIT STRING NamedBitList (RFC 3161 sec. 2.4.2). bit 0 is the MSB of
|
|
60
67
|
// the first content octet; a set bit outside the named set is surfaced by index.
|
|
61
68
|
var FAILURE_BITS = {
|
|
62
69
|
0: "badAlg", 2: "badRequest", 5: "badDataFormat", 14: "timeNotAvailable",
|
|
@@ -65,33 +72,23 @@ var FAILURE_BITS = {
|
|
|
65
72
|
|
|
66
73
|
// ---- shared leaves ---------------------------------------------------
|
|
67
74
|
|
|
68
|
-
// GeneralizedTime-only leaf. RFC 3161
|
|
75
|
+
// GeneralizedTime-only leaf. RFC 3161 sec. 2.4.2 requires genTime to be GeneralizedTime,
|
|
69
76
|
// never UTCTime; assert the tag before the (fractional-capable) codec read.
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
throw ctx.E("tsp/bad-gentime", "genTime must be a GeneralizedTime (RFC 3161 §2.4.2)");
|
|
73
|
-
}
|
|
74
|
-
// RFC 3161 genTime may carry sub-second precision (X.690 §11.7 fractional profile).
|
|
75
|
-
return asn1.read.time(n, { allowFractional: true });
|
|
76
|
-
});
|
|
77
|
+
// RFC 3161 genTime may carry sub-second precision (X.690 sec. 11.7 fractional profile).
|
|
78
|
+
var GEN_TIME = pkix.generalizedTime(NS, { code: "tsp/bad-gentime", message: "genTime must be a GeneralizedTime (RFC 3161 sec. 2.4.2)", allowFractional: true });
|
|
77
79
|
|
|
78
|
-
// tsa [0] EXPLICIT GeneralName (RFC 3161
|
|
79
|
-
// shared pkix.generalName primitive (RFC 5280
|
|
80
|
+
// tsa [0] EXPLICIT GeneralName (RFC 3161 sec. 2.4.2) -- validated + surfaced raw via the
|
|
81
|
+
// shared pkix.generalName primitive (RFC 5280 sec. 4.2.1.6), which checks the chosen
|
|
80
82
|
// alternative's form and content so a malformed GeneralName fails closed.
|
|
81
83
|
var GENERAL_NAME_RAW = pkix.generalName(NS, { code: "tsp/bad-tsa" });
|
|
82
84
|
|
|
83
|
-
// A PKIFreeText element MUST be a UTF8String (RFC 3161
|
|
84
|
-
var UTF8_TEXT =
|
|
85
|
-
if (n.tagClass !== "universal" || n.tagNumber !== TAGS.UTF8_STRING) {
|
|
86
|
-
throw ctx.E("tsp/bad-status-info", "PKIFreeText elements must be UTF8String");
|
|
87
|
-
}
|
|
88
|
-
return asn1.read.string(n);
|
|
89
|
-
});
|
|
85
|
+
// A PKIFreeText element MUST be a UTF8String (RFC 3161 sec. 2.4.2).
|
|
86
|
+
var UTF8_TEXT = pkix.utf8Text(NS, { code: "tsp/bad-status-info", message: "PKIFreeText elements must be UTF8String" });
|
|
90
87
|
|
|
91
88
|
// ---- MessageImprint --------------------------------------------------
|
|
92
89
|
|
|
93
90
|
// MessageImprint ::= SEQUENCE { hashAlgorithm AlgorithmIdentifier, hashedMessage
|
|
94
|
-
// OCTET STRING } (RFC 3161
|
|
91
|
+
// OCTET STRING } (RFC 3161 sec. 2.4.1). hashedMessage is a digest -- surfaced RAW.
|
|
95
92
|
var MESSAGE_IMPRINT = schema.seq([
|
|
96
93
|
schema.field("hashAlgorithm", ALGORITHM_IDENTIFIER),
|
|
97
94
|
schema.field("hashedMessage", schema.octetString()),
|
|
@@ -106,7 +103,7 @@ var MESSAGE_IMPRINT = schema.seq([
|
|
|
106
103
|
|
|
107
104
|
// Accuracy ::= SEQUENCE { seconds INTEGER OPTIONAL, millis [0] IMPLICIT INTEGER
|
|
108
105
|
// (1..999) OPTIONAL, micros [1] IMPLICIT INTEGER (1..999) OPTIONAL } (RFC 3161
|
|
109
|
-
//
|
|
106
|
+
// sec. 2.4.2). Every sub-field is optional; a missing one defaults to 0.
|
|
110
107
|
var ACCURACY = schema.seq([
|
|
111
108
|
schema.optional("seconds", schema.integerLeaf(), { whenUniversal: [TAGS.INTEGER] }),
|
|
112
109
|
schema.trailing([
|
|
@@ -119,13 +116,19 @@ var ACCURACY = schema.seq([
|
|
|
119
116
|
function sub(f) {
|
|
120
117
|
if (!f.present) return 0;
|
|
121
118
|
var v = f.value;
|
|
122
|
-
// millis / micros are constrained to 1..999 (RFC 3161
|
|
119
|
+
// millis / micros are constrained to 1..999 (RFC 3161 sec. 2.4.2); 0 is also
|
|
123
120
|
// excluded (and non-canonical for a positive count).
|
|
124
121
|
if (v < 1n || v > 999n) throw NS.E("tsp/bad-accuracy", "Accuracy millis/micros must be in 1..999");
|
|
125
122
|
return Number(v);
|
|
126
123
|
}
|
|
124
|
+
// Accuracy is the +/- deviation added to and subtracted from genTime (RFC
|
|
125
|
+
// 3161 sec. 2.4.2), so a negative seconds inverts the time window a verifier
|
|
126
|
+
// computes -- the same semantically-impossible class as a 0 millis/micros.
|
|
127
|
+
if (m.fields.seconds.present && m.fields.seconds.value < 0n) {
|
|
128
|
+
throw NS.E("tsp/bad-accuracy", "Accuracy seconds must not be negative (RFC 3161 sec. 2.4.2)");
|
|
129
|
+
}
|
|
127
130
|
return {
|
|
128
|
-
// seconds is an unbounded INTEGER
|
|
131
|
+
// seconds is an unbounded INTEGER -- keep it lossless as a BigInt (millis /
|
|
129
132
|
// micros are constrained to 1..999, so Number is exact for those).
|
|
130
133
|
seconds: m.fields.seconds.present ? m.fields.seconds.value : 0n,
|
|
131
134
|
millis: sub(m.fields.millis),
|
|
@@ -139,7 +142,7 @@ var ACCURACY = schema.seq([
|
|
|
139
142
|
// TSTInfo ::= SEQUENCE { version, policy, messageImprint, serialNumber, genTime,
|
|
140
143
|
// accuracy OPTIONAL, ordering DEFAULT FALSE, nonce OPTIONAL, tsa [0] EXPLICIT
|
|
141
144
|
// GeneralName OPTIONAL, extensions [1] IMPLICIT Extensions OPTIONAL } (RFC 3161
|
|
142
|
-
//
|
|
145
|
+
// sec. 2.4.2). The five mandatory fields come first; the optionals are consumed by tag.
|
|
143
146
|
var TST_INFO = schema.seq([
|
|
144
147
|
schema.field("version", VERSION),
|
|
145
148
|
schema.field("policy", schema.oidLeaf()),
|
|
@@ -156,9 +159,9 @@ var TST_INFO = schema.seq([
|
|
|
156
159
|
], {
|
|
157
160
|
assert: "sequence", code: "tsp/bad-tst-info", what: "TSTInfo",
|
|
158
161
|
build: function (m, ctx) {
|
|
159
|
-
// ordering BOOLEAN DEFAULT FALSE
|
|
162
|
+
// ordering BOOLEAN DEFAULT FALSE -- an explicit FALSE must be omitted (DER).
|
|
160
163
|
if (m.fields.ordering.present && m.fields.ordering.value === false) {
|
|
161
|
-
throw NS.E("tsp/bad-ordering", "ordering is BOOLEAN DEFAULT FALSE
|
|
164
|
+
throw NS.E("tsp/bad-ordering", "ordering is BOOLEAN DEFAULT FALSE -- an explicit FALSE must be omitted");
|
|
162
165
|
}
|
|
163
166
|
var policy = m.fields.policy.value;
|
|
164
167
|
var tsa = m.fields.tsa;
|
|
@@ -187,7 +190,7 @@ var TST_INFO = schema.seq([
|
|
|
187
190
|
// ---- PKIStatusInfo / TimeStampResp -----------------------------------
|
|
188
191
|
|
|
189
192
|
// PKIStatusInfo ::= SEQUENCE { status PKIStatus (INTEGER), statusString PKIFreeText
|
|
190
|
-
// OPTIONAL, failInfo PKIFailureInfo OPTIONAL } (RFC 3161
|
|
193
|
+
// OPTIONAL, failInfo PKIFailureInfo OPTIONAL } (RFC 3161 sec. 2.4.2, from RFC 4210).
|
|
191
194
|
var PKI_STATUS_INFO = schema.seq([
|
|
192
195
|
schema.field("status", schema.integerLeaf()),
|
|
193
196
|
schema.optional("statusString", schema.seqOf(UTF8_TEXT, { assert: "sequence", min: 1, code: "tsp/bad-status-info", what: "statusString" }), { whenUniversal: [TAGS.SEQUENCE] }),
|
|
@@ -197,7 +200,7 @@ var PKI_STATUS_INFO = schema.seq([
|
|
|
197
200
|
build: function (m) {
|
|
198
201
|
var status = m.fields.status.value;
|
|
199
202
|
// PKIStatus ::= INTEGER { granted(0) .. revocationNotification(5) }.
|
|
200
|
-
if (status < 0n || status > 5n) throw NS.E("tsp/bad-status", "PKIStatus " + status + " is outside 0..5 (RFC 3161
|
|
203
|
+
if (status < 0n || status > 5n) throw NS.E("tsp/bad-status", "PKIStatus " + status + " is outside 0..5 (RFC 3161 sec. 2.4.2)");
|
|
201
204
|
var failInfo = null;
|
|
202
205
|
if (m.fields.failInfo.present) {
|
|
203
206
|
var bs = m.fields.failInfo.value;
|
|
@@ -212,7 +215,7 @@ var PKI_STATUS_INFO = schema.seq([
|
|
|
212
215
|
},
|
|
213
216
|
});
|
|
214
217
|
|
|
215
|
-
// X.690
|
|
218
|
+
// X.690 sec. 11.2.2 -- a BIT STRING typed with a NamedBitList (PKIFailureInfo) MUST have
|
|
216
219
|
// all trailing 0 bits removed under DER: no trailing all-zero content octet, and the
|
|
217
220
|
// declared unusedBits must sit exactly below the lowest set bit of the last octet (so
|
|
218
221
|
// the encoding is minimal). The empty value encodes as 0 content octets, 0 unusedBits.
|
|
@@ -221,15 +224,15 @@ function _assertMinimalNamedBits(unusedBits, bytes) {
|
|
|
221
224
|
}
|
|
222
225
|
|
|
223
226
|
// Decode the set NamedBitList bits to their RFC 3161 names (bit 0 = MSB of byte 0).
|
|
224
|
-
// A set bit outside the defined set is an unsupported PKIFailureInfo value
|
|
225
|
-
// MUST error on a failInfo it does not understand (RFC 3161
|
|
227
|
+
// A set bit outside the defined set is an unsupported PKIFailureInfo value -- a client
|
|
228
|
+
// MUST error on a failInfo it does not understand (RFC 3161 sec. 2.4.2), so reject it
|
|
226
229
|
// rather than surfacing an opaque "bitN".
|
|
227
230
|
function _namedBits(bytes) {
|
|
228
231
|
var out = [];
|
|
229
232
|
for (var i = 0; i < bytes.length * 8; i++) {
|
|
230
233
|
if ((bytes[i >> 3] >> (7 - (i & 7))) & 1) {
|
|
231
234
|
var nm = FAILURE_BITS[i];
|
|
232
|
-
if (!nm) throw NS.E("tsp/bad-failinfo", "unsupported PKIFailureInfo bit " + i + " (RFC 3161
|
|
235
|
+
if (!nm) throw NS.E("tsp/bad-failinfo", "unsupported PKIFailureInfo bit " + i + " (RFC 3161 sec. 2.4.2)");
|
|
233
236
|
out.push(nm);
|
|
234
237
|
}
|
|
235
238
|
}
|
|
@@ -237,8 +240,10 @@ function _namedBits(bytes) {
|
|
|
237
240
|
}
|
|
238
241
|
|
|
239
242
|
// TimeStampResp ::= SEQUENCE { status PKIStatusInfo, timeStampToken TimeStampToken
|
|
240
|
-
// OPTIONAL } (RFC 3161
|
|
243
|
+
// OPTIONAL } (RFC 3161 sec. 2.4.2). There is NO version field. The token is a CMS
|
|
241
244
|
// ContentInfo, decoded via cms.parse; the status-to-token coupling is load-bearing.
|
|
245
|
+
// The top-level reject code predates the `<prefix>/not-a-<structure>` convention
|
|
246
|
+
// and stays frozen on the public error-code surface.
|
|
242
247
|
var TIME_STAMP_RESP = schema.seq([
|
|
243
248
|
schema.field("status", PKI_STATUS_INFO),
|
|
244
249
|
schema.optional("timeStampToken", schema.any(), { whenUniversal: [TAGS.SEQUENCE] }),
|
|
@@ -247,15 +252,15 @@ var TIME_STAMP_RESP = schema.seq([
|
|
|
247
252
|
build: function (m) {
|
|
248
253
|
var status = m.fields.status.value.result;
|
|
249
254
|
var present = m.fields.timeStampToken.present;
|
|
250
|
-
// RFC 3161
|
|
255
|
+
// RFC 3161 sec. 2.4.2 -- granted / grantedWithMods carry a token; any other status
|
|
251
256
|
// (rejection / waiting / revocation*) MUST NOT.
|
|
252
257
|
var granted = status.status === 0 || status.status === 1;
|
|
253
|
-
if (granted && !present) throw NS.E("tsp/missing-token", "a granted TimeStampResp must carry a timeStampToken (RFC 3161
|
|
254
|
-
if (!granted && present) throw NS.E("tsp/unexpected-token", "a non-granted TimeStampResp must not carry a timeStampToken (RFC 3161
|
|
258
|
+
if (granted && !present) throw NS.E("tsp/missing-token", "a granted TimeStampResp must carry a timeStampToken (RFC 3161 sec. 2.4.2)");
|
|
259
|
+
if (!granted && present) throw NS.E("tsp/unexpected-token", "a non-granted TimeStampResp must not carry a timeStampToken (RFC 3161 sec. 2.4.2)");
|
|
255
260
|
// failInfo is the reason a request was rejected, so it is present ONLY when the
|
|
256
|
-
// status is rejection(2)
|
|
257
|
-
if (status.status !== 2 && status.failInfo) throw NS.E("tsp/unexpected-failinfo", "failInfo is present only when the status is rejection(2) (RFC 3161
|
|
258
|
-
// A granted response's timeStampToken MUST be a well-formed TimeStampToken
|
|
261
|
+
// status is rejection(2) -- not on granted(0/1) nor on waiting / revocation* (3/4/5).
|
|
262
|
+
if (status.status !== 2 && status.failInfo) throw NS.E("tsp/unexpected-failinfo", "failInfo is present only when the status is rejection(2) (RFC 3161 sec. 2.4.2)");
|
|
263
|
+
// A granted response's timeStampToken MUST be a well-formed TimeStampToken --
|
|
259
264
|
// decode it (composing the CMS parser) rather than surfacing arbitrary SEQUENCE
|
|
260
265
|
// bytes as a token; a malformed token fails the response parse.
|
|
261
266
|
return {
|
|
@@ -275,8 +280,8 @@ var TIME_STAMP_RESP = schema.seq([
|
|
|
275
280
|
* @spec RFC 3161
|
|
276
281
|
* @related pki.schema.tsp.parseToken, pki.schema.tsp.parse
|
|
277
282
|
*
|
|
278
|
-
* Parse a bare `TSTInfo` payload (a DER `Buffer`)
|
|
279
|
-
* encapsulates
|
|
283
|
+
* Parse a bare `TSTInfo` payload (a DER `Buffer`) -- the structure a timestamp token
|
|
284
|
+
* encapsulates -- into `{ version, policy, messageImprint, serialNumber, genTime,
|
|
280
285
|
* accuracy, ordering, nonce, tsa, extensions }`. `messageImprint.hashedMessage` is
|
|
281
286
|
* the raw digest; `serialNumber` / `nonce` are lossless (BigInt + hex); `genTime` is
|
|
282
287
|
* a `Date` with sub-second precision. A malformed structure throws a typed
|
|
@@ -299,7 +304,7 @@ var parseTstInfo = pkix.makeParser({ pemLabel: null, PemError: PemError, ErrorCl
|
|
|
299
304
|
*
|
|
300
305
|
* Parse a DER `Buffer` or a PEM string into a `TimeStampResp`: `{ status,
|
|
301
306
|
* statusString, failInfo, timeStampToken }`. The status-to-token coupling is
|
|
302
|
-
* enforced
|
|
307
|
+
* enforced -- a granted response (status 0/1) carries `timeStampToken` (surfaced raw
|
|
303
308
|
* for `parseToken`), any other status does not. `failInfo` decodes the
|
|
304
309
|
* `PKIFailureInfo` named bits. A malformed structure throws a typed `TspError`.
|
|
305
310
|
*
|
|
@@ -318,13 +323,17 @@ var parse = pkix.makeParser({ pemLabel: null, PemError: PemError, ErrorClass: Ts
|
|
|
318
323
|
* @spec RFC 3161, RFC 5652
|
|
319
324
|
* @related pki.schema.tsp.parse, pki.schema.cms.parse
|
|
320
325
|
*
|
|
321
|
-
* Parse a `TimeStampToken` (a DER `Buffer` or PEM)
|
|
326
|
+
* Parse a `TimeStampToken` (a DER `Buffer` or PEM) -- a CMS SignedData whose
|
|
322
327
|
* encapsulated content is a `TSTInfo`. Composes `pki.schema.cms.parse`, asserts the
|
|
323
328
|
* `id-ct-TSTInfo` content type (`tsp/wrong-econtent-type`), that the content is
|
|
324
|
-
* attached (`tsp/detached-token`),
|
|
325
|
-
* RFC 3161
|
|
326
|
-
*
|
|
327
|
-
*
|
|
329
|
+
* attached (`tsp/detached-token`), the single-signer rule (`tsp/multi-signer`,
|
|
330
|
+
* RFC 3161 sec. 2.4.2), and that the signerInfo carries a SigningCertificate or
|
|
331
|
+
* SigningCertificateV2 signed attribute (`tsp/missing-signing-certificate`,
|
|
332
|
+
* RFC 3161 sec. 2.4.2 / RFC 5816 -- the hash-vs-certificate binding stays a
|
|
333
|
+
* verification-layer concern), then decodes the inner `TSTInfo`. Returns
|
|
334
|
+
* `{ tstInfo, eContent, signerInfo, certificates }` -- the decoded payload, the raw
|
|
335
|
+
* eContent bytes a verifier hashes for the CMS message-digest, and the CMS signer
|
|
336
|
+
* material.
|
|
328
337
|
*
|
|
329
338
|
* @example
|
|
330
339
|
* var token = pki.schema.tsp.parseToken(tokenDer);
|
|
@@ -333,10 +342,10 @@ var parse = pkix.makeParser({ pemLabel: null, PemError: PemError, ErrorClass: Ts
|
|
|
333
342
|
*/
|
|
334
343
|
function parseToken(input) {
|
|
335
344
|
// De-armor with TSP's label-agnostic PEM rules first (RFC 3161 has no standard
|
|
336
|
-
// label), THEN hand DER to the CMS parser
|
|
345
|
+
// label), THEN hand DER to the CMS parser -- otherwise cms.parse would reject any
|
|
337
346
|
// PEM block not labeled "CMS" before the TSP checks run.
|
|
338
347
|
var der = pkix.coerceToDer(input, { pemLabel: null, PemError: PemError, ErrorClass: TspError, prefix: "tsp" });
|
|
339
|
-
// Wrap the CMS decode so tsp.parse / parseToken keep their typed-TspError contract
|
|
348
|
+
// Wrap the CMS decode so tsp.parse / parseToken keep their typed-TspError contract --
|
|
340
349
|
// a malformed token surfaces tsp/bad-token, not a bare cms/* error.
|
|
341
350
|
var signed;
|
|
342
351
|
try { signed = cms.parse(der); }
|
|
@@ -344,13 +353,35 @@ function parseToken(input) {
|
|
|
344
353
|
if (e instanceof TspError) throw e;
|
|
345
354
|
throw new TspError("tsp/bad-token", "the timeStampToken did not decode as a CMS SignedData: " + ((e && e.message) || String(e)), e);
|
|
346
355
|
}
|
|
356
|
+
// cms.parse decodes every CMS content type; a TimeStampToken MUST be a
|
|
357
|
+
// SignedData (RFC 3161 sec. 2.4.2). Gate on the dispatched content type before
|
|
358
|
+
// any SignedData-only field access -- an AuthenticatedData / EnvelopedData
|
|
359
|
+
// whose eContentType happens to be id-ct-TSTInfo would otherwise reach a raw
|
|
360
|
+
// TypeError on the absent signerInfos.
|
|
361
|
+
if (signed.contentTypeName !== "signedData") {
|
|
362
|
+
throw new TspError("tsp/not-signed-data", "a TimeStampToken must be a CMS SignedData, got " + (signed.contentTypeName || signed.contentType));
|
|
363
|
+
}
|
|
347
364
|
var encap = signed.encapContentInfo;
|
|
348
365
|
if (encap.eContentType !== OID_TST_INFO) {
|
|
349
366
|
throw new TspError("tsp/wrong-econtent-type", "a TimeStampToken must encapsulate id-ct-TSTInfo, got " + encap.eContentType);
|
|
350
367
|
}
|
|
351
|
-
if (encap.eContent === null) throw new TspError("tsp/detached-token", "a TimeStampToken must carry attached eContent (RFC 3161
|
|
368
|
+
if (encap.eContent === null) throw new TspError("tsp/detached-token", "a TimeStampToken must carry attached eContent (RFC 3161 sec. 2.4.2)");
|
|
352
369
|
if (signed.signerInfos.length !== 1) {
|
|
353
|
-
throw new TspError("tsp/multi-signer", "a TimeStampToken must contain exactly one (TSA) signerInfo (RFC 3161
|
|
370
|
+
throw new TspError("tsp/multi-signer", "a TimeStampToken must contain exactly one (TSA) signerInfo (RFC 3161 sec. 2.4.2)");
|
|
371
|
+
}
|
|
372
|
+
// RFC 3161 sec. 2.4.2 / RFC 5816 -- the TSA certificate identifier MUST be present
|
|
373
|
+
// as a SigningCertificate (or SigningCertificateV2) signed attribute. Presence
|
|
374
|
+
// is a structural token property, same altitude as the single-signer rule; the
|
|
375
|
+
// hash-vs-certificate BINDING stays a verification-layer concern. signedAttrs
|
|
376
|
+
// is non-null here: the CMS parse already required it for a non-id-data
|
|
377
|
+
// eContentType (RFC 5652 sec. 5.3), and the eContentType is id-ct-TSTInfo.
|
|
378
|
+
var signedAttrs = signed.signerInfos[0].signedAttrs || [];
|
|
379
|
+
var hasSigningCert = false;
|
|
380
|
+
for (var a = 0; a < signedAttrs.length; a++) {
|
|
381
|
+
if (signedAttrs[a].type === OID_SIGNING_CERT || signedAttrs[a].type === OID_SIGNING_CERT_V2) { hasSigningCert = true; break; }
|
|
382
|
+
}
|
|
383
|
+
if (!hasSigningCert) {
|
|
384
|
+
throw new TspError("tsp/missing-signing-certificate", "a TimeStampToken signerInfo must carry a SigningCertificate or SigningCertificateV2 signed attribute (RFC 3161 sec. 2.4.2, RFC 5816)");
|
|
354
385
|
}
|
|
355
386
|
var tstInfo;
|
|
356
387
|
try { tstInfo = schema.embeddedDer(TST_INFO, encap.eContent, NS, { code: "tsp/bad-der", what: "the encapsulated TSTInfo" }); }
|
|
@@ -359,7 +390,7 @@ function parseToken(input) {
|
|
|
359
390
|
throw new TspError("tsp/bad-der", "the encapsulated TSTInfo did not decode: " + ((e && e.message) || String(e)), e);
|
|
360
391
|
}
|
|
361
392
|
// Surface the RAW eContent (the exact DER a verifier hashes for the CMS
|
|
362
|
-
// message-digest signed attribute) alongside the decoded TSTInfo
|
|
393
|
+
// message-digest signed attribute) alongside the decoded TSTInfo -- a re-serialized
|
|
363
394
|
// tstInfo may not byte-match, so the raw bytes are the verification feed.
|
|
364
395
|
return { tstInfo: tstInfo.result, eContent: encap.eContent, signerInfo: signed.signerInfos[0], certificates: signed.certificates };
|
|
365
396
|
}
|
|
@@ -381,6 +412,23 @@ function parseToken(input) {
|
|
|
381
412
|
*/
|
|
382
413
|
function pemDecode(text, label) { return pkix.pemDecode(text, label || null, PemError); }
|
|
383
414
|
|
|
415
|
+
/**
|
|
416
|
+
* @primitive pki.schema.tsp.pemEncode
|
|
417
|
+
* @signature pki.schema.tsp.pemEncode(der, label) -> string
|
|
418
|
+
* @since 0.1.23
|
|
419
|
+
* @status stable
|
|
420
|
+
* @spec RFC 7468, RFC 3161
|
|
421
|
+
* @related pki.schema.tsp.pemDecode
|
|
422
|
+
*
|
|
423
|
+
* Wrap DER bytes in a PEM envelope. RFC 3161 defines no standard PEM label, so
|
|
424
|
+
* `label` is REQUIRED -- the operator names the envelope explicitly (mirroring
|
|
425
|
+
* `pemDecode`, which accepts any label). Omitting it throws `pem/bad-label`.
|
|
426
|
+
*
|
|
427
|
+
* @example
|
|
428
|
+
* var pem = pki.schema.tsp.pemEncode(der, "TSP RESPONSE");
|
|
429
|
+
*/
|
|
430
|
+
function pemEncode(der, label) { return pkix.pemEncode(der, label, PemError); }
|
|
431
|
+
|
|
384
432
|
// A TimeStampResp root is a SEQUENCE of 1-2 whose first child is a PKIStatusInfo (a
|
|
385
433
|
// SEQUENCE whose own first child is an INTEGER status). Disjoint from the OID-first
|
|
386
434
|
// CMS ContentInfo, the INTEGER-first PKCS#8 key, and the exactly-3 signed-envelope
|
|
@@ -400,5 +448,6 @@ module.exports = {
|
|
|
400
448
|
parseTstInfo: parseTstInfo,
|
|
401
449
|
parseToken: parseToken,
|
|
402
450
|
pemDecode: pemDecode,
|
|
451
|
+
pemEncode: pemEncode,
|
|
403
452
|
matches: matches,
|
|
404
453
|
};
|
package/lib/schema-x509.js
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*
|
|
12
12
|
* @intro
|
|
13
13
|
* X.509 certificate handling per RFC 5280. The seed surface is
|
|
14
|
-
* `parse`
|
|
14
|
+
* `parse` -- turn a DER or PEM certificate into a structured,
|
|
15
15
|
* fully-decoded object: version, serial, signature algorithm, issuer
|
|
16
16
|
* and subject distinguished names, validity window (as real `Date`s),
|
|
17
17
|
* subject public-key info, and the extension list. The parser composes
|
|
@@ -48,15 +48,16 @@ var PemError = frameworkError.PemError;
|
|
|
48
48
|
* @spec RFC 7468, RFC 5280
|
|
49
49
|
* @related pki.schema.x509.pemEncode
|
|
50
50
|
*
|
|
51
|
-
* Extract the DER bytes from a PEM block
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
51
|
+
* Extract the DER bytes from a PEM block (default label `CERTIFICATE`, the
|
|
52
|
+
* RFC 7468 sec. 5 armor -- the canonical-label default every sibling format
|
|
53
|
+
* applies). Pass a `label` to enforce a different block type, or an explicit
|
|
54
|
+
* `null` to take the first block of any type. Throws `PemError` on a
|
|
55
|
+
* missing / mismatched envelope or a non-base64 body.
|
|
55
56
|
*
|
|
56
57
|
* @example
|
|
57
|
-
* var der = pki.schema.x509.pemDecode(pemText
|
|
58
|
+
* var der = pki.schema.x509.pemDecode(pemText);
|
|
58
59
|
*/
|
|
59
|
-
function pemDecode(text, label) { return pkix.pemDecode(text, label, PemError); }
|
|
60
|
+
function pemDecode(text, label) { return pkix.pemDecode(text, label === null ? null : (label || "CERTIFICATE"), PemError); }
|
|
60
61
|
|
|
61
62
|
/**
|
|
62
63
|
* @primitive pki.schema.x509.pemEncode
|
|
@@ -85,17 +86,19 @@ var NS = pkix.makeNS("x509", CertificateError, oid);
|
|
|
85
86
|
var ALGORITHM_IDENTIFIER = pkix.algorithmIdentifier(NS);
|
|
86
87
|
var NAME = pkix.name(NS);
|
|
87
88
|
|
|
88
|
-
// Validity ::= SEQUENCE { notBefore Time, notAfter Time }
|
|
89
|
-
//
|
|
89
|
+
// Validity ::= SEQUENCE { notBefore Time, notAfter Time } -- the outer tag is
|
|
90
|
+
// asserted (a SET-tagged body is a different ASN.1 type, not a Validity).
|
|
91
|
+
// pkix.time enforces the RFC 5280 sec. 4.1.2.5 encoding cutover on decode: a
|
|
92
|
+
// validity date through 2049 must be UTCTime, GeneralizedTime is 2050 onward.
|
|
90
93
|
var VALIDITY = schema.seq([
|
|
91
|
-
schema.field("notBefore",
|
|
92
|
-
schema.field("notAfter",
|
|
94
|
+
schema.field("notBefore", pkix.time(NS)),
|
|
95
|
+
schema.field("notAfter", pkix.time(NS)),
|
|
93
96
|
], {
|
|
94
|
-
assert: "
|
|
97
|
+
assert: "sequence", arity: { exact: 2 }, code: "x509/bad-validity", what: "Validity",
|
|
95
98
|
build: function (m) { return { notBefore: m.fields.notBefore.value, notAfter: m.fields.notAfter.value }; },
|
|
96
99
|
});
|
|
97
100
|
|
|
98
|
-
// SubjectPublicKeyInfo
|
|
101
|
+
// SubjectPublicKeyInfo -- the shared pkix.spki factory under the x509 NS.
|
|
99
102
|
var SPKI = pkix.spki(NS);
|
|
100
103
|
|
|
101
104
|
var EXTENSIONS = pkix.extensions(NS);
|
|
@@ -103,7 +106,7 @@ var EXTENSIONS = pkix.extensions(NS);
|
|
|
103
106
|
// Version ::= INTEGER { v1(0), v2(1), v3(2) }, [0] EXPLICIT DEFAULT v1. Read as a
|
|
104
107
|
// BigInt so an out-of-range value can't coerce to a float; reject an explicitly-
|
|
105
108
|
// encoded v1 (DER forbids encoding the DEFAULT).
|
|
106
|
-
// version ::= INTEGER { v1(0), v2(1), v3(2) } inside a [0] EXPLICIT DEFAULT v1
|
|
109
|
+
// version ::= INTEGER { v1(0), v2(1), v3(2) } inside a [0] EXPLICIT DEFAULT v1 --
|
|
107
110
|
// DER forbids encoding the default, so 0 is rejected; 1->v2, 2->v3.
|
|
108
111
|
var CERTIFICATE_VERSION = pkix.versionReader(NS, { "1": 2, "2": 3 });
|
|
109
112
|
|
|
@@ -122,19 +125,21 @@ var CERTIFICATE_TBS = schema.seq([
|
|
|
122
125
|
schema.field("subject", NAME),
|
|
123
126
|
schema.field("subjectPublicKeyInfo", SPKI),
|
|
124
127
|
schema.trailing([
|
|
125
|
-
|
|
126
|
-
|
|
128
|
+
// RFC 5280 sec. 4.1.2.8 -- the unique identifiers are [n] IMPLICIT BIT
|
|
129
|
+
// STRING (context PRIMITIVE, unusedBits validated), never an EXPLICIT wrap.
|
|
130
|
+
{ tag: 1, name: "issuerUniqueID", schema: schema.implicitBitString(1) },
|
|
131
|
+
{ tag: 2, name: "subjectUniqueID", schema: schema.implicitBitString(2) },
|
|
127
132
|
{ tag: 3, name: "extensions", schema: EXTENSIONS, explicit: true, emptyCode: "x509/bad-extensions" },
|
|
128
133
|
], { minTag: 1, maxTag: 3, unexpectedCode: "x509/bad-tbs", orderCode: "x509/bad-tbs" }),
|
|
129
|
-
], { assert: "
|
|
134
|
+
], { assert: "sequence", code: "x509/bad-tbs", what: "tbsCertificate" });
|
|
130
135
|
|
|
131
136
|
// Certificate ::= SEQUENCE { tbsCertificate, signatureAlgorithm, signatureValue
|
|
132
|
-
// BIT STRING }. The build runs the RFC 5280 cross-field checks (
|
|
133
|
-
// agreement,
|
|
137
|
+
// BIT STRING }. The build runs the RFC 5280 cross-field checks (sec. 4.1.1.2 sig-alg
|
|
138
|
+
// agreement, sec. 4.1.2.4 non-empty issuer, sec. 4.1.2.9 extensions-only-in-v3) after the
|
|
134
139
|
// structural walk, then assembles the public parse result. Raw-byte accessors
|
|
135
140
|
// (serialNumberHex, tbsBytes, sig-alg agreement) read off the match-tree nodes.
|
|
136
141
|
// Certificate ::= SEQUENCE { tbsCertificate, signatureAlgorithm, signatureValue }
|
|
137
|
-
//
|
|
142
|
+
// -- the shared SIGNED envelope. The certificate-specific invariants (outer==inner
|
|
138
143
|
// signatureAlgorithm agreement, non-empty issuer, v3-only extensions) live in the
|
|
139
144
|
// build; the envelope owns the SEQUENCE-of-3 shape and signature extraction.
|
|
140
145
|
var CERTIFICATE = pkix.signedEnvelope(NS, CERTIFICATE_TBS, {
|
|
@@ -142,25 +147,31 @@ var CERTIFICATE = pkix.signedEnvelope(NS, CERTIFICATE_TBS, {
|
|
|
142
147
|
build: function (e, ctx) {
|
|
143
148
|
var tbs = e.tbsMatch; // CERTIFICATE_TBS seq-match (no build)
|
|
144
149
|
|
|
145
|
-
// RFC 5280
|
|
150
|
+
// RFC 5280 sec. 4.1.1.2 -- outer signatureAlgorithm MUST equal tbsCertificate.signature.
|
|
146
151
|
if (!e.outerSignatureAlgorithmBytes.equals(tbs.fields.signature.node.bytes)) {
|
|
147
|
-
throw ctx.E("x509/bad-signature-algorithm", "signatureAlgorithm must match tbsCertificate.signature (RFC 5280
|
|
152
|
+
throw ctx.E("x509/bad-signature-algorithm", "signatureAlgorithm must match tbsCertificate.signature (RFC 5280 sec. 4.1.1.2)");
|
|
148
153
|
}
|
|
149
154
|
|
|
150
155
|
var version = tbs.fields.version.value; // 1 (default) | 2 | 3
|
|
151
156
|
var issuer = tbs.fields.issuer.value.result;
|
|
152
|
-
// RFC 5280
|
|
157
|
+
// RFC 5280 sec. 4.1.2.4 -- issuer MUST be non-empty (an empty subject is permitted, sec. 4.1.2.6).
|
|
153
158
|
if (!issuer.rdns.length) {
|
|
154
159
|
throw ctx.E("x509/bad-issuer", "issuer must be a non-empty distinguished name");
|
|
155
160
|
}
|
|
156
161
|
|
|
157
162
|
var extField = tbs.fields.extensions;
|
|
158
163
|
var hasExtensions = !!(extField && extField.present);
|
|
159
|
-
// RFC 5280
|
|
164
|
+
// RFC 5280 sec. 4.1.2.9 -- extensions appear only in a v3 certificate.
|
|
160
165
|
if (hasExtensions && version !== 3) {
|
|
161
166
|
throw ctx.E("x509/bad-version", "extensions are only permitted in a v3 certificate");
|
|
162
167
|
}
|
|
163
168
|
|
|
169
|
+
// RFC 5280 sec. 4.1.2.8 -- issuerUniqueID / subjectUniqueID MUST NOT
|
|
170
|
+
// appear when the version is 1; they require a v2 or v3 certificate.
|
|
171
|
+
if ((tbs.fields.issuerUniqueID.present || tbs.fields.subjectUniqueID.present) && version < 2) {
|
|
172
|
+
throw ctx.E("x509/bad-version", "issuerUniqueID/subjectUniqueID require a v2 or v3 certificate (RFC 5280 sec. 4.1.2.8)");
|
|
173
|
+
}
|
|
174
|
+
|
|
164
175
|
var serialNode = tbs.fields.serialNumber.node;
|
|
165
176
|
return {
|
|
166
177
|
version: version,
|
|
@@ -211,7 +222,7 @@ var parse = pkix.makeParser({ pemLabel: "CERTIFICATE", PemError: PemError, Error
|
|
|
211
222
|
|
|
212
223
|
// matches(root): does the decoded DER look like a Certificate? A CSR and a CRL
|
|
213
224
|
// share the outer SEQUENCE-of-3 envelope, so the discriminator is inside the
|
|
214
|
-
// tbs: a certificate has a Validity
|
|
225
|
+
// tbs: a certificate has a Validity -- a SEQUENCE of exactly two Time values -- at
|
|
215
226
|
// position [serial, signature, issuer, VALIDITY] after the optional [0] version.
|
|
216
227
|
// A CSR's certificationRequestInfo has subjectPublicKeyInfo there (no Validity),
|
|
217
228
|
// and a CRL leads with a bare Time; neither matches. Used by the orchestrator.
|