tunnelfetch 1.0.0

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.
Files changed (96) hide show
  1. package/LICENSE +28 -0
  2. package/README.md +617 -0
  3. package/README.zh-CN.md +470 -0
  4. package/package.json +74 -0
  5. package/src/client/cookies.js +429 -0
  6. package/src/client/decode.js +346 -0
  7. package/src/client/redirect.js +249 -0
  8. package/src/client.js +704 -0
  9. package/src/errors.js +181 -0
  10. package/src/http1/chunked.js +289 -0
  11. package/src/http1/index.js +10 -0
  12. package/src/http1/request.js +143 -0
  13. package/src/http1/response.js +493 -0
  14. package/src/http2/connection.js +1170 -0
  15. package/src/http2/constants.js +129 -0
  16. package/src/http2/frames.js +291 -0
  17. package/src/http2/hpack.js +420 -0
  18. package/src/http2/huffman.js +203 -0
  19. package/src/http2/index.js +21 -0
  20. package/src/index.js +46 -0
  21. package/src/pool.js +256 -0
  22. package/src/proxy/direct.js +62 -0
  23. package/src/proxy/http-connect.js +206 -0
  24. package/src/proxy/index.js +197 -0
  25. package/src/proxy/socks5.js +344 -0
  26. package/src/tls/aead.js +263 -0
  27. package/src/tls/connect.js +407 -0
  28. package/src/tls/constants.js +334 -0
  29. package/src/tls/extensions.js +376 -0
  30. package/src/tls/handshake-messages.js +901 -0
  31. package/src/tls/handshake.js +568 -0
  32. package/src/tls/handshake12.js +507 -0
  33. package/src/tls/index.js +44 -0
  34. package/src/tls/keyschedule.js +473 -0
  35. package/src/tls/record.js +872 -0
  36. package/src/tls/tickets.js +145 -0
  37. package/src/tls/transcript.js +101 -0
  38. package/src/tls/wire.js +224 -0
  39. package/src/transport.js +296 -0
  40. package/src/trust/der.js +551 -0
  41. package/src/trust/index.js +375 -0
  42. package/src/trust/name.js +235 -0
  43. package/src/trust/ocsp.js +759 -0
  44. package/src/trust/path.js +595 -0
  45. package/src/trust/roots.js +454 -0
  46. package/src/trust/x509.js +902 -0
  47. package/src/util/bytes.js +470 -0
  48. package/src/util/deadline.js +266 -0
  49. package/src/warmup-fixture.js +85 -0
  50. package/src/warmup.js +243 -0
  51. package/types/client/cookies.d.ts +159 -0
  52. package/types/client/decode.d.ts +54 -0
  53. package/types/client/redirect.d.ts +96 -0
  54. package/types/client.d.ts +323 -0
  55. package/types/errors.d.ts +141 -0
  56. package/types/http1/chunked.d.ts +48 -0
  57. package/types/http1/index.d.ts +3 -0
  58. package/types/http1/request.d.ts +44 -0
  59. package/types/http1/response.d.ts +183 -0
  60. package/types/http2/connection.d.ts +282 -0
  61. package/types/http2/constants.d.ts +95 -0
  62. package/types/http2/frames.d.ts +116 -0
  63. package/types/http2/hpack.d.ts +99 -0
  64. package/types/http2/huffman.d.ts +21 -0
  65. package/types/http2/index.d.ts +5 -0
  66. package/types/index.d.ts +17 -0
  67. package/types/pool.d.ts +135 -0
  68. package/types/proxy/direct.d.ts +26 -0
  69. package/types/proxy/http-connect.d.ts +37 -0
  70. package/types/proxy/index.d.ts +62 -0
  71. package/types/proxy/socks5.d.ts +47 -0
  72. package/types/tls/aead.d.ts +67 -0
  73. package/types/tls/connect.d.ts +280 -0
  74. package/types/tls/constants.d.ts +275 -0
  75. package/types/tls/extensions.d.ts +195 -0
  76. package/types/tls/handshake-messages.d.ts +430 -0
  77. package/types/tls/handshake.d.ts +90 -0
  78. package/types/tls/handshake12.d.ts +35 -0
  79. package/types/tls/index.d.ts +9 -0
  80. package/types/tls/keyschedule.d.ts +272 -0
  81. package/types/tls/record.d.ts +361 -0
  82. package/types/tls/tickets.d.ts +66 -0
  83. package/types/tls/transcript.d.ts +52 -0
  84. package/types/tls/wire.d.ts +106 -0
  85. package/types/transport.d.ts +222 -0
  86. package/types/trust/der.d.ts +239 -0
  87. package/types/trust/index.d.ts +194 -0
  88. package/types/trust/name.d.ts +33 -0
  89. package/types/trust/ocsp.d.ts +138 -0
  90. package/types/trust/path.d.ts +139 -0
  91. package/types/trust/roots.d.ts +36 -0
  92. package/types/trust/x509.d.ts +401 -0
  93. package/types/util/bytes.d.ts +183 -0
  94. package/types/util/deadline.d.ts +133 -0
  95. package/types/warmup-fixture.d.ts +11 -0
  96. package/types/warmup.d.ts +45 -0
@@ -0,0 +1,759 @@
1
+ // Stapled OCSP response verification (RFC 6960), on the strict DER reader.
2
+ //
3
+ // Why stapling, and only stapling. Revocation information can also be fetched from the CA's own
4
+ // responder or pulled as CRLs, and both are wrong for this package: a responder fetch is an
5
+ // extra network round trip in the middle of a metered handshake, made through the caller's
6
+ // proxy, and it tells the CA which origins the caller talks to — an unacceptable default twice
7
+ // over; CRLs are megabytes of list to download and cache on a runtime with no cross-request
8
+ // storage. A stapled response costs nothing extra: the server includes a CA-signed, time-stamped
9
+ // OCSP response in the handshake it was already sending (RFC 6066 s8 for TLS 1.2, RFC 8446
10
+ // s4.4.2.1 for TLS 1.3), and this module decides whether that response actually proves anything.
11
+ //
12
+ // The threat model, which dictates every check below: the staple arrives from THE SAME PEER we
13
+ // are trying to authenticate. Until its signature is verified against a key the certification
14
+ // path already vouches for, it is attacker-controlled bytes shaped like good news. So a staple
15
+ // is believed only when ALL of the following hold, and every other outcome is a typed error:
16
+ //
17
+ // 1. It parses as strict DER (this is a hostile-input parser exactly like x509.js, and is held
18
+ // to the same standard: reject ambiguity, reject trailing bytes, reject encoded DEFAULTs).
19
+ // 2. Its CertID matches the certificate in hand — issuer name hash, issuer key hash, and
20
+ // serial — computed from OUR copy of the validated path, not from anything the response
21
+ // says about itself.
22
+ // 3. Its signature verifies against the issuing CA's key, or against a delegated responder
23
+ // certificate that the issuing CA signed DIRECTLY and marked with the id-kp-OCSPSigning
24
+ // extended key usage (RFC 6960 s4.2.2.2). Without the EKU requirement, any TLS certificate
25
+ // the CA ever issued could mint "good" responses for its siblings.
26
+ // 4. Its validity window (thisUpdate / nextUpdate) covers the caller-injected `now` — never a
27
+ // wall clock, which on the target runtime is frozen per-slice and lies.
28
+ //
29
+ // Only then is the verdict read, and the verdict is not negotiable: `good` continues, `revoked`
30
+ // and `unknown` both refuse the connection. Treating `unknown` as anything but a failure would
31
+ // let a responder shrug a revoked serial back to life.
32
+ //
33
+ // What stapling deliberately cannot do — an attacker who can strip the staple entirely — is a
34
+ // policy question, not a verification one, and lives with the trust configuration in index.js.
35
+
36
+ import { CertificateError, codes } from '../errors.js';
37
+ import { equal, toHex } from '../util/bytes.js';
38
+ import {
39
+ TAG,
40
+ CLS,
41
+ readTlv,
42
+ readAll,
43
+ children,
44
+ content,
45
+ element,
46
+ expectTlv,
47
+ readInteger,
48
+ readOid,
49
+ readBitString,
50
+ readBoolean,
51
+ readGeneralizedTime,
52
+ parseError,
53
+ tagName,
54
+ } from './der.js';
55
+ import {
56
+ OID,
57
+ parseAlgorithmIdentifier,
58
+ parseCertificate,
59
+ parseSubjectPublicKeyInfo,
60
+ } from './x509.js';
61
+ import { verifySignedObject } from './path.js';
62
+
63
+ // Clock skew tolerated when judging the validity window. The responder's clock and the caller's
64
+ // injected `now` are different machines; a response produced seconds ago must not read as "from
65
+ // the future" on a slightly slow clock. Five minutes is the ballpark every deployed validator
66
+ // uses, and it is far below the days-long windows real responders publish.
67
+ const CLOCK_SKEW_MS = 5 * 60 * 1000;
68
+
69
+ // RFC 6960 s2.4 reads an absent nextUpdate as "newer revocation information is available all the
70
+ // time", which taken literally makes every such response stale the moment it is stapled. Real
71
+ // CAs that omit nextUpdate expect polling clients; for a staple the fail-closed reading with a
72
+ // tolerance is a hard age cap from thisUpdate. Ten days matches mozilla::pkix's
73
+ // maxOCSPLifetimeInDays, the most-reviewed fail-closed figure in deployment.
74
+ const MAX_AGE_WITHOUT_NEXT_UPDATE_MS = 10 * 24 * 3600 * 1000;
75
+
76
+ const RESPONSE_STATUS_NAME = {
77
+ 0: 'successful',
78
+ 1: 'malformedRequest',
79
+ 2: 'internalError',
80
+ 3: 'tryLater',
81
+ 5: 'sigRequired',
82
+ 6: 'unauthorized',
83
+ };
84
+
85
+ /** RFC 5280 s5.3.1 CRLReason values, for naming why a certificate was revoked. */
86
+ const CRL_REASON_NAME = {
87
+ 0: 'unspecified',
88
+ 1: 'keyCompromise',
89
+ 2: 'cACompromise',
90
+ 3: 'affiliationChanged',
91
+ 4: 'superseded',
92
+ 5: 'cessationOfOperation',
93
+ 6: 'certificateHold',
94
+ 8: 'removeFromCRL',
95
+ 9: 'privilegeWithdrawn',
96
+ 10: 'aACompromise',
97
+ };
98
+
99
+ /**
100
+ * CertID hash algorithms understood, with the digest length each must produce.
101
+ *
102
+ * SHA-1 is accepted HERE and nowhere else in this package, deliberately: the CertID hash is a
103
+ * lookup key, not a trust decision. What binds the response to the certificate is the CA's
104
+ * signature over the whole ResponseData; the hashes only say which issuer/serial the signed
105
+ * statement is about, the serial itself travels unhashed, and both hash inputs (issuer name,
106
+ * issuer key) are fixed by the CA rather than attacker-chosen — so a SHA-1 collision buys
107
+ * nothing. Meanwhile RFC 5019, the profile production responders actually implement, mandates
108
+ * SHA-1 CertIDs; refusing them would refuse essentially every real staple.
109
+ */
110
+ const CERTID_HASH = {
111
+ [OID.sha1]: { name: 'SHA-1', length: 20 },
112
+ [OID.sha256]: { name: 'SHA-256', length: 32 },
113
+ [OID.sha384]: { name: 'SHA-384', length: 48 },
114
+ [OID.sha512]: { name: 'SHA-512', length: 64 },
115
+ };
116
+
117
+ const iso = (ms) => new Date(ms).toISOString();
118
+
119
+ const ocspError = (code, message, detail) => new CertificateError(code, message, detail);
120
+
121
+ /** ENUMERATED with the small non-negative range OCSP uses. Anything else is out of protocol. */
122
+ function readEnumerated(bytes, tlv, what) {
123
+ expectTlv(tlv, { tag: TAG.ENUMERATED, constructed: false }, what);
124
+ const c = content(bytes, tlv);
125
+ // INTEGER encoding rules apply; every value the protocol defines fits one non-negative byte,
126
+ // so a longer (or high-bit) encoding is either non-minimal or out of range. Both are refused.
127
+ if (c.byteLength !== 1 || c[0] > 0x7f) {
128
+ throw parseError(tlv.start, `${what} must be a single byte in 0..127`);
129
+ }
130
+ return c[0];
131
+ }
132
+
133
+ /** NULL parameters (or absent) — the only AlgorithmIdentifier parameters a digest OID takes. */
134
+ function requireNullOrAbsentParams(algo, what) {
135
+ const p = algo.paramsTlv;
136
+ if (p === null) return;
137
+ if (p.cls === CLS.UNIVERSAL && p.tag === TAG.NULL && !p.constructed &&
138
+ p.contentStart === p.contentEnd) {
139
+ return;
140
+ }
141
+ throw parseError(p.start, `${what} parameters must be NULL or absent`);
142
+ }
143
+
144
+ /**
145
+ * Extension walk shared by responseExtensions and singleExtensions. `recognized` names the OIDs
146
+ * whose semantics this module has judged and may ignore; an unrecognized CRITICAL extension
147
+ * rejects the response (RFC 6960 s4.4, same fail-closed stance as RFC 5280 s6.1) because an
148
+ * extension we cannot read may change the meaning of everything we can.
149
+ */
150
+ function checkExtensions(bytes, wrapper, recognized, what) {
151
+ const inner = explicitInner(bytes, wrapper, what);
152
+ expectTlv(inner, { tag: TAG.SEQUENCE, constructed: true }, what);
153
+ const seen = new Set();
154
+ for (const extTlv of children(bytes, inner, what)) {
155
+ expectTlv(extTlv, { tag: TAG.SEQUENCE, constructed: true }, 'Extension');
156
+ const parts = children(bytes, extTlv, 'Extension');
157
+ if (parts.length < 2 || parts.length > 3) {
158
+ throw parseError(extTlv.start, 'Extension must be { extnID, critical?, extnValue }');
159
+ }
160
+ const extnId = readOid(bytes, parts[0], 'extnID');
161
+ let critical = false;
162
+ if (parts.length === 3) {
163
+ critical = readBoolean(bytes, parts[1], 'critical');
164
+ if (!critical) throw parseError(parts[1].start, 'critical FALSE must be omitted in DER');
165
+ }
166
+ expectTlv(parts[parts.length - 1], { tag: TAG.OCTET_STRING, constructed: false }, 'extnValue');
167
+ if (seen.has(extnId)) throw parseError(extTlv.start, `duplicate extension ${extnId} in ${what}`);
168
+ seen.add(extnId);
169
+ if (critical && !recognized.has(extnId)) {
170
+ throw ocspError(codes.OCSP_PARSE,
171
+ `stapled OCSP response carries unrecognized critical extension ${extnId} in ${what}; ` +
172
+ 'RFC 6960 s4.4 requires rejection rather than a guess at its meaning',
173
+ { oid: extnId, where: what });
174
+ }
175
+ }
176
+ }
177
+
178
+ // A nonce (RFC 6960 s4.4.1) binds a response to the request that carried the nonce. A staple
179
+ // answers no request of ours, so there is nothing to match and the extension imposes nothing —
180
+ // recognized, and deliberately inert.
181
+ const RECOGNIZED_RESPONSE_EXTENSIONS = new Set([OID.ocspNonce]);
182
+ const RECOGNIZED_SINGLE_EXTENSIONS = new Set();
183
+
184
+ /**
185
+ * CertID (RFC 6960 s4.1.1): which certificate a SingleResponse is talking about.
186
+ * @typedef {object} OcspCertId
187
+ * @property {string} hashOid
188
+ * @property {Uint8Array} issuerNameHash
189
+ * @property {Uint8Array} issuerKeyHash
190
+ * @property {Uint8Array} serialBytes INTEGER content bytes, minimal DER
191
+ */
192
+
193
+ /**
194
+ * One SingleResponse, parsed. Times are epoch ms.
195
+ * @typedef {object} OcspSingleResponse
196
+ * @property {OcspCertId} certId
197
+ * @property {{ kind: 'good' } | { kind: 'unknown' }
198
+ * | { kind: 'revoked', revocationTime: number, reason: number | null }} status
199
+ * @property {number} thisUpdate
200
+ * @property {number | null} nextUpdate
201
+ */
202
+
203
+ /**
204
+ * A parsed BasicOCSPResponse. `tbsBytes` is the exact original ResponseData element — the bytes
205
+ * the signature covers, never re-encoded.
206
+ * @typedef {object} OcspBasicResponse
207
+ * @property {Uint8Array} tbsBytes
208
+ * @property {import('./x509.js').AlgorithmId} signatureAlgorithm
209
+ * @property {Uint8Array} signature
210
+ * @property {{ kind: 'name', nameBytes: Uint8Array } | { kind: 'key', keyHash: Uint8Array }} responderId
211
+ * @property {number} producedAt epoch ms
212
+ * @property {OcspSingleResponse[]} singles
213
+ * @property {import('./x509.js').Certificate[]} certs attached responder certificates, parsed
214
+ */
215
+
216
+ /**
217
+ * @typedef {object} OcspResponse
218
+ * @property {number} responseStatus
219
+ * @property {string} responseStatusName
220
+ * @property {OcspBasicResponse | null} basic null exactly when responseStatus != successful
221
+ */
222
+
223
+ function parseCertId(bytes, tlv) {
224
+ expectTlv(tlv, { tag: TAG.SEQUENCE, constructed: true }, 'CertID');
225
+ const kids = children(bytes, tlv, 'CertID');
226
+ if (kids.length !== 4) {
227
+ throw parseError(tlv.start,
228
+ 'CertID must be { hashAlgorithm, issuerNameHash, issuerKeyHash, serialNumber }');
229
+ }
230
+ const algo = parseAlgorithmIdentifier(bytes, kids[0], 'CertID hashAlgorithm');
231
+ requireNullOrAbsentParams(algo, 'CertID hashAlgorithm');
232
+ expectTlv(kids[1], { tag: TAG.OCTET_STRING, constructed: false }, 'issuerNameHash');
233
+ expectTlv(kids[2], { tag: TAG.OCTET_STRING, constructed: false }, 'issuerKeyHash');
234
+ const serial = readInteger(bytes, kids[3], 'CertID serialNumber');
235
+ return {
236
+ hashOid: algo.oid,
237
+ issuerNameHash: content(bytes, kids[1]),
238
+ issuerKeyHash: content(bytes, kids[2]),
239
+ serialBytes: serial.bytes,
240
+ };
241
+ }
242
+
243
+ function parseCertStatus(bytes, tlv) {
244
+ if (tlv.cls !== CLS.CONTEXT) {
245
+ throw parseError(tlv.start,
246
+ `expected a context-tagged CertStatus, got ${tagName(tlv.cls, tlv.tag)}`);
247
+ }
248
+ switch (tlv.tag) {
249
+ case 0: // good [0] IMPLICIT NULL
250
+ case 2: { // unknown [2] IMPLICIT UnknownInfo (NULL)
251
+ if (tlv.constructed || tlv.contentStart !== tlv.contentEnd) {
252
+ throw parseError(tlv.start, `CertStatus [${tlv.tag}] must be an empty primitive (NULL)`);
253
+ }
254
+ return tlv.tag === 0 ? { kind: 'good' } : { kind: 'unknown' };
255
+ }
256
+ case 1: { // revoked [1] IMPLICIT RevokedInfo
257
+ if (!tlv.constructed) throw parseError(tlv.start, 'RevokedInfo must be constructed');
258
+ const kids = children(bytes, tlv, 'RevokedInfo');
259
+ if (kids.length < 1 || kids.length > 2) {
260
+ throw parseError(tlv.start, 'RevokedInfo must be { revocationTime, revocationReason? }');
261
+ }
262
+ const revocationTime = readGeneralizedTime(bytes, kids[0], 'revocationTime');
263
+ let reason = null;
264
+ if (kids.length === 2) {
265
+ const wrap = kids[1];
266
+ if (wrap.cls !== CLS.CONTEXT || wrap.tag !== 0 || !wrap.constructed) {
267
+ throw parseError(wrap.start, 'revocationReason must be [0] EXPLICIT CRLReason');
268
+ }
269
+ reason = readEnumerated(bytes, explicitInner(bytes, wrap, 'revocationReason'), 'CRLReason');
270
+ }
271
+ return { kind: 'revoked', revocationTime, reason };
272
+ }
273
+ default:
274
+ throw parseError(tlv.start, `unknown CertStatus tag [${tlv.tag}]`);
275
+ }
276
+ }
277
+
278
+ function parseSingleResponse(bytes, tlv) {
279
+ expectTlv(tlv, { tag: TAG.SEQUENCE, constructed: true }, 'SingleResponse');
280
+ const kids = children(bytes, tlv, 'SingleResponse');
281
+ let i = 0;
282
+ const next = (what) => {
283
+ if (i >= kids.length) throw parseError(tlv.end, `SingleResponse ends before ${what}`);
284
+ return kids[i++];
285
+ };
286
+ const certId = parseCertId(bytes, next('certID'));
287
+ const status = parseCertStatus(bytes, next('certStatus'));
288
+ const thisUpdate = readGeneralizedTime(bytes, next('thisUpdate'), 'thisUpdate');
289
+ let nextUpdate = null;
290
+ if (i < kids.length && kids[i].cls === CLS.CONTEXT && kids[i].tag === 0) {
291
+ const wrap = kids[i++];
292
+ if (!wrap.constructed) throw parseError(wrap.start, 'nextUpdate must be [0] EXPLICIT');
293
+ nextUpdate = readGeneralizedTime(bytes, explicitInner(bytes, wrap, 'nextUpdate'), 'nextUpdate');
294
+ if (nextUpdate < thisUpdate) {
295
+ // A window that ends before it begins is self-contradictory; there is no correct half to
296
+ // believe, so neither is believed.
297
+ throw parseError(wrap.start,
298
+ `nextUpdate ${iso(nextUpdate)} precedes thisUpdate ${iso(thisUpdate)}`);
299
+ }
300
+ }
301
+ if (i < kids.length && kids[i].cls === CLS.CONTEXT && kids[i].tag === 1) {
302
+ checkExtensions(bytes, kids[i++], RECOGNIZED_SINGLE_EXTENSIONS, 'singleExtensions');
303
+ }
304
+ if (i !== kids.length) throw parseError(kids[i].start, 'unexpected trailing SingleResponse field');
305
+ return { certId, status, thisUpdate, nextUpdate };
306
+ }
307
+
308
+ /** [tag] EXPLICIT wrapper: exactly one inner element filling the wrapper's content. */
309
+ function explicitInner(bytes, wrap, what) {
310
+ const inner = readTlv(bytes, wrap.contentStart);
311
+ if (inner.end !== wrap.contentEnd) throw parseError(inner.end, `trailing bytes in ${what}`);
312
+ return inner;
313
+ }
314
+
315
+ function parseResponseData(bytes, tlv) {
316
+ expectTlv(tlv, { tag: TAG.SEQUENCE, constructed: true }, 'ResponseData');
317
+ const kids = children(bytes, tlv, 'ResponseData');
318
+ let i = 0;
319
+ const next = (what) => {
320
+ if (i >= kids.length) throw parseError(tlv.end, `ResponseData ends before ${what}`);
321
+ return kids[i++];
322
+ };
323
+ if (i < kids.length && kids[i].cls === CLS.CONTEXT && kids[i].tag === 0 && kids[i].constructed) {
324
+ // version [0] EXPLICIT Version DEFAULT v1. Only v1 exists, so an encoded version is either
325
+ // the forbidden DEFAULT (DER: must be omitted) or a version this parser cannot promise to
326
+ // understand. Both die, each with its own accurate story.
327
+ const inner = explicitInner(bytes, next('version'), 'version');
328
+ const { value, negative } = readInteger(bytes, inner, 'ResponseData version');
329
+ if (!negative && value === 0) {
330
+ throw parseError(inner.start, 'ResponseData version v1 must be omitted (DEFAULT) in DER');
331
+ }
332
+ throw parseError(inner.start,
333
+ `unsupported ResponseData version ${negative ? 'negative' : value}; only v1 is defined`);
334
+ }
335
+ const ridTlv = next('responderID');
336
+ let responderId;
337
+ if (ridTlv.cls === CLS.CONTEXT && ridTlv.tag === 1 && ridTlv.constructed) {
338
+ const inner = explicitInner(bytes, ridTlv, 'responderID');
339
+ expectTlv(inner, { tag: TAG.SEQUENCE, constructed: true }, 'ResponderID byName');
340
+ responderId = { kind: 'name', nameBytes: element(bytes, inner) };
341
+ } else if (ridTlv.cls === CLS.CONTEXT && ridTlv.tag === 2 && ridTlv.constructed) {
342
+ const inner = explicitInner(bytes, ridTlv, 'responderID');
343
+ expectTlv(inner, { tag: TAG.OCTET_STRING, constructed: false }, 'ResponderID byKey');
344
+ const keyHash = content(bytes, inner);
345
+ // KeyHash is defined as the SHA-1 of the responder's public key; any other length cannot be
346
+ // one and could never match anything, so it is refused at parse rather than mismatched later.
347
+ if (keyHash.byteLength !== 20) {
348
+ throw parseError(inner.start, `ResponderID byKey must be a 20-byte SHA-1 KeyHash, got ${keyHash.byteLength}`);
349
+ }
350
+ responderId = { kind: 'key', keyHash };
351
+ } else {
352
+ throw parseError(ridTlv.start,
353
+ `responderID must be [1] byName or [2] byKey, got ${tagName(ridTlv.cls, ridTlv.tag)}`);
354
+ }
355
+ const producedAt = readGeneralizedTime(bytes, next('producedAt'), 'producedAt');
356
+ const listTlv = next('responses');
357
+ expectTlv(listTlv, { tag: TAG.SEQUENCE, constructed: true }, 'responses');
358
+ const singles = children(bytes, listTlv, 'responses').map((s) => parseSingleResponse(bytes, s));
359
+ if (i < kids.length && kids[i].cls === CLS.CONTEXT && kids[i].tag === 1) {
360
+ checkExtensions(bytes, kids[i++], RECOGNIZED_RESPONSE_EXTENSIONS, 'responseExtensions');
361
+ }
362
+ if (i !== kids.length) throw parseError(kids[i].start, 'unexpected trailing ResponseData field');
363
+ return { responderId, producedAt, singles };
364
+ }
365
+
366
+ function parseBasicResponse(bytes) {
367
+ const outer = readAll(bytes, 'BasicOCSPResponse');
368
+ expectTlv(outer, { tag: TAG.SEQUENCE, constructed: true }, 'BasicOCSPResponse');
369
+ const kids = children(bytes, outer, 'BasicOCSPResponse');
370
+ if (kids.length < 3 || kids.length > 4) {
371
+ throw parseError(outer.start,
372
+ 'BasicOCSPResponse must be { tbsResponseData, signatureAlgorithm, signature, certs? }');
373
+ }
374
+ const tbsTlv = expectTlv(kids[0], { tag: TAG.SEQUENCE, constructed: true }, 'tbsResponseData');
375
+ const { responderId, producedAt, singles } = parseResponseData(bytes, tbsTlv);
376
+ const signatureAlgorithm = parseAlgorithmIdentifier(bytes, kids[1], 'OCSP signatureAlgorithm');
377
+ const { bytes: signature } = readBitString(bytes, kids[2], 'OCSP signature');
378
+ const certs = [];
379
+ if (kids.length === 4) {
380
+ const wrap = kids[3];
381
+ if (wrap.cls !== CLS.CONTEXT || wrap.tag !== 0 || !wrap.constructed) {
382
+ throw parseError(wrap.start, 'BasicOCSPResponse certs must be [0] EXPLICIT');
383
+ }
384
+ const inner = explicitInner(bytes, wrap, 'certs');
385
+ expectTlv(inner, { tag: TAG.SEQUENCE, constructed: true }, 'certs');
386
+ for (const [index, certTlv] of children(bytes, inner, 'certs').entries()) {
387
+ try {
388
+ certs.push(parseCertificate(element(bytes, certTlv)));
389
+ } catch (e) {
390
+ // An attached blob that is not a certificate cannot vouch for anything, and tolerating
391
+ // it would mean carrying unparseable bytes into a trust decision.
392
+ throw new CertificateError(codes.CERT_PARSE,
393
+ `attached certificate ${index} in BasicOCSPResponse.certs does not parse: ` +
394
+ `${e?.message ?? e}`,
395
+ { index });
396
+ }
397
+ }
398
+ }
399
+ return {
400
+ tbsBytes: element(bytes, tbsTlv),
401
+ signatureAlgorithm,
402
+ signature,
403
+ responderId,
404
+ producedAt,
405
+ singles,
406
+ certs,
407
+ };
408
+ }
409
+
410
+ /**
411
+ * Parse a DER OCSPResponse (RFC 6960 s4.2.1) into a fully-walked structure.
412
+ *
413
+ * A parser, not a judge, exactly like parseCertificate: it throws OCSP_PARSE on malformed or
414
+ * self-contradictory bytes and on constructs this package refuses to interpret (unknown
415
+ * responseType, unrecognized critical extensions); whether the parsed response is TRUE is
416
+ * verifyOcspStaple's problem.
417
+ *
418
+ * @param {Uint8Array} der
419
+ * @returns {OcspResponse}
420
+ */
421
+ export function parseOcspResponse(der) {
422
+ try {
423
+ if (!(der instanceof Uint8Array)) {
424
+ throw parseError(0, 'OCSP response must be a Uint8Array of DER');
425
+ }
426
+ const outer = readAll(der, 'OCSPResponse');
427
+ expectTlv(outer, { tag: TAG.SEQUENCE, constructed: true }, 'OCSPResponse');
428
+ const kids = children(der, outer, 'OCSPResponse');
429
+ if (kids.length < 1 || kids.length > 2) {
430
+ throw parseError(outer.start, 'OCSPResponse must be { responseStatus, responseBytes? }');
431
+ }
432
+ const responseStatus = readEnumerated(der, kids[0], 'responseStatus');
433
+ const responseStatusName = RESPONSE_STATUS_NAME[responseStatus] ?? 'unknown status';
434
+ if (responseStatus !== 0) {
435
+ if (kids.length !== 1) {
436
+ // RFC 6960 s4.2.1: on error the responseBytes field is not set. A non-success status
437
+ // dragging a response body along is two stories in one message.
438
+ throw parseError(kids[1].start,
439
+ `OCSPResponse with responseStatus ${responseStatusName}(${responseStatus}) must not carry responseBytes`);
440
+ }
441
+ return { responseStatus, responseStatusName, basic: null };
442
+ }
443
+ if (kids.length !== 2) {
444
+ throw parseError(outer.start, 'successful OCSPResponse is missing responseBytes');
445
+ }
446
+ const wrap = kids[1];
447
+ if (wrap.cls !== CLS.CONTEXT || wrap.tag !== 0 || !wrap.constructed) {
448
+ throw parseError(wrap.start, 'responseBytes must be [0] EXPLICIT');
449
+ }
450
+ const inner = explicitInner(der, wrap, 'responseBytes');
451
+ expectTlv(inner, { tag: TAG.SEQUENCE, constructed: true }, 'ResponseBytes');
452
+ const [typeTlv, respTlv, ...extra] = children(der, inner, 'ResponseBytes');
453
+ if (!typeTlv || !respTlv || extra.length) {
454
+ throw parseError(inner.start, 'ResponseBytes must be { responseType, response }');
455
+ }
456
+ const responseType = readOid(der, typeTlv, 'responseType');
457
+ if (responseType !== OID.ocspBasic) {
458
+ throw parseError(typeTlv.start,
459
+ `responseType ${responseType} is not id-pkix-ocsp-basic (${OID.ocspBasic}); no other ` +
460
+ 'response type is defined for stapling');
461
+ }
462
+ expectTlv(respTlv, { tag: TAG.OCTET_STRING, constructed: false }, 'response');
463
+ const basic = parseBasicResponse(content(der, respTlv));
464
+ return { responseStatus, responseStatusName, basic };
465
+ } catch (e) {
466
+ // Low-level DER failures arrive as CERT_PARSE from der.js/x509.js; re-badge them so a caller
467
+ // switching on codes sees one story for "the staple did not parse", with the byte-precise
468
+ // message preserved.
469
+ if (e instanceof CertificateError && e.code === codes.CERT_PARSE) {
470
+ throw ocspError(codes.OCSP_PARSE,
471
+ `stapled OCSP response is malformed: ${e.message}`, e.detail);
472
+ }
473
+ throw e;
474
+ }
475
+ }
476
+
477
+ /**
478
+ * The issuer of the certificate under check, reduced to what OCSP verification needs. Built by
479
+ * the caller from the VALIDATED path — the certificate that actually signed the leaf, or the
480
+ * trust anchor when the leaf sits directly under one — never from the unverified wire chain.
481
+ * @typedef {object} OcspIssuer
482
+ * @property {Uint8Array} subjectBytes exact subject Name DER
483
+ * @property {Uint8Array} spkiDer SubjectPublicKeyInfo DER
484
+ * @property {string} subjectText for error messages
485
+ */
486
+
487
+ /**
488
+ * What a verified `good` staple reports. Every other outcome throws; there is no boolean.
489
+ * @typedef {object} OcspVerdict
490
+ * @property {'good'} status
491
+ * @property {number} producedAt epoch ms
492
+ * @property {number} thisUpdate epoch ms
493
+ * @property {number | null} nextUpdate epoch ms
494
+ * @property {boolean} delegated whether a delegated responder certificate signed, rather than
495
+ * the CA key itself
496
+ */
497
+
498
+ const describeResponderId = (rid) =>
499
+ rid.kind === 'name'
500
+ ? `responder name ${toHex(rid.nameBytes).slice(0, 40)}…`
501
+ : `responder key hash ${toHex(rid.keyHash)}`;
502
+
503
+ /**
504
+ * Verify a stapled OCSP response against the certificate it must vouch for.
505
+ *
506
+ * Every check the module comment promises happens here, in this order: parse, match the CertID
507
+ * to `leaf`/`issuer`, establish the signer (the CA itself or an RFC 6960 s4.2.2.2 delegated
508
+ * responder), verify the signature over the original tbsResponseData bytes, and only then read
509
+ * the verdict and its freshness window. `revoked` and `unknown` always throw; `good` throws
510
+ * unless the window covers `now`.
511
+ *
512
+ * @param {object} args
513
+ * @param {Uint8Array} args.staple DER OCSPResponse, exactly as the peer stapled it
514
+ * @param {import('./x509.js').Certificate} args.leaf the validated leaf the staple must cover
515
+ * @param {OcspIssuer} args.issuer the leaf's issuer, from the validated path
516
+ * @param {number} args.now epoch ms, injected — never a wall clock read here
517
+ * @returns {Promise<OcspVerdict>} every failure throws a typed CertificateError (OCSP_* codes)
518
+ */
519
+ export async function verifyOcspStaple({ staple, leaf, issuer, now }) {
520
+ const parsed = parseOcspResponse(staple);
521
+ const serialHex = leaf.serialNumber;
522
+ if (parsed.responseStatus !== 0) {
523
+ // tryLater and friends are unsigned refusals. As a staple they prove nothing about the
524
+ // certificate — and an attacker holding a revoked certificate would love them to count as
525
+ // "checked". A server that staples one has stapled nothing of value, and a staple that IS
526
+ // present must be valid (see the policy in trust/index.js).
527
+ throw ocspError(codes.OCSP_UNVERIFIED,
528
+ `stapled OCSPResponse has responseStatus ${parsed.responseStatusName}` +
529
+ `(${parsed.responseStatus}); only successful(0) carries a signed certificate status, so ` +
530
+ `it cannot vouch for serial 0x${serialHex}`,
531
+ { responseStatus: parsed.responseStatus, serial: serialHex });
532
+ }
533
+ const basic = /** @type {OcspBasicResponse} */ (parsed.basic);
534
+
535
+ // --- 1. CertID: is this response about the certificate in hand? -----------------------------
536
+ // The hashes are recomputed from OUR validated material: the leaf's issuer Name exactly as the
537
+ // leaf encodes it (RFC 6960 s4.1.1 hashes the issuer field of the certificate being checked)
538
+ // and the issuer's public key bit-string content from the validated path. Nothing the response
539
+ // asserts about identity is taken at its word.
540
+ const issuerKeyBytes = parseSubjectPublicKeyInfo(issuer.spkiDer).keyBytes;
541
+ /** @type {Map<string, { nameHash: Uint8Array, keyHash: Uint8Array }>} */
542
+ const digestCache = new Map();
543
+ const hashesFor = async (name) => {
544
+ let entry = digestCache.get(name);
545
+ if (!entry) {
546
+ entry = {
547
+ nameHash: new Uint8Array(await crypto.subtle.digest(name, leaf.issuer.bytes)),
548
+ keyHash: new Uint8Array(await crypto.subtle.digest(name, issuerKeyBytes)),
549
+ };
550
+ digestCache.set(name, entry);
551
+ }
552
+ return entry;
553
+ };
554
+
555
+ let single = null;
556
+ const covered = [];
557
+ for (const s of basic.singles) {
558
+ const alg = CERTID_HASH[s.certId.hashOid];
559
+ if (!alg) {
560
+ throw ocspError(codes.OCSP_PARSE,
561
+ `stapled OCSP response uses CertID hash algorithm ${s.certId.hashOid}, which is not ` +
562
+ 'supported (SHA-1, SHA-256, SHA-384 and SHA-512 are)',
563
+ { oid: s.certId.hashOid });
564
+ }
565
+ if (s.certId.issuerNameHash.byteLength !== alg.length ||
566
+ s.certId.issuerKeyHash.byteLength !== alg.length) {
567
+ throw ocspError(codes.OCSP_PARSE,
568
+ `stapled OCSP response CertID declares ${alg.name} but carries ` +
569
+ `${s.certId.issuerNameHash.byteLength}/${s.certId.issuerKeyHash.byteLength}-byte hashes`,
570
+ { oid: s.certId.hashOid });
571
+ }
572
+ covered.push(`serial 0x${toHex(s.certId.serialBytes)} (${alg.name})`);
573
+ if (toHex(s.certId.serialBytes) !== serialHex) continue;
574
+ const { nameHash, keyHash } = await hashesFor(alg.name);
575
+ if (equal(nameHash, s.certId.issuerNameHash) && equal(keyHash, s.certId.issuerKeyHash)) {
576
+ single = s;
577
+ break;
578
+ }
579
+ }
580
+ if (single === null) {
581
+ throw ocspError(codes.OCSP_MISMATCH,
582
+ `stapled OCSP response does not cover certificate serial 0x${serialHex} issued by ` +
583
+ `"${issuer.subjectText}"; it covers ${covered.length ? covered.join(', ') : 'no certificates'}. ` +
584
+ 'A response for a different certificate proves nothing about this one',
585
+ { serial: serialHex, covered });
586
+ }
587
+
588
+ // --- 2. Who signed, and are they allowed to? ------------------------------------------------
589
+ const sha1 = async (b) => new Uint8Array(await crypto.subtle.digest('SHA-1', b));
590
+ const rid = basic.responderId;
591
+ const signedByIssuer = rid.kind === 'name'
592
+ ? equal(rid.nameBytes, issuer.subjectBytes)
593
+ : equal(rid.keyHash, await sha1(issuerKeyBytes));
594
+
595
+ let signerSpkiDer;
596
+ let signerText;
597
+ let delegated = false;
598
+ if (signedByIssuer) {
599
+ signerSpkiDer = issuer.spkiDer;
600
+ signerText = issuer.subjectText;
601
+ } else {
602
+ // RFC 6960 s4.2.2.2 (Authorized Responders): a responder other than the CA must present a
603
+ // certificate that the CA that issued the leaf issued DIRECTLY, carrying id-kp-OCSPSigning.
604
+ // Each requirement below closes a specific hole, named where it is enforced.
605
+ let responder = null;
606
+ for (const cert of basic.certs) {
607
+ const match = rid.kind === 'name'
608
+ ? equal(cert.subject.bytes, rid.nameBytes)
609
+ : equal(await sha1(cert.spki.keyBytes), rid.keyHash);
610
+ if (match) {
611
+ responder = cert;
612
+ break;
613
+ }
614
+ }
615
+ if (responder === null) {
616
+ throw ocspError(codes.OCSP_UNVERIFIED,
617
+ `stapled OCSP response is signed by ${describeResponderId(rid)}, which is neither the ` +
618
+ `issuing CA "${issuer.subjectText}" nor any certificate attached to the response; ` +
619
+ 'there is no key to verify it against',
620
+ { serial: serialHex });
621
+ }
622
+ const subject = responder.subject.text;
623
+ if (!equal(responder.issuer.bytes, issuer.subjectBytes)) {
624
+ // Without direct issuance, any CA anywhere could bless a "responder" for this CA's
625
+ // certificates. The trust in a delegated responder flows from exactly one place: the CA
626
+ // whose certificate is being checked.
627
+ throw ocspError(codes.OCSP_UNVERIFIED,
628
+ `OCSP responder certificate "${subject}" was issued by "${responder.issuer.text}", not ` +
629
+ `by "${issuer.subjectText}" which issued the certificate being checked; RFC 6960 ` +
630
+ 's4.2.2.2 requires direct issuance',
631
+ { responder: subject });
632
+ }
633
+ // The EKU must name id-kp-OCSPSigning explicitly; anyExtendedKeyUsage does not count here.
634
+ // Without this check, every ordinary TLS certificate the CA issued could sign "good"
635
+ // responses for every other — key compromise of any customer would defeat revocation itself.
636
+ if (!responder.extendedKeyUsage || !responder.extendedKeyUsage.includes(OID.ocspSigning)) {
637
+ throw ocspError(codes.OCSP_UNVERIFIED,
638
+ `OCSP responder certificate "${subject}" does not carry the id-kp-OCSPSigning extended ` +
639
+ `key usage (has: ${responder.extendedKeyUsage?.join(', ') ?? 'no EKU'}); a certificate ` +
640
+ 'the CA did not designate for OCSP signing cannot answer for its revocations',
641
+ { responder: subject, eku: responder.extendedKeyUsage ? [...responder.extendedKeyUsage] : null });
642
+ }
643
+ if (now < responder.notBefore || now > responder.notAfter) {
644
+ throw ocspError(codes.OCSP_UNVERIFIED,
645
+ `OCSP responder certificate "${subject}" is not valid at ${iso(now)} ` +
646
+ `(validity ${iso(responder.notBefore)} .. ${iso(responder.notAfter)})`,
647
+ { responder: subject, notBefore: responder.notBefore, notAfter: responder.notAfter });
648
+ }
649
+ if (responder.keyUsage && !responder.keyUsage.digitalSignature) {
650
+ throw ocspError(codes.OCSP_UNVERIFIED,
651
+ `OCSP responder certificate "${subject}" has a keyUsage without digitalSignature, ` +
652
+ 'which signing a response requires',
653
+ { responder: subject });
654
+ }
655
+ // id-pkix-ocsp-nocheck asks relying parties not to check the responder certificate's own
656
+ // revocation status — which is already this module's behaviour (there is no recursion to
657
+ // suppress), so the extension is recognized rather than fatal even when marked critical.
658
+ const unknownCritical = responder.unknownCriticalExtensions.filter((o) => o !== OID.ocspNocheck);
659
+ if (unknownCritical.length > 0) {
660
+ throw ocspError(codes.OCSP_UNVERIFIED,
661
+ `OCSP responder certificate "${subject}" carries unrecognized critical extension(s) ` +
662
+ `${unknownCritical.join(', ')}`,
663
+ { responder: subject, oids: unknownCritical });
664
+ }
665
+ try {
666
+ await verifySignedObject(responder, issuer.spkiDer, issuer.subjectText);
667
+ } catch (e) {
668
+ throw ocspError(codes.OCSP_UNVERIFIED,
669
+ `OCSP responder certificate "${subject}" does not verify under the CA key: ` +
670
+ `${e?.message ?? e}`,
671
+ { responder: subject, cause: e?.code });
672
+ }
673
+ signerSpkiDer = responder.spki.spkiDer;
674
+ signerText = `${subject} (delegated OCSP responder)`;
675
+ delegated = true;
676
+ }
677
+
678
+ // --- 3. The signature itself, over the peer's exact tbsResponseData bytes -------------------
679
+ // This is the moment the staple stops being attacker-controlled bytes: everything matched and
680
+ // selected above is only believed because this signature covers it. verifySignedObject is the
681
+ // same verifier certificates go through, so weak algorithms die by OID before any cryptography
682
+ // runs, and ECDSA/PSS handling cannot drift from the path validator's.
683
+ try {
684
+ await verifySignedObject(
685
+ {
686
+ tbsBytes: basic.tbsBytes,
687
+ signature: basic.signature,
688
+ signatureAlgorithm: basic.signatureAlgorithm,
689
+ subject: { text: 'the stapled OCSP response' },
690
+ },
691
+ signerSpkiDer,
692
+ signerText,
693
+ );
694
+ } catch (e) {
695
+ throw ocspError(codes.OCSP_UNVERIFIED,
696
+ `stapled OCSP response for serial 0x${serialHex} cannot be trusted: ${e?.message ?? e}`,
697
+ { serial: serialHex, cause: e?.code });
698
+ }
699
+
700
+ // --- 4. The verdict — revoked and unknown first, so neither can hide behind staleness -------
701
+ if (single.status.kind === 'revoked') {
702
+ const reason = single.status.reason;
703
+ const reasonText = reason === null
704
+ ? 'no reason given'
705
+ : `reason ${CRL_REASON_NAME[reason] ?? 'unrecognized'}(${reason})`;
706
+ // Age does not soften this one: a CA once said "revoked" about this serial, and revocations
707
+ // effectively never un-happen (certificateHold aside, and failing closed on a hold is the
708
+ // right side to be wrong on).
709
+ throw ocspError(codes.OCSP_REVOKED,
710
+ `OCSP: certificate serial 0x${serialHex} ("${leaf.subject.text}") is revoked, ` +
711
+ `${reasonText}, since ${iso(single.status.revocationTime)}`,
712
+ {
713
+ serial: serialHex,
714
+ subject: leaf.subject.text,
715
+ revocationTime: single.status.revocationTime,
716
+ reason,
717
+ reasonName: reason === null ? null : CRL_REASON_NAME[reason] ?? null,
718
+ });
719
+ }
720
+ if (single.status.kind === 'unknown') {
721
+ throw ocspError(codes.OCSP_UNKNOWN,
722
+ `OCSP: the responder for "${issuer.subjectText}" does not know certificate serial ` +
723
+ `0x${serialHex}; a serial the CA's own responder cannot vouch for is not treated as good`,
724
+ { serial: serialHex });
725
+ }
726
+
727
+ // --- 5. Freshness of the `good`, against the injected clock ---------------------------------
728
+ if (single.thisUpdate > now + CLOCK_SKEW_MS) {
729
+ throw ocspError(codes.OCSP_STALE,
730
+ `stapled OCSP response for serial 0x${serialHex} is from the future: thisUpdate ` +
731
+ `${iso(single.thisUpdate)} vs now ${iso(now)} (skew allowance ${CLOCK_SKEW_MS / 1000}s)`,
732
+ { serial: serialHex, thisUpdate: single.thisUpdate, now });
733
+ }
734
+ if (single.nextUpdate !== null) {
735
+ if (now > single.nextUpdate + CLOCK_SKEW_MS) {
736
+ // An expired "good" is a replayable one: without this check, one captured response would
737
+ // vouch for a certificate forever, which is precisely what revocation exists to end.
738
+ throw ocspError(codes.OCSP_STALE,
739
+ `stapled OCSP response for serial 0x${serialHex} expired: nextUpdate ` +
740
+ `${iso(single.nextUpdate)} vs now ${iso(now)} (window ${iso(single.thisUpdate)} .. ` +
741
+ `${iso(single.nextUpdate)})`,
742
+ { serial: serialHex, thisUpdate: single.thisUpdate, nextUpdate: single.nextUpdate, now });
743
+ }
744
+ } else if (now - single.thisUpdate > MAX_AGE_WITHOUT_NEXT_UPDATE_MS) {
745
+ throw ocspError(codes.OCSP_STALE,
746
+ `stapled OCSP response for serial 0x${serialHex} has no nextUpdate and its thisUpdate ` +
747
+ `${iso(single.thisUpdate)} is older than the ${MAX_AGE_WITHOUT_NEXT_UPDATE_MS / 86400000}-day ` +
748
+ `cap at now ${iso(now)}`,
749
+ { serial: serialHex, thisUpdate: single.thisUpdate, now });
750
+ }
751
+
752
+ return {
753
+ status: 'good',
754
+ producedAt: basic.producedAt,
755
+ thisUpdate: single.thisUpdate,
756
+ nextUpdate: single.nextUpdate,
757
+ delegated,
758
+ };
759
+ }