@blamejs/pki 0.1.9 → 0.1.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,571 @@
1
+ // SPDX-License-Identifier: Apache-2.0
2
+ // Copyright (c) blamejs contributors
3
+ "use strict";
4
+ /**
5
+ * @module pki.schema.ocsp
6
+ * @nav Schema
7
+ * @title OCSP
8
+ * @order 150
9
+ * @slug ocsp
10
+ *
11
+ * @intro
12
+ * OCSP request and response handling per RFC 6960 (§4.1 OCSPRequest, §4.2
13
+ * OCSPResponse). Two entry points — `parseRequest` and `parseResponse` — turn a
14
+ * DER or PEM message into a structured object. A response is a two-stage
15
+ * OID-dispatch: `OCSPResponse` carries an ENUMERATED `responseStatus` and an
16
+ * OPTIONAL `responseBytes`; when the `responseType` is `id-pkix-ocsp-basic` its
17
+ * `response` OCTET STRING content is a fresh DER `BasicOCSPResponse` that is
18
+ * decoded and walked, so the per-certificate statuses surface structurally.
19
+ *
20
+ * OCSP is a signed protocol: the bytes an external verifier must hash are surfaced
21
+ * RAW and never re-serialized. `tbsRequestBytes` and `tbsResponseDataBytes` are the
22
+ * exact on-wire `tbsRequest` / `ResponseData` TLVs; each `CertID` surfaces its
23
+ * `issuerNameHash` / `issuerKeyHash` as raw octets, and the responder's `byKey`
24
+ * hash and the raw `signature` bytes are left for the caller to verify. Embedded
25
+ * certificates are surfaced as raw DER, so an unknown alternative never fails the
26
+ * parse. DER-only, fail-closed.
27
+ *
28
+ * @card
29
+ * Parse DER / PEM OCSP requests and responses (RFC 6960) into structured,
30
+ * validated fields — per-certificate status, responder identity, raw tbs bytes for
31
+ * external verification, certificates kept raw, fail-closed.
32
+ */
33
+
34
+ var asn1 = require("./asn1-der");
35
+ var schema = require("./schema-engine");
36
+ var pkix = require("./schema-pkix");
37
+ var oid = require("./oid");
38
+ var frameworkError = require("./framework-error");
39
+
40
+ var OcspError = frameworkError.OcspError;
41
+ var PemError = frameworkError.PemError;
42
+
43
+ // The ocsp error namespace the schema engine walks under.
44
+ var NS = pkix.makeNS("ocsp", OcspError, oid);
45
+
46
+ var ALGORITHM_IDENTIFIER = pkix.algorithmIdentifier(NS);
47
+ var NAME = pkix.name(NS);
48
+ var EXTENSIONS = pkix.extensions(NS);
49
+
50
+ // Version ::= INTEGER { v1(0) }. OCSP defines only v1(0), which DER forbids
51
+ // encoding (it is the DEFAULT), so ANY explicitly-encoded version is a fault; the
52
+ // empty accept map rejects every value and absence yields the default 1.
53
+ var VERSION = pkix.versionReader(NS, {});
54
+
55
+ // id-pkix-ocsp-basic is the one ResponseBytes.responseType this build decodes; any
56
+ // other is recognized-and-deferred. Resolved from the registry, never a literal.
57
+ var OID_OCSP_BASIC = oid.byName("ocspBasic");
58
+
59
+ // OCSPResponseStatus ::= ENUMERATED — value 4 is "not used" and >= 7 is undefined,
60
+ // so both are rejected (RFC 6960 §4.2.1).
61
+ var STATUS_NAMES = { "0": "successful", "1": "malformedRequest", "2": "internalError", "3": "tryLater", "5": "sigRequired", "6": "unauthorized" };
62
+
63
+ // CRLReason ::= ENUMERATED — value 7 is "not used"; the rest are RFC 5280 §5.3.1.
64
+ var CRL_REASONS = { "0": "unspecified", "1": "keyCompromise", "2": "cACompromise", "3": "affiliationChanged", "4": "superseded", "5": "cessationOfOperation", "6": "certificateHold", "8": "removeFromCRL", "9": "privilegeWithdrawn", "10": "aACompromise" };
65
+
66
+ // ---- shared leaves ---------------------------------------------------
67
+
68
+ // A GeneralizedTime-only leaf. OCSP times (producedAt / thisUpdate / nextUpdate /
69
+ // revocationTime) are GeneralizedTime, never UTCTime (RFC 6960); assert the tag
70
+ // before the codec reads it so a UTCTime is rejected ocsp/bad-time.
71
+ var GENERALIZED_TIME = schema.decode(function (n, ctx) {
72
+ if (n.tagClass !== "universal" || n.tagNumber !== asn1.TAGS.GENERALIZED_TIME) {
73
+ throw ctx.E("ocsp/bad-time", "OCSP time must be a GeneralizedTime (RFC 6960)");
74
+ }
75
+ return asn1.read.time(n);
76
+ });
77
+
78
+ // OCSPResponseStatus decode-and-whitelist leaf. read.enumerated is tag-checked, so
79
+ // an INTEGER-encoded status is rejected at the leaf (asn1/unexpected-tag).
80
+ var OCSP_RESPONSE_STATUS = schema.decode(function (n, ctx) {
81
+ var v = asn1.read.enumerated(n);
82
+ var key = v.toString();
83
+ if (!Object.prototype.hasOwnProperty.call(STATUS_NAMES, key)) throw ctx.E("ocsp/bad-response-status", "undefined OCSPResponseStatus " + key + " (RFC 6960 §4.2.1)");
84
+ return { code: Number(v), name: STATUS_NAMES[key] };
85
+ });
86
+
87
+ // CRLReason decode-and-whitelist leaf.
88
+ var CRL_REASON = schema.decode(function (n, ctx) {
89
+ var v = asn1.read.enumerated(n);
90
+ var key = v.toString();
91
+ if (!Object.prototype.hasOwnProperty.call(CRL_REASONS, key)) throw ctx.E("ocsp/bad-revocation-reason", "undefined CRLReason " + key + " (RFC 5280 §5.3.1)");
92
+ return CRL_REASONS[key];
93
+ });
94
+
95
+ // A certs [0] SEQUENCE OF Certificate element. Unlike the CMS CertificateChoices
96
+ // (a tagged CHOICE), an OCSP certs element is a plain Certificate — a universal
97
+ // SEQUENCE (RFC 5280 §4.1) — so assert that shape before surfacing its raw DER, and
98
+ // reject a non-SEQUENCE element rather than reporting arbitrary bytes as a cert.
99
+ function certificateBytes() {
100
+ return schema.decode(function (n, ctx) {
101
+ if (n.tagClass !== "universal" || n.tagNumber !== asn1.TAGS.SEQUENCE || !n.children) {
102
+ throw ctx.E("ocsp/bad-certs", "each certs element must be a Certificate (a SEQUENCE) (RFC 6960 §4.1.1/§4.2.1)");
103
+ }
104
+ return n.bytes;
105
+ });
106
+ }
107
+
108
+ // requestorName [1] EXPLICIT GeneralName — GeneralName ::= CHOICE (RFC 5280
109
+ // §4.2.1.6). We keep the value RAW (no pkix.generalName factory yet), but validate
110
+ // the chosen alternative's tag AND form/content so a malformed GeneralName fails
111
+ // closed rather than passing the signed-request requestorName guard: the constructed
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);
161
+
162
+ // An OCSP signature BIT STRING is a byte string handed to a cryptographic verifier,
163
+ // so it MUST be octet-aligned (0 unused bits); a non-octet-aligned signature is
164
+ // malformed. Shared by the request Signature and the BasicOCSPResponse.
165
+ function _rawSignature(field) {
166
+ var sig = field.value;
167
+ if (sig.unusedBits !== 0) throw NS.E("ocsp/bad-signature", "an OCSP signature BIT STRING must be octet-aligned (0 unused bits)");
168
+ return sig.bytes;
169
+ }
170
+
171
+ // certs [0] EXPLICIT SEQUENCE OF Certificate — each element raw. Shared by the
172
+ // request Signature and the BasicOCSPResponse.
173
+ var CERTS = schema.seqOf(certificateBytes(), { assert: "sequence", code: "ocsp/bad-certs", what: "certs" });
174
+
175
+ // ---- CertID (shared request + response) ------------------------------
176
+
177
+ // CertID ::= SEQUENCE { hashAlgorithm AlgorithmIdentifier, issuerNameHash OCTET
178
+ // STRING, issuerKeyHash OCTET STRING, serialNumber CertificateSerialNumber }
179
+ // (RFC 6960 §4.1.1). The two hashes are surfaced raw for the caller to verify.
180
+ var CERT_ID = schema.seq([
181
+ schema.field("hashAlgorithm", ALGORITHM_IDENTIFIER),
182
+ schema.field("issuerNameHash", schema.octetString()),
183
+ schema.field("issuerKeyHash", schema.octetString()),
184
+ schema.field("serialNumber", schema.integerLeaf()),
185
+ ], {
186
+ assert: "sequence", arity: { exact: 4 }, code: "ocsp/bad-cert-id", what: "CertID",
187
+ build: function (m) {
188
+ return {
189
+ hashAlgorithm: m.fields.hashAlgorithm.value.result,
190
+ issuerNameHash: m.fields.issuerNameHash.value,
191
+ issuerKeyHash: m.fields.issuerKeyHash.value,
192
+ serialNumber: m.fields.serialNumber.value,
193
+ serialNumberHex: m.fields.serialNumber.node.content.toString("hex"),
194
+ };
195
+ },
196
+ });
197
+
198
+ // ---- response tree ---------------------------------------------------
199
+
200
+ // RevokedInfo ::= SEQUENCE { revocationTime GeneralizedTime, revocationReason [0]
201
+ // EXPLICIT CRLReason OPTIONAL } (RFC 6960 §4.2.1). Walked on the [1] IMPLICIT node,
202
+ // so assert:"constructed" (the context tag replaced the universal SEQUENCE tag).
203
+ var REVOKED_INFO = schema.seq([
204
+ schema.field("revocationTime", GENERALIZED_TIME),
205
+ schema.trailing([
206
+ { tag: 0, name: "revocationReason", schema: CRL_REASON, explicit: true, emptyCode: "ocsp/bad-revoked-info" },
207
+ ], { minTag: 0, maxTag: 0, unexpectedCode: "ocsp/bad-revoked-info", orderCode: "ocsp/bad-revoked-info" }),
208
+ ], {
209
+ assert: "constructed", code: "ocsp/bad-revoked-info", what: "RevokedInfo",
210
+ build: function (m) {
211
+ return {
212
+ revocationTime: m.fields.revocationTime.value,
213
+ revocationReason: m.fields.revocationReason.present ? m.fields.revocationReason.value : null,
214
+ };
215
+ },
216
+ });
217
+
218
+ // CertStatus ::= CHOICE { good [0] IMPLICIT NULL, revoked [1] IMPLICIT RevokedInfo,
219
+ // unknown [2] IMPLICIT NULL } (RFC 6960 §4.2.1). good/unknown are primitive empty
220
+ // context nodes; revoked is a constructed context [1] whose body is RevokedInfo.
221
+ var CERT_STATUS = schema.choice([
222
+ { when: { tagClass: "context", tagNumber: 0 }, schema: schema.implicitNull(0) },
223
+ { when: { tagClass: "context", tagNumber: 1 }, schema: REVOKED_INFO },
224
+ { when: { tagClass: "context", tagNumber: 2 }, schema: schema.implicitNull(2) },
225
+ ], { code: "ocsp/bad-cert-status", what: "CertStatus" });
226
+
227
+ function _shapeCertStatus(field) {
228
+ var t = field.node.tagNumber;
229
+ if (t === 0) return { type: "good" };
230
+ if (t === 2) return { type: "unknown" };
231
+ var ri = field.value.result;
232
+ return { type: "revoked", revocationTime: ri.revocationTime, revocationReason: ri.revocationReason };
233
+ }
234
+
235
+ // SingleResponse ::= SEQUENCE { certID, certStatus, thisUpdate GeneralizedTime,
236
+ // nextUpdate [0] EXPLICIT GeneralizedTime OPTIONAL, singleExtensions [1] EXPLICIT
237
+ // Extensions OPTIONAL } (RFC 6960 §4.2.1). certStatus is consumed positionally
238
+ // (before thisUpdate), so its context [0] good arm cannot be confused with the
239
+ // trailing nextUpdate [0].
240
+ var SINGLE_RESPONSE = schema.seq([
241
+ schema.field("certID", CERT_ID),
242
+ schema.field("certStatus", CERT_STATUS),
243
+ schema.field("thisUpdate", GENERALIZED_TIME),
244
+ schema.trailing([
245
+ { tag: 0, name: "nextUpdate", schema: GENERALIZED_TIME, explicit: true, emptyCode: "ocsp/bad-single-response" },
246
+ { tag: 1, name: "singleExtensions", schema: EXTENSIONS, explicit: true, emptyCode: "ocsp/bad-single-response" },
247
+ ], { minTag: 0, maxTag: 1, unexpectedCode: "ocsp/bad-single-response", orderCode: "ocsp/bad-single-response" }),
248
+ ], {
249
+ assert: "sequence", code: "ocsp/bad-single-response", what: "SingleResponse",
250
+ build: function (m) {
251
+ return {
252
+ certID: m.fields.certID.value.result,
253
+ certStatus: _shapeCertStatus(m.fields.certStatus),
254
+ thisUpdate: m.fields.thisUpdate.value,
255
+ nextUpdate: m.fields.nextUpdate.present ? m.fields.nextUpdate.value : null,
256
+ singleExtensions: m.fields.singleExtensions.present ? m.fields.singleExtensions.value.result : null,
257
+ };
258
+ },
259
+ });
260
+
261
+ // ResponderID ::= CHOICE { byName [1] Name, byKey [2] KeyHash } (RFC 6960 §4.2.1) —
262
+ // both EXPLICIT (the module is EXPLICIT TAGS). byKey is an EXPLICIT-wrapped universal
263
+ // OCTET STRING (0xA2 04 ...), NOT an IMPLICIT primitive [2].
264
+ var RESPONDER_ID = schema.choice([
265
+ { when: { tagClass: "context", tagNumber: 1 }, schema: schema.explicit(1, NAME, { code: "ocsp/bad-responder-id" }) },
266
+ { when: { tagClass: "context", tagNumber: 2 }, schema: schema.explicit(2, schema.octetString(), { code: "ocsp/bad-responder-id" }) },
267
+ ], { code: "ocsp/bad-responder-id", what: "ResponderID" });
268
+
269
+ function _shapeResponderID(field) {
270
+ if (field.node.tagNumber === 1) return { byName: field.value.result };
271
+ // RFC 6960 §4.2.1 — KeyHash is the SHA-1 hash of the responder's public key, so a
272
+ // conformant byKey ResponderID is exactly 20 octets; reject any other length.
273
+ var keyHash = field.value;
274
+ if (keyHash.length !== 20) throw NS.E("ocsp/bad-responder-id", "ResponderID byKey (KeyHash) must be a 20-byte SHA-1 hash (RFC 6960 §4.2.1)");
275
+ return { byKey: keyHash };
276
+ }
277
+
278
+ // ResponseData ::= SEQUENCE { version [0] EXPLICIT DEFAULT v1, responderID,
279
+ // producedAt GeneralizedTime, responses SEQUENCE OF SingleResponse,
280
+ // responseExtensions [1] EXPLICIT Extensions OPTIONAL } (RFC 6960 §4.2.1).
281
+ var RESPONSE_DATA = schema.seq([
282
+ schema.optional("version", VERSION, { tag: 0, explicit: true, default: 1, emptyCode: "ocsp/bad-version" }),
283
+ schema.field("responderID", RESPONDER_ID),
284
+ schema.field("producedAt", GENERALIZED_TIME),
285
+ schema.field("responses", schema.seqOf(SINGLE_RESPONSE, { assert: "sequence", min: 1, code: "ocsp/bad-responses", what: "responses" })),
286
+ schema.trailing([
287
+ { tag: 1, name: "responseExtensions", schema: EXTENSIONS, explicit: true, emptyCode: "ocsp/bad-response-data" },
288
+ ], { minTag: 1, maxTag: 1, unexpectedCode: "ocsp/bad-response-data", orderCode: "ocsp/bad-response-data" }),
289
+ ], {
290
+ assert: "sequence", code: "ocsp/bad-response-data", what: "ResponseData",
291
+ build: function (m) {
292
+ return {
293
+ version: m.fields.version.value,
294
+ responderID: _shapeResponderID(m.fields.responderID),
295
+ producedAt: m.fields.producedAt.value,
296
+ responses: m.fields.responses.value.items.map(function (it) { return it.value.result; }),
297
+ responseExtensions: m.fields.responseExtensions.present ? m.fields.responseExtensions.value.result : null,
298
+ };
299
+ },
300
+ });
301
+
302
+ // BasicOCSPResponse ::= SEQUENCE { tbsResponseData ResponseData, signatureAlgorithm
303
+ // AlgorithmIdentifier, signature BIT STRING, certs [0] EXPLICIT SEQUENCE OF
304
+ // Certificate OPTIONAL } (RFC 6960 §4.2.1). tbsResponseData.node.bytes is the exact
305
+ // signed region (no CMS-style re-tag divergence — the clean case).
306
+ var BASIC_OCSP_RESPONSE = schema.seq([
307
+ schema.field("tbsResponseData", RESPONSE_DATA),
308
+ schema.field("signatureAlgorithm", ALGORITHM_IDENTIFIER),
309
+ schema.field("signature", schema.bitString()),
310
+ schema.trailing([
311
+ { tag: 0, name: "certs", schema: CERTS, explicit: true, emptyCode: "ocsp/bad-basic-response" },
312
+ ], { minTag: 0, maxTag: 0, unexpectedCode: "ocsp/bad-basic-response", orderCode: "ocsp/bad-basic-response" }),
313
+ ], {
314
+ assert: "sequence", code: "ocsp/bad-basic-response", what: "BasicOCSPResponse",
315
+ build: function (m) {
316
+ var tbs = m.fields.tbsResponseData.value.result;
317
+ return {
318
+ tbsResponseDataBytes: m.fields.tbsResponseData.node.bytes,
319
+ version: tbs.version,
320
+ responderID: tbs.responderID,
321
+ producedAt: tbs.producedAt,
322
+ responses: tbs.responses,
323
+ responseExtensions: tbs.responseExtensions,
324
+ signatureAlgorithm: m.fields.signatureAlgorithm.value.result,
325
+ signature: _rawSignature(m.fields.signature),
326
+ certs: m.fields.certs.present ? m.fields.certs.value.items.map(function (it) { return it.value; }) : [],
327
+ };
328
+ },
329
+ });
330
+
331
+ // ResponseBytes ::= SEQUENCE { responseType OBJECT IDENTIFIER, response OCTET STRING }
332
+ // (RFC 6960 §4.2.1). For id-pkix-ocsp-basic the response OCTET STRING content is a
333
+ // fresh DER BasicOCSPResponse (decoded + walked); an unknown responseType is
334
+ // recognized-and-deferred with a precise ocsp/unsupported-response-type.
335
+ var RESPONSE_BYTES = schema.seq([
336
+ schema.field("responseType", schema.oidLeaf()),
337
+ schema.field("response", schema.octetString()),
338
+ ], {
339
+ assert: "sequence", arity: { exact: 2 }, code: "ocsp/bad-response-bytes", what: "ResponseBytes",
340
+ build: function (m, ctx) {
341
+ var responseType = m.fields.responseType.value;
342
+ var raw = m.fields.response.value;
343
+ if (responseType !== OID_OCSP_BASIC) {
344
+ throw NS.E("ocsp/unsupported-response-type", (ctx.oid.name(responseType) || responseType) + " is not id-pkix-ocsp-basic (RFC 6960 §4.2.1)");
345
+ }
346
+ var inner;
347
+ try { inner = asn1.decode(raw); }
348
+ catch (e) { throw NS.E("ocsp/bad-der", "BasicOCSPResponse DER did not decode: " + ((e && e.message) || String(e)), e); }
349
+ return {
350
+ responseType: responseType,
351
+ responseTypeName: ctx.oid.name(responseType) || null,
352
+ response: raw,
353
+ basicResponse: schema.walk(BASIC_OCSP_RESPONSE, inner, ctx).result,
354
+ };
355
+ },
356
+ });
357
+
358
+ // OCSPResponse ::= SEQUENCE { responseStatus OCSPResponseStatus, responseBytes [0]
359
+ // EXPLICIT ResponseBytes OPTIONAL } (RFC 6960 §4.2.1). The status <-> responseBytes
360
+ // biconditional (successful iff responseBytes present) is the OCSP-specific
361
+ // cross-field guard.
362
+ var OCSP_RESPONSE = schema.seq([
363
+ schema.field("responseStatus", OCSP_RESPONSE_STATUS),
364
+ schema.optional("responseBytes", RESPONSE_BYTES, { tag: 0, explicit: true, emptyCode: "ocsp/bad-ocsp-response" }),
365
+ ], {
366
+ assert: "sequence", arity: { min: 1 }, code: "ocsp/bad-ocsp-response", what: "OCSPResponse",
367
+ build: function (m) {
368
+ var status = m.fields.responseStatus.value;
369
+ var present = m.fields.responseBytes.present;
370
+ // RFC 6960 §4.2.1 — a successful response MUST carry responseBytes; any other
371
+ // status MUST NOT (there is nothing to convey but the status).
372
+ if (status.code === 0 && !present) throw NS.E("ocsp/bad-response-bytes", "a successful OCSPResponse must carry responseBytes (RFC 6960 §4.2.1)");
373
+ if (status.code !== 0 && present) throw NS.E("ocsp/bad-response-bytes", "a non-successful OCSPResponse must not carry responseBytes (RFC 6960 §4.2.1)");
374
+ var rb = present ? m.fields.responseBytes.value.result : null;
375
+ return {
376
+ responseStatus: status,
377
+ responseBytes: rb ? { responseType: rb.responseType, responseTypeName: rb.responseTypeName, response: rb.response } : null,
378
+ basicResponse: rb ? rb.basicResponse : null,
379
+ };
380
+ },
381
+ });
382
+
383
+ // ---- request tree ----------------------------------------------------
384
+
385
+ // Request ::= SEQUENCE { reqCert CertID, singleRequestExtensions [0] EXPLICIT
386
+ // Extensions OPTIONAL } (RFC 6960 §4.1.1).
387
+ var REQUEST = schema.seq([
388
+ schema.field("reqCert", CERT_ID),
389
+ schema.trailing([
390
+ { tag: 0, name: "singleRequestExtensions", schema: EXTENSIONS, explicit: true, emptyCode: "ocsp/bad-request" },
391
+ ], { minTag: 0, maxTag: 0, unexpectedCode: "ocsp/bad-request", orderCode: "ocsp/bad-request" }),
392
+ ], {
393
+ assert: "sequence", code: "ocsp/bad-request", what: "Request",
394
+ build: function (m) {
395
+ return {
396
+ certID: m.fields.reqCert.value.result,
397
+ singleRequestExtensions: m.fields.singleRequestExtensions.present ? m.fields.singleRequestExtensions.value.result : null,
398
+ };
399
+ },
400
+ });
401
+
402
+ // Signature ::= SEQUENCE { signatureAlgorithm AlgorithmIdentifier, signature BIT
403
+ // STRING, certs [0] EXPLICIT SEQUENCE OF Certificate OPTIONAL } (RFC 6960 §4.1.1).
404
+ var SIGNATURE = schema.seq([
405
+ schema.field("signatureAlgorithm", ALGORITHM_IDENTIFIER),
406
+ schema.field("signature", schema.bitString()),
407
+ schema.trailing([
408
+ { tag: 0, name: "certs", schema: CERTS, explicit: true, emptyCode: "ocsp/bad-signature" },
409
+ ], { minTag: 0, maxTag: 0, unexpectedCode: "ocsp/bad-signature", orderCode: "ocsp/bad-signature" }),
410
+ ], {
411
+ assert: "sequence", code: "ocsp/bad-signature", what: "Signature",
412
+ build: function (m) {
413
+ return {
414
+ signatureAlgorithm: m.fields.signatureAlgorithm.value.result,
415
+ signature: _rawSignature(m.fields.signature),
416
+ certs: m.fields.certs.present ? m.fields.certs.value.items.map(function (it) { return it.value; }) : [],
417
+ };
418
+ },
419
+ });
420
+
421
+ // TBSRequest ::= SEQUENCE { version [0] EXPLICIT DEFAULT v1, requestorName [1]
422
+ // EXPLICIT GeneralName OPTIONAL, requestList SEQUENCE OF Request, requestExtensions
423
+ // [2] EXPLICIT Extensions OPTIONAL } (RFC 6960 §4.1.1). requestorName is surfaced
424
+ // raw (GeneralName is a wide CHOICE with no factory yet).
425
+ var TBS_REQUEST = schema.seq([
426
+ schema.optional("version", VERSION, { tag: 0, explicit: true, default: 1, emptyCode: "ocsp/bad-version" }),
427
+ schema.optional("requestorName", GENERAL_NAME_RAW, { tag: 1, explicit: true, emptyCode: "ocsp/bad-requestor-name" }),
428
+ schema.field("requestList", schema.seqOf(REQUEST, { assert: "sequence", min: 1, code: "ocsp/bad-request-list", what: "requestList" })),
429
+ schema.trailing([
430
+ { tag: 2, name: "requestExtensions", schema: EXTENSIONS, explicit: true, emptyCode: "ocsp/bad-tbs-request" },
431
+ ], { minTag: 2, maxTag: 2, unexpectedCode: "ocsp/bad-tbs-request", orderCode: "ocsp/bad-tbs-request" }),
432
+ ], {
433
+ assert: "sequence", code: "ocsp/bad-tbs-request", what: "TBSRequest",
434
+ build: function (m) {
435
+ var rn = m.fields.requestorName;
436
+ return {
437
+ version: m.fields.version.value,
438
+ requestorName: rn.present ? { bytes: rn.value.bytes, tagClass: rn.value.tagClass, tagNumber: rn.value.tagNumber } : null,
439
+ requestList: m.fields.requestList.value.items.map(function (it) { return it.value.result; }),
440
+ requestExtensions: m.fields.requestExtensions.present ? m.fields.requestExtensions.value.result : null,
441
+ };
442
+ },
443
+ });
444
+
445
+ // OCSPRequest ::= SEQUENCE { tbsRequest TBSRequest, optionalSignature [0] EXPLICIT
446
+ // Signature OPTIONAL } (RFC 6960 §4.1.1). tbsRequest.node.bytes is the raw signed
447
+ // region for external verification.
448
+ var OCSP_REQUEST = schema.seq([
449
+ schema.field("tbsRequest", TBS_REQUEST),
450
+ schema.optional("optionalSignature", SIGNATURE, { tag: 0, explicit: true, emptyCode: "ocsp/not-a-request" }),
451
+ ], {
452
+ assert: "sequence", arity: { min: 1 }, code: "ocsp/not-a-request", what: "OCSPRequest",
453
+ build: function (m) {
454
+ var tbs = m.fields.tbsRequest.value.result;
455
+ var optionalSignature = m.fields.optionalSignature.present ? m.fields.optionalSignature.value.result : null;
456
+ // RFC 6960 §4.1.2 — if the request is signed, the requestor SHALL specify its
457
+ // name in requestorName (the identity the signature binds to). A signed request
458
+ // without it is internally inconsistent; reject fail-closed.
459
+ if (optionalSignature && tbs.requestorName === null) {
460
+ throw NS.E("ocsp/missing-requestor-name", "a signed OCSPRequest must specify requestorName (RFC 6960 §4.1.2)");
461
+ }
462
+ return {
463
+ tbsRequestBytes: m.fields.tbsRequest.node.bytes,
464
+ version: tbs.version,
465
+ requestorName: tbs.requestorName,
466
+ requestList: tbs.requestList,
467
+ requestExtensions: tbs.requestExtensions,
468
+ optionalSignature: optionalSignature,
469
+ };
470
+ },
471
+ });
472
+
473
+ /**
474
+ * @primitive pki.schema.ocsp.parseRequest
475
+ * @signature pki.schema.ocsp.parseRequest(input) -> ocspRequest
476
+ * @since 0.1.11
477
+ * @status experimental
478
+ * @spec RFC 6960
479
+ * @related pki.schema.ocsp.parseResponse, pki.schema.parse
480
+ *
481
+ * Parse a DER `Buffer` or a PEM (`OCSP REQUEST`) string into a structured
482
+ * OCSPRequest: `{ tbsRequestBytes, version, requestorName, requestList,
483
+ * requestExtensions, optionalSignature }`. `tbsRequestBytes` is the exact on-wire
484
+ * `tbsRequest` TLV for external signature verification; each `requestList` entry
485
+ * carries its `CertID` (with the two issuer hashes raw). A malformed structure
486
+ * throws a typed `OcspError` (`ocsp/*`) and a leaf-level codec fault surfaces as
487
+ * `asn1/*`.
488
+ *
489
+ * @example
490
+ * var req = pki.schema.ocsp.parseRequest(der);
491
+ * req.requestList[0].certID.serialNumberHex; // -> "1332"
492
+ */
493
+ var parseRequest = pkix.makeParser({ pemLabel: "OCSP REQUEST", PemError: PemError, ErrorClass: OcspError, prefix: "ocsp", what: "OCSP request", topSchema: OCSP_REQUEST, ns: NS });
494
+
495
+ /**
496
+ * @primitive pki.schema.ocsp.parseResponse
497
+ * @signature pki.schema.ocsp.parseResponse(input) -> ocspResponse
498
+ * @since 0.1.11
499
+ * @status experimental
500
+ * @spec RFC 6960
501
+ * @related pki.schema.ocsp.parseRequest, pki.schema.parse
502
+ *
503
+ * Parse a DER `Buffer` or a PEM (`OCSP RESPONSE`) string into a structured
504
+ * OCSPResponse: `{ responseStatus, responseBytes, basicResponse }`. For a successful
505
+ * basic response, `basicResponse` carries `{ tbsResponseDataBytes, responderID,
506
+ * producedAt, responses, signatureAlgorithm, signature, certs }`; each
507
+ * `responses[i].certStatus` is `{ type: "good" | "revoked" | "unknown" }`. A
508
+ * non-successful status carries no `responseBytes`. A malformed structure throws a
509
+ * typed `OcspError` (`ocsp/*`); an unsupported `responseType` throws
510
+ * `ocsp/unsupported-response-type`.
511
+ *
512
+ * @example
513
+ * var res = pki.schema.ocsp.parseResponse(der);
514
+ * res.responseStatus.name; // -> "successful"
515
+ * res.basicResponse.responses[0].certStatus.type; // -> "good" | "revoked" | "unknown"
516
+ */
517
+ var parseResponse = pkix.makeParser({ pemLabel: "OCSP RESPONSE", PemError: PemError, ErrorClass: OcspError, prefix: "ocsp", what: "OCSP response", topSchema: OCSP_RESPONSE, ns: NS });
518
+
519
+ /**
520
+ * @primitive pki.schema.ocsp.pemDecode
521
+ * @signature pki.schema.ocsp.pemDecode(text, label?) -> Buffer
522
+ * @since 0.1.11
523
+ * @status experimental
524
+ * @spec RFC 7468, RFC 6960
525
+ * @related pki.schema.ocsp.parseResponse
526
+ *
527
+ * Extract the DER bytes from a PEM OCSP block (default label `OCSP RESPONSE`; pass
528
+ * `"OCSP REQUEST"` for a request). Throws `PemError` on a missing / mismatched
529
+ * envelope or a non-base64 body.
530
+ *
531
+ * @example
532
+ * var der = pki.schema.ocsp.pemDecode(pemText, "OCSP REQUEST");
533
+ */
534
+ function pemDecode(text, label) { return pkix.pemDecode(text, label || "OCSP RESPONSE", PemError); }
535
+
536
+ // An OCSPResponse root is the only registered structure that leads with an
537
+ // ENUMERATED child (a SEQUENCE of 1-2: responseStatus + an optional context [0]
538
+ // responseBytes). Every other root leads with a SEQUENCE (x509/crl/csr/ocsp-request),
539
+ // an OBJECT IDENTIFIER (cms), or an INTEGER (pkcs8), so the detector is
540
+ // unconditionally exclusive.
541
+ function matchesResponse(root) {
542
+ var T = asn1.TAGS;
543
+ if (!root || root.tagClass !== "universal" || root.tagNumber !== T.SEQUENCE || !root.children) return false;
544
+ var k = root.children;
545
+ if (k.length < 1 || k.length > 2) return false;
546
+ if (!(k[0].tagClass === "universal" && k[0].tagNumber === T.ENUMERATED)) return false;
547
+ if (k.length === 2 && !(k[1].tagClass === "context" && k[1].tagNumber === 0 && k[1].children)) return false;
548
+ return true;
549
+ }
550
+
551
+ // An OCSPRequest root is a SEQUENCE of 1-2 whose first child is the tbsRequest
552
+ // SEQUENCE (optionally followed by a context [0] EXPLICIT signature). It leads with
553
+ // a SEQUENCE like x509/crl/csr, but those require EXACTLY 3 children (the
554
+ // signed-envelope trio), while an OCSPRequest is 1-2 — so it is excluded by arity.
555
+ function matchesRequest(root) {
556
+ var T = asn1.TAGS;
557
+ if (!root || root.tagClass !== "universal" || root.tagNumber !== T.SEQUENCE || !root.children) return false;
558
+ var k = root.children;
559
+ if (k.length < 1 || k.length > 2) return false;
560
+ if (!(k[0].tagClass === "universal" && k[0].tagNumber === T.SEQUENCE && k[0].children)) return false;
561
+ if (k.length === 2 && !(k[1].tagClass === "context" && k[1].tagNumber === 0 && k[1].children)) return false;
562
+ return true;
563
+ }
564
+
565
+ module.exports = {
566
+ parseRequest: parseRequest,
567
+ parseResponse: parseResponse,
568
+ pemDecode: pemDecode,
569
+ matchesRequest: matchesRequest,
570
+ matchesResponse: matchesResponse,
571
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@blamejs/pki",
3
- "version": "0.1.9",
3
+ "version": "0.1.11",
4
4
  "description": "Pure-JavaScript PKI toolkit that owns its stack — X.509, ASN.1/DER, CMS, PQC-first.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "blamejs contributors",
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:3cf64a06-eef0-417c-a199-787130b9149a",
5
+ "serialNumber": "urn:uuid:f4519864-8754-4475-8ee0-b2556afddae1",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-07-05T08:37:25.927Z",
8
+ "timestamp": "2026-07-05T20:10:05.842Z",
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.9",
22
+ "bom-ref": "@blamejs/pki@0.1.11",
23
23
  "type": "application",
24
24
  "name": "pki",
25
- "version": "0.1.9",
25
+ "version": "0.1.11",
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.9",
29
+ "purl": "pkg:npm/%40blamejs/pki@0.1.11",
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.9",
57
+ "ref": "@blamejs/pki@0.1.11",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]