@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/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 a `PKIStatusInfo` plus, on success, a `TimeStampToken`;
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 imprint-to-request and nonce-to-request
25
- * round-trips, the ESS signing-certificate binding, the timestamping EKU, and the
26
- * signature are verification-layer concerns above parse altitude. DER-only,
27
- * fail-closed.
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 per-response status,
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) } the only legal value is 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
- // §2.4.2); resolved from the registry, never a dotted literal.
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 §2.4.2). bit 0 is the MSB of
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 §2.4.2 requires genTime to be GeneralizedTime,
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
- var GEN_TIME = schema.decode(function (n, ctx) {
71
- if (n.tagClass !== "universal" || n.tagNumber !== TAGS.GENERALIZED_TIME) {
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 §2.4.2) validated + surfaced raw via the
79
- // shared pkix.generalName primitive (RFC 5280 §4.2.1.6), which checks the chosen
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 §2.4.2).
84
- var UTF8_TEXT = schema.decode(function (n, ctx) {
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 §2.4.1). hashedMessage is a digest surfaced RAW.
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
- // §2.4.2). Every sub-field is optional; a missing one defaults to 0.
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 §2.4.2); 0 is also
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 keep it lossless as a BigInt (millis /
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
- // §2.4.2). The five mandatory fields come first; the optionals are consumed by tag.
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 an explicit FALSE must be omitted (DER).
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 an explicit FALSE must be omitted");
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 §2.4.2, from RFC 4210).
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 §2.4.2)");
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 §11.2.2 a BIT STRING typed with a NamedBitList (PKIFailureInfo) MUST have
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 a client
225
- // MUST error on a failInfo it does not understand (RFC 3161 §2.4.2), so reject it
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 §2.4.2)");
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 §2.4.2). There is NO version field. The token is a CMS
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 §2.4.2 granted / grantedWithMods carry a token; any other status
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 §2.4.2)");
254
- if (!granted && present) throw NS.E("tsp/unexpected-token", "a non-granted TimeStampResp must not carry a timeStampToken (RFC 3161 §2.4.2)");
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) not on granted(0/1) nor on waiting / revocation* (3/4/5).
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 §2.4.2)");
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`) the structure a timestamp token
279
- * encapsulates into `{ version, policy, messageImprint, serialNumber, genTime,
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 a granted response (status 0/1) carries `timeStampToken` (surfaced raw
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) a CMS SignedData whose
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`), and the single-signer rule (`tsp/multi-signer`,
325
- * RFC 3161 §2.4.2), then decodes the inner `TSTInfo`. Returns `{ tstInfo, eContent,
326
- * signerInfo, certificates }` — the decoded payload, the raw eContent bytes a verifier
327
- * hashes for the CMS message-digest, and the CMS signer material.
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 otherwise cms.parse would reject any
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 §2.4.2)");
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 §2.4.2)");
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 a re-serialized
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
  };
@@ -11,7 +11,7 @@
11
11
  *
12
12
  * @intro
13
13
  * X.509 certificate handling per RFC 5280. The seed surface is
14
- * `parse` turn a DER or PEM certificate into a structured,
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. With `label` given (e.g.
52
- * `"CERTIFICATE"`) the block type must match; without it, the first block
53
- * is taken. Throws `PemError` on a missing / mismatched envelope or a
54
- * non-base64 body.
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, "CERTIFICATE");
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 }. The historical guard
89
- // checked only children.length===2 (not the SEQUENCE tag) -> assert "constructed".
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", schema.time(NS)),
92
- schema.field("notAfter", schema.time(NS)),
94
+ schema.field("notBefore", pkix.time(NS)),
95
+ schema.field("notAfter", pkix.time(NS)),
93
96
  ], {
94
- assert: "constructed", arity: { exact: 2 }, code: "x509/bad-validity", what: "Validity",
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 the shared pkix.spki factory under the x509 NS.
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
- { tag: 1, name: "issuerUniqueID", schema: schema.any() },
126
- { tag: 2, name: "subjectUniqueID", schema: schema.any() },
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: "constructed", code: "x509/bad-tbs", what: "tbsCertificate" });
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 (§4.1.1.2 sig-alg
133
- // agreement, §4.1.2.4 non-empty issuer, §4.1.2.9 extensions-only-in-v3) after the
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
- // the shared SIGNED envelope. The certificate-specific invariants (outer==inner
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 §4.1.1.2 outer signatureAlgorithm MUST equal tbsCertificate.signature.
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 §4.1.1.2)");
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 §4.1.2.4 issuer MUST be non-empty (an empty subject is permitted, §4.1.2.6).
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 §4.1.2.9 extensions appear only in a v3 certificate.
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 a SEQUENCE of exactly two Time values at
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.