UNPKG

ctjs

Version:

CTjs is a full set of classes necessary to work with any kind of Certificate Transparency log (V1 as from RFC6962, or V2 as from RFC6962-bis). In CTjs you could find all necessary validation/verification functions for all related data shipped with full-fe

682 lines (597 loc) 22.4 kB
/* eslint-disable no-useless-escape */ import * as asn1js from "asn1js"; import { getParametersValue, fromBase64, stringToArrayBuffer, toBase64, arrayBufferToString } from "pvutils"; import { SeqStream } from "bytestreamjs"; import { Certificate, PublicKeyInfo } from "pkijs"; import SignedTreeHead from "./SignedTreeHead.js"; import MerkleTreeLeaf from "./MerkleTreeLeaf.js"; import SignedCertificateTimestamp from "./SignedCertificateTimestamp.js"; import DigitallySigned from "./DigitallySigned.js"; import LogEntryType from "./LogEntryType.js"; //************************************************************************************** function handleResult(api) { return async result => { if(result.ok) return result.json(); let errorMessage = result.statusText; try { const errorJSON = await result.json(); if("error_message" in errorJSON) errorMessage = errorJSON.error_message; } catch(ex){} throw new Error(`ERROR while fetching ${api}: ${errorMessage}`); }; } //************************************************************************************** function handleError(api) { return error => { if("stack" in error) throw new Error(`API '${api}' error: ${error.stack}`); throw new Error(`API '${api}' error: ${error}`); }; } //************************************************************************************** export default class LogV1 { //********************************************************************************** /** * Constructor for Log class * @param {Object} [parameters={}] * @property {Object} [schema] asn1js parsed value */ constructor(parameters = {}) { //region Internal properties of the object /** * @type {Function} * @description fetch */ this.fetch = getParametersValue(parameters, "fetch", LogV1.constants("fetch")); /** * @type {Function} * @description encode */ this.encode = getParametersValue(parameters, "encode", LogV1.constants("encode")); if("log_id" in parameters) { /** * @type {String} * @description logID */ this.logID = stringToArrayBuffer(fromBase64(parameters.log_id)); } if("description" in parameters) /** * @type {String} * @description description */ this.description = getParametersValue(parameters, "description", LogV1.constants("description")); if("key" in parameters) { const asn1 = asn1js.fromBER(stringToArrayBuffer(fromBase64(parameters.key))); if(asn1.offset !== (-1)) { /** * @type {PublicKeyInfo} * @description key */ this.key = new PublicKeyInfo({ schema: asn1.result }); } } /** * @type {String} * @description url */ this.url = getParametersValue(parameters, "url", LogV1.constants("url")); if("maximum_merge_delay" in parameters) { /** * @type {Number} * @description maximumMergeDelay */ this.maximumMergeDelay = getParametersValue(parameters, "maximum_merge_delay", LogV1.constants("maximumMergeDelay")); } if("final_sth" in parameters) { this.finalSTH = { treeSize: parameters.final_sth.tree_size, timestamp: new Date(parameters.final_sth.timestamp), rootHash: stringToArrayBuffer(fromBase64(parameters.final_sth.sha256_root_hash)), signature: new DigitallySigned({ stream: new SeqStream({ buffer: stringToArrayBuffer(fromBase64(parameters.final_sth.tree_head_signature)) }) }) }; } //endregion } //********************************************************************************** /** * Return value for a constant by name * @param {string} name String name for a constant */ static constants(name) { switch(name) { case "fetch": return async () => { return Promise.reject("Uninitialized fetch function for LogV1 class"); }; case "encode": return () => { throw new Error("Uninitialized encode function for LogV1 class"); }; case "description": case "url": return ""; case "key": return (new PublicKeyInfo()); case "maximumMergeDelay": return 0; default: throw new Error(`Invalid constant name for LogV1 class: ${name}`); } } //********************************************************************************** set url(value) { if(value === "") return; const match = value.match(/(?:http[s]?:\/\/)?([^?\/s]+.*)/); if(match === null) throw new Error("Base URL for LogV1 class must be set to a correct value"); this._url = `https://${match[1].replace(/\/*$/g, "")}/ct/v1`; } //**********************************************************************************\ get url() { return this._url; } //********************************************************************************** /** * Implement call to "add-chain" Certificate Transparency Log API * @param {Array.<Certificate>} chain Array of certificates. The first element is the end-entity certificate * @return {Promise<SignedCertificateTimestamp>} */ async add_chain(chain) { const api = "add-chain"; /** * @type {Object} * @property {Number} sct_version The version of the SignedCertificateTimestamp structure, in decimal * @property {String} id The log ID, base64 encoded * @property {Number} timestamp The SCT timestamp, in decimal * @property {String} extensions An opaque type for future expansion * @property {String} signature The SCT signature, base64 encoded */ const json = await this.fetch( `${this.url}/${api}`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ chain: Array.from(chain, element => toBase64(arrayBufferToString(element.toSchema().toBER(false)))) }) }) .then(handleResult(api), handleError(api)); return (new SignedCertificateTimestamp({ json })); } //********************************************************************************** /** * Implement call to "add-pre-chain" Certificate Transparency Log API * @param {Array.<Certificate>} chain Array of certificates. The first element is the pre-certificate for end-entity * @return {Promise<SignedCertificateTimestamp>} */ async add_pre_chain(chain) { const api = "add-pre-chain"; /** * @type {Object} * @property {Number} sct_version The version of the SignedCertificateTimestamp structure, in decimal * @property {String} id The log ID, base64 encoded * @property {Number} timestamp The SCT timestamp, in decimal * @property {String} extensions An opaque type for future expansion * @property {String} signature The SCT signature, base64 encoded */ const json = await this.fetch( `${this.url}/${api}`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ chain: Array.from(chain, element => toBase64(arrayBufferToString(element.toSchema().toBER(false)))) }) }) .then(handleResult(api), handleError(api)); return (new SignedCertificateTimestamp({ json })); } //********************************************************************************** /** * Implement call to "get-sth" Certificate Transparency Log API * @return {Promise<SignedTreeHead>} Latest Signed Tree Head */ async get_sth() { const api = "get-sth"; const json = await this.fetch(`${this.url}/${api}`) .then(handleResult(api), handleError(api)); return (new SignedTreeHead({ json })); } //********************************************************************************** /** * Implement call to "get-sth-consistency" Certificate Transparency Log API * @param {Number} first The tree_size of the first tree, in decimal * @param {Number} second The tree_size of the second tree, in decimal * @return {Promise<Array.<ArrayBuffer>>} An array of Merkle Tree nodes */ async get_sth_consistency(first, second) { const api = "get-sth-consistency"; /** * @type {Object} * @property {Array} consistency An array of Merkle Tree nodes, base64 encoded */ const json = await this.fetch(`${this.url}/${api}?first=${first}&second=${second}`) .then(handleResult(api), handleError(api)); return Array.from(json.consistency, element => stringToArrayBuffer(fromBase64(element))); } //********************************************************************************** /** * Implement call to "get-proof-by-hash" Certificate Transparency Log API * @param {MerkleTreeLeaf|ArrayBuffer} hash ArrayBuffer with hash or a MerkleTreeLeaf value making a hash for * @param {Number} tree_size The tree_size of the tree on which to base the proof in decimal * @return {Promise<Object.<leaf_index, audit_path>>} */ async get_proof_by_hash(hash, tree_size) { //region Initial variables const api = "get-proof-by-hash"; let _hash; //endregion //region Calculate correct hash for passing to API if("byteLength" in hash) _hash = hash; else _hash = await hash.hash(); //endregion /** * @type {Object} * @property {Number} leaf_index The 0-based index of the end entity corresponding to the "hash" parameter * @property {Array} audit_path An array of base64-encoded Merkle Tree nodes proving the inclusion of the chosen certificate */ const json = await this.fetch(`${this.url}/${api}?hash=${this.encode(toBase64(arrayBufferToString(_hash)))}&tree_size=${tree_size}`) .then(handleResult(api), handleError(api)); return { leaf_index: json.leaf_index, audit_path: Array.from(json.audit_path, element => stringToArrayBuffer(fromBase64(element))) }; } //********************************************************************************** /** * Implement call to "get-entries" Certificate Transparency Log API * @param {Number} start 0-based index of first entry to retrieve, in decimal * @param {Number} end 0-based index of last entry to retrieve, in decimal * @param {Boolean} [request=true] Request or not additional absent entries * @param {Boolean} [getX509=true] Get or not X.509 certificate entries * @param {Boolean} [getPreCert=true] Get or not pre-certificate entries * @return {Promise<Array>} */ async get_entries_raw(start, end, request = true, getX509 = true, getPreCert = true) { //region Initial variables const api = "get-entries"; const entriesArray = []; //endregion /** * @typedef entry * @type {Object} * @property {String} leaf_input The base64-encoded MerkleTreeLeaf structure * @property {String} extra_data The base64-encoded unsigned data pertaining to the log entry * @type {Object} * @property {Array.<entry>} entries An array of objects */ const json = await this.fetch(`${this.url}/${api}?start=${start}&end=${end}`) .then(handleResult(api), handleError(api)); for(const entry of json.entries) { //region Initial variables let extraData; const stream = new SeqStream({ buffer: stringToArrayBuffer(fromBase64(entry.extra_data)) }); //endregion const merkleTreeLeaf = new MerkleTreeLeaf({ stream: new SeqStream({ buffer: stringToArrayBuffer(fromBase64(entry.leaf_input)) }) }); switch(merkleTreeLeaf.entry.entryType) { case LogEntryType.constants("x509_entry"): { if(getX509 === false) continue; extraData = []; stream.getUint24(); // Overall data length, useless at the moment while(stream.length) { const certificateLength = stream.getUint24(); const certificateBlock = (new Uint8Array(stream.getBlock(certificateLength))).buffer.slice(0); extraData.push(certificateBlock); } } break; case LogEntryType.constants("precert_entry"): { if(getPreCert === false) continue; //region Get information about "pre_certificate" value const preCertificateLength = stream.getUint24(); const preCertificate = (new Uint8Array(stream.getBlock(preCertificateLength))).buffer.slice(0); //endregion //region Get information about "precertificate_chain" array const preCertificateChain = []; stream.getUint24(); // Overall data length, useless at the moment while(stream.length) { const certificateLength = stream.getUint24(); preCertificateChain.push((new Uint8Array(stream.getBlock(certificateLength))).buffer.slice(0)); } //endregion extraData = { pre_certificate: preCertificate, precertificate_chain: preCertificateChain }; } break; default: } entriesArray.push({ leaf: merkleTreeLeaf, extra_data: extraData }); } //region Check we have all requested entries (some CT logs could return only a part) if(request && (entriesArray.length)) { const least = entriesArray[entriesArray.length - 1]; try { const proof = await this.get_proof_by_hash(least.leaf, end); if(proof.leaf_index !== end) { const additionalEntries = await this.get_entries_raw(proof.leaf_index + 1, end, request, getX509, getPreCert); entriesArray.push(...additionalEntries); } } catch(ex){} } //endregion return entriesArray; } //********************************************************************************** /** * Implement call to "get-entries" Certificate Transparency Log API * @param {Number} start 0-based index of first entry to retrieve, in decimal * @param {Number} end 0-based index of last entry to retrieve, in decimal * @param {Boolean} [request=true] Request or not additional absent entries * @param {Boolean} [getX509=true] Get or not X.509 certificate entries * @param {Boolean} [getPreCert=true] Get or not pre-certificate entries * @return {Promise<Array>} */ async get_entries(start, end, request = true, getX509 = true, getPreCert = true) { const result = []; const entries = await this.get_entries_raw(start, end, request, getX509, getPreCert); for(const entry of entries) { let extraData; switch(entry.leaf.entry.entryType) { case LogEntryType.constants("x509_entry"): { if(getX509 === false) continue; extraData = []; for(const certificate of entry.extra_data) { const asn1 = asn1js.fromBER(certificate); if(asn1.offset === (-1)) throw new Error("Object's stream was not correct for MerkleTreeLeaf extra_data"); extraData.push(new Certificate({ schema: asn1.result })); } } break; case LogEntryType.constants("precert_entry"): { if(getPreCert === false) continue; //region Get information about "pre_certificate" value const asn1 = asn1js.fromBER(entry.extra_data.pre_certificate); if(asn1.offset === (-1)) throw new Error("Object's stream was not correct for MerkleTreeLeaf extra_data"); const preCertificate = new Certificate({ schema: asn1.result }); //endregion //region Get information about "precertificate_chain" array const preCertificateChain = []; for(const preCertificateChainElement of entry.extra_data.precertificate_chain) { const asn1 = asn1js.fromBER(preCertificateChainElement); if(asn1.offset === (-1)) throw new Error("Object's stream was not correct for MerkleTreeLeaf extra_data"); preCertificateChain.push(new Certificate({ schema: asn1.result })); } //endregion extraData = { pre_certificate: preCertificate, precertificate_chain: preCertificateChain }; } break; default: } result.push({ leaf: entry.leaf, extra_data: extraData }); } return result; } //********************************************************************************** /** * Implement call to "get-entries" Certificate Transparency Log API and return leafs only * @param {Number} start 0-based index of first entry to retrieve, in decimal * @param {Number} end 0-based index of last entry to retrieve, in decimal * @param {Boolean} [request=true] Request or not additional absent entries * @return {Promise<Array>} */ async get_leafs(start, end, request = true) { //region Initial variables const api = "get-entries"; //endregion /** * @typedef entry * @type {Object} * @property {String} leaf_input The base64-encoded MerkleTreeLeaf structure * @property {String} extra_data The base64-encoded unsigned data pertaining to the log entry * @type {Object} * @property {Array.<entry>} entries An array of objects */ const json = await this.fetch(`${this.url}/${api}?start=${start}&end=${end}`) .then(handleResult(api), handleError(api)); //region Make major result array const result = Array.from(json.entries, element => new MerkleTreeLeaf({ stream: new SeqStream({ buffer: stringToArrayBuffer(fromBase64(element.leaf_input)) }) })); //endregion //region Check we have all requested entries (some CT logs could return only a part) if(request && (result.length > 0)) { try { const proof = await this.get_proof_by_hash(result[result.length - 1], end); if(proof.leaf_index !== end) { const additionalEntries = await this.get_leafs(proof.leaf_index + 1, end, request); result.push(...additionalEntries); } } catch(ex){} } //endregion return result; } //********************************************************************************** /** * Implement call to "get-roots" Certificate Transparency Log API * @return {Promise<Array.<Certificate>>} */ async get_roots() { const api = "get-roots"; /** * @type {Object} * @property {Array} certificates An array of base64-encoded root certificates that are acceptable to the log */ const json = await this.fetch(`${this.url}/${api}`) .then(handleResult(api), handleError(api)); return Array.from(json.certificates, element => { const asn1 = asn1js.fromBER(stringToArrayBuffer(fromBase64(element))); if(asn1.offset === (-1)) throw new Error("Incorrect data returned after get-roots call"); return (new Certificate({ schema: asn1.result })); }); } //********************************************************************************** /** * Implement call to "get-entry-and-proof" Certificate Transparency Log API * @param {Number} leaf_index The index of the desired entry * @param {Number} tree_size The tree_size of the tree for which the proof is * @return {Promise<Object>} */ async get_entry_and_proof(leaf_index, tree_size) { //region Initial variables const api = "get-entry-and-proof"; //endregion /** * @type {Object} * @property {String} leaf The base64-encoded MerkleTreeLeaf structure * @property {String} extra_data The base64-encoded unsigned data pertaining to the log entry * @property {Array.<String>} audit_path An array of base64-encoded Merkle Tree nodes proving the inclusion of the chosen certificate */ const json = await this.fetch(`${this.url}/${api}?leaf_index=${leaf_index}&tree_size=${tree_size}`) .then(handleResult(api), handleError(api)); //region Initial variables let extraData; const stream = new SeqStream({ buffer: stringToArrayBuffer(fromBase64(json.extra_data)) }); //endregion const merkleTreeLeaf = new MerkleTreeLeaf({ stream: new SeqStream({ buffer: stringToArrayBuffer(fromBase64(json.leaf)) }) }); switch(merkleTreeLeaf.entry.entryType) { case 0: { extraData = []; stream.getUint24(); // Overall data length, useless at the moment while(stream.length) { const certificateLength = stream.getUint24(); const asn1 = asn1js.fromBER((new Uint8Array(stream.getBlock(certificateLength))).buffer.slice(0)); if(asn1.offset === (-1)) throw new Error("Object's stream was not correct for MerkleTreeLeaf extra_data"); extraData.push(new Certificate({ schema: asn1.result })); } } break; case 1: { //region Get information about "pre_certificate" value const preCertificateLength = stream.getUint24(); const asn1 = asn1js.fromBER((new Uint8Array(stream.getBlock(preCertificateLength))).buffer.slice(0)); if(asn1.offset === (-1)) throw new Error("Object's stream was not correct for MerkleTreeLeaf extra_data"); const preCertificate = new Certificate({ schema: asn1.result }); //endregion //region Get information about "precertificate_chain" array const preCertificateChain = []; stream.getUint24(); // Overall data length, useless at the moment while(stream.length) { const certificateLength = stream.getUint24(); const asn1 = asn1js.fromBER((new Uint8Array(stream.getBlock(certificateLength))).buffer.slice(0)); if(asn1.offset === (-1)) throw new Error("Object's stream was not correct for MerkleTreeLeaf extra_data"); preCertificateChain.push(new Certificate({ schema: asn1.result })); } //endregion extraData = { pre_certificate: preCertificate, precertificate_chain: preCertificateChain }; } break; default: } return { leaf: merkleTreeLeaf, extra_data: extraData, audit_path: Array.from(json.audit_path, element => stringToArrayBuffer(fromBase64(element))) }; } //********************************************************************************** } //**************************************************************************************