import * as asn1js from "asn1js"; import { getParametersValue, utilFromBase, utilToBase, bufferToHexCodes, toBase64, fromBase64, arrayBufferToString, stringToArrayBuffer } from "pvutils"; import { ByteStream, SeqStream } from "bytestreamjs"; import { getCrypto, getEngine } from "./common.js"; import PublicKeyInfo from "./PublicKeyInfo.js"; //************************************************************************************** export class SignedCertificateTimestamp { //********************************************************************************** /** * Constructor for SignedCertificateTimestamp class * @param {Object} [parameters={}] * @param {Object} [parameters.schema] asn1js parsed value to initialize the class from */ constructor(parameters = {}) { //region Internal properties of the object /** * @type {number} * @desc version */ this.version = getParametersValue(parameters, "version", SignedCertificateTimestamp.defaultValues("version")); /** * @type {ArrayBuffer} * @desc logID */ this.logID = getParametersValue(parameters, "logID", SignedCertificateTimestamp.defaultValues("logID")); /** * @type {Date} * @desc timestamp */ this.timestamp = getParametersValue(parameters, "timestamp", SignedCertificateTimestamp.defaultValues("timestamp")); /** * @type {ArrayBuffer} * @desc extensions */ this.extensions = getParametersValue(parameters, "extensions", SignedCertificateTimestamp.defaultValues("extensions")); /** * @type {string} * @desc hashAlgorithm */ this.hashAlgorithm = getParametersValue(parameters, "hashAlgorithm", SignedCertificateTimestamp.defaultValues("hashAlgorithm")); /** * @type {string} * @desc signatureAlgorithm */ this.signatureAlgorithm = getParametersValue(parameters, "signatureAlgorithm", SignedCertificateTimestamp.defaultValues("signatureAlgorithm")); /** * @type {Object} * @desc signature */ this.signature = getParametersValue(parameters, "signature", SignedCertificateTimestamp.defaultValues("signature")); //endregion //region If input argument array contains "schema" for this object if("schema" in parameters) this.fromSchema(parameters.schema); //endregion //region If input argument array contains "stream" if("stream" in parameters) this.fromStream(parameters.stream); //endregion } //********************************************************************************** /** * Return default values for all class members * @param {string} memberName String name for a class member */ static defaultValues(memberName) { switch(memberName) { case "version": return 0; case "logID": case "extensions": return new ArrayBuffer(0); case "timestamp": return new Date(0); case "hashAlgorithm": case "signatureAlgorithm": return ""; case "signature": return new asn1js.Any(); default: throw new Error(`Invalid member name for SignedCertificateTimestamp class: ${memberName}`); } } //********************************************************************************** /** * Convert parsed asn1js object into current class * @param {!Object} schema */ fromSchema(schema) { if((schema instanceof asn1js.RawData) === false) throw new Error("Object's schema was not verified against input data for SignedCertificateTimestamp"); const seqStream = new SeqStream({ stream: new ByteStream({ buffer: schema.data }) }); this.fromStream(seqStream); } //********************************************************************************** /** * Convert SeqStream data into current class * @param {!SeqStream} stream */ fromStream(stream) { 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(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); if(asn1.offset === (-1)) throw new Error("Object's stream was not correct for SignedCertificateTimestamp"); this.signature = asn1.result; //endregion if(blockLength !== (47 + extensionsLength + signatureLength)) throw new Error("Object's stream was not correct for SignedCertificateTimestamp"); } } //********************************************************************************** /** * Convert current object to asn1js object and set correct values * @returns {Object} asn1js object */ toSchema() { const stream = this.toStream(); return new asn1js.RawData({ data: stream.stream.buffer }); } //********************************************************************************** /** * Convert current object to SeqStream data * @returns {SeqStream} SeqStream object */ toStream() { const stream = new SeqStream(); stream.appendUint16(47 + this.extensions.byteLength + this.signature.valueBeforeDecode.byteLength); stream.appendChar(this.version); stream.appendView(new Uint8Array(this.logID)); const timeBuffer = new ArrayBuffer(8); const timeView = new Uint8Array(timeBuffer); const baseArray = 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; } //********************************************************************************** /** * Convertion for the class to JSON object * @returns {Object} */ toJSON() { return { version: this.version, logID: bufferToHexCodes(this.logID), timestamp: this.timestamp, extensions: bufferToHexCodes(this.extensions), hashAlgorithm: this.hashAlgorithm, signatureAlgorithm: this.signatureAlgorithm, signature: this.signature.toJSON() }; } //********************************************************************************** /** * Verify SignedCertificateTimestamp for specific input data * @param {Object[]} logs Array of objects with information about each CT Log (like here: https://ct.grahamedgecombe.com/logs.json) * @param {String} logs.log_id Identifier of the CT Log encoded in BASE-64 format * @param {String} logs.key Public key of the CT Log encoded in BASE-64 format * @param {ArrayBuffer} data Data to verify signature against. Could be encoded Certificate or encoded PreCert * @param {Number} [dataType=0] Type = 0 (data is encoded Certificate), type = 1 (data is encoded PreCert) * @return {Promise} */ async verify(logs, data, dataType = 0) { //region Initial variables let logId = toBase64(arrayBufferToString(this.logID)); let publicKeyBase64 = null; let publicKeyInfo; let stream = new SeqStream(); //endregion //region Found and init public key for(const log of logs) { if(log.log_id === logId) { publicKeyBase64 = log.key; break; } } if(publicKeyBase64 === null) throw new Error(`Public key not found for CT with logId: ${logId}`); const asn1 = asn1js.fromBER(stringToArrayBuffer(fromBase64(publicKeyBase64))); if(asn1.offset === (-1)) throw new Error(`Incorrect key value for CT Log with logId: ${logId}`); publicKeyInfo = new PublicKeyInfo({ schema: asn1.result }); //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 = 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 getEngine().subtle.verifyWithPublicKey( stream._stream._buffer.slice(0, stream._length), { valueBlock: { valueHex: this.signature.toBER(false) } }, publicKeyInfo, { algorithmId: "" }, "SHA-256" ); //endregion } //********************************************************************************** } //************************************************************************************** /** * Class from RFC6962 */ export default class SignedCertificateTimestampList { //********************************************************************************** /** * Constructor for SignedCertificateTimestampList class * @param {Object} [parameters={}] * @param {Object} [parameters.schema] asn1js parsed value to initialize the class from */ constructor(parameters = {}) { //region Internal properties of the object /** * @type {Array.} * @desc timestamps */ this.timestamps = getParametersValue(parameters, "timestamps", SignedCertificateTimestampList.defaultValues("timestamps")); //endregion //region If input argument array contains "schema" for this object if("schema" in parameters) this.fromSchema(parameters.schema); //endregion } //********************************************************************************** /** * Return default values for all class members * @param {string} memberName String name for a class member */ static defaultValues(memberName) { switch(memberName) { case "timestamps": return []; default: throw new Error(`Invalid member name for SignedCertificateTimestampList class: ${memberName}`); } } //********************************************************************************** /** * Compare values with default values for all class members * @param {string} memberName String name for a class member * @param {*} memberValue Value to compare with default value */ static compareWithDefault(memberName, memberValue) { switch(memberName) { case "timestamps": return (memberValue.length === 0); default: throw new Error(`Invalid member name for SignedCertificateTimestampList class: ${memberName}`); } } //********************************************************************************** /** * Return value of pre-defined ASN.1 schema for current class * * ASN.1 schema: * ```asn1 * SignedCertificateTimestampList ::= OCTET STRING * ``` * * @param {Object} parameters Input parameters for the schema * @returns {Object} asn1js schema object */ static schema(parameters = {}) { /** * @type {Object} * @property {string} [blockName] * @property {string} [optional] */ const names = getParametersValue(parameters, "names", {}); if(("optional" in names) === false) names.optional = false; return (new asn1js.OctetString({ name: (names.blockName || "SignedCertificateTimestampList"), optional: names.optional })); } //********************************************************************************** /** * Convert parsed asn1js object into current class * @param {!Object} schema */ fromSchema(schema) { //region Check the schema is valid if((schema instanceof asn1js.OctetString) === false) throw new Error("Object's schema was not verified against input data for SignedCertificateTimestampList"); //endregion //region Get internal properties from parsed schema const seqStream = new SeqStream({ stream: new ByteStream({ buffer: schema.valueBlock.valueHex }) }); let dataLength = seqStream.getUint16(); if(dataLength !== seqStream.length) throw new Error("Object's schema was not verified against input data for SignedCertificateTimestampList"); while(seqStream.length) this.timestamps.push(new SignedCertificateTimestamp({ stream: seqStream })); //endregion } //********************************************************************************** /** * Convert current object to asn1js object and set correct values * @returns {Object} asn1js object */ toSchema() { //region Initial variables const stream = new SeqStream(); let overallLength = 0; const timestampsData = []; //endregion //region Get overall length for(const timestamp of this.timestamps) { const timestampStream = timestamp.toStream(); timestampsData.push(timestampStream); overallLength += timestampStream.stream.buffer.byteLength; } //endregion stream.appendUint16(overallLength); //region Set data from all timestamps for(const timestamp of timestampsData) stream.appendView(timestamp.stream.view); //endregion return new asn1js.OctetString({ valueHex: stream.stream.buffer.slice(0) }); } //********************************************************************************** /** * Convertion for the class to JSON object * @returns {Object} */ toJSON() { return { timestamps: Array.from(this.timestamps, element => element.toJSON()) }; } //********************************************************************************** } //************************************************************************************** /** * Verify SignedCertificateTimestamp for specific certificate content * @param {Certificate} certificate Certificate for which verification would be performed * @param {Certificate} issuerCertificate Certificate of the issuer of target certificate * @param {Object[]} logs Array of objects with information about each CT Log (like here: https://ct.grahamedgecombe.com/logs.json) * @param {String} logs.log_id Identifier of the CT Log encoded in BASE-64 format * @param {String} logs.key Public key of the CT Log encoded in BASE-64 format * @param {Number} [index=-1] Index of SignedCertificateTimestamp inside SignedCertificateTimestampList (for -1 would verify all) * @return {Array} Array of verification results */ export async function verifySCTsForCertificate(certificate, issuerCertificate, logs, index = (-1)) { //region Initial variables let parsedValue = null; let tbs; let issuerId; const stream = new SeqStream(); let preCert; //endregion //region Get a "crypto" extension const crypto = getCrypto(); if(typeof crypto === "undefined") return Promise.reject("Unable to create WebCrypto object"); //endregion //region Remove certificate extension for(let i = 0; i < certificate.extensions.length; i++) { switch(certificate.extensions[i].extnID) { case "1.3.6.1.4.1.11129.2.4.2": { parsedValue = certificate.extensions[i].parsedValue; if(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 tbs = certificate.encodeTBS().toBER(false); //endregion //region Initialize "issuer_key_hash" value 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)); preCert = stream._stream._buffer.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, 1); verifyArray.push(verifyResult); } return verifyArray; } if(index >= parsedValue.timestamps.length) index = (parsedValue.timestamps.length - 1); return [await parsedValue.timestamps[index].verify(logs, preCert, 1)]; //endregion } //**********************************************************************************