@blamejs/pki 0.1.10 → 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.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,16 @@ 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.11 — 2026-07-05
8
+
9
+ An OCSP request and response parser joins the pki.schema family.
10
+
11
+ ### Added
12
+
13
+ - pki.schema.ocsp.parseRequest and pki.schema.ocsp.parseResponse — an OCSP request / response parser per RFC 6960 (§4.1 OCSPRequest, §4.2 OCSPResponse). parseRequest turns a DER Buffer or an 'OCSP REQUEST' PEM string into { tbsRequestBytes, version, requestorName, requestList, requestExtensions, optionalSignature }, each requestList entry carrying its CertID with the two issuer hashes raw. parseResponse turns a DER Buffer or an 'OCSP RESPONSE' PEM string into { responseStatus, responseBytes, basicResponse }; for a successful basic response, basicResponse carries { tbsResponseDataBytes, responderID, producedAt, responses, signatureAlgorithm, signature, certs } and each responses[i].certStatus is { type: 'good' | 'revoked' | 'unknown' } (a revoked entry adds its revocationTime and revocationReason). A malformed structure throws a typed OcspError (ocsp/*), an unsupported responseType throws ocsp/unsupported-response-type, and a leaf-level codec fault surfaces as asn1/*. pki.schema.ocsp.pemDecode handles the PEM envelope, and pki.schema.parse detect-and-routes both OCSP shapes.
14
+ - pki.asn1.read.nullImplicit and the pki.schema.engine.implicitNull(tag) leaf — read a context-tagged IMPLICIT NULL (the shape the OCSP CertStatus good [0] and unknown [2] arms take), the primitive-leaf sibling of implicitBitString and implicitOctetString. It rejects a constructed or non-empty context node fail-closed.
15
+ - An ocsp-parse coverage-guided fuzz harness joins the CI fuzzing matrix (jazzer.js + libFuzzer, per pull request and nightly), driving both OCSP entry points on mutated input.
16
+
7
17
  ## v0.1.10 — 2026-07-05
8
18
 
9
19
  A CMS SignedData parser joins the pki.schema family; coverage-guided fuzzing now runs in CI.
package/README.md CHANGED
@@ -196,9 +196,10 @@ is callable today; nothing below is a stub.
196
196
  | `pki.schema.csr` | Parse DER / PEM PKCS#10 certification requests per RFC 2986 — subject DN, public key, requested attributes, signature, fail-closed — `parse`, `pemDecode`, `pemEncode` |
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
+ | `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` |
199
200
  | `pki.schema.engine` | The declarative ASN.1 structure-schema engine every format parser composes — `walk` plus the schema combinators |
200
201
  | `pki.C` / `pki.constants` | Version-stable constants — functional scale helpers (`C.TIME.*`, `C.BYTES.*`), codec `LIMITS`, `version` |
201
- | `pki.errors` | The `PkiError` taxonomy — `defineClass` plus `ConstantsError` / `Asn1Error` / `OidError` / `PemError` / `CertificateError` / `CrlError` / `CsrError` / `Pkcs8Error` / `CmsError`, each carrying a stable `code` in `domain/reason` form |
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 |
202
203
  | `pki` CLI | `pki version`, `pki oid <dotted\|name>`, `pki parse <cert>` |
203
204
 
204
205
  ### CLI
@@ -220,6 +221,63 @@ composites) are on the roadmap and ride this same core. See
220
221
  [ROADMAP.md](ROADMAP.md) for the full plan and current status of each area, and
221
222
  [CHANGELOG.md](CHANGELOG.md) for what has landed.
222
223
 
224
+ ## Architecture
225
+
226
+ Every PKI format is a thin, declarative schema over one shared engine. A parser
227
+ declares the ASN.1 structure as data and hands it to `walk`; it never advances a
228
+ child cursor, re-checks a tag, or re-rolls PEM handling by hand. So each
229
+ structural rule — bounds-checked positional reads, optional / context-tagged
230
+ field ordering, SET-OF ascending-order and uniqueness, arity, and fail-closed
231
+ typed errors — is written **once** in the engine, and no new format can
232
+ reintroduce the bug class it prevents. Adding a format is a schema declaration
233
+ plus a documentation comment block, not new parse logic.
234
+
235
+ ```
236
+ Detect + route pki.schema.parse ── detects the format, routes to a sibling
237
+
238
+ Format parsers x509 crl csr pkcs8 cms ocsp (siblings)
239
+ (one schema each) └──────┴─────┴──────┴──────┴─────┘
240
+ │ every sibling COMPOSES ↓
241
+ Shared structure pki.schema.engine PKIX sub-schemas
242
+ walk + combinators · AlgorithmIdentifier · Name ·
243
+ (positional reads, Extension · version reader ·
244
+ tag ordering, SET-OF the one coerce → decode → walk
245
+ uniqueness, arity, parse-entry (PEM cap + DER
246
+ fail-closed errors) wrapping shared, never copied)
247
+ │ built on ↓
248
+ Foundation pki.asn1 pki.oid pki.errors pki.C
249
+ DER codec · OID registry · PkiError · constants
250
+ (strict, (two-way, taxonomy (LIMITS,
251
+ bounded) PQC-seeded) (domain/reason) scale)
252
+
253
+ Crypto pki.webcrypto ──▶ node:crypto
254
+ W3C SubtleCrypto over the platform: the classical set +
255
+ post-quantum ML-DSA / SLH-DSA signatures + ML-KEM keygen
256
+ ```
257
+
258
+ **Foundation.** The strict, bounded DER codec (`pki.asn1`), the two-way OID
259
+ registry (`pki.oid`), the `PkiError` taxonomy (`pki.errors`), and the
260
+ version-stable constants (`pki.C`). These have no PKI knowledge; they are the
261
+ bytes-and-names layer everything else stands on.
262
+
263
+ **Shared structure.** The declarative schema engine (`pki.schema.engine`) and the
264
+ PKIX sub-schemas it is fed — `AlgorithmIdentifier`, `Name`, `Extension`, the
265
+ bounded version reader, and the single coerce → decode → walk parse-entry that
266
+ every format's `parse` is bound to. Because input coercion, the PEM size cap, and
267
+ the DER-decode wrapping live here once, a format cannot diverge on a guard.
268
+
269
+ **Format parsers.** `x509`, `crl`, `csr`, `pkcs8`, `cms`, and `ocsp` are siblings:
270
+ each is a schema declaration composed from the shared pieces, emitting its own
271
+ typed `domain/reason` error codes. `pki.schema.parse` inspects a decoded root and
272
+ detect-and-routes to the matching sibling; the detectors are mutually exclusive by
273
+ construction, so routing is unambiguous regardless of registration order.
274
+
275
+ **Crypto.** `pki.webcrypto` is a W3C `SubtleCrypto` engine over `node:crypto`,
276
+ carrying the classical suite plus post-quantum ML-DSA / SLH-DSA signatures and
277
+ ML-KEM key generation. Sign/verify resolves algorithms through the same OID
278
+ registry the parsers read, so the signing surface and the parsing surface share
279
+ one algorithm vocabulary.
280
+
223
281
  ## Security posture
224
282
 
225
283
  - **Zero npm runtime dependencies, nothing vendored.** The cryptography runs on
package/lib/asn1-der.js CHANGED
@@ -381,6 +381,20 @@ function readNull(node) {
381
381
  return null;
382
382
  }
383
383
 
384
+ // Read a [tag] IMPLICIT NULL — a context-class PRIMITIVE node with empty content.
385
+ // The IMPLICIT tag replaces the universal NULL tag, so there is no inner universal
386
+ // node. The empty-content and primitive-form rules of a universal NULL still hold.
387
+ // Used for the OCSP CertStatus good [0] / unknown [2] arms (RFC 6960 §4.2.1).
388
+ function readNullImplicit(node, tag) {
389
+ if (node.tagClass !== "context" || node.tagNumber !== tag) {
390
+ throw new Asn1Error("asn1/unexpected-tag", "readNullImplicit: expected context tag [" + tag +
391
+ "], got " + node.tagClass + "/" + node.tagNumber);
392
+ }
393
+ _expectPrimitive(node, "readNullImplicit");
394
+ if (node.content.length !== 0) throw new Asn1Error("asn1/bad-null", "IMPLICIT NULL must have empty content");
395
+ return null;
396
+ }
397
+
384
398
  /**
385
399
  * @primitive pki.asn1.readOid
386
400
  * @signature pki.asn1.read.oid(node) -> "1.2.840.113549.1.1.11"
@@ -810,6 +824,7 @@ module.exports = {
810
824
  octetString: readOctetString,
811
825
  octetStringImplicit: readOctetStringImplicit,
812
826
  nullValue: readNull,
827
+ nullImplicit: readNullImplicit,
813
828
  oid: readOid,
814
829
  string: readString,
815
830
  time: readTime,
@@ -141,6 +141,10 @@ var Pkcs8Error = defineClass("Pkcs8Error", { withCause: true });
141
141
  // SignedData (RFC 5652). Carries the underlying leaf fault as `.cause`.
142
142
  var CmsError = defineClass("CmsError", { withCause: true });
143
143
 
144
+ // OcspError — a byte sequence that is not a well-formed OCSP request or response
145
+ // (OCSPRequest / OCSPResponse — RFC 6960 §4). Carries the leaf fault as `.cause`.
146
+ var OcspError = defineClass("OcspError", { withCause: true });
147
+
144
148
  module.exports = {
145
149
  PkiError: PkiError,
146
150
  defineClass: defineClass,
@@ -154,4 +158,5 @@ module.exports = {
154
158
  CsrError: CsrError,
155
159
  Pkcs8Error: Pkcs8Error,
156
160
  CmsError: CmsError,
161
+ OcspError: OcspError,
157
162
  };
package/lib/oid.js CHANGED
@@ -65,6 +65,20 @@ var FAMILIES = {
65
65
  // PKIX private extensions (authorityInfoAccess et al).
66
66
  pkixAccess: { base: [1, 3, 6, 1, 5, 5, 7, 1], of: { authorityInfoAccess: 1 } },
67
67
 
68
+ // PKIX Access Descriptor methods (id-ad, RFC 5280 §4.2.2.1/§4.2.2.2). id-ad-ocsp
69
+ // is the arc the OCSP responder OIDs hang under; id-ad-caIssuers names the AIA
70
+ // CA-issuers access method.
71
+ adAccess: { base: [1, 3, 6, 1, 5, 5, 7, 48], of: { ocsp: 1, caIssuers: 2 } },
72
+
73
+ // OCSP (RFC 6960) on the id-pkix-ocsp arc (= id-ad-ocsp). id-pkix-ocsp-basic is
74
+ // the ResponseBytes.responseType this build decodes; id-pkix-ocsp-nonce (§4.4.1)
75
+ // names the nonce extension; the remaining members name the other OCSP extensions
76
+ // (CRL references, acceptable-response-types, archive-cutoff, service-locator,
77
+ // preferred-signature-algorithms, extended-revoke — RFC 6960 §4.4, RFC 9654).
78
+ ocsp: { base: [1, 3, 6, 1, 5, 5, 7, 48, 1], of: {
79
+ ocspBasic: 1, ocspNonce: 2, ocspCrl: 3, ocspResponse: 4, ocspNoCheck: 5,
80
+ ocspArchiveCutoff: 6, ocspServiceLocator: 7, ocspPrefSigAlgs: 8, ocspExtendedRevoke: 9 } },
81
+
68
82
  // PKCS#1 RSA public-key + RSASSA signature algorithms.
69
83
  rsa: { base: [1, 2, 840, 113549, 1, 1], of: {
70
84
  rsaEncryption: 1, rsassaPss: 10, sha256WithRSAEncryption: 11,
@@ -95,6 +109,10 @@ var FAMILIES = {
95
109
  // Edwards / Montgomery curves (RFC 8410).
96
110
  edwards: { base: [1, 3, 101], of: { X25519: 110, X448: 111, Ed25519: 112, Ed448: 113 } },
97
111
 
112
+ // OIW Secsig SHA-1 (1.3.14.3.2.26) — the most common OCSP CertID hashAlgorithm;
113
+ // nistHash covers only SHA-256 and above.
114
+ oiwSecsig: { base: [1, 3, 14, 3, 2], of: { sha1: 26 } },
115
+
98
116
  // NIST hash functions (SHA-2, SHA-3, SHAKE).
99
117
  nistHash: { base: [2, 16, 840, 1, 101, 3, 4, 2], of: {
100
118
  sha256: 1, sha384: 2, sha512: 3, "sha3-256": 8, "sha3-512": 10, shake256: 12 } },
package/lib/schema-all.js CHANGED
@@ -37,6 +37,7 @@ var crl = require("./schema-crl");
37
37
  var csr = require("./schema-csr");
38
38
  var pkcs8 = require("./schema-pkcs8");
39
39
  var cms = require("./schema-cms");
40
+ var ocsp = require("./schema-ocsp");
40
41
  var frameworkError = require("./framework-error");
41
42
 
42
43
  var SchemaError = frameworkError.SchemaError;
@@ -65,6 +66,26 @@ var FORMATS = [
65
66
  detect: cms.matches,
66
67
  parse: function (input) { return cms.parse(input); },
67
68
  },
69
+ {
70
+ // OCSPRequest ::= SEQUENCE { tbsRequest SEQUENCE, optionalSignature [0] EXPLICIT
71
+ // OPTIONAL } — a SEQUENCE of 1-2 whose first child is the tbsRequest SEQUENCE.
72
+ // 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), so it detects
74
+ // unambiguously regardless of registry order (RFC 6960 §4.1.1).
75
+ name: "ocsp-request",
76
+ module: ocsp,
77
+ detect: ocsp.matchesRequest,
78
+ parse: function (input) { return ocsp.parseRequest(input); },
79
+ },
80
+ {
81
+ // OCSPResponse ::= SEQUENCE { responseStatus ENUMERATED, responseBytes [0]
82
+ // EXPLICIT OPTIONAL } — the only registered root that leads with an ENUMERATED
83
+ // child, so it is disjoint from every other format (RFC 6960 §4.2.1).
84
+ name: "ocsp-response",
85
+ module: ocsp,
86
+ detect: ocsp.matchesResponse,
87
+ parse: function (input) { return ocsp.parseResponse(input); },
88
+ },
68
89
  {
69
90
  // PKCS#8 PrivateKeyInfo / OneAsymmetricKey — SEQUENCE whose first child is an
70
91
  // INTEGER (version) and third an OCTET STRING (privateKey); disjoint from the
@@ -120,7 +141,7 @@ var FORMATS = [
120
141
  * The names of every registered format, in detection order.
121
142
  *
122
143
  * @example
123
- * pki.schema.all(); // → ["cms", "pkcs8", "csr", "crl", "x509"]
144
+ * pki.schema.all(); // → ["cms", "ocsp-request", "ocsp-response", "pkcs8", "csr", "crl", "x509"]
124
145
  */
125
146
  function all() { return FORMATS.map(function (f) { return f.name; }); }
126
147
 
@@ -163,6 +184,7 @@ module.exports = {
163
184
  csr: { parse: csr.parse, pemDecode: csr.pemDecode, pemEncode: csr.pemEncode },
164
185
  pkcs8: { parse: pkcs8.parse, parseEncrypted: pkcs8.parseEncrypted, pemDecode: pkcs8.pemDecode, pemEncode: pkcs8.pemEncode },
165
186
  cms: { parse: cms.parse, pemDecode: cms.pemDecode, pemEncode: cms.pemEncode },
187
+ ocsp: { parseRequest: ocsp.parseRequest, parseResponse: ocsp.parseResponse, pemDecode: ocsp.pemDecode },
166
188
  all: all,
167
189
  parse: parse,
168
190
  };
@@ -104,6 +104,11 @@ function implicitBitString(tag) { return { kind: "leaf", read: function (n) { va
104
104
  // implicitBitString, for e.g. the CMS SignerIdentifier subjectKeyIdentifier [0]
105
105
  // (RFC 5652 §5.3). Yields the raw content bytes.
106
106
  function implicitOctetString(tag) { return { kind: "leaf", read: function (n) { return asn1.read.octetStringImplicit(n, tag); } }; }
107
+ // A [tag] IMPLICIT NULL leaf (context-class primitive, empty content) — the sibling
108
+ // of implicitBitString/implicitOctetString, for e.g. the OCSP CertStatus good [0] /
109
+ // unknown [2] arms (RFC 6960 §4.2.1). Yields null; rejects a constructed or
110
+ // non-empty [tag] node fail-closed.
111
+ function implicitNull(tag) { return { kind: "leaf", read: function (n) { return asn1.read.nullImplicit(n, tag); } }; }
107
112
  function any() { return { kind: "any" }; }
108
113
  function decode(fn) { return { kind: "decode", fn: fn }; }
109
114
 
@@ -376,6 +381,7 @@ module.exports = {
376
381
  // leaves
377
382
  oidLeaf: oidLeaf, integerLeaf: integerLeaf, boolean: boolean, octetString: octetString,
378
383
  bitString: bitString, implicitBitString: implicitBitString, implicitOctetString: implicitOctetString,
384
+ implicitNull: implicitNull,
379
385
  any: any, decode: decode, time: time,
380
386
  // engine
381
387
  walk: walk,
@@ -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.10",
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:e5089c51-5287-4fe7-bbfc-520b07cbb2f2",
5
+ "serialNumber": "urn:uuid:f4519864-8754-4475-8ee0-b2556afddae1",
6
6
  "version": 1,
7
7
  "metadata": {
8
- "timestamp": "2026-07-05T18:32:46.745Z",
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.10",
22
+ "bom-ref": "@blamejs/pki@0.1.11",
23
23
  "type": "application",
24
24
  "name": "pki",
25
- "version": "0.1.10",
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.10",
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.10",
57
+ "ref": "@blamejs/pki@0.1.11",
58
58
  "dependsOn": []
59
59
  }
60
60
  ]