tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

BasicOCSPResponse.ts (15483B)


      1 import * as asn1js from "asn1js";
      2 import * as pvutils from "pvutils";
      3 import * as common from "./common";
      4 import { ResponseData, ResponseDataJson, ResponseDataSchema } from "./ResponseData";
      5 import { AlgorithmIdentifier, AlgorithmIdentifierJson, AlgorithmIdentifierSchema } from "./AlgorithmIdentifier";
      6 import { Certificate, CertificateJson, CertificateSchema, checkCA } from "./Certificate";
      7 import { CertID } from "./CertID";
      8 import { RelativeDistinguishedNames } from "./RelativeDistinguishedNames";
      9 import { CertificateChainValidationEngine } from "./CertificateChainValidationEngine";
     10 import * as Schema from "./Schema";
     11 import { PkiObject, PkiObjectParameters } from "./PkiObject";
     12 import { AsnError } from "./errors";
     13 import { EMPTY_STRING } from "./constants";
     14 
     15 const TBS_RESPONSE_DATA = "tbsResponseData";
     16 const SIGNATURE_ALGORITHM = "signatureAlgorithm";
     17 const SIGNATURE = "signature";
     18 const CERTS = "certs";
     19 const BASIC_OCSP_RESPONSE = "BasicOCSPResponse";
     20 const BASIC_OCSP_RESPONSE_TBS_RESPONSE_DATA = `${BASIC_OCSP_RESPONSE}.${TBS_RESPONSE_DATA}`;
     21 const BASIC_OCSP_RESPONSE_SIGNATURE_ALGORITHM = `${BASIC_OCSP_RESPONSE}.${SIGNATURE_ALGORITHM}`;
     22 const BASIC_OCSP_RESPONSE_SIGNATURE = `${BASIC_OCSP_RESPONSE}.${SIGNATURE}`;
     23 const BASIC_OCSP_RESPONSE_CERTS = `${BASIC_OCSP_RESPONSE}.${CERTS}`;
     24 const CLEAR_PROPS = [
     25  BASIC_OCSP_RESPONSE_TBS_RESPONSE_DATA,
     26  BASIC_OCSP_RESPONSE_SIGNATURE_ALGORITHM,
     27  BASIC_OCSP_RESPONSE_SIGNATURE,
     28  BASIC_OCSP_RESPONSE_CERTS
     29 ];
     30 
     31 export interface IBasicOCSPResponse {
     32  tbsResponseData: ResponseData;
     33  signatureAlgorithm: AlgorithmIdentifier;
     34  signature: asn1js.BitString;
     35  certs?: Certificate[];
     36 }
     37 
     38 export interface CertificateStatus {
     39  isForCertificate: boolean;
     40  /**
     41   * 0 = good, 1 = revoked, 2 = unknown
     42   */
     43  status: number;
     44 }
     45 
     46 export type BasicOCSPResponseParameters = PkiObjectParameters & Partial<IBasicOCSPResponse>;
     47 
     48 export interface BasicOCSPResponseVerifyParams {
     49  trustedCerts?: Certificate[];
     50 }
     51 
     52 export interface BasicOCSPResponseJson {
     53  tbsResponseData: ResponseDataJson;
     54  signatureAlgorithm: AlgorithmIdentifierJson;
     55  signature: asn1js.BitStringJson;
     56  certs?: CertificateJson[];
     57 }
     58 
     59 /**
     60 * Represents the BasicOCSPResponse structure described in [RFC6960](https://datatracker.ietf.org/doc/html/rfc6960)
     61 */
     62 export class BasicOCSPResponse extends PkiObject implements IBasicOCSPResponse {
     63 
     64  public static override CLASS_NAME = "BasicOCSPResponse";
     65 
     66  public tbsResponseData!: ResponseData;
     67  public signatureAlgorithm!: AlgorithmIdentifier;
     68  public signature!: asn1js.BitString;
     69  public certs?: Certificate[];
     70 
     71  /**
     72   * Initializes a new instance of the {@link BasicOCSPResponse} class
     73   * @param parameters Initialization parameters
     74   */
     75  constructor(parameters: BasicOCSPResponseParameters = {}) {
     76    super();
     77 
     78    this.tbsResponseData = pvutils.getParametersValue(parameters, TBS_RESPONSE_DATA, BasicOCSPResponse.defaultValues(TBS_RESPONSE_DATA));
     79    this.signatureAlgorithm = pvutils.getParametersValue(parameters, SIGNATURE_ALGORITHM, BasicOCSPResponse.defaultValues(SIGNATURE_ALGORITHM));
     80    this.signature = pvutils.getParametersValue(parameters, SIGNATURE, BasicOCSPResponse.defaultValues(SIGNATURE));
     81    if (CERTS in parameters) {
     82      this.certs = pvutils.getParametersValue(parameters, CERTS, BasicOCSPResponse.defaultValues(CERTS));
     83    }
     84 
     85    if (parameters.schema) {
     86      this.fromSchema(parameters.schema);
     87    }
     88  }
     89 
     90  /**
     91   * Returns default values for all class members
     92   * @param memberName String name for a class member
     93   * @returns Default value
     94   */
     95  public static override defaultValues(memberName: typeof TBS_RESPONSE_DATA): ResponseData;
     96  public static override defaultValues(memberName: typeof SIGNATURE_ALGORITHM): AlgorithmIdentifier;
     97  public static override defaultValues(memberName: typeof SIGNATURE): asn1js.BitString;
     98  public static override defaultValues(memberName: typeof CERTS): Certificate[];
     99  public static override defaultValues(memberName: string): any {
    100    switch (memberName) {
    101      case TBS_RESPONSE_DATA:
    102        return new ResponseData();
    103      case SIGNATURE_ALGORITHM:
    104        return new AlgorithmIdentifier();
    105      case SIGNATURE:
    106        return new asn1js.BitString();
    107      case CERTS:
    108        return [];
    109      default:
    110        return super.defaultValues(memberName);
    111    }
    112  }
    113 
    114  /**
    115   * Compare values with default values for all class members
    116   * @param memberName String name for a class member
    117   * @param memberValue Value to compare with default value
    118   */
    119  public static compareWithDefault(memberName: string, memberValue: any): boolean {
    120    switch (memberName) {
    121      case "type":
    122        {
    123          let comparisonResult = ((ResponseData.compareWithDefault("tbs", memberValue.tbs)) &&
    124            (ResponseData.compareWithDefault("responderID", memberValue.responderID)) &&
    125            (ResponseData.compareWithDefault("producedAt", memberValue.producedAt)) &&
    126            (ResponseData.compareWithDefault("responses", memberValue.responses)));
    127 
    128          if ("responseExtensions" in memberValue)
    129            comparisonResult = comparisonResult && (ResponseData.compareWithDefault("responseExtensions", memberValue.responseExtensions));
    130 
    131          return comparisonResult;
    132        }
    133      case SIGNATURE_ALGORITHM:
    134        return ((memberValue.algorithmId === EMPTY_STRING) && (("algorithmParams" in memberValue) === false));
    135      case SIGNATURE:
    136        return (memberValue.isEqual(BasicOCSPResponse.defaultValues(memberName)));
    137      case CERTS:
    138        return (memberValue.length === 0);
    139      default:
    140        return super.defaultValues(memberName);
    141    }
    142  }
    143 
    144  /**
    145   * @inheritdoc
    146   * @asn ASN.1 schema
    147   * ```asn
    148   * BasicOCSPResponse ::= SEQUENCE {
    149   *    tbsResponseData      ResponseData,
    150   *    signatureAlgorithm   AlgorithmIdentifier,
    151   *    signature            BIT STRING,
    152   *    certs            [0] EXPLICIT SEQUENCE OF Certificate OPTIONAL }
    153   *```
    154   */
    155  public static override schema(parameters: Schema.SchemaParameters<{
    156    tbsResponseData?: ResponseDataSchema;
    157    signatureAlgorithm?: AlgorithmIdentifierSchema;
    158    signature?: string;
    159    certs?: CertificateSchema;
    160  }> = {}): Schema.SchemaType {
    161    const names = pvutils.getParametersValue<NonNullable<typeof parameters.names>>(parameters, "names", {});
    162 
    163    return (new asn1js.Sequence({
    164      name: (names.blockName || BASIC_OCSP_RESPONSE),
    165      value: [
    166        ResponseData.schema(names.tbsResponseData || {
    167          names: {
    168            blockName: BASIC_OCSP_RESPONSE_TBS_RESPONSE_DATA
    169          }
    170        }),
    171        AlgorithmIdentifier.schema(names.signatureAlgorithm || {
    172          names: {
    173            blockName: BASIC_OCSP_RESPONSE_SIGNATURE_ALGORITHM
    174          }
    175        }),
    176        new asn1js.BitString({ name: (names.signature || BASIC_OCSP_RESPONSE_SIGNATURE) }),
    177        new asn1js.Constructed({
    178          optional: true,
    179          idBlock: {
    180            tagClass: 3, // CONTEXT-SPECIFIC
    181            tagNumber: 0 // [0]
    182          },
    183          value: [
    184            new asn1js.Sequence({
    185              value: [new asn1js.Repeated({
    186                name: BASIC_OCSP_RESPONSE_CERTS,
    187                value: Certificate.schema(names.certs || {})
    188              })]
    189            })
    190          ]
    191        })
    192      ]
    193    }));
    194  }
    195 
    196  public fromSchema(schema: Schema.SchemaType): void {
    197    // Clear input data first
    198    pvutils.clearProps(schema, CLEAR_PROPS);
    199    //#endregion
    200 
    201    // Check the schema is valid
    202    const asn1 = asn1js.compareSchema(schema,
    203      schema,
    204      BasicOCSPResponse.schema()
    205    );
    206    AsnError.assertSchema(asn1, this.className);
    207 
    208    //#region Get internal properties from parsed schema
    209    this.tbsResponseData = new ResponseData({ schema: asn1.result[BASIC_OCSP_RESPONSE_TBS_RESPONSE_DATA] });
    210    this.signatureAlgorithm = new AlgorithmIdentifier({ schema: asn1.result[BASIC_OCSP_RESPONSE_SIGNATURE_ALGORITHM] });
    211    this.signature = asn1.result[BASIC_OCSP_RESPONSE_SIGNATURE];
    212 
    213    if (BASIC_OCSP_RESPONSE_CERTS in asn1.result) {
    214      this.certs = Array.from(asn1.result[BASIC_OCSP_RESPONSE_CERTS], element => new Certificate({ schema: element }));
    215    }
    216    //#endregion
    217  }
    218 
    219  public toSchema(): asn1js.Sequence {
    220    //#region Create array for output sequence
    221    const outputArray = [];
    222 
    223    outputArray.push(this.tbsResponseData.toSchema());
    224    outputArray.push(this.signatureAlgorithm.toSchema());
    225    outputArray.push(this.signature);
    226 
    227    //#region Create array of certificates
    228    if (this.certs) {
    229      outputArray.push(new asn1js.Constructed({
    230        idBlock: {
    231          tagClass: 3, // CONTEXT-SPECIFIC
    232          tagNumber: 0 // [0]
    233        },
    234        value: [
    235          new asn1js.Sequence({
    236            value: Array.from(this.certs, o => o.toSchema())
    237          })
    238        ]
    239      }));
    240    }
    241    //#endregion
    242    //#endregion
    243 
    244    //#region Construct and return new ASN.1 schema for this object
    245    return (new asn1js.Sequence({
    246      value: outputArray
    247    }));
    248    //#endregion
    249  }
    250 
    251  public toJSON(): BasicOCSPResponseJson {
    252    const res: BasicOCSPResponseJson = {
    253      tbsResponseData: this.tbsResponseData.toJSON(),
    254      signatureAlgorithm: this.signatureAlgorithm.toJSON(),
    255      signature: this.signature.toJSON(),
    256    };
    257 
    258    if (this.certs) {
    259      res.certs = Array.from(this.certs, o => o.toJSON());
    260    }
    261 
    262    return res;
    263  }
    264 
    265  /**
    266   * Get OCSP response status for specific certificate
    267   * @param certificate Certificate to be checked
    268   * @param issuerCertificate Certificate of issuer for certificate to be checked
    269   * @param crypto Crypto engine
    270   */
    271  public async getCertificateStatus(certificate: Certificate, issuerCertificate: Certificate, crypto = common.getCrypto(true)): Promise<CertificateStatus> {
    272    //#region Initial variables
    273    const result = {
    274      isForCertificate: false,
    275      status: 2 // 0 = good, 1 = revoked, 2 = unknown
    276    };
    277 
    278    const hashesObject: Record<string, number> = {};
    279 
    280    const certIDs: CertID[] = [];
    281    //#endregion
    282 
    283    //#region Create all "certIDs" for input certificates
    284    for (const response of this.tbsResponseData.responses) {
    285      const hashAlgorithm = crypto.getAlgorithmByOID(response.certID.hashAlgorithm.algorithmId, true, "CertID.hashAlgorithm");
    286 
    287      if (!hashesObject[hashAlgorithm.name]) {
    288        hashesObject[hashAlgorithm.name] = 1;
    289 
    290        const certID = new CertID();
    291 
    292        certIDs.push(certID);
    293        await certID.createForCertificate(certificate, {
    294          hashAlgorithm: hashAlgorithm.name,
    295          issuerCertificate
    296        }, crypto);
    297      }
    298    }
    299    //#endregion
    300 
    301    //#region Compare all response's "certIDs" with identifiers for input certificate
    302    for (const response of this.tbsResponseData.responses) {
    303      for (const id of certIDs) {
    304        if (response.certID.isEqual(id)) {
    305          result.isForCertificate = true;
    306 
    307          try {
    308            switch (response.certStatus.idBlock.isConstructed) {
    309              case true:
    310                if (response.certStatus.idBlock.tagNumber === 1)
    311                  result.status = 1; // revoked
    312 
    313                break;
    314              case false:
    315                switch (response.certStatus.idBlock.tagNumber) {
    316                  case 0: // good
    317                    result.status = 0;
    318                    break;
    319                  case 2: // unknown
    320                    result.status = 2;
    321                    break;
    322                  default:
    323                }
    324 
    325                break;
    326              default:
    327            }
    328          }
    329          catch {
    330            // nothing
    331          }
    332 
    333          return result;
    334        }
    335      }
    336    }
    337 
    338    return result;
    339    //#endregion
    340  }
    341 
    342  /**
    343   * Make signature for current OCSP Basic Response
    344   * @param privateKey Private key for "subjectPublicKeyInfo" structure
    345   * @param hashAlgorithm Hashing algorithm. Default SHA-1
    346   * @param crypto Crypto engine
    347   */
    348  async sign(privateKey: CryptoKey, hashAlgorithm = "SHA-1", crypto = common.getCrypto(true)): Promise<void> {
    349    // Get a private key from function parameter
    350    if (!privateKey) {
    351      throw new Error("Need to provide a private key for signing");
    352    }
    353 
    354    //#region Get a "default parameters" for current algorithm and set correct signature algorithm
    355    const signatureParams = await crypto.getSignatureParameters(privateKey, hashAlgorithm);
    356 
    357    const algorithm = signatureParams.parameters.algorithm;
    358    if (!("name" in algorithm)) {
    359      throw new Error("Empty algorithm");
    360    }
    361    this.signatureAlgorithm = signatureParams.signatureAlgorithm;
    362    //#endregion
    363 
    364    //#region Create TBS data for signing
    365    this.tbsResponseData.tbsView = new Uint8Array(this.tbsResponseData.toSchema(true).toBER());
    366    //#endregion
    367 
    368    //#region Signing TBS data on provided private key
    369    const signature = await crypto.signWithPrivateKey(this.tbsResponseData.tbsView as BufferSource, privateKey, { algorithm });
    370    this.signature = new asn1js.BitString({ valueHex: signature });
    371    //#endregion
    372  }
    373 
    374  /**
    375   * Verify existing OCSP Basic Response
    376   * @param params Additional parameters
    377   * @param crypto Crypto engine
    378   */
    379  public async verify(params: BasicOCSPResponseVerifyParams = {}, crypto = common.getCrypto(true)): Promise<boolean> {
    380    //#region Initial variables
    381    let signerCert: Certificate | null = null;
    382    let certIndex = -1;
    383    const trustedCerts: Certificate[] = params.trustedCerts || [];
    384    //#endregion
    385 
    386    //#region Check amount of certificates
    387    if (!this.certs) {
    388      throw new Error("No certificates attached to the BasicOCSPResponse");
    389    }
    390    //#endregion
    391 
    392    //#region Find correct value for "responderID"
    393    switch (true) {
    394      case (this.tbsResponseData.responderID instanceof RelativeDistinguishedNames): // [1] Name
    395        for (const [index, certificate] of this.certs.entries()) {
    396          if (certificate.subject.isEqual(this.tbsResponseData.responderID)) {
    397            certIndex = index;
    398            break;
    399          }
    400        }
    401        break;
    402      case (this.tbsResponseData.responderID instanceof asn1js.OctetString): // [2] KeyHash
    403        for (const [index, cert] of this.certs.entries()) {
    404          const hash = await crypto.digest({ name: "sha-1" }, cert.subjectPublicKeyInfo.subjectPublicKey.valueBlock.valueHexView as BufferSource);
    405          if (pvutils.isEqualBuffer(hash, this.tbsResponseData.responderID.valueBlock.valueHex)) {
    406            certIndex = index;
    407            break;
    408          }
    409        }
    410        break;
    411      default:
    412        throw new Error("Wrong value for responderID");
    413    }
    414    //#endregion
    415 
    416    //#region Make additional verification for signer's certificate
    417    if (certIndex === (-1))
    418      throw new Error("Correct certificate was not found in OCSP response");
    419 
    420    signerCert = this.certs[certIndex];
    421 
    422    const additionalCerts: Certificate[] = [signerCert];
    423    for (const cert of this.certs) {
    424      const caCert = await checkCA(cert, signerCert);
    425      if (caCert) {
    426        additionalCerts.push(caCert);
    427      }
    428    }
    429    const certChain = new CertificateChainValidationEngine({
    430      certs: additionalCerts,
    431      trustedCerts,
    432    });
    433 
    434    const verificationResult = await certChain.verify({}, crypto);
    435    if (!verificationResult.result) {
    436      throw new Error("Validation of signer's certificate failed");
    437    }
    438 
    439    return crypto.verifyWithPublicKey(this.tbsResponseData.tbsView as BufferSource, this.signature, this.certs[certIndex].subjectPublicKeyInfo, this.signatureAlgorithm);
    440  }
    441 
    442 }