@blamejs/pki 0.1.11 → 0.1.13
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 +18 -0
- package/README.md +28 -22
- package/lib/asn1-der.js +41 -4
- package/lib/framework-error.js +5 -0
- package/lib/oid.js +20 -2
- package/lib/schema-all.js +20 -3
- package/lib/schema-engine.js +16 -2
- package/lib/schema-ocsp.js +4 -53
- package/lib/schema-pkix.js +57 -0
- package/lib/schema-tsp.js +411 -0
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,24 @@ All notable changes to `@blamejs/pki` are documented here. The format
|
|
|
4
4
|
follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); this
|
|
5
5
|
project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
6
6
|
|
|
7
|
+
## v0.1.13 — 2026-07-05
|
|
8
|
+
|
|
9
|
+
An RFC 3161 timestamp parser joins the pki.schema family.
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- pki.schema.tsp — an RFC 3161 timestamp parser. pki.schema.tsp.parse turns a DER Buffer or PEM into a TimeStampResp ({ status, statusString, failInfo, timeStampToken }) with the granted-carries-token / rejected-carries-none coupling enforced (tsp/missing-token, tsp/unexpected-token) and the PKIFailureInfo named bits decoded. pki.schema.tsp.parseToken parses a TimeStampToken by composing pki.schema.cms.parse and asserting the id-ct-TSTInfo content type (tsp/wrong-econtent-type), attached content (tsp/detached-token), and the single (TSA) signer (tsp/multi-signer), returning the decoded TSTInfo plus the signer material. pki.schema.tsp.parseTstInfo decodes a bare TSTInfo. The TSTInfo mandatory version-1, the GeneralizedTime-only genTime, the accuracy 1..999 range, the ordering DEFAULT FALSE omission, and the PKIStatus 0..5 range are all enforced fail-closed. pki.schema.parse detect-and-routes a TimeStampResp.
|
|
14
|
+
- The codec and schema engine gain three composable primitives that TSP required: pki.asn1.read.integerImplicit / pki.schema.engine.implicitInteger(tag) (a context-tagged IMPLICIT INTEGER, for the Accuracy millis / micros fields); pki.schema.engine.implicitSeqOf(tag, item) (an order-preserving IMPLICIT SEQUENCE OF, the sibling of implicitSetOf without the SET ordering rule, for the extensions field); and RFC 3161 / X.690 §11.7 fractional-seconds GeneralizedTime support in pki.asn1.read.time (a '.'-separated, trailing-zero-free, Z-terminated fraction, surfaced at millisecond precision).
|
|
15
|
+
- The OID registry gains the id-kp extended-key-purpose family (including id-kp-timeStamping) and the id-aa S/MIME authenticated-attribute family (signingCertificate / signingCertificateV2 / timeStampToken), so a parsed TSA certificate's key purpose and a signer's ESS binding attribute resolve by name.
|
|
16
|
+
|
|
17
|
+
## v0.1.12 — 2026-07-05
|
|
18
|
+
|
|
19
|
+
SLH-DSA object identifiers corrected and completed to all twelve FIPS 205 parameter sets.
|
|
20
|
+
|
|
21
|
+
### Fixed
|
|
22
|
+
|
|
23
|
+
- SLH-DSA OID resolution — id-slh-dsa-shake-128s and id-slh-dsa-shake-256s were mapped to the arcs of id-slh-dsa-sha2-256s (.24) and id-slh-dsa-shake-128f (.27), so pki.oid.name / pki.oid.byName resolved them incorrectly. All twelve Pure SLH-DSA parameter sets (sha2-128s/128f/192s/192f/256s/256f and shake-128s/128f/192s/192f/256s/256f) are now registered at their correct arcs .20 through .31 per RFC 9909 §3; the previously-absent nine sets now resolve as well. WebCrypto SLH-DSA sign/verify was unaffected — it selects by algorithm name, not through the OID registry.
|
|
24
|
+
|
|
7
25
|
## v0.1.11 — 2026-07-05
|
|
8
26
|
|
|
9
27
|
An OCSP request and response parser joins the pki.schema family.
|
package/README.md
CHANGED
|
@@ -197,9 +197,10 @@ is callable today; nothing below is a stub.
|
|
|
197
197
|
| `pki.schema.pkcs8` | Parse DER / PEM PKCS#8 private keys per RFC 5208 / 5958 — algorithm, raw key bytes, attributes, optional public key, fail-closed; encrypted keys recognized (not decrypted) — `parse`, `parseEncrypted`, `pemDecode`, `pemEncode` |
|
|
198
198
|
| `pki.schema.cms` | Parse DER / PEM CMS SignedData per RFC 5652 — encapsulated content, signer infos, raw signed-attribute bytes for external verification, certificates / CRLs kept raw, fail-closed; non-SignedData content types recognized (not decoded) — `parse`, `pemDecode`, `pemEncode` |
|
|
199
199
|
| `pki.schema.ocsp` | Parse DER / PEM OCSP requests and responses per RFC 6960 — per-certificate status (good / revoked / unknown), responder identity, raw tbs bytes for external verification, certificates kept raw, fail-closed; non-basic response types recognized (not decoded) — `parseRequest`, `parseResponse`, `pemDecode` |
|
|
200
|
+
| `pki.schema.tsp` | Parse DER / PEM RFC 3161 timestamp responses and tokens — the TSTInfo payload (imprint, genTime with sub-second precision, serial, nonce, accuracy), the status-to-token coupling, and the token wrapper composed over CMS with the single-signer rule, fail-closed — `parse`, `parseTstInfo`, `parseToken`, `pemDecode` |
|
|
200
201
|
| `pki.schema.engine` | The declarative ASN.1 structure-schema engine every format parser composes — `walk` plus the schema combinators |
|
|
201
202
|
| `pki.C` / `pki.constants` | Version-stable constants — functional scale helpers (`C.TIME.*`, `C.BYTES.*`), codec `LIMITS`, `version` |
|
|
202
|
-
| `pki.errors` | The `PkiError` taxonomy — `defineClass` plus `ConstantsError` / `Asn1Error` / `OidError` / `PemError` / `CertificateError` / `CrlError` / `CsrError` / `Pkcs8Error` / `CmsError` / `OcspError`, each carrying a stable `code` in `domain/reason` form |
|
|
203
|
+
| `pki.errors` | The `PkiError` taxonomy — `defineClass` plus `ConstantsError` / `Asn1Error` / `OidError` / `PemError` / `CertificateError` / `CrlError` / `CsrError` / `Pkcs8Error` / `CmsError` / `OcspError` / `TspError`, each carrying a stable `code` in `domain/reason` form |
|
|
203
204
|
| `pki` CLI | `pki version`, `pki oid <dotted\|name>`, `pki parse <cert>` |
|
|
204
205
|
|
|
205
206
|
### CLI
|
|
@@ -233,26 +234,31 @@ reintroduce the bug class it prevents. Adding a format is a schema declaration
|
|
|
233
234
|
plus a documentation comment block, not new parse logic.
|
|
234
235
|
|
|
235
236
|
```
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
237
|
+
┌─ Detect + route ────────────────────────────────────────────────────────┐
|
|
238
|
+
│ pki.schema.parse — inspect the DER root, route to the matching sibling │
|
|
239
|
+
└─────────────────────────────────────────────────────────────────────────┘
|
|
240
|
+
│
|
|
241
|
+
┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ ┌──────┐ Format
|
|
242
|
+
│ x509 │ │ crl │ │ csr │ │pkcs8 │ │ cms │ │ ocsp │ │ tsp │ parsers
|
|
243
|
+
└──────┘ └──────┘ └──────┘ └──────┘ └──────┘ └──────┘ └──────┘ (siblings)
|
|
244
|
+
│ every sibling composes ↓
|
|
245
|
+
┌─ Shared structure ──────────────────────────────────────────────────────┐
|
|
246
|
+
│ pki.schema.engine — walk + combinators (positional reads, tag order, │
|
|
247
|
+
│ SET-OF uniqueness, arity, typed errors) · the PKIX sub-schemas — │
|
|
248
|
+
│ AlgorithmIdentifier · Name · Extension · a bounded version reader — │
|
|
249
|
+
│ and the one coerce → decode → walk parse-entry (PEM cap + DER wrap). │
|
|
250
|
+
└─────────────────────────────────────────────────────────────────────────┘
|
|
251
|
+
│ built on ↓
|
|
252
|
+
┌─ Foundation ────────────────────────────────────────────────────────────┐
|
|
253
|
+
│ pki.asn1 — strict, bounded DER codec · pki.oid — two-way, PQC-seeded │
|
|
254
|
+
│ OID registry · pki.errors — the PkiError domain/reason taxonomy · │
|
|
255
|
+
│ pki.C — version-stable LIMITS + scale constants. │
|
|
256
|
+
└─────────────────────────────────────────────────────────────────────────┘
|
|
257
|
+
|
|
258
|
+
┌─ Crypto ────────────────────────────────────────────────────────────────┐
|
|
259
|
+
│ pki.webcrypto ──▶ node:crypto — a W3C SubtleCrypto engine: the │
|
|
260
|
+
│ classical set + post-quantum ML-DSA / SLH-DSA + ML-KEM key generation. │
|
|
261
|
+
└─────────────────────────────────────────────────────────────────────────┘
|
|
256
262
|
```
|
|
257
263
|
|
|
258
264
|
**Foundation.** The strict, bounded DER codec (`pki.asn1`), the two-way OID
|
|
@@ -266,7 +272,7 @@ bounded version reader, and the single coerce → decode → walk parse-entry th
|
|
|
266
272
|
every format's `parse` is bound to. Because input coercion, the PEM size cap, and
|
|
267
273
|
the DER-decode wrapping live here once, a format cannot diverge on a guard.
|
|
268
274
|
|
|
269
|
-
**Format parsers.** `x509`, `crl`, `csr`, `pkcs8`, `cms`, and `
|
|
275
|
+
**Format parsers.** `x509`, `crl`, `csr`, `pkcs8`, `cms`, `ocsp`, and `tsp` are siblings:
|
|
270
276
|
each is a schema declaration composed from the shared pieces, emitting its own
|
|
271
277
|
typed `domain/reason` error codes. `pki.schema.parse` inspects a decoded root and
|
|
272
278
|
detect-and-routes to the matching sibling; the detectors are mutually exclusive by
|
package/lib/asn1-der.js
CHANGED
|
@@ -318,6 +318,18 @@ function readEnumerated(node) {
|
|
|
318
318
|
return _readIntegerLikeContent(node, "ENUMERATED", "readEnumerated");
|
|
319
319
|
}
|
|
320
320
|
|
|
321
|
+
// Read a [tag] IMPLICIT INTEGER — a context-class PRIMITIVE node whose content is an
|
|
322
|
+
// integer body (the IMPLICIT tag replaces the universal INTEGER tag). Same minimal
|
|
323
|
+
// two's-complement content rules as a universal INTEGER. The integer counterpart of
|
|
324
|
+
// readBitStringImplicit, for the RFC 3161 Accuracy millis [0] / micros [1] fields.
|
|
325
|
+
function readIntegerImplicit(node, tag) {
|
|
326
|
+
if (node.tagClass !== "context" || node.tagNumber !== tag) {
|
|
327
|
+
throw new Asn1Error("asn1/unexpected-tag", "readIntegerImplicit: expected context tag [" + tag +
|
|
328
|
+
"], got " + node.tagClass + "/" + node.tagNumber);
|
|
329
|
+
}
|
|
330
|
+
return _readIntegerLikeContent(node, "INTEGER", "readIntegerImplicit");
|
|
331
|
+
}
|
|
332
|
+
|
|
321
333
|
// The BIT STRING content decoder (the leading unused-bit octet + body, with the
|
|
322
334
|
// DER zero-pad rule). Shared by the universal reader and the IMPLICIT
|
|
323
335
|
// context-tagged reader — the caller asserts the tag first.
|
|
@@ -533,8 +545,17 @@ function _decodeUtf32be(buf) {
|
|
|
533
545
|
|
|
534
546
|
var UTC_RE = /^(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})Z$/;
|
|
535
547
|
var GEN_RE = /^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})Z$/;
|
|
536
|
-
|
|
537
|
-
|
|
548
|
+
// The X.690 §11.7 DER fractional-seconds GeneralizedTime profile: seconds present,
|
|
549
|
+
// a `.` decimal separator (never `,`), a non-empty fraction, Z-terminated. The
|
|
550
|
+
// trailing-zeros rule (§11.7.3) is enforced separately. Used by RFC 3161 genTime.
|
|
551
|
+
var GEN_FRAC_RE = /^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})\.(\d+)Z$/;
|
|
552
|
+
|
|
553
|
+
// readTime(node, opts?) — opts.allowFractional enables the RFC 3161 / X.690 §11.7
|
|
554
|
+
// fractional-seconds GeneralizedTime profile. Fractional is OFF by default because
|
|
555
|
+
// most profiles (RFC 5280 certificate/CRL Validity, §4.1.2.5.2) require the integer-
|
|
556
|
+
// second YYYYMMDDHHMMSSZ form and MUST NOT include a fraction; only callers that
|
|
557
|
+
// permit sub-second precision (TSP genTime) pass allowFractional.
|
|
558
|
+
function readTime(node, opts) {
|
|
538
559
|
if (node.tagClass !== "universal") throw new Asn1Error("asn1/expected-time", "readTime: not a universal time type");
|
|
539
560
|
_expectPrimitive(node, "readTime");
|
|
540
561
|
var s = node.content.toString("latin1");
|
|
@@ -547,7 +568,19 @@ function readTime(node) {
|
|
|
547
568
|
year += (year < 50) ? 2000 : 1900;
|
|
548
569
|
} else if (node.tagNumber === TAGS.GENERALIZED_TIME) {
|
|
549
570
|
m = GEN_RE.exec(s);
|
|
550
|
-
if (!m)
|
|
571
|
+
if (!m) {
|
|
572
|
+
if (!(opts && opts.allowFractional)) throw new Asn1Error("asn1/bad-generalizedtime", "GeneralizedTime must be YYYYMMDDHHMMSSZ, got " + JSON.stringify(s));
|
|
573
|
+
m = GEN_FRAC_RE.exec(s);
|
|
574
|
+
if (!m) throw new Asn1Error("asn1/bad-generalizedtime", "GeneralizedTime must be YYYYMMDDHHMMSS[.fraction]Z, got " + JSON.stringify(s));
|
|
575
|
+
// X.690 §11.7.3 — the fractional part MUST NOT end in a zero (and is non-empty
|
|
576
|
+
// by the regex). ".5Z" is valid; ".50Z" and ".500Z" are non-canonical. X.690
|
|
577
|
+
// §11.7 places NO cap on the fraction length (RFC 3161 §2.4.2 gives the 5-digit
|
|
578
|
+
// example 19990609001326.34352Z), so a fraction of any length is accepted; the
|
|
579
|
+
// returned Date is millisecond-precision, and a caller needing the exact fraction
|
|
580
|
+
// reads it from node.content (the raw bytes, surfaced losslessly like the OID /
|
|
581
|
+
// serial paths).
|
|
582
|
+
if (m[7].charAt(m[7].length - 1) === "0") throw new Asn1Error("asn1/bad-generalizedtime", "GeneralizedTime fraction must not have trailing zeros, got " + JSON.stringify(s));
|
|
583
|
+
}
|
|
551
584
|
year = parseInt(m[1], 10);
|
|
552
585
|
} else {
|
|
553
586
|
throw new Asn1Error("asn1/expected-time", "readTime: tag " + node.tagNumber + " is not a time type");
|
|
@@ -562,9 +595,12 @@ function readTime(node) {
|
|
|
562
595
|
// GeneralizedTime year below 100 (0099 -> 1999). setUTCFullYear(year, ...)
|
|
563
596
|
// takes the year literally and uses that year's own calendar, so years
|
|
564
597
|
// below 100 and proleptic leap years are handled correctly.
|
|
598
|
+
// A fractional GeneralizedTime carries sub-second precision; surface it on the
|
|
599
|
+
// Date at millisecond resolution (the first three fraction digits, zero-padded).
|
|
600
|
+
var ms = (node.tagNumber === TAGS.GENERALIZED_TIME && m[7]) ? parseInt((m[7] + "000").slice(0, 3), 10) : 0;
|
|
565
601
|
var d = new Date(0);
|
|
566
602
|
d.setUTCFullYear(year, month - 1, day);
|
|
567
|
-
d.setUTCHours(hour, min, sec,
|
|
603
|
+
d.setUTCHours(hour, min, sec, ms);
|
|
568
604
|
if (isNaN(d.getTime())) throw new Asn1Error("asn1/bad-time", "unparseable time " + JSON.stringify(s));
|
|
569
605
|
// Date/setUTC* silently roll over out-of-range fields (Feb 30 -> Mar 2,
|
|
570
606
|
// month 13 -> next Jan, hour 25 -> +1 day). Reject unless every component
|
|
@@ -818,6 +854,7 @@ module.exports = {
|
|
|
818
854
|
read: {
|
|
819
855
|
boolean: readBoolean,
|
|
820
856
|
integer: readInteger,
|
|
857
|
+
integerImplicit: readIntegerImplicit,
|
|
821
858
|
enumerated: readEnumerated,
|
|
822
859
|
bitString: readBitString,
|
|
823
860
|
bitStringImplicit: readBitStringImplicit,
|
package/lib/framework-error.js
CHANGED
|
@@ -145,6 +145,10 @@ var CmsError = defineClass("CmsError", { withCause: true });
|
|
|
145
145
|
// (OCSPRequest / OCSPResponse — RFC 6960 §4). Carries the leaf fault as `.cause`.
|
|
146
146
|
var OcspError = defineClass("OcspError", { withCause: true });
|
|
147
147
|
|
|
148
|
+
// TspError — a byte sequence that is not a well-formed RFC 3161 timestamp token,
|
|
149
|
+
// TSTInfo, or TimeStampResp. Carries the underlying leaf fault as `.cause`.
|
|
150
|
+
var TspError = defineClass("TspError", { withCause: true });
|
|
151
|
+
|
|
148
152
|
module.exports = {
|
|
149
153
|
PkiError: PkiError,
|
|
150
154
|
defineClass: defineClass,
|
|
@@ -159,4 +163,5 @@ module.exports = {
|
|
|
159
163
|
Pkcs8Error: Pkcs8Error,
|
|
160
164
|
CmsError: CmsError,
|
|
161
165
|
OcspError: OcspError,
|
|
166
|
+
TspError: TspError,
|
|
162
167
|
};
|
package/lib/oid.js
CHANGED
|
@@ -79,6 +79,11 @@ var FAMILIES = {
|
|
|
79
79
|
ocspBasic: 1, ocspNonce: 2, ocspCrl: 3, ocspResponse: 4, ocspNoCheck: 5,
|
|
80
80
|
ocspArchiveCutoff: 6, ocspServiceLocator: 7, ocspPrefSigAlgs: 8, ocspExtendedRevoke: 9 } },
|
|
81
81
|
|
|
82
|
+
// PKIX extended key purposes (id-kp, RFC 5280 §4.2.1.12). timeStamping is required
|
|
83
|
+
// — critical and sole — on an RFC 3161 TSA signing certificate (§2.3).
|
|
84
|
+
pkixKp: { base: [1, 3, 6, 1, 5, 5, 7, 3], of: {
|
|
85
|
+
serverAuth: 1, clientAuth: 2, codeSigning: 3, emailProtection: 4, timeStamping: 8, ocspSigning: 9 } },
|
|
86
|
+
|
|
82
87
|
// PKCS#1 RSA public-key + RSASSA signature algorithms.
|
|
83
88
|
rsa: { base: [1, 2, 840, 113549, 1, 1], of: {
|
|
84
89
|
rsaEncryption: 1, rsassaPss: 10, sha256WithRSAEncryption: 11,
|
|
@@ -98,6 +103,12 @@ var FAMILIES = {
|
|
|
98
103
|
// S/MIME content types on the PKCS#9 smime arc (RFC 5652, RFC 3161): id-ct.
|
|
99
104
|
smimeCt: { base: [1, 2, 840, 113549, 1, 9, 16, 1], of: { authData: 2, tSTInfo: 4 } },
|
|
100
105
|
|
|
106
|
+
// S/MIME authenticated attributes (id-aa, RFC 2634 / RFC 5035 / RFC 5816). The ESS
|
|
107
|
+
// signing-certificate attributes bind a CMS / TSP SignerInfo to its signing cert;
|
|
108
|
+
// signingCertificateV2 (ESSCertIDv2) carries a non-SHA-1 cert hash (RFC 5816 §2.2.1).
|
|
109
|
+
smimeAa: { base: [1, 2, 840, 113549, 1, 9, 16, 2], of: {
|
|
110
|
+
signingCertificate: 12, timeStampToken: 14, signingCertificateV2: 47 } },
|
|
111
|
+
|
|
101
112
|
// ANSI X9.62 EC public key, named curve, and ECDSA signatures.
|
|
102
113
|
ansiX962: { base: [1, 2, 840, 10045], of: {
|
|
103
114
|
ecPublicKey: [2, 1], prime256v1: [3, 1, 7],
|
|
@@ -118,10 +129,17 @@ var FAMILIES = {
|
|
|
118
129
|
sha256: 1, sha384: 2, sha512: 3, "sha3-256": 8, "sha3-512": 10, shake256: 12 } },
|
|
119
130
|
|
|
120
131
|
// NIST signature algorithms — FIPS 204 ML-DSA + FIPS 205 SLH-DSA share the
|
|
121
|
-
// signature arc 2.16.840.1.101.3.4.3.
|
|
132
|
+
// signature arc 2.16.840.1.101.3.4.3. The 12 Pure SLH-DSA parameter sets are
|
|
133
|
+
// assigned sequentially (RFC 9909 §3): the SHA-2 sets .20-.25, then the SHAKE
|
|
134
|
+
// sets .26-.31.
|
|
122
135
|
nistSig: { base: [2, 16, 840, 1, 101, 3, 4, 3], of: {
|
|
123
136
|
"id-ml-dsa-44": 17, "id-ml-dsa-65": 18, "id-ml-dsa-87": 19,
|
|
124
|
-
"id-slh-dsa-sha2-128s": 20, "id-slh-dsa-
|
|
137
|
+
"id-slh-dsa-sha2-128s": 20, "id-slh-dsa-sha2-128f": 21,
|
|
138
|
+
"id-slh-dsa-sha2-192s": 22, "id-slh-dsa-sha2-192f": 23,
|
|
139
|
+
"id-slh-dsa-sha2-256s": 24, "id-slh-dsa-sha2-256f": 25,
|
|
140
|
+
"id-slh-dsa-shake-128s": 26, "id-slh-dsa-shake-128f": 27,
|
|
141
|
+
"id-slh-dsa-shake-192s": 28, "id-slh-dsa-shake-192f": 29,
|
|
142
|
+
"id-slh-dsa-shake-256s": 30, "id-slh-dsa-shake-256f": 31 } },
|
|
125
143
|
|
|
126
144
|
// NIST KEM — FIPS 203 ML-KEM (arc 2.16.840.1.101.3.4.4).
|
|
127
145
|
nistKem: { base: [2, 16, 840, 1, 101, 3, 4, 4], of: {
|
package/lib/schema-all.js
CHANGED
|
@@ -38,6 +38,7 @@ var csr = require("./schema-csr");
|
|
|
38
38
|
var pkcs8 = require("./schema-pkcs8");
|
|
39
39
|
var cms = require("./schema-cms");
|
|
40
40
|
var ocsp = require("./schema-ocsp");
|
|
41
|
+
var tsp = require("./schema-tsp");
|
|
41
42
|
var frameworkError = require("./framework-error");
|
|
42
43
|
|
|
43
44
|
var SchemaError = frameworkError.SchemaError;
|
|
@@ -66,12 +67,27 @@ var FORMATS = [
|
|
|
66
67
|
detect: cms.matches,
|
|
67
68
|
parse: function (input) { return cms.parse(input); },
|
|
68
69
|
},
|
|
70
|
+
{
|
|
71
|
+
// TimeStampResp ::= SEQUENCE { status PKIStatusInfo, timeStampToken OPTIONAL } —
|
|
72
|
+
// a SEQUENCE of 1-2 whose first child is a PKIStatusInfo SEQUENCE whose own first
|
|
73
|
+
// child is an INTEGER status. This detector is a strict REFINEMENT of the
|
|
74
|
+
// ocsp-request one (both are SEQUENCE-of-1-2 with a SEQUENCE first child), so it
|
|
75
|
+
// MUST be checked first: a tokenless TimeStampResp (a bare PKIStatusInfo) would
|
|
76
|
+
// otherwise be shadowed by ocsp-request, whereas an OCSPRequest's tbsRequest never
|
|
77
|
+
// leads with a universal INTEGER and so never matches here (RFC 3161 §2.4.2). A
|
|
78
|
+
// bare timestamp token is a CMS ContentInfo and routes to cms; a bare TSTInfo is
|
|
79
|
+
// an internal payload (reached via pki.schema.tsp.parseTstInfo).
|
|
80
|
+
name: "tsp",
|
|
81
|
+
module: tsp,
|
|
82
|
+
detect: tsp.matches,
|
|
83
|
+
parse: function (input) { return tsp.parse(input); },
|
|
84
|
+
},
|
|
69
85
|
{
|
|
70
86
|
// OCSPRequest ::= SEQUENCE { tbsRequest SEQUENCE, optionalSignature [0] EXPLICIT
|
|
71
87
|
// OPTIONAL } — a SEQUENCE of 1-2 whose first child is the tbsRequest SEQUENCE.
|
|
72
88
|
// Leads with a SEQUENCE like the signed-envelope trio, but is excluded by arity
|
|
73
|
-
// (the trio is EXACTLY 3 children; an OCSPRequest is 1-2)
|
|
74
|
-
//
|
|
89
|
+
// (the trio is EXACTLY 3 children; an OCSPRequest is 1-2). Checked AFTER tsp,
|
|
90
|
+
// whose detector is a strict refinement (RFC 6960 §4.1.1).
|
|
75
91
|
name: "ocsp-request",
|
|
76
92
|
module: ocsp,
|
|
77
93
|
detect: ocsp.matchesRequest,
|
|
@@ -141,7 +157,7 @@ var FORMATS = [
|
|
|
141
157
|
* The names of every registered format, in detection order.
|
|
142
158
|
*
|
|
143
159
|
* @example
|
|
144
|
-
* pki.schema.all(); // → ["cms", "ocsp-request", "ocsp-response", "pkcs8", "csr", "crl", "x509"]
|
|
160
|
+
* pki.schema.all(); // → ["cms", "tsp", "ocsp-request", "ocsp-response", "pkcs8", "csr", "crl", "x509"]
|
|
145
161
|
*/
|
|
146
162
|
function all() { return FORMATS.map(function (f) { return f.name; }); }
|
|
147
163
|
|
|
@@ -185,6 +201,7 @@ module.exports = {
|
|
|
185
201
|
pkcs8: { parse: pkcs8.parse, parseEncrypted: pkcs8.parseEncrypted, pemDecode: pkcs8.pemDecode, pemEncode: pkcs8.pemEncode },
|
|
186
202
|
cms: { parse: cms.parse, pemDecode: cms.pemDecode, pemEncode: cms.pemEncode },
|
|
187
203
|
ocsp: { parseRequest: ocsp.parseRequest, parseResponse: ocsp.parseResponse, pemDecode: ocsp.pemDecode },
|
|
204
|
+
tsp: { parse: tsp.parse, parseTstInfo: tsp.parseTstInfo, parseToken: tsp.parseToken, pemDecode: tsp.pemDecode },
|
|
188
205
|
all: all,
|
|
189
206
|
parse: parse,
|
|
190
207
|
};
|
package/lib/schema-engine.js
CHANGED
|
@@ -109,6 +109,11 @@ function implicitOctetString(tag) { return { kind: "leaf", read: function (n) {
|
|
|
109
109
|
// unknown [2] arms (RFC 6960 §4.2.1). Yields null; rejects a constructed or
|
|
110
110
|
// non-empty [tag] node fail-closed.
|
|
111
111
|
function implicitNull(tag) { return { kind: "leaf", read: function (n) { return asn1.read.nullImplicit(n, tag); } }; }
|
|
112
|
+
// A [tag] IMPLICIT INTEGER leaf (context-class primitive) — the integer sibling of
|
|
113
|
+
// implicitBitString, for the RFC 3161 Accuracy millis [0] / micros [1] fields
|
|
114
|
+
// (context-tagged primitive INTEGERs). Yields a BigInt; a constructed or wrong-tag
|
|
115
|
+
// node fails asn1/* at the codec.
|
|
116
|
+
function implicitInteger(tag) { return { kind: "leaf", read: function (n) { return asn1.read.integerImplicit(n, tag); } }; }
|
|
112
117
|
function any() { return { kind: "any" }; }
|
|
113
118
|
function decode(fn) { return { kind: "decode", fn: fn }; }
|
|
114
119
|
|
|
@@ -196,6 +201,15 @@ function implicitSetOf(tag, item, opts) {
|
|
|
196
201
|
return { kind: "repeat", item: item, assert: "implicit", implicitTag: tag, derSetOrder: true, code: opts.code, what: opts.what,
|
|
197
202
|
min: opts.min, unique: opts.unique, dupCode: opts.dupCode, build: opts.build };
|
|
198
203
|
}
|
|
204
|
+
// [tag] IMPLICIT SEQUENCE OF item — the context tag REPLACES the universal SEQUENCE
|
|
205
|
+
// tag, so the node is a context-class constructed [tag] whose direct children are the
|
|
206
|
+
// items. Order-preserving: the SEQUENCE-OF sibling of implicitSetOf WITHOUT the SET
|
|
207
|
+
// ascending-DER-order rule (RFC 3161 extensions [1] IMPLICIT Extensions).
|
|
208
|
+
function implicitSeqOf(tag, item, opts) {
|
|
209
|
+
opts = opts || {};
|
|
210
|
+
return { kind: "repeat", item: item, assert: "implicit", implicitTag: tag, code: opts.code, what: opts.what,
|
|
211
|
+
min: opts.min, unique: opts.unique, dupCode: opts.dupCode, build: opts.build };
|
|
212
|
+
}
|
|
199
213
|
|
|
200
214
|
// ---- the walk engine -------------------------------------------------
|
|
201
215
|
|
|
@@ -377,11 +391,11 @@ function _consumeTrailing(fld, kids, start, fields, ctx) {
|
|
|
377
391
|
module.exports = {
|
|
378
392
|
// structural
|
|
379
393
|
seq: seq, field: field, optional: optional, explicit: explicit, trailing: trailing,
|
|
380
|
-
seqOf: seqOf, setOf: setOf, setOfUnique: setOfUnique, implicitSetOf: implicitSetOf, choice: choice,
|
|
394
|
+
seqOf: seqOf, setOf: setOf, setOfUnique: setOfUnique, implicitSetOf: implicitSetOf, implicitSeqOf: implicitSeqOf, choice: choice,
|
|
381
395
|
// leaves
|
|
382
396
|
oidLeaf: oidLeaf, integerLeaf: integerLeaf, boolean: boolean, octetString: octetString,
|
|
383
397
|
bitString: bitString, implicitBitString: implicitBitString, implicitOctetString: implicitOctetString,
|
|
384
|
-
implicitNull: implicitNull,
|
|
398
|
+
implicitNull: implicitNull, implicitInteger: implicitInteger,
|
|
385
399
|
any: any, decode: decode, time: time,
|
|
386
400
|
// engine
|
|
387
401
|
walk: walk,
|
package/lib/schema-ocsp.js
CHANGED
|
@@ -105,59 +105,10 @@ function certificateBytes() {
|
|
|
105
105
|
});
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
-
// requestorName [1] EXPLICIT GeneralName —
|
|
109
|
-
//
|
|
110
|
-
// the
|
|
111
|
-
|
|
112
|
-
// alternatives (otherName [0], x400Address [3], directoryName [4], ediPartyName [5])
|
|
113
|
-
// must be constructed; rfc822Name [1] / dNSName [2] / uniformResourceIdentifier [6]
|
|
114
|
-
// are primitive IA5String (7-bit); iPAddress [7] is a 4- or 16-octet OCTET STRING;
|
|
115
|
-
// registeredID [8] is a primitive OBJECT IDENTIFIER.
|
|
116
|
-
var GN_CONSTRUCTED = { 0: 1, 3: 1, 4: 1, 5: 1 };
|
|
117
|
-
var GN_IA5 = { 1: 1, 2: 1, 6: 1 };
|
|
118
|
-
function _validateGeneralName(n, ctx) {
|
|
119
|
-
if (n.tagClass !== "context" || n.tagNumber < 0 || n.tagNumber > 8) {
|
|
120
|
-
throw ctx.E("ocsp/bad-requestor-name", "requestorName must be a GeneralName (context tag [0]..[8]) (RFC 6960 §4.1.1)");
|
|
121
|
-
}
|
|
122
|
-
var t = n.tagNumber;
|
|
123
|
-
var constructed = !!n.children;
|
|
124
|
-
if (GN_CONSTRUCTED[t]) {
|
|
125
|
-
if (!constructed || n.children.length < 1) throw ctx.E("ocsp/bad-requestor-name", "GeneralName [" + t + "] must be a non-empty constructed value (RFC 5280 §4.2.1.6)");
|
|
126
|
-
if (t === 0) {
|
|
127
|
-
// otherName ::= SEQUENCE { type-id OBJECT IDENTIFIER, value [0] EXPLICIT ANY } —
|
|
128
|
-
// both fields mandatory: exactly two children, a type-id OID and a [0] EXPLICIT
|
|
129
|
-
// value wrapper carrying exactly one inner value.
|
|
130
|
-
if (n.children.length !== 2) throw ctx.E("ocsp/bad-requestor-name", "GeneralName otherName [0] must be a SEQUENCE { type-id, value [0] } (RFC 5280 §4.2.1.6)");
|
|
131
|
-
try { asn1.read.oid(n.children[0]); }
|
|
132
|
-
catch (e) { throw ctx.E("ocsp/bad-requestor-name", "GeneralName otherName [0] must lead with a type-id OBJECT IDENTIFIER", e); }
|
|
133
|
-
var v = n.children[1];
|
|
134
|
-
if (!(v.tagClass === "context" && v.tagNumber === 0 && v.children && v.children.length === 1)) {
|
|
135
|
-
throw ctx.E("ocsp/bad-requestor-name", "GeneralName otherName [0] value must be a [0] EXPLICIT wrapper carrying exactly one value");
|
|
136
|
-
}
|
|
137
|
-
} else if (t === 4) {
|
|
138
|
-
// directoryName [4] EXPLICIT Name — validate the wrapped RDNSequence.
|
|
139
|
-
if (n.children.length !== 1) throw ctx.E("ocsp/bad-requestor-name", "GeneralName directoryName [4] must wrap exactly one Name (RFC 5280 §4.2.1.6)");
|
|
140
|
-
schema.walk(NAME, n.children[0], ctx);
|
|
141
|
-
}
|
|
142
|
-
// x400Address [3] / ediPartyName [5]: validated to non-empty constructed form
|
|
143
|
-
// (their full ORAddress / EDIPartyName bodies are above the requestorName altitude).
|
|
144
|
-
} else {
|
|
145
|
-
if (constructed) throw ctx.E("ocsp/bad-requestor-name", "GeneralName [" + t + "] must be primitive (RFC 5280 §4.2.1.6)");
|
|
146
|
-
if (GN_IA5[t]) {
|
|
147
|
-
if (n.content.length === 0) throw ctx.E("ocsp/bad-requestor-name", "GeneralName [" + t + "] must be a non-empty IA5String");
|
|
148
|
-
for (var i = 0; i < n.content.length; i++) {
|
|
149
|
-
if (n.content[i] > 0x7f) throw ctx.E("ocsp/bad-requestor-name", "GeneralName [" + t + "] must be an IA5String (7-bit)");
|
|
150
|
-
}
|
|
151
|
-
} else if (t === 7) {
|
|
152
|
-
if (n.content.length !== 4 && n.content.length !== 16) throw ctx.E("ocsp/bad-requestor-name", "GeneralName iPAddress [7] must be a 4- or 16-octet address");
|
|
153
|
-
} else if (t === 8) {
|
|
154
|
-
try { asn1.decodeOidContent(n.content); }
|
|
155
|
-
catch (e) { throw ctx.E("ocsp/bad-requestor-name", "GeneralName registeredID [8] must be a valid OBJECT IDENTIFIER", e); }
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
return { bytes: n.bytes, tagClass: n.tagClass, tagNumber: n.tagNumber };
|
|
159
|
-
}
|
|
160
|
-
var GENERAL_NAME_RAW = schema.decode(_validateGeneralName);
|
|
108
|
+
// requestorName [1] EXPLICIT GeneralName (RFC 6960 §4.1.1) — validated + surfaced raw
|
|
109
|
+
// via the shared pkix.generalName primitive (RFC 5280 §4.2.1.6), so a malformed
|
|
110
|
+
// GeneralName fails closed and the OCSP + TSP parsers cannot drift on this grammar.
|
|
111
|
+
var GENERAL_NAME_RAW = pkix.generalName(NS, { code: "ocsp/bad-requestor-name" });
|
|
161
112
|
|
|
162
113
|
// An OCSP signature BIT STRING is a byte string handed to a cryptographic verifier,
|
|
163
114
|
// so it MUST be octet-aligned (0 unused bits); a non-octet-aligned signature is
|
package/lib/schema-pkix.js
CHANGED
|
@@ -208,6 +208,62 @@ function name(ns) {
|
|
|
208
208
|
});
|
|
209
209
|
}
|
|
210
210
|
|
|
211
|
+
// GeneralName ::= CHOICE (RFC 5280 §4.2.1.6). A validate-and-surface-raw leaf for a
|
|
212
|
+
// caller that keeps the value RAW but needs it to be a well-formed GeneralName: it
|
|
213
|
+
// checks the chosen alternative's tag, form (constructed vs primitive per X.690
|
|
214
|
+
// §10.2), and content — otherName [0] is a SEQUENCE { type-id OID, value [0] EXPLICIT
|
|
215
|
+
// }; x400Address [3] / ediPartyName [5] are non-empty constructed; directoryName [4]
|
|
216
|
+
// EXPLICIT wraps a valid Name; rfc822Name [1] / dNSName [2] / uniformResourceIdentifier
|
|
217
|
+
// [6] are primitive non-empty IA5String (7-bit); iPAddress [7] is a 4- or 16-octet
|
|
218
|
+
// OCTET STRING; registeredID [8] is a primitive OBJECT IDENTIFIER — then surfaces the
|
|
219
|
+
// value RAW ({ bytes, tagClass, tagNumber }). `opts.code` is the caller's error code
|
|
220
|
+
// (e.g. ocsp/bad-requestor-name, tsp/bad-tsa). Shared so the two parsers cannot drift.
|
|
221
|
+
var GN_CONSTRUCTED = { 0: 1, 3: 1, 4: 1, 5: 1 };
|
|
222
|
+
var GN_IA5 = { 1: 1, 2: 1, 6: 1 };
|
|
223
|
+
function generalName(ns, opts) {
|
|
224
|
+
opts = opts || {};
|
|
225
|
+
var code = opts.code || (ns.prefix + "/bad-general-name");
|
|
226
|
+
var NAME = name(ns);
|
|
227
|
+
return schema.decode(function (n, ctx) {
|
|
228
|
+
if (n.tagClass !== "context" || n.tagNumber < 0 || n.tagNumber > 8) {
|
|
229
|
+
throw ctx.E(code, "value must be a GeneralName (context tag [0]..[8]) (RFC 5280 §4.2.1.6)");
|
|
230
|
+
}
|
|
231
|
+
var t = n.tagNumber;
|
|
232
|
+
var constructed = !!n.children;
|
|
233
|
+
if (GN_CONSTRUCTED[t]) {
|
|
234
|
+
if (!constructed || n.children.length < 1) throw ctx.E(code, "GeneralName [" + t + "] must be a non-empty constructed value (RFC 5280 §4.2.1.6)");
|
|
235
|
+
if (t === 0) {
|
|
236
|
+
// otherName ::= SEQUENCE { type-id OBJECT IDENTIFIER, value [0] EXPLICIT ANY }.
|
|
237
|
+
if (n.children.length !== 2) throw ctx.E(code, "GeneralName otherName [0] must be a SEQUENCE { type-id, value [0] }");
|
|
238
|
+
try { asn1.read.oid(n.children[0]); }
|
|
239
|
+
catch (e) { throw ctx.E(code, "GeneralName otherName [0] must lead with a type-id OBJECT IDENTIFIER", e); }
|
|
240
|
+
var v = n.children[1];
|
|
241
|
+
if (!(v.tagClass === "context" && v.tagNumber === 0 && v.children && v.children.length === 1)) {
|
|
242
|
+
throw ctx.E(code, "GeneralName otherName [0] value must be a [0] EXPLICIT wrapper carrying exactly one value");
|
|
243
|
+
}
|
|
244
|
+
} else if (t === 4) {
|
|
245
|
+
// directoryName [4] EXPLICIT Name — validate the wrapped RDNSequence.
|
|
246
|
+
if (n.children.length !== 1) throw ctx.E(code, "GeneralName directoryName [4] must wrap exactly one Name");
|
|
247
|
+
schema.walk(NAME, n.children[0], ctx);
|
|
248
|
+
}
|
|
249
|
+
} else {
|
|
250
|
+
if (constructed) throw ctx.E(code, "GeneralName [" + t + "] must be primitive (X.690 §10.2)");
|
|
251
|
+
if (GN_IA5[t]) {
|
|
252
|
+
if (n.content.length === 0) throw ctx.E(code, "GeneralName [" + t + "] must be a non-empty IA5String");
|
|
253
|
+
for (var i = 0; i < n.content.length; i++) {
|
|
254
|
+
if (n.content[i] > 0x7f) throw ctx.E(code, "GeneralName [" + t + "] must be an IA5String (7-bit)");
|
|
255
|
+
}
|
|
256
|
+
} else if (t === 7) {
|
|
257
|
+
if (n.content.length !== 4 && n.content.length !== 16) throw ctx.E(code, "GeneralName iPAddress [7] must be a 4- or 16-octet address");
|
|
258
|
+
} else if (t === 8) {
|
|
259
|
+
try { asn1.decodeOidContent(n.content); }
|
|
260
|
+
catch (e) { throw ctx.E(code, "GeneralName registeredID [8] must be a valid OBJECT IDENTIFIER", e); }
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return { bytes: n.bytes, tagClass: n.tagClass, tagNumber: n.tagNumber };
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
|
|
211
267
|
// Extension ::= SEQUENCE { extnID OID, critical BOOLEAN DEFAULT FALSE,
|
|
212
268
|
// extnValue OCTET STRING }. `critical` is a universal BOOLEAN present-by-count
|
|
213
269
|
// (not context-tagged), so the per-extension decode handles the 2-vs-3-child
|
|
@@ -362,6 +418,7 @@ module.exports = {
|
|
|
362
418
|
attributeTypeAndValue: attributeTypeAndValue,
|
|
363
419
|
relativeDistinguishedName: relativeDistinguishedName,
|
|
364
420
|
name: name,
|
|
421
|
+
generalName: generalName,
|
|
365
422
|
attribute: attribute,
|
|
366
423
|
extension: extension,
|
|
367
424
|
extensions: extensions,
|
|
@@ -0,0 +1,411 @@
|
|
|
1
|
+
// SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
// Copyright (c) blamejs contributors
|
|
3
|
+
"use strict";
|
|
4
|
+
/**
|
|
5
|
+
* @module pki.schema.tsp
|
|
6
|
+
* @nav Schema
|
|
7
|
+
* @title TSP
|
|
8
|
+
* @order 170
|
|
9
|
+
* @slug tsp
|
|
10
|
+
*
|
|
11
|
+
* @intro
|
|
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`;
|
|
14
|
+
* `parse` decodes it and enforces the status-to-token coupling (a granted
|
|
15
|
+
* response carries a token, a rejection does not). A `TimeStampToken` is itself a
|
|
16
|
+
* CMS SignedData whose encapsulated content is a `TSTInfo`, so `parseToken`
|
|
17
|
+
* composes the CMS parser, asserts the `id-ct-TSTInfo` content type and the
|
|
18
|
+
* single-signer rule, and decodes the inner `TSTInfo`. `parseTstInfo` decodes a
|
|
19
|
+
* bare `TSTInfo` payload directly.
|
|
20
|
+
*
|
|
21
|
+
* The parser surfaces everything a verifier needs and interprets nothing it
|
|
22
|
+
* cannot: `messageImprint.hashAlgorithm` and the raw `hashedMessage`, the
|
|
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.
|
|
28
|
+
*
|
|
29
|
+
* @card
|
|
30
|
+
* Parse DER / PEM RFC 3161 timestamp responses and tokens — per-response status,
|
|
31
|
+
* the TSTInfo payload (imprint, genTime, serial, nonce, accuracy), raw verifier
|
|
32
|
+
* inputs, single-signer token composition over CMS, fail-closed.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
var asn1 = require("./asn1-der");
|
|
36
|
+
var schema = require("./schema-engine");
|
|
37
|
+
var pkix = require("./schema-pkix");
|
|
38
|
+
var oid = require("./oid");
|
|
39
|
+
var cms = require("./schema-cms");
|
|
40
|
+
var frameworkError = require("./framework-error");
|
|
41
|
+
|
|
42
|
+
var TspError = frameworkError.TspError;
|
|
43
|
+
var PemError = frameworkError.PemError;
|
|
44
|
+
|
|
45
|
+
var NS = pkix.makeNS("tsp", TspError, oid);
|
|
46
|
+
|
|
47
|
+
var ALGORITHM_IDENTIFIER = pkix.algorithmIdentifier(NS);
|
|
48
|
+
var EXTENSION = pkix.extension(NS);
|
|
49
|
+
|
|
50
|
+
// TSTInfo.version is INTEGER { v1(1) } — the only legal value is 1.
|
|
51
|
+
var VERSION = pkix.versionReader(NS, { "1": 1 });
|
|
52
|
+
|
|
53
|
+
// 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
|
+
var OID_TST_INFO = oid.byName("tSTInfo");
|
|
56
|
+
|
|
57
|
+
var TAGS = asn1.TAGS;
|
|
58
|
+
|
|
59
|
+
// PKIFailureInfo ::= BIT STRING NamedBitList (RFC 3161 §2.4.2). bit 0 is the MSB of
|
|
60
|
+
// the first content octet; a set bit outside the named set is surfaced by index.
|
|
61
|
+
var FAILURE_BITS = {
|
|
62
|
+
0: "badAlg", 2: "badRequest", 5: "badDataFormat", 14: "timeNotAvailable",
|
|
63
|
+
15: "unacceptedPolicy", 16: "unacceptedExtension", 17: "addInfoNotAvailable", 25: "systemFailure",
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
// ---- shared leaves ---------------------------------------------------
|
|
67
|
+
|
|
68
|
+
// GeneralizedTime-only leaf. RFC 3161 §2.4.2 requires genTime to be GeneralizedTime,
|
|
69
|
+
// 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
|
+
|
|
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
|
+
// alternative's form and content so a malformed GeneralName fails closed.
|
|
81
|
+
var GENERAL_NAME_RAW = pkix.generalName(NS, { code: "tsp/bad-tsa" });
|
|
82
|
+
|
|
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
|
+
});
|
|
90
|
+
|
|
91
|
+
// ---- MessageImprint --------------------------------------------------
|
|
92
|
+
|
|
93
|
+
// MessageImprint ::= SEQUENCE { hashAlgorithm AlgorithmIdentifier, hashedMessage
|
|
94
|
+
// OCTET STRING } (RFC 3161 §2.4.1). hashedMessage is a digest — surfaced RAW.
|
|
95
|
+
var MESSAGE_IMPRINT = schema.seq([
|
|
96
|
+
schema.field("hashAlgorithm", ALGORITHM_IDENTIFIER),
|
|
97
|
+
schema.field("hashedMessage", schema.octetString()),
|
|
98
|
+
], {
|
|
99
|
+
assert: "sequence", arity: { exact: 2 }, code: "tsp/bad-message-imprint", what: "MessageImprint",
|
|
100
|
+
build: function (m) {
|
|
101
|
+
return { hashAlgorithm: m.fields.hashAlgorithm.value.result, hashedMessage: m.fields.hashedMessage.value };
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
// ---- Accuracy --------------------------------------------------------
|
|
106
|
+
|
|
107
|
+
// Accuracy ::= SEQUENCE { seconds INTEGER OPTIONAL, millis [0] IMPLICIT INTEGER
|
|
108
|
+
// (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.
|
|
110
|
+
var ACCURACY = schema.seq([
|
|
111
|
+
schema.optional("seconds", schema.integerLeaf(), { whenUniversal: [TAGS.INTEGER] }),
|
|
112
|
+
schema.trailing([
|
|
113
|
+
{ tag: 0, name: "millis", schema: schema.implicitInteger(0) },
|
|
114
|
+
{ tag: 1, name: "micros", schema: schema.implicitInteger(1) },
|
|
115
|
+
], { minTag: 0, maxTag: 1, unexpectedCode: "tsp/bad-accuracy", orderCode: "tsp/bad-accuracy" }),
|
|
116
|
+
], {
|
|
117
|
+
assert: "sequence", code: "tsp/bad-accuracy", what: "Accuracy",
|
|
118
|
+
build: function (m) {
|
|
119
|
+
function sub(f) {
|
|
120
|
+
if (!f.present) return 0;
|
|
121
|
+
var v = f.value;
|
|
122
|
+
// millis / micros are constrained to 1..999 (RFC 3161 §2.4.2); 0 is also
|
|
123
|
+
// excluded (and non-canonical for a positive count).
|
|
124
|
+
if (v < 1n || v > 999n) throw NS.E("tsp/bad-accuracy", "Accuracy millis/micros must be in 1..999");
|
|
125
|
+
return Number(v);
|
|
126
|
+
}
|
|
127
|
+
return {
|
|
128
|
+
// seconds is an unbounded INTEGER — keep it lossless as a BigInt (millis /
|
|
129
|
+
// micros are constrained to 1..999, so Number is exact for those).
|
|
130
|
+
seconds: m.fields.seconds.present ? m.fields.seconds.value : 0n,
|
|
131
|
+
millis: sub(m.fields.millis),
|
|
132
|
+
micros: sub(m.fields.micros),
|
|
133
|
+
};
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// ---- TSTInfo ---------------------------------------------------------
|
|
138
|
+
|
|
139
|
+
// TSTInfo ::= SEQUENCE { version, policy, messageImprint, serialNumber, genTime,
|
|
140
|
+
// accuracy OPTIONAL, ordering DEFAULT FALSE, nonce OPTIONAL, tsa [0] EXPLICIT
|
|
141
|
+
// 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.
|
|
143
|
+
var TST_INFO = schema.seq([
|
|
144
|
+
schema.field("version", VERSION),
|
|
145
|
+
schema.field("policy", schema.oidLeaf()),
|
|
146
|
+
schema.field("messageImprint", MESSAGE_IMPRINT),
|
|
147
|
+
schema.field("serialNumber", schema.integerLeaf()),
|
|
148
|
+
schema.field("genTime", GEN_TIME),
|
|
149
|
+
schema.optional("accuracy", ACCURACY, { whenUniversal: [TAGS.SEQUENCE] }),
|
|
150
|
+
schema.optional("ordering", schema.boolean(), { whenUniversal: [TAGS.BOOLEAN] }),
|
|
151
|
+
schema.optional("nonce", schema.integerLeaf(), { whenUniversal: [TAGS.INTEGER] }),
|
|
152
|
+
schema.trailing([
|
|
153
|
+
{ tag: 0, name: "tsa", schema: GENERAL_NAME_RAW, explicit: true, emptyCode: "tsp/bad-tsa" },
|
|
154
|
+
{ tag: 1, name: "extensions", schema: schema.implicitSeqOf(1, EXTENSION, { min: 1, unique: function (it) { return it.value.oid; }, dupCode: "tsp/duplicate-extension", code: "tsp/bad-extensions", what: "extensions" }) },
|
|
155
|
+
], { minTag: 0, maxTag: 1, unexpectedCode: "tsp/bad-tst-info", orderCode: "tsp/bad-tst-info" }),
|
|
156
|
+
], {
|
|
157
|
+
assert: "sequence", code: "tsp/bad-tst-info", what: "TSTInfo",
|
|
158
|
+
build: function (m, ctx) {
|
|
159
|
+
// ordering BOOLEAN DEFAULT FALSE — an explicit FALSE must be omitted (DER).
|
|
160
|
+
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");
|
|
162
|
+
}
|
|
163
|
+
var policy = m.fields.policy.value;
|
|
164
|
+
var tsa = m.fields.tsa;
|
|
165
|
+
return {
|
|
166
|
+
version: m.fields.version.value,
|
|
167
|
+
policy: policy,
|
|
168
|
+
policyName: ctx.oid.name(policy) || null,
|
|
169
|
+
messageImprint: m.fields.messageImprint.value.result,
|
|
170
|
+
serialNumber: m.fields.serialNumber.value,
|
|
171
|
+
serialNumberHex: m.fields.serialNumber.node.content.toString("hex"),
|
|
172
|
+
genTime: m.fields.genTime.value,
|
|
173
|
+
// genTime is surfaced as a millisecond-precision Date; genTimeFraction is the
|
|
174
|
+
// exact fractional-seconds digits (lossless for sub-millisecond precision), or
|
|
175
|
+
// null when genTime carries no fraction.
|
|
176
|
+
genTimeFraction: (function () { var g = /\.(\d+)Z$/.exec(m.fields.genTime.node.content.toString("latin1")); return g ? g[1] : null; })(),
|
|
177
|
+
accuracy: m.fields.accuracy.present ? m.fields.accuracy.value.result : null,
|
|
178
|
+
ordering: m.fields.ordering.present ? m.fields.ordering.value : false,
|
|
179
|
+
nonce: m.fields.nonce.present ? m.fields.nonce.value : null,
|
|
180
|
+
nonceHex: m.fields.nonce.present ? m.fields.nonce.node.content.toString("hex") : null,
|
|
181
|
+
tsa: tsa.present ? tsa.value : null,
|
|
182
|
+
extensions: m.fields.extensions.present ? m.fields.extensions.value.items.map(function (it) { return it.value; }) : null,
|
|
183
|
+
};
|
|
184
|
+
},
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// ---- PKIStatusInfo / TimeStampResp -----------------------------------
|
|
188
|
+
|
|
189
|
+
// PKIStatusInfo ::= SEQUENCE { status PKIStatus (INTEGER), statusString PKIFreeText
|
|
190
|
+
// OPTIONAL, failInfo PKIFailureInfo OPTIONAL } (RFC 3161 §2.4.2, from RFC 4210).
|
|
191
|
+
var PKI_STATUS_INFO = schema.seq([
|
|
192
|
+
schema.field("status", schema.integerLeaf()),
|
|
193
|
+
schema.optional("statusString", schema.seqOf(UTF8_TEXT, { assert: "sequence", min: 1, code: "tsp/bad-status-info", what: "statusString" }), { whenUniversal: [TAGS.SEQUENCE] }),
|
|
194
|
+
schema.optional("failInfo", schema.bitString(), { whenUniversal: [TAGS.BIT_STRING] }),
|
|
195
|
+
], {
|
|
196
|
+
assert: "sequence", code: "tsp/bad-status-info", what: "PKIStatusInfo",
|
|
197
|
+
build: function (m) {
|
|
198
|
+
var status = m.fields.status.value;
|
|
199
|
+
// 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)");
|
|
201
|
+
var failInfo = null;
|
|
202
|
+
if (m.fields.failInfo.present) {
|
|
203
|
+
var bs = m.fields.failInfo.value;
|
|
204
|
+
_assertMinimalNamedBits(bs.unusedBits, bs.bytes);
|
|
205
|
+
failInfo = { unusedBits: bs.unusedBits, bytes: bs.bytes, bits: _namedBits(bs.bytes) };
|
|
206
|
+
}
|
|
207
|
+
return {
|
|
208
|
+
status: Number(status),
|
|
209
|
+
statusString: m.fields.statusString.present ? m.fields.statusString.value.items.map(function (it) { return it.value; }) : null,
|
|
210
|
+
failInfo: failInfo,
|
|
211
|
+
};
|
|
212
|
+
},
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
// X.690 §11.2.2 — a BIT STRING typed with a NamedBitList (PKIFailureInfo) MUST have
|
|
216
|
+
// all trailing 0 bits removed under DER: no trailing all-zero content octet, and the
|
|
217
|
+
// declared unusedBits must sit exactly below the lowest set bit of the last octet (so
|
|
218
|
+
// the encoding is minimal). The empty value encodes as 0 content octets, 0 unusedBits.
|
|
219
|
+
function _assertMinimalNamedBits(unusedBits, bytes) {
|
|
220
|
+
if (bytes.length === 0) {
|
|
221
|
+
if (unusedBits !== 0) throw NS.E("tsp/bad-failinfo", "an empty PKIFailureInfo must encode with 0 unused bits (X.690 §11.2.2)");
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
var last = bytes[bytes.length - 1];
|
|
225
|
+
if (last === 0) throw NS.E("tsp/bad-failinfo", "PKIFailureInfo NamedBitList must not have a trailing all-zero octet (X.690 §11.2.2)");
|
|
226
|
+
if (((last >> unusedBits) & 1) !== 1) throw NS.E("tsp/bad-failinfo", "PKIFailureInfo NamedBitList must have all trailing zero bits removed (X.690 §11.2.2)");
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Decode the set NamedBitList bits to their RFC 3161 names (bit 0 = MSB of byte 0).
|
|
230
|
+
// A set bit outside the defined set is an unsupported PKIFailureInfo value — a client
|
|
231
|
+
// MUST error on a failInfo it does not understand (RFC 3161 §2.4.2), so reject it
|
|
232
|
+
// rather than surfacing an opaque "bitN".
|
|
233
|
+
function _namedBits(bytes) {
|
|
234
|
+
var out = [];
|
|
235
|
+
for (var i = 0; i < bytes.length * 8; i++) {
|
|
236
|
+
if ((bytes[i >> 3] >> (7 - (i & 7))) & 1) {
|
|
237
|
+
var nm = FAILURE_BITS[i];
|
|
238
|
+
if (!nm) throw NS.E("tsp/bad-failinfo", "unsupported PKIFailureInfo bit " + i + " (RFC 3161 §2.4.2)");
|
|
239
|
+
out.push(nm);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return out;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
// TimeStampResp ::= SEQUENCE { status PKIStatusInfo, timeStampToken TimeStampToken
|
|
246
|
+
// OPTIONAL } (RFC 3161 §2.4.2). There is NO version field. The token is a CMS
|
|
247
|
+
// ContentInfo, decoded via cms.parse; the status-to-token coupling is load-bearing.
|
|
248
|
+
var TIME_STAMP_RESP = schema.seq([
|
|
249
|
+
schema.field("status", PKI_STATUS_INFO),
|
|
250
|
+
schema.optional("timeStampToken", schema.any(), { whenUniversal: [TAGS.SEQUENCE] }),
|
|
251
|
+
], {
|
|
252
|
+
assert: "sequence", arity: { min: 1 }, code: "tsp/bad-response", what: "TimeStampResp",
|
|
253
|
+
build: function (m) {
|
|
254
|
+
var status = m.fields.status.value.result;
|
|
255
|
+
var present = m.fields.timeStampToken.present;
|
|
256
|
+
// RFC 3161 §2.4.2 — granted / grantedWithMods carry a token; any other status
|
|
257
|
+
// (rejection / waiting / revocation*) MUST NOT.
|
|
258
|
+
var granted = status.status === 0 || status.status === 1;
|
|
259
|
+
if (granted && !present) throw NS.E("tsp/missing-token", "a granted TimeStampResp must carry a timeStampToken (RFC 3161 §2.4.2)");
|
|
260
|
+
if (!granted && present) throw NS.E("tsp/unexpected-token", "a non-granted TimeStampResp must not carry a timeStampToken (RFC 3161 §2.4.2)");
|
|
261
|
+
// failInfo is the reason a request was rejected, so it is present ONLY when the
|
|
262
|
+
// status is rejection(2) — not on granted(0/1) nor on waiting / revocation* (3/4/5).
|
|
263
|
+
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)");
|
|
264
|
+
// A granted response's timeStampToken MUST be a well-formed TimeStampToken —
|
|
265
|
+
// decode it (composing the CMS parser) rather than surfacing arbitrary SEQUENCE
|
|
266
|
+
// bytes as a token; a malformed token fails the response parse.
|
|
267
|
+
return {
|
|
268
|
+
status: status.status,
|
|
269
|
+
statusString: status.statusString,
|
|
270
|
+
failInfo: status.failInfo,
|
|
271
|
+
timeStampToken: present ? parseToken(m.fields.timeStampToken.value.bytes) : null,
|
|
272
|
+
};
|
|
273
|
+
},
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* @primitive pki.schema.tsp.parseTstInfo
|
|
278
|
+
* @signature pki.schema.tsp.parseTstInfo(input) -> tstInfo
|
|
279
|
+
* @since 0.1.13
|
|
280
|
+
* @status experimental
|
|
281
|
+
* @spec RFC 3161
|
|
282
|
+
* @related pki.schema.tsp.parseToken, pki.schema.tsp.parse
|
|
283
|
+
*
|
|
284
|
+
* Parse a bare `TSTInfo` payload (a DER `Buffer`) — the structure a timestamp token
|
|
285
|
+
* encapsulates — into `{ version, policy, messageImprint, serialNumber, genTime,
|
|
286
|
+
* accuracy, ordering, nonce, tsa, extensions }`. `messageImprint.hashedMessage` is
|
|
287
|
+
* the raw digest; `serialNumber` / `nonce` are lossless (BigInt + hex); `genTime` is
|
|
288
|
+
* a `Date` with sub-second precision. A malformed structure throws a typed
|
|
289
|
+
* `TspError` (`tsp/*`); a leaf-level codec fault surfaces as `asn1/*`.
|
|
290
|
+
*
|
|
291
|
+
* @example
|
|
292
|
+
* var tst = pki.schema.tsp.parseTstInfo(der);
|
|
293
|
+
* tst.genTime; // -> Date
|
|
294
|
+
* tst.messageImprint.hashedMessage; // -> Buffer (the raw digest)
|
|
295
|
+
*/
|
|
296
|
+
var parseTstInfo = pkix.makeParser({ pemLabel: null, PemError: PemError, ErrorClass: TspError, prefix: "tsp", what: "TSTInfo", topSchema: TST_INFO, ns: NS });
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* @primitive pki.schema.tsp.parse
|
|
300
|
+
* @signature pki.schema.tsp.parse(input) -> timeStampResp
|
|
301
|
+
* @since 0.1.13
|
|
302
|
+
* @status experimental
|
|
303
|
+
* @spec RFC 3161
|
|
304
|
+
* @related pki.schema.tsp.parseToken, pki.schema.parse
|
|
305
|
+
*
|
|
306
|
+
* Parse a DER `Buffer` or a PEM string into a `TimeStampResp`: `{ status,
|
|
307
|
+
* statusString, failInfo, timeStampToken }`. The status-to-token coupling is
|
|
308
|
+
* enforced — a granted response (status 0/1) carries `timeStampToken` (surfaced raw
|
|
309
|
+
* for `parseToken`), any other status does not. `failInfo` decodes the
|
|
310
|
+
* `PKIFailureInfo` named bits. A malformed structure throws a typed `TspError`.
|
|
311
|
+
*
|
|
312
|
+
* @example
|
|
313
|
+
* var res = pki.schema.tsp.parse(der);
|
|
314
|
+
* res.status; // -> 0 (granted)
|
|
315
|
+
* res.timeStampToken.tstInfo.genTime; // -> Date (a granted token is decoded)
|
|
316
|
+
*/
|
|
317
|
+
var parse = pkix.makeParser({ pemLabel: null, PemError: PemError, ErrorClass: TspError, prefix: "tsp", what: "TimeStampResp", topSchema: TIME_STAMP_RESP, ns: NS });
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* @primitive pki.schema.tsp.parseToken
|
|
321
|
+
* @signature pki.schema.tsp.parseToken(input) -> tstInfo
|
|
322
|
+
* @since 0.1.13
|
|
323
|
+
* @status experimental
|
|
324
|
+
* @spec RFC 3161, RFC 5652
|
|
325
|
+
* @related pki.schema.tsp.parse, pki.schema.cms.parse
|
|
326
|
+
*
|
|
327
|
+
* Parse a `TimeStampToken` (a DER `Buffer` or PEM) — a CMS SignedData whose
|
|
328
|
+
* encapsulated content is a `TSTInfo`. Composes `pki.schema.cms.parse`, asserts the
|
|
329
|
+
* `id-ct-TSTInfo` content type (`tsp/wrong-econtent-type`), that the content is
|
|
330
|
+
* attached (`tsp/detached-token`), and the single-signer rule (`tsp/multi-signer`,
|
|
331
|
+
* RFC 3161 §2.4.2), then decodes the inner `TSTInfo`. Returns `{ tstInfo, eContent,
|
|
332
|
+
* signerInfo, certificates }` — the decoded payload, the raw eContent bytes a verifier
|
|
333
|
+
* hashes for the CMS message-digest, and the CMS signer material.
|
|
334
|
+
*
|
|
335
|
+
* @example
|
|
336
|
+
* var token = pki.schema.tsp.parseToken(tokenDer);
|
|
337
|
+
* token.tstInfo.genTime; // -> Date
|
|
338
|
+
* token.signerInfo.sid; // -> the TSA signer identifier
|
|
339
|
+
*/
|
|
340
|
+
function parseToken(input) {
|
|
341
|
+
// De-armor with TSP's label-agnostic PEM rules first (RFC 3161 has no standard
|
|
342
|
+
// label), THEN hand DER to the CMS parser — otherwise cms.parse would reject any
|
|
343
|
+
// PEM block not labeled "CMS" before the TSP checks run.
|
|
344
|
+
var der = pkix.coerceToDer(input, { pemLabel: null, PemError: PemError, ErrorClass: TspError, prefix: "tsp" });
|
|
345
|
+
// Wrap the CMS decode so tsp.parse / parseToken keep their typed-TspError contract —
|
|
346
|
+
// a malformed token surfaces tsp/bad-token, not a bare cms/* error.
|
|
347
|
+
var signed;
|
|
348
|
+
try { signed = cms.parse(der); }
|
|
349
|
+
catch (e) {
|
|
350
|
+
if (e instanceof TspError) throw e;
|
|
351
|
+
throw new TspError("tsp/bad-token", "the timeStampToken did not decode as a CMS SignedData: " + ((e && e.message) || String(e)), e);
|
|
352
|
+
}
|
|
353
|
+
var encap = signed.encapContentInfo;
|
|
354
|
+
if (encap.eContentType !== OID_TST_INFO) {
|
|
355
|
+
throw new TspError("tsp/wrong-econtent-type", "a TimeStampToken must encapsulate id-ct-TSTInfo, got " + encap.eContentType);
|
|
356
|
+
}
|
|
357
|
+
if (encap.eContent === null) throw new TspError("tsp/detached-token", "a TimeStampToken must carry attached eContent (RFC 3161 §2.4.2)");
|
|
358
|
+
if (signed.signerInfos.length !== 1) {
|
|
359
|
+
throw new TspError("tsp/multi-signer", "a TimeStampToken must contain exactly one (TSA) signerInfo (RFC 3161 §2.4.2)");
|
|
360
|
+
}
|
|
361
|
+
var tstInfo;
|
|
362
|
+
try { tstInfo = schema.walk(TST_INFO, asn1.decode(encap.eContent), NS); }
|
|
363
|
+
catch (e) {
|
|
364
|
+
if (e instanceof TspError) throw e;
|
|
365
|
+
throw new TspError("tsp/bad-der", "the encapsulated TSTInfo did not decode: " + ((e && e.message) || String(e)), e);
|
|
366
|
+
}
|
|
367
|
+
// Surface the RAW eContent (the exact DER a verifier hashes for the CMS
|
|
368
|
+
// message-digest signed attribute) alongside the decoded TSTInfo — a re-serialized
|
|
369
|
+
// tstInfo may not byte-match, so the raw bytes are the verification feed.
|
|
370
|
+
return { tstInfo: tstInfo.result, eContent: encap.eContent, signerInfo: signed.signerInfos[0], certificates: signed.certificates };
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
/**
|
|
374
|
+
* @primitive pki.schema.tsp.pemDecode
|
|
375
|
+
* @signature pki.schema.tsp.pemDecode(text, label?) -> Buffer
|
|
376
|
+
* @since 0.1.13
|
|
377
|
+
* @status experimental
|
|
378
|
+
* @spec RFC 7468, RFC 3161
|
|
379
|
+
* @related pki.schema.tsp.parse
|
|
380
|
+
*
|
|
381
|
+
* Extract the DER bytes from a PEM block (RFC 3161 defines no standard label, so the
|
|
382
|
+
* first block is taken unless `label` is given). Throws `PemError` on a missing
|
|
383
|
+
* envelope or a non-base64 body.
|
|
384
|
+
*
|
|
385
|
+
* @example
|
|
386
|
+
* var der = pki.schema.tsp.pemDecode(pemText);
|
|
387
|
+
*/
|
|
388
|
+
function pemDecode(text, label) { return pkix.pemDecode(text, label || null, PemError); }
|
|
389
|
+
|
|
390
|
+
// A TimeStampResp root is a SEQUENCE of 1-2 whose first child is a PKIStatusInfo (a
|
|
391
|
+
// SEQUENCE whose own first child is an INTEGER status). Disjoint from the OID-first
|
|
392
|
+
// CMS ContentInfo, the INTEGER-first PKCS#8 key, and the exactly-3 signed-envelope
|
|
393
|
+
// trio, so it detects unambiguously regardless of registry order.
|
|
394
|
+
function matches(root) {
|
|
395
|
+
if (!root || root.tagClass !== "universal" || root.tagNumber !== TAGS.SEQUENCE || !root.children) return false;
|
|
396
|
+
var k = root.children;
|
|
397
|
+
if (k.length < 1 || k.length > 2) return false;
|
|
398
|
+
var si = k[0];
|
|
399
|
+
if (!(si.tagClass === "universal" && si.tagNumber === TAGS.SEQUENCE && si.children && si.children.length >= 1)) return false;
|
|
400
|
+
if (!(si.children[0].tagClass === "universal" && si.children[0].tagNumber === TAGS.INTEGER)) return false;
|
|
401
|
+
if (k.length === 2 && !(k[1].tagClass === "universal" && k[1].tagNumber === TAGS.SEQUENCE)) return false;
|
|
402
|
+
return true;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
module.exports = {
|
|
406
|
+
parse: parse,
|
|
407
|
+
parseTstInfo: parseTstInfo,
|
|
408
|
+
parseToken: parseToken,
|
|
409
|
+
pemDecode: pemDecode,
|
|
410
|
+
matches: matches,
|
|
411
|
+
};
|
package/package.json
CHANGED
package/sbom.cdx.json
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
"$schema": "http://cyclonedx.org/schema/bom-1.5.schema.json",
|
|
3
3
|
"bomFormat": "CycloneDX",
|
|
4
4
|
"specVersion": "1.5",
|
|
5
|
-
"serialNumber": "urn:uuid:
|
|
5
|
+
"serialNumber": "urn:uuid:f3d799b0-bde8-4f05-8577-e064f7a420f1",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-07-
|
|
8
|
+
"timestamp": "2026-07-06T00:04:32.672Z",
|
|
9
9
|
"lifecycles": [
|
|
10
10
|
{
|
|
11
11
|
"phase": "build"
|
|
@@ -19,14 +19,14 @@
|
|
|
19
19
|
}
|
|
20
20
|
],
|
|
21
21
|
"component": {
|
|
22
|
-
"bom-ref": "@blamejs/pki@0.1.
|
|
22
|
+
"bom-ref": "@blamejs/pki@0.1.13",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "pki",
|
|
25
|
-
"version": "0.1.
|
|
25
|
+
"version": "0.1.13",
|
|
26
26
|
"scope": "required",
|
|
27
27
|
"author": "blamejs contributors",
|
|
28
28
|
"description": "Pure-JavaScript PKI toolkit that owns its stack — X.509, ASN.1/DER, CMS, PQC-first.",
|
|
29
|
-
"purl": "pkg:npm/%40blamejs/pki@0.1.
|
|
29
|
+
"purl": "pkg:npm/%40blamejs/pki@0.1.13",
|
|
30
30
|
"properties": [],
|
|
31
31
|
"externalReferences": [
|
|
32
32
|
{
|
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
"components": [],
|
|
55
55
|
"dependencies": [
|
|
56
56
|
{
|
|
57
|
-
"ref": "@blamejs/pki@0.1.
|
|
57
|
+
"ref": "@blamejs/pki@0.1.13",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|