tor-browser

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

SignedCertificateTimestamp.ts (15000B)


      1 import * as asn1js from "asn1js";
      2 import * as pvutils from "pvutils";
      3 import * as bs from "bytestreamjs";
      4 import * as common from "./common";
      5 import { PublicKeyInfo } from "./PublicKeyInfo";
      6 import * as Schema from "./Schema";
      7 import { AlgorithmIdentifier } from "./AlgorithmIdentifier";
      8 import { Certificate } from "./Certificate";
      9 import { PkiObject, PkiObjectParameters } from "./PkiObject";
     10 import { EMPTY_BUFFER, EMPTY_STRING } from "./constants";
     11 import { SignedCertificateTimestampList } from "./SignedCertificateTimestampList";
     12 import { id_SignedCertificateTimestampList } from "./ObjectIdentifiers";
     13 
     14 const VERSION = "version";
     15 const LOG_ID = "logID";
     16 const EXTENSIONS = "extensions";
     17 const TIMESTAMP = "timestamp";
     18 const HASH_ALGORITHM = "hashAlgorithm";
     19 const SIGNATURE_ALGORITHM = "signatureAlgorithm";
     20 const SIGNATURE = "signature";
     21 
     22 const NONE = "none";
     23 const MD5 = "md5";
     24 const SHA1 = "sha1";
     25 const SHA224 = "sha224";
     26 const SHA256 = "sha256";
     27 const SHA384 = "sha384";
     28 const SHA512 = "sha512";
     29 const ANONYMOUS = "anonymous";
     30 const RSA = "rsa";
     31 const DSA = "dsa";
     32 const ECDSA = "ecdsa";
     33 
     34 export interface ISignedCertificateTimestamp {
     35  version: number;
     36  logID: ArrayBuffer;
     37  timestamp: Date;
     38  extensions: ArrayBuffer;
     39  hashAlgorithm: string;
     40  signatureAlgorithm: string;
     41  signature: ArrayBuffer;
     42 }
     43 
     44 export interface SignedCertificateTimestampJson {
     45  version: number;
     46  logID: string;
     47  timestamp: Date;
     48  extensions: string;
     49  hashAlgorithm: string;
     50  signatureAlgorithm: string;
     51  signature: string;
     52 }
     53 
     54 export type SignedCertificateTimestampParameters = PkiObjectParameters & Partial<ISignedCertificateTimestamp> & { stream?: bs.SeqStream; };
     55 
     56 export interface Log {
     57  /**
     58   * Identifier of the CT Log encoded in BASE-64 format
     59   */
     60  log_id: string;
     61  /**
     62   * Public key of the CT Log encoded in BASE-64 format
     63   */
     64  key: string;
     65 }
     66 
     67 export class SignedCertificateTimestamp extends PkiObject implements ISignedCertificateTimestamp {
     68 
     69  public static override CLASS_NAME = "SignedCertificateTimestamp";
     70 
     71  public version!: number;
     72  public logID!: ArrayBuffer;
     73  public timestamp!: Date;
     74  public extensions!: ArrayBuffer;
     75  public hashAlgorithm!: string;
     76  public signatureAlgorithm!: string;
     77  public signature!: ArrayBuffer;
     78 
     79  /**
     80   * Initializes a new instance of the {@link SignedCertificateTimestamp} class
     81   * @param parameters Initialization parameters
     82   */
     83  constructor(parameters: SignedCertificateTimestampParameters = {}) {
     84    super();
     85 
     86    this.version = pvutils.getParametersValue(parameters, VERSION, SignedCertificateTimestamp.defaultValues(VERSION));
     87    this.logID = pvutils.getParametersValue(parameters, LOG_ID, SignedCertificateTimestamp.defaultValues(LOG_ID));
     88    this.timestamp = pvutils.getParametersValue(parameters, TIMESTAMP, SignedCertificateTimestamp.defaultValues(TIMESTAMP));
     89    this.extensions = pvutils.getParametersValue(parameters, EXTENSIONS, SignedCertificateTimestamp.defaultValues(EXTENSIONS));
     90    this.hashAlgorithm = pvutils.getParametersValue(parameters, HASH_ALGORITHM, SignedCertificateTimestamp.defaultValues(HASH_ALGORITHM));
     91    this.signatureAlgorithm = pvutils.getParametersValue(parameters, SIGNATURE_ALGORITHM, SignedCertificateTimestamp.defaultValues(SIGNATURE_ALGORITHM));
     92    this.signature = pvutils.getParametersValue(parameters, SIGNATURE, SignedCertificateTimestamp.defaultValues(SIGNATURE));
     93 
     94    if ("stream" in parameters && parameters.stream) {
     95      this.fromStream(parameters.stream);
     96    }
     97 
     98    if (parameters.schema) {
     99      this.fromSchema(parameters.schema);
    100    }
    101  }
    102 
    103  /**
    104   * Returns default values for all class members
    105   * @param memberName String name for a class member
    106   * @returns Default value
    107   */
    108  public static override defaultValues(memberName: typeof VERSION): number;
    109  public static override defaultValues(memberName: typeof LOG_ID): ArrayBuffer;
    110  public static override defaultValues(memberName: typeof EXTENSIONS): ArrayBuffer;
    111  public static override defaultValues(memberName: typeof TIMESTAMP): Date;
    112  public static override defaultValues(memberName: typeof HASH_ALGORITHM): string;
    113  public static override defaultValues(memberName: typeof SIGNATURE_ALGORITHM): string;
    114  public static override defaultValues(memberName: typeof SIGNATURE): ArrayBuffer;
    115  public static override defaultValues(memberName: string): any {
    116    switch (memberName) {
    117      case VERSION:
    118        return 0;
    119      case LOG_ID:
    120      case EXTENSIONS:
    121        return EMPTY_BUFFER;
    122      case TIMESTAMP:
    123        return new Date(0);
    124      case HASH_ALGORITHM:
    125      case SIGNATURE_ALGORITHM:
    126        return EMPTY_STRING;
    127      case SIGNATURE:
    128        return EMPTY_BUFFER;
    129      default:
    130        return super.defaultValues(memberName);
    131    }
    132  }
    133 
    134  public fromSchema(schema: Schema.SchemaType): void {
    135    if ((schema instanceof asn1js.RawData) === false)
    136      throw new Error("Object's schema was not verified against input data for SignedCertificateTimestamp");
    137 
    138    const seqStream = new bs.SeqStream({
    139      stream: new bs.ByteStream({
    140        buffer: schema.data
    141      })
    142    });
    143 
    144    this.fromStream(seqStream);
    145  }
    146 
    147  /**
    148   * Converts SeqStream data into current class
    149   * @param stream
    150   */
    151  public fromStream(stream: bs.SeqStream): void {
    152    const blockLength = stream.getUint16();
    153 
    154    this.version = (stream.getBlock(1))[0];
    155 
    156    if (this.version === 0) {
    157      this.logID = (new Uint8Array(stream.getBlock(32))).buffer.slice(0);
    158      this.timestamp = new Date(pvutils.utilFromBase(new Uint8Array(stream.getBlock(8)), 8));
    159 
    160      //#region Extensions
    161      const extensionsLength = stream.getUint16();
    162      this.extensions = (new Uint8Array(stream.getBlock(extensionsLength))).buffer.slice(0);
    163      //#endregion
    164 
    165      //#region Hash algorithm
    166      switch ((stream.getBlock(1))[0]) {
    167        case 0:
    168          this.hashAlgorithm = NONE;
    169          break;
    170        case 1:
    171          this.hashAlgorithm = MD5;
    172          break;
    173        case 2:
    174          this.hashAlgorithm = SHA1;
    175          break;
    176        case 3:
    177          this.hashAlgorithm = SHA224;
    178          break;
    179        case 4:
    180          this.hashAlgorithm = SHA256;
    181          break;
    182        case 5:
    183          this.hashAlgorithm = SHA384;
    184          break;
    185        case 6:
    186          this.hashAlgorithm = SHA512;
    187          break;
    188        default:
    189          throw new Error("Object's stream was not correct for SignedCertificateTimestamp");
    190      }
    191      //#endregion
    192 
    193      //#region Signature algorithm
    194      switch ((stream.getBlock(1))[0]) {
    195        case 0:
    196          this.signatureAlgorithm = ANONYMOUS;
    197          break;
    198        case 1:
    199          this.signatureAlgorithm = RSA;
    200          break;
    201        case 2:
    202          this.signatureAlgorithm = DSA;
    203          break;
    204        case 3:
    205          this.signatureAlgorithm = ECDSA;
    206          break;
    207        default:
    208          throw new Error("Object's stream was not correct for SignedCertificateTimestamp");
    209      }
    210      //#endregion
    211 
    212      //#region Signature
    213      const signatureLength = stream.getUint16();
    214      this.signature = new Uint8Array(stream.getBlock(signatureLength)).buffer.slice(0);
    215      //#endregion
    216 
    217      if (blockLength !== (47 + extensionsLength + signatureLength)) {
    218        throw new Error("Object's stream was not correct for SignedCertificateTimestamp");
    219      }
    220    }
    221  }
    222 
    223  public toSchema(): asn1js.RawData {
    224    const stream = this.toStream();
    225 
    226    return new asn1js.RawData({ data: stream.stream.buffer });
    227  }
    228 
    229  /**
    230   * Converts current object to SeqStream data
    231   * @returns SeqStream object
    232   */
    233  public toStream(): bs.SeqStream {
    234    const stream = new bs.SeqStream();
    235 
    236    stream.appendUint16(47 + this.extensions.byteLength + this.signature.byteLength);
    237    stream.appendChar(this.version);
    238    stream.appendView(new Uint8Array(this.logID));
    239 
    240    const timeBuffer = new ArrayBuffer(8);
    241    const timeView = new Uint8Array(timeBuffer);
    242 
    243    const baseArray = pvutils.utilToBase(this.timestamp.valueOf(), 8);
    244    timeView.set(new Uint8Array(baseArray), 8 - baseArray.byteLength);
    245 
    246    stream.appendView(timeView);
    247    stream.appendUint16(this.extensions.byteLength);
    248 
    249    if (this.extensions.byteLength)
    250      stream.appendView(new Uint8Array(this.extensions));
    251 
    252    let _hashAlgorithm;
    253 
    254    switch (this.hashAlgorithm.toLowerCase()) {
    255      case NONE:
    256        _hashAlgorithm = 0;
    257        break;
    258      case MD5:
    259        _hashAlgorithm = 1;
    260        break;
    261      case SHA1:
    262        _hashAlgorithm = 2;
    263        break;
    264      case SHA224:
    265        _hashAlgorithm = 3;
    266        break;
    267      case SHA256:
    268        _hashAlgorithm = 4;
    269        break;
    270      case SHA384:
    271        _hashAlgorithm = 5;
    272        break;
    273      case SHA512:
    274        _hashAlgorithm = 6;
    275        break;
    276      default:
    277        throw new Error(`Incorrect data for hashAlgorithm: ${this.hashAlgorithm}`);
    278    }
    279 
    280    stream.appendChar(_hashAlgorithm);
    281 
    282    let _signatureAlgorithm;
    283 
    284    switch (this.signatureAlgorithm.toLowerCase()) {
    285      case ANONYMOUS:
    286        _signatureAlgorithm = 0;
    287        break;
    288      case RSA:
    289        _signatureAlgorithm = 1;
    290        break;
    291      case DSA:
    292        _signatureAlgorithm = 2;
    293        break;
    294      case ECDSA:
    295        _signatureAlgorithm = 3;
    296        break;
    297      default:
    298        throw new Error(`Incorrect data for signatureAlgorithm: ${this.signatureAlgorithm}`);
    299    }
    300 
    301    stream.appendChar(_signatureAlgorithm);
    302 
    303    stream.appendUint16(this.signature.byteLength);
    304    stream.appendView(new Uint8Array(this.signature));
    305 
    306    return stream;
    307  }
    308 
    309  public toJSON(): SignedCertificateTimestampJson {
    310    return {
    311      version: this.version,
    312      logID: pvutils.bufferToHexCodes(this.logID),
    313      timestamp: this.timestamp,
    314      extensions: pvutils.bufferToHexCodes(this.extensions),
    315      hashAlgorithm: this.hashAlgorithm,
    316      signatureAlgorithm: this.signatureAlgorithm,
    317      signature: pvutils.bufferToHexCodes(this.signature),
    318    };
    319  }
    320 
    321  /**
    322   * Verify SignedCertificateTimestamp for specific input data
    323   * @param logs Array of objects with information about each CT Log (like here: https://ct.grahamedgecombe.com/logs.json)
    324   * @param data Data to verify signature against. Could be encoded Certificate or encoded PreCert
    325   * @param dataType Type = 0 (data is encoded Certificate), type = 1 (data is encoded PreCert)
    326   * @param crypto Crypto engine
    327   */
    328  async verify(logs: Log[], data: ArrayBuffer, dataType = 0, crypto = common.getCrypto(true)): Promise<boolean> {
    329    //#region Initial variables
    330    const logId = pvutils.toBase64(pvutils.arrayBufferToString(this.logID));
    331 
    332    let publicKeyBase64 = null;
    333 
    334    const stream = new bs.SeqStream();
    335    //#endregion
    336 
    337    //#region Found and init public key
    338    for (const log of logs) {
    339      if (log.log_id === logId) {
    340        publicKeyBase64 = log.key;
    341        break;
    342      }
    343    }
    344 
    345    if (!publicKeyBase64) {
    346      throw new Error(`Public key not found for CT with logId: ${logId}`);
    347    }
    348 
    349    const pki = pvutils.stringToArrayBuffer(pvutils.fromBase64(publicKeyBase64));
    350    const publicKeyInfo = PublicKeyInfo.fromBER(pki);
    351    //#endregion
    352 
    353    //#region Initialize signed data block
    354    stream.appendChar(0x00); // sct_version
    355    stream.appendChar(0x00); // signature_type = certificate_timestamp
    356 
    357    const timeBuffer = new ArrayBuffer(8);
    358    const timeView = new Uint8Array(timeBuffer);
    359 
    360    const baseArray = pvutils.utilToBase(this.timestamp.valueOf(), 8);
    361    timeView.set(new Uint8Array(baseArray), 8 - baseArray.byteLength);
    362 
    363    stream.appendView(timeView);
    364 
    365    stream.appendUint16(dataType);
    366 
    367    if (dataType === 0)
    368      stream.appendUint24(data.byteLength);
    369 
    370    stream.appendView(new Uint8Array(data));
    371 
    372    stream.appendUint16(this.extensions.byteLength);
    373 
    374    if (this.extensions.byteLength !== 0)
    375      stream.appendView(new Uint8Array(this.extensions));
    376    //#endregion
    377 
    378    //#region Perform verification
    379    return crypto.verifyWithPublicKey(
    380      stream.buffer.slice(0, stream.length),
    381      new asn1js.OctetString({ valueHex: this.signature }),
    382      publicKeyInfo,
    383      { algorithmId: EMPTY_STRING } as AlgorithmIdentifier,
    384      "SHA-256"
    385    );
    386    //#endregion
    387  }
    388 
    389 }
    390 
    391 export interface Log {
    392  /**
    393   * Identifier of the CT Log encoded in BASE-64 format
    394   */
    395  log_id: string;
    396  /**
    397   * Public key of the CT Log encoded in BASE-64 format
    398   */
    399  key: string;
    400 }
    401 
    402 /**
    403 * Verify SignedCertificateTimestamp for specific certificate content
    404 * @param certificate Certificate for which verification would be performed
    405 * @param issuerCertificate Certificate of the issuer of target certificate
    406 * @param logs Array of objects with information about each CT Log (like here: https://ct.grahamedgecombe.com/logs.json)
    407 * @param index Index of SignedCertificateTimestamp inside SignedCertificateTimestampList (for -1 would verify all)
    408 * @param crypto Crypto engine
    409 * @return Array of verification results
    410 */
    411 export async function verifySCTsForCertificate(certificate: Certificate, issuerCertificate: Certificate, logs: Log[], index = (-1), crypto = common.getCrypto(true)) {
    412  let parsedValue: SignedCertificateTimestampList | null = null;
    413 
    414  const stream = new bs.SeqStream();
    415 
    416  //#region Remove certificate extension
    417  if (certificate.extensions) {
    418    for (let i = certificate.extensions.length - 1; i >=0; i--) {
    419      switch (certificate.extensions[i].extnID) {
    420        case id_SignedCertificateTimestampList:
    421          {
    422            parsedValue = certificate.extensions[i].parsedValue;
    423 
    424            if (!parsedValue || parsedValue.timestamps.length === 0)
    425              throw new Error("Nothing to verify in the certificate");
    426 
    427            certificate.extensions.splice(i, 1);
    428          }
    429          break;
    430        default:
    431      }
    432    }
    433  }
    434  //#endregion
    435 
    436  //#region Check we do have what to verify
    437  if (parsedValue === null)
    438    throw new Error("No SignedCertificateTimestampList extension in the specified certificate");
    439  //#endregion
    440 
    441  //#region Prepare modifier TBS value
    442  const tbs = certificate.encodeTBS().toBER();
    443  //#endregion
    444 
    445  //#region Initialize "issuer_key_hash" value
    446  const issuerId = await crypto.digest({ name: "SHA-256" }, new Uint8Array(issuerCertificate.subjectPublicKeyInfo.toSchema().toBER(false)));
    447  //#endregion
    448 
    449  //#region Make final "PreCert" value
    450  stream.appendView(new Uint8Array(issuerId));
    451  stream.appendUint24(tbs.byteLength);
    452  stream.appendView(new Uint8Array(tbs));
    453 
    454  const preCert = stream.stream.slice(0, stream.length);
    455  //#endregion
    456 
    457  //#region Call verification function for specified index
    458  if (index === (-1)) {
    459    const verifyArray = [];
    460 
    461    for (const timestamp of parsedValue.timestamps) {
    462      const verifyResult = await timestamp.verify(logs, preCert.buffer, 1, crypto);
    463      verifyArray.push(verifyResult);
    464    }
    465 
    466    return verifyArray;
    467  }
    468 
    469  if (index >= parsedValue.timestamps.length)
    470    index = (parsedValue.timestamps.length - 1);
    471 
    472  return [await parsedValue.timestamps[index].verify(logs, preCert.buffer, 1, crypto)];
    473  //#endregion
    474 }