@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.
- package/CHANGELOG.md +24 -0
- package/README.md +60 -1
- package/lib/asn1-der.js +30 -0
- package/lib/framework-error.js +10 -0
- package/lib/oid.js +31 -2
- package/lib/schema-all.js +38 -2
- package/lib/schema-cms.js +375 -0
- package/lib/schema-engine.js +12 -1
- package/lib/schema-ocsp.js +571 -0
- package/package.json +1 -1
- package/sbom.cdx.json +6 -6
|
@@ -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
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:f4519864-8754-4475-8ee0-b2556afddae1",
|
|
6
6
|
"version": 1,
|
|
7
7
|
"metadata": {
|
|
8
|
-
"timestamp": "2026-07-
|
|
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.
|
|
22
|
+
"bom-ref": "@blamejs/pki@0.1.11",
|
|
23
23
|
"type": "application",
|
|
24
24
|
"name": "pki",
|
|
25
|
-
"version": "0.1.
|
|
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.
|
|
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.
|
|
57
|
+
"ref": "@blamejs/pki@0.1.11",
|
|
58
58
|
"dependsOn": []
|
|
59
59
|
}
|
|
60
60
|
]
|