summaryrefslogtreecommitdiffstats
path: root/third_party/js/PKI.js/src/SignedCertificateTimestamp.ts
blob: fa9e01b5d1164b5d8cef1f5976d7f23047fa870c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
import * as asn1js from "asn1js";
import * as pvutils from "pvutils";
import * as bs from "bytestreamjs";
import * as common from "./common";
import { PublicKeyInfo } from "./PublicKeyInfo";
import * as Schema from "./Schema";
import { AlgorithmIdentifier } from "./AlgorithmIdentifier";
import { Certificate } from "./Certificate";
import { AsnError } from "./errors";
import { PkiObject, PkiObjectParameters } from "./PkiObject";
import { EMPTY_BUFFER, EMPTY_STRING } from "./constants";
import { SignedCertificateTimestampList } from "./SignedCertificateTimestampList";
import { id_SignedCertificateTimestampList } from "./ObjectIdentifiers";

const VERSION = "version";
const LOG_ID = "logID";
const EXTENSIONS = "extensions";
const TIMESTAMP = "timestamp";
const HASH_ALGORITHM = "hashAlgorithm";
const SIGNATURE_ALGORITHM = "signatureAlgorithm";
const SIGNATURE = "signature";

const NONE = "none";
const MD5 = "md5";
const SHA1 = "sha1";
const SHA224 = "sha224";
const SHA256 = "sha256";
const SHA384 = "sha384";
const SHA512 = "sha512";
const ANONYMOUS = "anonymous";
const RSA = "rsa";
const DSA = "dsa";
const ECDSA = "ecdsa";

export interface ISignedCertificateTimestamp {
  version: number;
  logID: ArrayBuffer;
  timestamp: Date;
  extensions: ArrayBuffer;
  hashAlgorithm: string;
  signatureAlgorithm: string;
  signature: Schema.SchemaType;
}

export interface SignedCertificateTimestampJson {
  version: number;
  logID: string;
  timestamp: Date;
  extensions: string;
  hashAlgorithm: string;
  signatureAlgorithm: string;
  signature: Schema.SchemaType;
}

export type SignedCertificateTimestampParameters = PkiObjectParameters & Partial<ISignedCertificateTimestamp> & { stream?: bs.SeqStream; };

export interface Log {
  /**
   * Identifier of the CT Log encoded in BASE-64 format
   */
  log_id: string;
  /**
   * Public key of the CT Log encoded in BASE-64 format
   */
  key: string;
}

export class SignedCertificateTimestamp extends PkiObject implements ISignedCertificateTimestamp {

  public static override CLASS_NAME = "SignedCertificateTimestamp";

  public version!: number;
  public logID!: ArrayBuffer;
  public timestamp!: Date;
  public extensions!: ArrayBuffer;
  public hashAlgorithm!: string;
  public signatureAlgorithm!: string;
  public signature: asn1js.BaseBlock;

  /**
   * Initializes a new instance of the {@link SignedCertificateTimestamp} class
   * @param parameters Initialization parameters
   */
  constructor(parameters: SignedCertificateTimestampParameters = {}) {
    super();

    this.version = pvutils.getParametersValue(parameters, VERSION, SignedCertificateTimestamp.defaultValues(VERSION));
    this.logID = pvutils.getParametersValue(parameters, LOG_ID, SignedCertificateTimestamp.defaultValues(LOG_ID));
    this.timestamp = pvutils.getParametersValue(parameters, TIMESTAMP, SignedCertificateTimestamp.defaultValues(TIMESTAMP));
    this.extensions = pvutils.getParametersValue(parameters, EXTENSIONS, SignedCertificateTimestamp.defaultValues(EXTENSIONS));
    this.hashAlgorithm = pvutils.getParametersValue(parameters, HASH_ALGORITHM, SignedCertificateTimestamp.defaultValues(HASH_ALGORITHM));
    this.signatureAlgorithm = pvutils.getParametersValue(parameters, SIGNATURE_ALGORITHM, SignedCertificateTimestamp.defaultValues(SIGNATURE_ALGORITHM));
    this.signature = pvutils.getParametersValue(parameters, SIGNATURE, SignedCertificateTimestamp.defaultValues(SIGNATURE));

    if ("stream" in parameters && parameters.stream) {
      this.fromStream(parameters.stream);
    }

    if (parameters.schema) {
      this.fromSchema(parameters.schema);
    }
  }

  /**
   * Returns default values for all class members
   * @param memberName String name for a class member
   * @returns Default value
   */
  public static override defaultValues(memberName: typeof VERSION): number;
  public static override defaultValues(memberName: typeof LOG_ID): ArrayBuffer;
  public static override defaultValues(memberName: typeof EXTENSIONS): ArrayBuffer;
  public static override defaultValues(memberName: typeof TIMESTAMP): Date;
  public static override defaultValues(memberName: typeof HASH_ALGORITHM): string;
  public static override defaultValues(memberName: typeof SIGNATURE_ALGORITHM): string;
  public static override defaultValues(memberName: typeof SIGNATURE): Schema.SchemaType;
  public static override defaultValues(memberName: string): any {
    switch (memberName) {
      case VERSION:
        return 0;
      case LOG_ID:
      case EXTENSIONS:
        return EMPTY_BUFFER;
      case TIMESTAMP:
        return new Date(0);
      case HASH_ALGORITHM:
      case SIGNATURE_ALGORITHM:
        return EMPTY_STRING;
      case SIGNATURE:
        return new asn1js.Any();
      default:
        return super.defaultValues(memberName);
    }
  }

  public fromSchema(schema: Schema.SchemaType): void {
    if ((schema instanceof asn1js.RawData) === false)
      throw new Error("Object's schema was not verified against input data for SignedCertificateTimestamp");

    const seqStream = new bs.SeqStream({
      stream: new bs.ByteStream({
        buffer: schema.data
      })
    });

    this.fromStream(seqStream);
  }

  /**
   * Converts SeqStream data into current class
   * @param stream
   */
  public fromStream(stream: bs.SeqStream): void {
    const blockLength = stream.getUint16();

    this.version = (stream.getBlock(1))[0];

    if (this.version === 0) {
      this.logID = (new Uint8Array(stream.getBlock(32))).buffer.slice(0);
      this.timestamp = new Date(pvutils.utilFromBase(new Uint8Array(stream.getBlock(8)), 8));

      //#region Extensions
      const extensionsLength = stream.getUint16();
      this.extensions = (new Uint8Array(stream.getBlock(extensionsLength))).buffer.slice(0);
      //#endregion

      //#region Hash algorithm
      switch ((stream.getBlock(1))[0]) {
        case 0:
          this.hashAlgorithm = NONE;
          break;
        case 1:
          this.hashAlgorithm = MD5;
          break;
        case 2:
          this.hashAlgorithm = SHA1;
          break;
        case 3:
          this.hashAlgorithm = SHA224;
          break;
        case 4:
          this.hashAlgorithm = SHA256;
          break;
        case 5:
          this.hashAlgorithm = SHA384;
          break;
        case 6:
          this.hashAlgorithm = SHA512;
          break;
        default:
          throw new Error("Object's stream was not correct for SignedCertificateTimestamp");
      }
      //#endregion

      //#region Signature algorithm
      switch ((stream.getBlock(1))[0]) {
        case 0:
          this.signatureAlgorithm = ANONYMOUS;
          break;
        case 1:
          this.signatureAlgorithm = RSA;
          break;
        case 2:
          this.signatureAlgorithm = DSA;
          break;
        case 3:
          this.signatureAlgorithm = ECDSA;
          break;
        default:
          throw new Error("Object's stream was not correct for SignedCertificateTimestamp");
      }
      //#endregion

      //#region Signature
      const signatureLength = stream.getUint16();
      const signatureData = new Uint8Array(stream.getBlock(signatureLength)).buffer.slice(0);

      const asn1 = asn1js.fromBER(signatureData);
      AsnError.assert(asn1, "SignedCertificateTimestamp");
      this.signature = asn1.result;
      //#endregion

      if (blockLength !== (47 + extensionsLength + signatureLength)) {
        throw new Error("Object's stream was not correct for SignedCertificateTimestamp");
      }
    }
  }

  public toSchema(): asn1js.RawData {
    const stream = this.toStream();

    return new asn1js.RawData({ data: stream.stream.buffer });
  }

  /**
   * Converts current object to SeqStream data
   * @returns SeqStream object
   */
  public toStream(): bs.SeqStream {
    const stream = new bs.SeqStream();

    stream.appendUint16(47 + this.extensions.byteLength + this.signature.valueBeforeDecodeView.byteLength);
    stream.appendChar(this.version);
    stream.appendView(new Uint8Array(this.logID));

    const timeBuffer = new ArrayBuffer(8);
    const timeView = new Uint8Array(timeBuffer);

    const baseArray = pvutils.utilToBase(this.timestamp.valueOf(), 8);
    timeView.set(new Uint8Array(baseArray), 8 - baseArray.byteLength);

    stream.appendView(timeView);
    stream.appendUint16(this.extensions.byteLength);

    if (this.extensions.byteLength)
      stream.appendView(new Uint8Array(this.extensions));

    let _hashAlgorithm;

    switch (this.hashAlgorithm.toLowerCase()) {
      case NONE:
        _hashAlgorithm = 0;
        break;
      case MD5:
        _hashAlgorithm = 1;
        break;
      case SHA1:
        _hashAlgorithm = 2;
        break;
      case SHA224:
        _hashAlgorithm = 3;
        break;
      case SHA256:
        _hashAlgorithm = 4;
        break;
      case SHA384:
        _hashAlgorithm = 5;
        break;
      case SHA512:
        _hashAlgorithm = 6;
        break;
      default:
        throw new Error(`Incorrect data for hashAlgorithm: ${this.hashAlgorithm}`);
    }

    stream.appendChar(_hashAlgorithm);

    let _signatureAlgorithm;

    switch (this.signatureAlgorithm.toLowerCase()) {
      case ANONYMOUS:
        _signatureAlgorithm = 0;
        break;
      case RSA:
        _signatureAlgorithm = 1;
        break;
      case DSA:
        _signatureAlgorithm = 2;
        break;
      case ECDSA:
        _signatureAlgorithm = 3;
        break;
      default:
        throw new Error(`Incorrect data for signatureAlgorithm: ${this.signatureAlgorithm}`);
    }

    stream.appendChar(_signatureAlgorithm);

    const _signature = this.signature.toBER(false);

    stream.appendUint16(_signature.byteLength);
    stream.appendView(new Uint8Array(_signature));

    return stream;
  }

  public toJSON(): SignedCertificateTimestampJson {
    return {
      version: this.version,
      logID: pvutils.bufferToHexCodes(this.logID),
      timestamp: this.timestamp,
      extensions: pvutils.bufferToHexCodes(this.extensions),
      hashAlgorithm: this.hashAlgorithm,
      signatureAlgorithm: this.signatureAlgorithm,
      signature: this.signature.toJSON()
    };
  }

  /**
   * Verify SignedCertificateTimestamp for specific input data
   * @param logs Array of objects with information about each CT Log (like here: https://ct.grahamedgecombe.com/logs.json)
   * @param data Data to verify signature against. Could be encoded Certificate or encoded PreCert
   * @param dataType Type = 0 (data is encoded Certificate), type = 1 (data is encoded PreCert)
   * @param crypto Crypto engine
   */
  async verify(logs: Log[], data: ArrayBuffer, dataType = 0, crypto = common.getCrypto(true)): Promise<boolean> {
    //#region Initial variables
    const logId = pvutils.toBase64(pvutils.arrayBufferToString(this.logID));

    let publicKeyBase64 = null;

    const stream = new bs.SeqStream();
    //#endregion

    //#region Found and init public key
    for (const log of logs) {
      if (log.log_id === logId) {
        publicKeyBase64 = log.key;
        break;
      }
    }

    if (!publicKeyBase64) {
      throw new Error(`Public key not found for CT with logId: ${logId}`);
    }

    const pki = pvutils.stringToArrayBuffer(pvutils.fromBase64(publicKeyBase64));
    const publicKeyInfo = PublicKeyInfo.fromBER(pki);
    //#endregion

    //#region Initialize signed data block
    stream.appendChar(0x00); // sct_version
    stream.appendChar(0x00); // signature_type = certificate_timestamp

    const timeBuffer = new ArrayBuffer(8);
    const timeView = new Uint8Array(timeBuffer);

    const baseArray = pvutils.utilToBase(this.timestamp.valueOf(), 8);
    timeView.set(new Uint8Array(baseArray), 8 - baseArray.byteLength);

    stream.appendView(timeView);

    stream.appendUint16(dataType);

    if (dataType === 0)
      stream.appendUint24(data.byteLength);

    stream.appendView(new Uint8Array(data));

    stream.appendUint16(this.extensions.byteLength);

    if (this.extensions.byteLength !== 0)
      stream.appendView(new Uint8Array(this.extensions));
    //#endregion

    //#region Perform verification
    return crypto.verifyWithPublicKey(
      stream.buffer.slice(0, stream.length),
      new asn1js.OctetString({ valueHex: this.signature.toBER(false) }),
      publicKeyInfo,
      { algorithmId: EMPTY_STRING } as AlgorithmIdentifier,
      "SHA-256"
    );
    //#endregion
  }

}

export interface Log {
  /**
   * Identifier of the CT Log encoded in BASE-64 format
   */
  log_id: string;
  /**
   * Public key of the CT Log encoded in BASE-64 format
   */
  key: string;
}

/**
 * Verify SignedCertificateTimestamp for specific certificate content
 * @param certificate Certificate for which verification would be performed
 * @param issuerCertificate Certificate of the issuer of target certificate
 * @param logs Array of objects with information about each CT Log (like here: https://ct.grahamedgecombe.com/logs.json)
 * @param index Index of SignedCertificateTimestamp inside SignedCertificateTimestampList (for -1 would verify all)
 * @param crypto Crypto engine
 * @return Array of verification results
 */
export async function verifySCTsForCertificate(certificate: Certificate, issuerCertificate: Certificate, logs: Log[], index = (-1), crypto = common.getCrypto(true)) {
  let parsedValue: SignedCertificateTimestampList | null = null;

  const stream = new bs.SeqStream();

  //#region Remove certificate extension
  for (let i = 0; certificate.extensions && i < certificate.extensions.length; i++) {
    switch (certificate.extensions[i].extnID) {
      case id_SignedCertificateTimestampList:
        {
          parsedValue = certificate.extensions[i].parsedValue;

          if (!parsedValue || parsedValue.timestamps.length === 0)
            throw new Error("Nothing to verify in the certificate");

          certificate.extensions.splice(i, 1);
        }
        break;
      default:
    }
  }
  //#endregion

  //#region Check we do have what to verify
  if (parsedValue === null)
    throw new Error("No SignedCertificateTimestampList extension in the specified certificate");
  //#endregion

  //#region Prepare modifier TBS value
  const tbs = certificate.encodeTBS().toBER();
  //#endregion

  //#region Initialize "issuer_key_hash" value
  const issuerId = await crypto.digest({ name: "SHA-256" }, new Uint8Array(issuerCertificate.subjectPublicKeyInfo.toSchema().toBER(false)));
  //#endregion

  //#region Make final "PreCert" value
  stream.appendView(new Uint8Array(issuerId));
  stream.appendUint24(tbs.byteLength);
  stream.appendView(new Uint8Array(tbs));

  const preCert = stream.stream.slice(0, stream.length);
  //#endregion

  //#region Call verification function for specified index
  if (index === (-1)) {
    const verifyArray = [];

    for (const timestamp of parsedValue.timestamps) {
      const verifyResult = await timestamp.verify(logs, preCert.buffer, 1, crypto);
      verifyArray.push(verifyResult);
    }

    return verifyArray;
  }

  if (index >= parsedValue.timestamps.length)
    index = (parsedValue.timestamps.length - 1);

  return [await parsedValue.timestamps[index].verify(logs, preCert.buffer, 1, crypto)];
  //#endregion
}