• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

PeculiarVentures / PKI.js / 30216016876

26 Jul 2026 07:02PM UTC coverage: 71.926% (+1.2%) from 70.735%
30216016876

Pull #502

github

web-flow
Merge f0e60a084 into c26c95cc9
Pull Request #502: fix(ocsp): add OCSP responder authorization checks (RFC 6960 §4.2.2.2)

3296 of 5432 branches covered (60.68%)

144 of 160 new or added lines in 1 file covered. (90.0%)

5698 of 7922 relevant lines covered (71.93%)

1913.96 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

84.77
/src/BasicOCSPResponse.ts
1
import * as asn1js from "asn1js";
2
import * as pvtsutils from "pvtsutils";
3
import * as pvutils from "pvutils";
4
import * as common from "./common";
5
import { ResponseData, ResponseDataJson, ResponseDataSchema } from "./ResponseData";
6
import { AlgorithmIdentifier, AlgorithmIdentifierJson, AlgorithmIdentifierSchema } from "./AlgorithmIdentifier";
7
import { Certificate, CertificateJson, CertificateSchema, checkCA } from "./Certificate";
8
import { CertID } from "./CertID";
9
import { ExtKeyUsage } from "./ExtKeyUsage";
10
import { RelativeDistinguishedNames } from "./RelativeDistinguishedNames";
11
import { CertificateChainValidationEngine } from "./CertificateChainValidationEngine";
12
import * as Schema from "./Schema";
13
import { PkiObject, PkiObjectParameters } from "./PkiObject";
14
import { AsnError } from "./errors";
15
import { EMPTY_STRING } from "./constants";
16

17
const TBS_RESPONSE_DATA = "tbsResponseData";
42✔
18
const SIGNATURE_ALGORITHM = "signatureAlgorithm";
42✔
19
const SIGNATURE = "signature";
42✔
20
const CERTS = "certs";
42✔
21
const BASIC_OCSP_RESPONSE = "BasicOCSPResponse";
42✔
22
const BASIC_OCSP_RESPONSE_TBS_RESPONSE_DATA = `${BASIC_OCSP_RESPONSE}.${TBS_RESPONSE_DATA}`;
42✔
23
const BASIC_OCSP_RESPONSE_SIGNATURE_ALGORITHM = `${BASIC_OCSP_RESPONSE}.${SIGNATURE_ALGORITHM}`;
42✔
24
const BASIC_OCSP_RESPONSE_SIGNATURE = `${BASIC_OCSP_RESPONSE}.${SIGNATURE}`;
42✔
25
const BASIC_OCSP_RESPONSE_CERTS = `${BASIC_OCSP_RESPONSE}.${CERTS}`;
42✔
26
const CLEAR_PROPS = [
42✔
27
  BASIC_OCSP_RESPONSE_TBS_RESPONSE_DATA,
28
  BASIC_OCSP_RESPONSE_SIGNATURE_ALGORITHM,
29
  BASIC_OCSP_RESPONSE_SIGNATURE,
30
  BASIC_OCSP_RESPONSE_CERTS
31
];
32

33
/**
34
 * Extended Key Usage OID `id-kp-OCSPSigning` (RFC 6960 §4.2.2.2.1).
35
 * An OCSP responder delegated by the issuer MUST carry this EKU.
36
 */
37
const ID_KP_OCSP_SIGNING = "1.3.6.1.5.5.7.3.9";
42✔
38
/**
39
 * Extension OID `id-ce-keyUsage` (RFC 5280 §4.2.1.3).
40
 */
41
const ID_CE_KEY_USAGE = "2.5.29.15";
42✔
42
/**
43
 * Extension OID `id-ce-extKeyUsage` (RFC 5280 §4.2.1.12).
44
 */
45
const ID_CE_EXT_KEY_USAGE = "2.5.29.37";
42✔
46

47
export interface IBasicOCSPResponse {
48
  tbsResponseData: ResponseData;
49
  signatureAlgorithm: AlgorithmIdentifier;
50
  signature: asn1js.BitString;
51
  certs?: Certificate[];
52
}
53

54
export interface CertificateStatus {
55
  isForCertificate: boolean;
56
  /**
57
   * 0 = good, 1 = revoked, 2 = unknown
58
   */
59
  status: number;
60
}
61

62
export type BasicOCSPResponseParameters = PkiObjectParameters & Partial<IBasicOCSPResponse>;
63

64
export interface BasicOCSPResponseVerifyParams {
65
  /**
66
   * Trust anchors used for building the signer's certificate chain.
67
   *
68
   * **Important:** being merely chained to a certificate in `trustedCerts` does
69
   * NOT authorize a certificate to sign OCSP responses. Inclusion in
70
   * `trustedCerts` only makes a chain verifiable; authorization to respond for a
71
   * given CA still requires being that CA, being a delegated responder issued by
72
   * that CA with `id-kp-OCSPSigning`, or being listed in `trustedResponders`.
73
   */
74
  trustedCerts?: Certificate[];
75
  /**
76
   * Candidate issuer certificates used to resolve the `CertID` of each
77
   * `SingleResponse` and to validate delegated OCSP responders.
78
   *
79
   * This context is optional. For each `SingleResponse`, PKI.js looks for an
80
   * issuer whose `issuerNameHash` / `issuerKeyHash` (computed using the
81
   * response's hash algorithm) match the `CertID`. Issuers are also searched
82
   * in embedded response certificates, `trustedCerts`, and the validated
83
   * signer path. `verify()` fails closed only when the issuer cannot be found
84
   * in any of those sources.
85
   */
86
  issuerCerts?: Certificate[];
87
  /**
88
   * Certificates explicitly authorized to sign OCSP responses, independent
89
   * of CA delegation. Matching is by exact DER of the full certificate;
90
   * chaining to one of these certificates does NOT inherit this permission.
91
   *
92
   * For a matched certificate, chain validation, expiry, and
93
   * `id-kp-OCSPSigning` are skipped. The OCSP signature MUST still verify
94
   * against the pinned certificate's public key. Use this for locally
95
   * trusted responders that are not the issuing CA and are not formally
96
   * delegated. For normal delegated responders, leave this list empty.
97
   */
98
  trustedResponders?: Certificate[];
99
}
100

101
export interface BasicOCSPResponseJson {
102
  tbsResponseData: ResponseDataJson;
103
  signatureAlgorithm: AlgorithmIdentifierJson;
104
  signature: asn1js.BitStringJson;
105
  certs?: CertificateJson[];
106
}
107

108
/**
109
 * Represents the BasicOCSPResponse structure described in [RFC6960](https://datatracker.ietf.org/doc/html/rfc6960)
110
 */
111
export class BasicOCSPResponse extends PkiObject implements IBasicOCSPResponse {
112

113
  public static override CLASS_NAME = "BasicOCSPResponse";
42✔
114

115
  public tbsResponseData!: ResponseData;
116
  public signatureAlgorithm!: AlgorithmIdentifier;
117
  public signature!: asn1js.BitString;
118
  public certs?: Certificate[];
119

120
  /**
121
   * Initializes a new instance of the {@link BasicOCSPResponse} class
122
   * @param parameters Initialization parameters
123
   */
124
  constructor(parameters: BasicOCSPResponseParameters = {}) {
158✔
125
    super();
158✔
126

127
    this.tbsResponseData = pvutils.getParametersValue(parameters, TBS_RESPONSE_DATA, BasicOCSPResponse.defaultValues(TBS_RESPONSE_DATA));
158✔
128
    this.signatureAlgorithm = pvutils.getParametersValue(parameters, SIGNATURE_ALGORITHM, BasicOCSPResponse.defaultValues(SIGNATURE_ALGORITHM));
158✔
129
    this.signature = pvutils.getParametersValue(parameters, SIGNATURE, BasicOCSPResponse.defaultValues(SIGNATURE));
158✔
130
    if (CERTS in parameters) {
158!
131
      this.certs = pvutils.getParametersValue(parameters, CERTS, BasicOCSPResponse.defaultValues(CERTS));
×
132
    }
133

134
    if (parameters.schema) {
158✔
135
      this.fromSchema(parameters.schema);
80✔
136
    }
137
  }
138

139
  /**
140
   * Returns default values for all class members
141
   * @param memberName String name for a class member
142
   * @returns Default value
143
   */
144
  public static override defaultValues(memberName: typeof TBS_RESPONSE_DATA): ResponseData;
145
  public static override defaultValues(memberName: typeof SIGNATURE_ALGORITHM): AlgorithmIdentifier;
146
  public static override defaultValues(memberName: typeof SIGNATURE): asn1js.BitString;
147
  public static override defaultValues(memberName: typeof CERTS): Certificate[];
148
  public static override defaultValues(memberName: string): any {
149
    switch (memberName) {
474!
150
      case TBS_RESPONSE_DATA:
151
        return new ResponseData();
158✔
152
      case SIGNATURE_ALGORITHM:
153
        return new AlgorithmIdentifier();
158✔
154
      case SIGNATURE:
155
        return new asn1js.BitString();
158✔
156
      case CERTS:
157
        return [];
×
158
      default:
159
        return super.defaultValues(memberName);
×
160
    }
161
  }
162

163
  /**
164
   * Compare values with default values for all class members
165
   * @param memberName String name for a class member
166
   * @param memberValue Value to compare with default value
167
   */
168
  public static compareWithDefault(memberName: string, memberValue: any): boolean {
169
    switch (memberName) {
×
170
      case "type":
171
        {
172
          let comparisonResult = ((ResponseData.compareWithDefault("tbs", memberValue.tbs)) &&
×
173
            (ResponseData.compareWithDefault("responderID", memberValue.responderID)) &&
174
            (ResponseData.compareWithDefault("producedAt", memberValue.producedAt)) &&
175
            (ResponseData.compareWithDefault("responses", memberValue.responses)));
176

177
          if ("responseExtensions" in memberValue)
×
178
            comparisonResult = comparisonResult && (ResponseData.compareWithDefault("responseExtensions", memberValue.responseExtensions));
×
179

180
          return comparisonResult;
×
181
        }
182
      case SIGNATURE_ALGORITHM:
183
        return ((memberValue.algorithmId === EMPTY_STRING) && (("algorithmParams" in memberValue) === false));
×
184
      case SIGNATURE:
185
        return (memberValue.isEqual(BasicOCSPResponse.defaultValues(memberName)));
×
186
      case CERTS:
187
        return (memberValue.length === 0);
×
188
      default:
189
        return super.defaultValues(memberName);
×
190
    }
191
  }
192

193
  /**
194
   * @inheritdoc
195
   * @asn ASN.1 schema
196
   * ```asn
197
   * BasicOCSPResponse ::= SEQUENCE {
198
   *    tbsResponseData      ResponseData,
199
   *    signatureAlgorithm   AlgorithmIdentifier,
200
   *    signature            BIT STRING,
201
   *    certs            [0] EXPLICIT SEQUENCE OF Certificate OPTIONAL }
202
   *```
203
   */
204
  public static override schema(parameters: Schema.SchemaParameters<{
80✔
205
    tbsResponseData?: ResponseDataSchema;
206
    signatureAlgorithm?: AlgorithmIdentifierSchema;
207
    signature?: string;
208
    certs?: CertificateSchema;
209
  }> = {}): Schema.SchemaType {
210
    const names = pvutils.getParametersValue<NonNullable<typeof parameters.names>>(parameters, "names", {});
80✔
211

212
    return (new asn1js.Sequence({
80✔
213
      name: (names.blockName || BASIC_OCSP_RESPONSE),
160✔
214
      value: [
215
        ResponseData.schema(names.tbsResponseData || {
160✔
216
          names: {
217
            blockName: BASIC_OCSP_RESPONSE_TBS_RESPONSE_DATA
218
          }
219
        }),
220
        AlgorithmIdentifier.schema(names.signatureAlgorithm || {
160✔
221
          names: {
222
            blockName: BASIC_OCSP_RESPONSE_SIGNATURE_ALGORITHM
223
          }
224
        }),
225
        new asn1js.BitString({ name: (names.signature || BASIC_OCSP_RESPONSE_SIGNATURE) }),
160✔
226
        new asn1js.Constructed({
227
          optional: true,
228
          idBlock: {
229
            tagClass: 3, // CONTEXT-SPECIFIC
230
            tagNumber: 0 // [0]
231
          },
232
          value: [
233
            new asn1js.Sequence({
234
              value: [new asn1js.Repeated({
235
                name: BASIC_OCSP_RESPONSE_CERTS,
236
                value: Certificate.schema(names.certs || {})
160✔
237
              })]
238
            })
239
          ]
240
        })
241
      ]
242
    }));
243
  }
244

245
  public fromSchema(schema: Schema.SchemaType): void {
246
    // Clear input data first
247
    pvutils.clearProps(schema, CLEAR_PROPS);
80✔
248
    //#endregion
249

250
    // Check the schema is valid
251
    const asn1 = asn1js.compareSchema(schema,
80✔
252
      schema,
253
      BasicOCSPResponse.schema()
254
    );
255
    AsnError.assertSchema(asn1, this.className);
80✔
256

257
    //#region Get internal properties from parsed schema
258
    this.tbsResponseData = new ResponseData({ schema: asn1.result[BASIC_OCSP_RESPONSE_TBS_RESPONSE_DATA] });
80✔
259
    this.signatureAlgorithm = new AlgorithmIdentifier({ schema: asn1.result[BASIC_OCSP_RESPONSE_SIGNATURE_ALGORITHM] });
80✔
260
    this.signature = asn1.result[BASIC_OCSP_RESPONSE_SIGNATURE];
80✔
261

262
    if (BASIC_OCSP_RESPONSE_CERTS in asn1.result) {
80!
263
      this.certs = Array.from(asn1.result[BASIC_OCSP_RESPONSE_CERTS], element => new Certificate({ schema: element }));
158✔
264
    }
265
    //#endregion
266
  }
267

268
  public toSchema(): asn1js.Sequence {
269
    //#region Create array for output sequence
270
    const outputArray = [];
80✔
271

272
    outputArray.push(this.tbsResponseData.toSchema());
80✔
273
    outputArray.push(this.signatureAlgorithm.toSchema());
80✔
274
    outputArray.push(this.signature);
80✔
275

276
    //#region Create array of certificates
277
    if (this.certs) {
80!
278
      outputArray.push(new asn1js.Constructed({
80✔
279
        idBlock: {
280
          tagClass: 3, // CONTEXT-SPECIFIC
281
          tagNumber: 0 // [0]
282
        },
283
        value: [
284
          new asn1js.Sequence({
285
            value: Array.from(this.certs, o => o.toSchema())
158✔
286
          })
287
        ]
288
      }));
289
    }
290
    //#endregion
291
    //#endregion
292

293
    //#region Construct and return new ASN.1 schema for this object
294
    return (new asn1js.Sequence({
80✔
295
      value: outputArray
296
    }));
297
    //#endregion
298
  }
299

300
  public toJSON(): BasicOCSPResponseJson {
301
    const res: BasicOCSPResponseJson = {
×
302
      tbsResponseData: this.tbsResponseData.toJSON(),
303
      signatureAlgorithm: this.signatureAlgorithm.toJSON(),
304
      signature: this.signature.toJSON(),
305
    };
306

307
    if (this.certs) {
×
308
      res.certs = Array.from(this.certs, o => o.toJSON());
×
309
    }
310

311
    return res;
×
312
  }
313

314
  /**
315
   * Get OCSP response status for specific certificate.
316
   *
317
   * **Important:** this method only inspects the `SingleResponse` entries in
318
   * the response — it does **not** authenticate the responder. A successful
319
   * call MUST NOT be relied upon unless `verify()` has already returned
320
   * `true` for this response (under suitable trust parameters). Without
321
   * verification, an attacker-supplied response can claim any status.
322
   *
323
   * @param certificate Certificate to be checked
324
   * @param issuerCertificate Certificate of issuer for certificate to be checked
325
   * @param crypto Crypto engine
326
   */
327
  public async getCertificateStatus(certificate: Certificate, issuerCertificate: Certificate, crypto = common.getCrypto(true)): Promise<CertificateStatus> {
8✔
328
    //#region Initial variables
329
    const result = {
8✔
330
      isForCertificate: false,
331
      status: 2 // 0 = good, 1 = revoked, 2 = unknown
332
    };
333

334
    const hashesObject: Record<string, number> = {};
8✔
335

336
    const certIDs: CertID[] = [];
8✔
337
    //#endregion
338

339
    //#region Create all "certIDs" for input certificates
340
    for (const response of this.tbsResponseData.responses) {
8✔
341
      const hashAlgorithm = crypto.getAlgorithmByOID(response.certID.hashAlgorithm.algorithmId, true, "CertID.hashAlgorithm");
8✔
342

343
      if (!hashesObject[hashAlgorithm.name]) {
8!
344
        hashesObject[hashAlgorithm.name] = 1;
8✔
345

346
        const certID = new CertID();
8✔
347

348
        certIDs.push(certID);
8✔
349
        await certID.createForCertificate(certificate, {
8✔
350
          hashAlgorithm: hashAlgorithm.name,
351
          issuerCertificate
352
        }, crypto);
353
      }
354
    }
355
    //#endregion
356

357
    //#region Compare all response's "certIDs" with identifiers for input certificate
358
    for (const response of this.tbsResponseData.responses) {
8✔
359
      for (const id of certIDs) {
8✔
360
        if (response.certID.isEqual(id)) {
8✔
361
          result.isForCertificate = true;
6✔
362

363
          try {
6✔
364
            switch (response.certStatus.idBlock.isConstructed) {
6!
365
              case true:
366
                if (response.certStatus.idBlock.tagNumber === 1)
×
367
                  result.status = 1; // revoked
×
368

369
                break;
×
370
              case false:
371
                switch (response.certStatus.idBlock.tagNumber) {
6!
372
                  case 0: // good
373
                    result.status = 0;
6✔
374
                    break;
6✔
375
                  case 2: // unknown
376
                    result.status = 2;
×
377
                    break;
×
378
                  default:
379
                }
380

381
                break;
6✔
382
              default:
383
            }
384
          }
385
          catch {
386
            // nothing
387
          }
388

389
          return result;
6✔
390
        }
391
      }
392
    }
393

394
    return result;
2✔
395
    //#endregion
396
  }
397

398
  /**
399
   * Make signature for current OCSP Basic Response
400
   * @param privateKey Private key for "subjectPublicKeyInfo" structure
401
   * @param hashAlgorithm Hashing algorithm. Default SHA-1
402
   * @param crypto Crypto engine
403
   */
404
  async sign(privateKey: CryptoKey, hashAlgorithm = "SHA-1", crypto = common.getCrypto(true)): Promise<void> {
156✔
405
    // Get a private key from function parameter
406
    if (!privateKey) {
78!
407
      throw new Error("Need to provide a private key for signing");
×
408
    }
409

410
    //#region Get a "default parameters" for current algorithm and set correct signature algorithm
411
    const signatureParams = await crypto.getSignatureParameters(privateKey, hashAlgorithm);
78✔
412

413
    const algorithm = signatureParams.parameters.algorithm;
78✔
414
    if (!("name" in algorithm)) {
78!
415
      throw new Error("Empty algorithm");
×
416
    }
417
    this.signatureAlgorithm = signatureParams.signatureAlgorithm;
78✔
418
    //#endregion
419

420
    //#region Create TBS data for signing
421
    this.tbsResponseData.tbsView = new Uint8Array(this.tbsResponseData.toSchema(true).toBER());
78✔
422
    //#endregion
423

424
    //#region Signing TBS data on provided private key
425
    const signature = await crypto.signWithPrivateKey(this.tbsResponseData.tbsView as BufferSource, privateKey, { algorithm });
78✔
426
    this.signature = new asn1js.BitString({ valueHex: signature });
78✔
427
    //#endregion
428
  }
429

430
  /**
431
   * Verify the OCSP Basic Response (RFC 6960).
432
   *
433
   * For every candidate matching the `ResponderID`, three checks are
434
   * performed in order:
435
   *  1. Cryptographic signature verification against the candidate's public key.
436
   *  2. Chain validation against `trustedCerts` (skipped for `trustedResponders`).
437
   *  3. Authorization per RFC 6960 §4.2.2.2: direct issuer, delegated responder
438
   *     with `id-kp-OCSPSigning`, or explicit `trustedResponders` entry.
439
   *
440
   * @returns `true` if a candidate passes all checks. Returns `false` on
441
   *          cryptographic signature mismatch. Throws on hard errors
442
   *          (chain failure, unauthorized responder, unsupported algorithm).
443
   */
444
  public async verify(params: BasicOCSPResponseVerifyParams = {}, crypto = common.getCrypto(true)): Promise<boolean> {
184✔
445
    //#region 0. Validate inputs
446
    if (!this.certs || this.certs.length === 0) {
92!
NEW
447
      throw new Error("No certificates attached to the BasicOCSPResponse");
×
448
    }
449

450
    const embeddedCerts: Certificate[] = this.certs;
92✔
451
    const trustedCerts: Certificate[] = params.trustedCerts || [];
92✔
452
    const issuerCerts: Certificate[] = params.issuerCerts || [];
92✔
453
    const trustedResponders: Certificate[] = params.trustedResponders || [];
92✔
454

455
    //#region 1. Collect all responder candidates
456
    const candidateIndices = await BasicOCSPResponse.collectResponderCandidates(
92✔
457
      embeddedCerts,
458
      this.tbsResponseData.responderID,
459
      crypto,
460
    );
461

462
    if (candidateIndices.length === 0) {
92!
NEW
463
      throw new Error("No certificate matching the OCSP responderID was found in the response");
×
464
    }
465

466
    //#region 2. OCSP signature verification per candidate
467
    // We do NOT deduplicate by (subject + SPKI) before verifying: two
468
    // re-issued certs may share an identity but differ in validity / EKU /
469
    // extensions, and the caller should not be penalised for the order in
470
    // which they appear. We accept the response as soon as ANY candidate
471
    // passes all steps.
472
    //
473
    // A *cryptographic* mismatch is a normal "this key didn't sign it"
474
    // signal and is folded into the (possibly multiple) "no valid
475
    // candidate" outcome. Genuine errors (unsupported algorithm, malformed
476
    // AlgorithmIdentifier, CryptoEngine failure) are surfaced via
477
    // `firstCryptoError` and re-thrown if no candidate verifies. We keep
478
    // the FIRST such error — it is usually closest to the root cause.
479
    const errors: string[] = [];
92✔
480
    let firstCryptoError: Error | null = null;
92✔
481
    let signatureMismatch = false;
92✔
482
    let hadValidSignature = false;
92✔
483

484
    for (const idx of candidateIndices) {
92✔
485
      const signerCert = embeddedCerts[idx];
94✔
486

487
      //#region 2. Signature verification
488
      let signatureOk: boolean;
489
      try {
94✔
490
        signatureOk = await this.verifyResponseSignature(signerCert, crypto);
94✔
491
      } catch (err) {
492
        // Hard error — preserve the first one and try the next candidate.
493
        if (firstCryptoError === null) {
2!
494
          firstCryptoError = err as Error;
2✔
495
        }
496
        errors.push(`candidate #${idx} signature error: ${(err as Error).message}`);
2✔
497
        continue;
2✔
498
      }
499
      if (!signatureOk) {
92✔
500
        signatureMismatch = true;
6✔
501
        continue;
6✔
502
      }
503

504
      hadValidSignature = true;
86✔
505

506
      try {
86✔
507
        //#region 3. Chain validation (skipped for explicitly trusted responders)
508
        const isTrustedResponder = trustedResponders.length > 0 &&
86✔
509
          BasicOCSPResponse.containsCertificateByDer(trustedResponders, signerCert);
510

511
        let validatedPathCerts: Certificate[] = [];
94✔
512
        if (!isTrustedResponder) {
94✔
513
          validatedPathCerts = await BasicOCSPResponse.validateSignerChain(
58✔
514
            signerCert,
515
            embeddedCerts,
516
            issuerCerts,
517
            trustedCerts,
518
            crypto,
519
          );
520
        }
521

522
        //#region 4. Authorization
523
        // Assemble issuer candidates from: issuerCerts, embedded certs,
524
        // trustedCerts, and the validated signer path (but NOT
525
        // trustedResponders — explicitly trusted responders are not
526
        // implicitly issuers). Deduplicate by EXACT DER so cross-signed
527
        // CA copies that share subject+SPKI are all retained; this matches
528
        // `findIssuersForCertID()` and lets the authorization loop try
529
        // each alternative issuing path.
530
        const authIssuerCandidates: Certificate[] = [];
82✔
531
        for (const cert of [...issuerCerts, ...embeddedCerts, ...trustedCerts, ...validatedPathCerts]) {
82✔
532
          if (!BasicOCSPResponse.containsCertificateByDer(authIssuerCandidates, cert)) {
382✔
533
            authIssuerCandidates.push(cert);
192✔
534
          }
535
        }
536

537
        let allAuthorized = true;
82✔
538
        for (const singleResponse of this.tbsResponseData.responses) {
82✔
539
          const authorized = await BasicOCSPResponse.isAuthorizedResponderForCertID(
86✔
540
            signerCert,
541
            singleResponse.certID,
542
            authIssuerCandidates,
543
            trustedResponders,
544
            crypto,
545
          );
546
          if (!authorized) {
86✔
547
            allAuthorized = false;
20✔
548
            break;
20✔
549
          }
550
        }
551

552
        if (allAuthorized) {
82✔
553
          // One fully verified candidate is sufficient.
554
          return true;
62✔
555
        } else {
556
          errors.push(`candidate #${idx}: responder is not authorized for all SingleResponses`);
20✔
557
        }
558
      } catch (err) {
559
        errors.push(`candidate #${idx}: ${(err as Error).message}`);
4✔
560
      }
561
    }
562

563
    // No candidate fully verified. Distinguish the failure mode.
564
    if (signatureMismatch && errors.length === 0 && firstCryptoError === null) {
30✔
565
      // Every candidate failed only at the signature step with a clean
566
      // mismatch — this is the documented `return false` path.
567
      return false;
4✔
568
    }
569
    if (hadValidSignature) {
26✔
570
      // At least one candidate had a cryptographically valid signature
571
      // but failed chain validation or authorization — surface those
572
      // higher-level reasons rather than a misleading crypto error.
573
      throw new Error(
24✔
574
        `OCSP responder verification failed. Reasons: ${errors.join("; ")}`
575
      );
576
    }
577
    if (firstCryptoError) {
2!
578
      // Re-throw genuine signature errors rather than hiding them as `false`.
579
      throw firstCryptoError;
2✔
580
    }
NEW
581
    throw new Error(
×
582
      `OCSP responder verification failed. Reasons: ${errors.join("; ")}`
583
    );
584
  }
585

586
  //#region Issuer / certificate helpers
587

588
  /**
589
   * Return `true` if `certs` already contains a certificate that is
590
   * DER-identical to `cert`.
591
   *
592
   * We deliberately compare the full DER encoding — not subject + public key
593
   * identity — so that cross-signed certificates (which share subject and key
594
   * but have different issuers/paths) are treated as distinct candidates and
595
   * retained for both chain building and issuer matching.
596
   */
597
  private static containsCertificateByDer(certs: Certificate[], cert: Certificate): boolean {
598
    const certDer = cert.toSchema(true).toBER(false);
632✔
599
    return certs.some(c =>
632✔
600
      pvtsutils.BufferSourceConverter.isEqual(c.toSchema(true).toBER(false), certDer)
766✔
601
    );
602
  }
603

604
  //#region Responder candidate collection (multi-candidate)
605

606
  /**
607
   * Collect all certificates from `certs` that match the `responderID`.
608
   * Returns indices into `certs`.
609
   */
610
  private static async collectResponderCandidates(
611
    certs: Certificate[],
612
    responderID: RelativeDistinguishedNames | asn1js.OctetString,
613
    crypto = common.getCrypto(true),
92✔
614
  ): Promise<number[]> {
615
    const indices: number[] = [];
92✔
616

617
    if (responderID instanceof RelativeDistinguishedNames) {
92✔
618
      // byName: match on subject DN
619
      for (const [index, certificate] of certs.entries()) {
90✔
620
        if (certificate.subject.isEqual(responderID)) {
182✔
621
          indices.push(index);
96✔
622
        }
623
      }
624
    } else if (responderID instanceof asn1js.OctetString) {
2!
625
      // byKey: match on SHA-1 of subjectPublicKeyInfo.subjectPublicKey
626
      for (const [index, certificate] of certs.entries()) {
2✔
627
        const spk = certificate.subjectPublicKeyInfo.subjectPublicKey;
4✔
628
        if (!spk.valueBlock?.valueHexView) {
4!
NEW
629
          continue;
×
630
        }
631
        const hash = await crypto.digest(
4✔
632
          { name: "sha-1" },
633
          spk.valueBlock.valueHexView as BufferSource,
634
        );
635
        if (pvutils.isEqualBuffer(hash, responderID.valueBlock.valueHex)) {
4✔
636
          indices.push(index);
2✔
637
        }
638
      }
639
    } else {
NEW
640
      throw new Error("Unsupported OCSP responderID type");
×
641
    }
642

643
    return indices;
92✔
644
  }
645

646
  //#region OCSP response signature verification
647

648
  /**
649
   * Verify the OCSP response signature against `candidateCert`'s public key.
650
   * Returns `true` on valid signature, `false` on mismatch. Hard errors
651
   * (unsupported algorithm, malformed data) are thrown.
652
   */
653
  private async verifyResponseSignature(
654
    candidateCert: Certificate,
655
    crypto = common.getCrypto(true),
94✔
656
  ): Promise<boolean> {
657
    return crypto.verifyWithPublicKey(
94✔
658
      this.tbsResponseData.tbsView as BufferSource,
659
      this.signature,
660
      candidateCert.subjectPublicKeyInfo,
661
      this.signatureAlgorithm,
662
    );
663
  }
664

665
  //#region Chain building & validation
666

667
  /**
668
   * Build and validate the certificate chain for a candidate signer.
669
   *
670
   * The signer is appended as the **last** element of the local certificate
671
   * list (the engine always treats `certs[certs.length - 1]` as the leaf),
672
   * and the CA candidates used to build the chain come from `issuerCerts`
673
   * and embedded `certs`. `trustedCerts` is intentionally NOT added to the
674
   * local list — it is passed only as trust-anchor material, which is what
675
   * `CertificateChainValidationEngine` expects.
676
   *
677
   * @returns the validated issuer path (issuer certificates actually used
678
   *          to anchor the signer, excluding the signer itself).
679
   * @throws on chain validation failure or when the engine cannot produce
680
   *         a `certificatePath`.
681
   */
682
  private static async validateSignerChain(
683
    signerCert: Certificate,
684
    embeddedCerts: Certificate[],
685
    issuerCerts: Certificate[],
686
    trustedCerts: Certificate[],
687
    crypto = common.getCrypto(true),
58✔
688
  ): Promise<Certificate[]> {
689
    // Collect CA candidates, append the signer as leaf (last entry), and
690
    // deduplicate by exact DER so cross-signed copies with different issuers
691
    // are all retained for path building.
692
    const additionalCerts: Certificate[] = [];
58✔
693

694
    const addIfCa = (cert: Certificate): void => {
58✔
695
      const caCert = checkCA(cert, signerCert);
190✔
696
      if (caCert && !BasicOCSPResponse.containsCertificateByDer(additionalCerts, caCert)) {
190✔
697
        additionalCerts.push(caCert);
94✔
698
      }
699
    };
700

701
    // CA candidates from issuerCerts take priority over embedded certs so
702
    // callers can pin a specific cross-signed or pre-built issuer chain.
703
    for (const cert of issuerCerts) {
58✔
704
      addIfCa(cert);
48✔
705
    }
706
    for (const cert of embeddedCerts) {
58✔
707
      addIfCa(cert);
142✔
708
    }
709

710
    // The signer must occupy the leaf slot (last entry).
711
    additionalCerts.push(signerCert);
58✔
712

713
    const engine = new CertificateChainValidationEngine({
58✔
714
      certs: additionalCerts,
715
      trustedCerts,
716
    });
717

718
    const result = await engine.verify({}, crypto);
58✔
719
    if (!result.result || !result.certificatePath) {
58✔
720
      throw new Error(`Validation of signer's certificate chain failed${result.resultMessage ? `: ${result.resultMessage}` : ""}`);
4!
721
    }
722

723
    // `certificatePath[0]` is the leaf (signer). Return the issuer portion.
724
    return result.certificatePath.slice(1);
54✔
725
  }
726

727
  /**
728
   * Resolve **all** distinct issuer certificates for a `CertID` from a list
729
   * of candidates.
730
   *
731
   * The OCSP `CertID` identifies the CA only by:
732
   *   * hash of the DER encoding of the issuer name, and
733
   *   * hash of the **contents** of `SubjectPublicKey` (without tag/length).
734
   *
735
   * Therefore two CA certificates can legitimately share the same
736
   * `issuerNameHash` / `issuerKeyHash` while differing in SPKI
737
   * `AlgorithmIdentifier`, serial number, validity or extensions. This
738
   * function treats any such match as a separate result and lets the caller
739
   * try each one independently, rather than collapsing them into a single
740
   * identity or declaring them ambiguous.
741
   *
742
   * Candidates are still deduplicated by exact DER so duplicate copies do
743
   * not cause redundant work.
744
   *
745
   * @returns the list of candidate issuer certificates (possibly empty).
746
   */
747
  private static async findIssuersForCertID(
748
    certID: CertID,
749
    issuerCandidates: Certificate[],
750
    crypto = common.getCrypto(true),
22✔
751
  ): Promise<Certificate[]> {
752
    const responseHashAlg = crypto.getAlgorithmByOID(certID.hashAlgorithm.algorithmId, false);
22✔
753
    if (!responseHashAlg || !("name" in responseHashAlg)) {
22!
NEW
754
      return [];
×
755
    }
756
    const hashAlgName: string = (responseHashAlg as { name: string }).name;
22✔
757

758
    // Deduplicate by exact DER. OCSP cannot distinguish CAs that collide on
759
    // name/key hashes, so any distinct DER copy is a valid independent
760
    // candidate.
761
    const uniqueByDer: Certificate[] = [];
22✔
762
    for (const candidate of issuerCandidates) {
22✔
763
      if (!BasicOCSPResponse.containsCertificateByDer(uniqueByDer, candidate)) {
72!
764
        uniqueByDer.push(candidate);
72✔
765
      }
766
    }
767

768
    const matched: Certificate[] = [];
22✔
769
    for (const candidate of uniqueByDer) {
22✔
770
      const candidateID = new CertID();
72✔
771
      try {
72✔
772
        await candidateID.createForCertificate(candidate, {
72✔
773
          hashAlgorithm: hashAlgName,
774
          issuerCertificate: candidate,
775
        }, crypto);
776
      } catch {
NEW
777
        continue;
×
778
      }
779

780
      if (candidateID.hashAlgorithm.algorithmId !== certID.hashAlgorithm.algorithmId) {
72!
NEW
781
        continue;
×
782
      }
783
      if (!pvtsutils.BufferSourceConverter.isEqual(
72✔
784
        candidateID.issuerNameHash.valueBlock.valueHexView,
785
        certID.issuerNameHash.valueBlock.valueHexView,
786
      )) {
787
        continue;
48✔
788
      }
789
      if (!pvtsutils.BufferSourceConverter.isEqual(
24!
790
        candidateID.issuerKeyHash.valueBlock.valueHexView,
791
        certID.issuerKeyHash.valueBlock.valueHexView,
792
      )) {
NEW
793
        continue;
×
794
      }
795
      matched.push(candidate);
24✔
796
    }
797

798
    return matched;
22✔
799
  }
800

801
  /**
802
   * Return `true` if `certificate` itself corresponds to the issuer
803
   * identified by `certID` — i.e. hashing the certificate's subject and the
804
   * contents of its `SubjectPublicKey` with the CertID's hash algorithm
805
   * reproduces `issuerNameHash` and `issuerKeyHash`.
806
   *
807
   * This avoids depending on which copy of the CA cert was reachable from
808
   * the response when deciding the direct-issuer case.
809
   */
810
  private static async certificateMatchesIssuerID(
811
    certificate: Certificate,
812
    certID: CertID,
813
    crypto = common.getCrypto(true),
58✔
814
  ): Promise<boolean> {
815
    const responseHashAlg = crypto.getAlgorithmByOID(certID.hashAlgorithm.algorithmId, false);
58✔
816
    if (!responseHashAlg || !("name" in responseHashAlg)) {
58!
NEW
817
      return false;
×
818
    }
819
    const hashAlgName: string = (responseHashAlg as { name: string }).name;
58✔
820

821
    try {
58✔
822
      const nameHash = await crypto.digest(
58✔
823
        { name: hashAlgName },
824
        certificate.subject.toSchema().toBER(false),
825
      );
826
      if (!pvtsutils.BufferSourceConverter.isEqual(
58✔
827
        new Uint8Array(nameHash),
828
        certID.issuerNameHash.valueBlock.valueHexView,
829
      )) {
830
        return false;
38✔
831
      }
832

833
      const keyBytes = certificate.subjectPublicKeyInfo.subjectPublicKey.valueBlock.valueHexView;
20✔
834
      const keyHash = await crypto.digest({ name: hashAlgName }, keyBytes as BufferSource);
20✔
835
      return pvtsutils.BufferSourceConverter.isEqual(
20✔
836
        new Uint8Array(keyHash),
837
        certID.issuerKeyHash.valueBlock.valueHexView,
838
      );
839
    } catch {
NEW
840
      return false;
×
841
    }
842
  }
843

844
  /**
845
   * Check whether `signerCert` is authorized to respond for the certificate
846
   * identified by `certID` under RFC 6960 §4.2.2.2.
847
   *
848
   * Authorization is granted when at least one of the following holds:
849
   *
850
   *  1. **Direct issuer** — `signerCert` itself matches the
851
   *     `issuerNameHash` / `issuerKeyHash` of `certID`.
852
   *
853
   *  2. **Delegated OCSP responder** — there exists an issuer matching the
854
   *     `certID` that issued `signerCert` (issuer name matches subject and
855
   *     the certificate signature verifies), the `signerCert` contains
856
   *     `id-kp-OCSPSigning` in its EKU, and (when a KeyUsage extension is
857
   *     present) the KeyUsage permits `digitalSignature`.
858
   *
859
   *  3. **Explicitly trusted responder** — `signerCert` is DER-identical to
860
   *     one of the certificates in `trustedResponders`.
861
   *
862
   * All other cases return `false`.
863
   */
864
  private static async isAuthorizedResponderForCertID(
865
    signerCert: Certificate,
866
    certID: CertID,
867
    issuerCandidates: Certificate[],
868
    trustedResponders: Certificate[],
869
    crypto = common.getCrypto(true),
86✔
870
  ): Promise<boolean> {
871
    //#region 3) Explicitly trusted responder (exact DER match)
872
    if (trustedResponders.length > 0 &&
86✔
873
      BasicOCSPResponse.containsCertificateByDer(trustedResponders, signerCert)) {
874
      return true;
28✔
875
    }
876
    //#endregion
877

878
    //#region 1) Direct issuer — signerCert itself matches the CertID.
879
    if (await BasicOCSPResponse.certificateMatchesIssuerID(signerCert, certID, crypto)) {
58✔
880
      return true;
20✔
881
    }
882
    //#endregion
883

884
    //#region 2) Delegated OCSP responder (RFC 6960 §4.2.2.2.1)
885
    // Signer MUST carry `id-kp-OCSPSigning`, regardless of which issuer
886
    // we resolve below — short-circuit once to avoid wasted work.
887
    if (!BasicOCSPResponse.certHasOCSPSigningEKU(signerCert)) {
38✔
888
      return false;
12✔
889
    }
890
    if (!BasicOCSPResponse.certKeyUsagePermitsDigitalSignature(signerCert)) {
26✔
891
      return false;
4✔
892
    }
893

894
    const issuers = await BasicOCSPResponse.findIssuersForCertID(certID, issuerCandidates, crypto);
22✔
895
    for (const issuerCert of issuers) {
22✔
896
      // a) signerCert MUST be issued by this resolved issuer.
897
      if (!signerCert.issuer.isEqual(issuerCert.subject)) {
20✔
898
        continue;
2✔
899
      }
900
      // b) signer's certificate signature MUST be verifiable with this
901
      //    issuer's public key.
902
      try {
18✔
903
        const issued = await signerCert.verify(issuerCert, crypto);
18✔
904
        if (issued) {
18!
905
          return true;
18✔
906
        }
907
      } catch {
908
        // try next candidate issuer
909
      }
910
    }
911
    //#endregion
912

913
    return false;
4✔
914
  }
915

916
  /**
917
   * Return `true` if `cert` carries `id-kp-OCSPSigning` in its Extended Key
918
   * Usage extension. If the extension is absent the function returns `false`.
919
   */
920
  private static certHasOCSPSigningEKU(cert: Certificate): boolean {
921
    const ext = cert.extensions?.find(e => e.extnID === ID_CE_EXT_KEY_USAGE);
72✔
922
    if (!ext) {
38✔
923
      return false;
2✔
924
    }
925
    const parsed = ext.parsedValue;
36✔
926
    if (parsed instanceof ExtKeyUsage) {
36✔
927
      return parsed.keyPurposes.includes(ID_KP_OCSP_SIGNING);
34✔
928
    }
929
    // Fall back to manual parsing in case parsedValue did not deserialize.
930
    try {
2✔
931
      const asn1 = asn1js.fromBER(ext.extnValue.valueBlock.valueHexView as BufferSource);
2✔
932
      if (asn1.offset === -1) return false;
2!
933
      // extnValue for ExtKeyUsage MUST be a SEQUENCE of OID.
NEW
934
      if (!(asn1.result instanceof asn1js.Sequence)) return false;
×
NEW
935
      const eku = new ExtKeyUsage({ schema: asn1.result });
×
NEW
936
      return eku.keyPurposes.includes(ID_KP_OCSP_SIGNING);
×
937
    } catch {
NEW
938
      return false;
×
939
    }
940
  }
941

942
  /**
943
   * Return `true` if `cert` either has no `KeyUsage` extension (per RFC 5280
944
   * the usage is then unconstrained) or the present `KeyUsage` extension
945
   * permits `digitalSignature` (bit 0).
946
   *
947
   * Per RFC 5280 §4.2.1.3 the KeyUsage bit string is encoded MSB-first inside
948
   * the extension value: `digitalSignature` is bit 0, which is the high bit
949
   * (0x80) of the first content byte of the BIT STRING.
950
   */
951
  private static certKeyUsagePermitsDigitalSignature(cert: Certificate): boolean {
952
    const ext = cert.extensions?.find(e => e.extnID === ID_CE_KEY_USAGE);
26✔
953
    if (!ext) {
26✔
954
      return true; // no KeyUsage => unconstrained
2✔
955
    }
956
    try {
24✔
957
      const asn1 = asn1js.fromBER(ext.extnValue.valueBlock.valueHexView as BufferSource);
24✔
958
      if (asn1.offset === -1) return false;
24✔
959
      // extnValue for KeyUsage MUST be a BIT STRING.
960
      if (!(asn1.result instanceof asn1js.BitString)) return false;
22!
961
      const bits = new Uint8Array(asn1.result.valueBlock.valueHexView);
22✔
962
      if (bits.length === 0) return false;
22!
963
      // Bit 0 (digitalSignature) is the most-significant bit of the first byte.
964
      return (bits[0] & 0x80) !== 0;
22✔
965
    } catch {
NEW
966
      return false;
×
967
    }
968
  }
969

970
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc