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 }