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

783 lines (658 loc) 27.3 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); var _asn1js = require("asn1js"); var asn1js = _interopRequireWildcard(_asn1js); var _pvutils = require("pvutils"); var _bytestreamjs = require("bytestreamjs"); var _pkijs = require("pkijs"); var _SignedTreeHead = require("./SignedTreeHead.js"); var _SignedTreeHead2 = _interopRequireDefault(_SignedTreeHead); var _MerkleTreeLeaf = require("./MerkleTreeLeaf.js"); var _MerkleTreeLeaf2 = _interopRequireDefault(_MerkleTreeLeaf); var _SignedCertificateTimestamp = require("./SignedCertificateTimestamp.js"); var _SignedCertificateTimestamp2 = _interopRequireDefault(_SignedCertificateTimestamp); var _DigitallySigned = require("./DigitallySigned.js"); var _DigitallySigned2 = _interopRequireDefault(_DigitallySigned); var _LogEntryType = require("./LogEntryType.js"); var _LogEntryType2 = _interopRequireDefault(_LogEntryType); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } } function _asyncToGenerator(fn) { return function () { var gen = fn.apply(this, arguments); return new Promise(function (resolve, reject) { function step(key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { return Promise.resolve(value).then(function (value) { step("next", value); }, function (err) { step("throw", err); }); } } return step("next"); }); }; } /* eslint-disable no-useless-escape */ //************************************************************************************** function handleResult(api) { return (() => { var _ref = _asyncToGenerator(function* (result) { if (result.ok) return result.json(); let errorMessage = result.statusText; try { const errorJSON = yield result.json(); if ("error_message" in errorJSON) errorMessage = errorJSON.error_message; } catch (ex) {} throw new Error(`ERROR while fetching ${api}: ${errorMessage}`); }); return function (_x) { return _ref.apply(this, arguments); }; })(); } //************************************************************************************** function handleError(api) { return error => { if ("stack" in error) throw new Error(`API '${api}' error: ${error.stack}`); throw new Error(`API '${api}' error: ${error}`); }; } //************************************************************************************** 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 = (0, _pvutils.getParametersValue)(parameters, "fetch", LogV1.constants("fetch")); /** * @type {Function} * @description encode */ this.encode = (0, _pvutils.getParametersValue)(parameters, "encode", LogV1.constants("encode")); if ("log_id" in parameters) { /** * @type {String} * @description logID */ this.logID = (0, _pvutils.stringToArrayBuffer)((0, _pvutils.fromBase64)(parameters.log_id)); } if ("description" in parameters) /** * @type {String} * @description description */ this.description = (0, _pvutils.getParametersValue)(parameters, "description", LogV1.constants("description")); if ("key" in parameters) { const asn1 = asn1js.fromBER((0, _pvutils.stringToArrayBuffer)((0, _pvutils.fromBase64)(parameters.key))); if (asn1.offset !== -1) { /** * @type {PublicKeyInfo} * @description key */ this.key = new _pkijs.PublicKeyInfo({ schema: asn1.result }); } } /** * @type {String} * @description url */ this.url = (0, _pvutils.getParametersValue)(parameters, "url", LogV1.constants("url")); if ("maximum_merge_delay" in parameters) { /** * @type {Number} * @description maximumMergeDelay */ this.maximumMergeDelay = (0, _pvutils.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: (0, _pvutils.stringToArrayBuffer)((0, _pvutils.fromBase64)(parameters.final_sth.sha256_root_hash)), signature: new _DigitallySigned2.default({ stream: new _bytestreamjs.SeqStream({ buffer: (0, _pvutils.stringToArrayBuffer)((0, _pvutils.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 _asyncToGenerator(function* () { 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 _pkijs.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>} */ add_chain(chain) { var _this = this; return _asyncToGenerator(function* () { 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 = yield _this.fetch(`${_this.url}/${api}`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ chain: Array.from(chain, function (element) { return (0, _pvutils.toBase64)((0, _pvutils.arrayBufferToString)(element.toSchema().toBER(false))); }) }) }).then(handleResult(api), handleError(api)); return new _SignedCertificateTimestamp2.default({ 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>} */ add_pre_chain(chain) { var _this2 = this; return _asyncToGenerator(function* () { 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 = yield _this2.fetch(`${_this2.url}/${api}`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ chain: Array.from(chain, function (element) { return (0, _pvutils.toBase64)((0, _pvutils.arrayBufferToString)(element.toSchema().toBER(false))); }) }) }).then(handleResult(api), handleError(api)); return new _SignedCertificateTimestamp2.default({ json }); })(); } //********************************************************************************** /** * Implement call to "get-sth" Certificate Transparency Log API * @return {Promise<SignedTreeHead>} Latest Signed Tree Head */ get_sth() { var _this3 = this; return _asyncToGenerator(function* () { const api = "get-sth"; const json = yield _this3.fetch(`${_this3.url}/${api}`).then(handleResult(api), handleError(api)); return new _SignedTreeHead2.default({ 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 */ get_sth_consistency(first, second) { var _this4 = this; return _asyncToGenerator(function* () { const api = "get-sth-consistency"; /** * @type {Object} * @property {Array} consistency An array of Merkle Tree nodes, base64 encoded */ const json = yield _this4.fetch(`${_this4.url}/${api}?first=${first}&second=${second}`).then(handleResult(api), handleError(api)); return Array.from(json.consistency, function (element) { return (0, _pvutils.stringToArrayBuffer)((0, _pvutils.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>>} */ get_proof_by_hash(hash, tree_size) { var _this5 = this; return _asyncToGenerator(function* () { //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 = yield 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 = yield _this5.fetch(`${_this5.url}/${api}?hash=${_this5.encode((0, _pvutils.toBase64)((0, _pvutils.arrayBufferToString)(_hash)))}&tree_size=${tree_size}`).then(handleResult(api), handleError(api)); return { leaf_index: json.leaf_index, audit_path: Array.from(json.audit_path, function (element) { return (0, _pvutils.stringToArrayBuffer)((0, _pvutils.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>} */ get_entries_raw(start, end, request = true, getX509 = true, getPreCert = true) { var _this6 = this; return _asyncToGenerator(function* () { //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 = yield _this6.fetch(`${_this6.url}/${api}?start=${start}&end=${end}`).then(handleResult(api), handleError(api)); var _iteratorNormalCompletion = true; var _didIteratorError = false; var _iteratorError = undefined; try { for (var _iterator = json.entries[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { const entry = _step.value; //region Initial variables let extraData; const stream = new _bytestreamjs.SeqStream({ buffer: (0, _pvutils.stringToArrayBuffer)((0, _pvutils.fromBase64)(entry.extra_data)) }); //endregion const merkleTreeLeaf = new _MerkleTreeLeaf2.default({ stream: new _bytestreamjs.SeqStream({ buffer: (0, _pvutils.stringToArrayBuffer)((0, _pvutils.fromBase64)(entry.leaf_input)) }) }); switch (merkleTreeLeaf.entry.entryType) { case _LogEntryType2.default.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 _LogEntryType2.default.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) } catch (err) { _didIteratorError = true; _iteratorError = err; } finally { try { if (!_iteratorNormalCompletion && _iterator.return) { _iterator.return(); } } finally { if (_didIteratorError) { throw _iteratorError; } } } if (request && entriesArray.length) { const least = entriesArray[entriesArray.length - 1]; try { const proof = yield _this6.get_proof_by_hash(least.leaf, end); if (proof.leaf_index !== end) { const additionalEntries = yield _this6.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>} */ get_entries(start, end, request = true, getX509 = true, getPreCert = true) { var _this7 = this; return _asyncToGenerator(function* () { const result = []; const entries = yield _this7.get_entries_raw(start, end, request, getX509, getPreCert); var _iteratorNormalCompletion2 = true; var _didIteratorError2 = false; var _iteratorError2 = undefined; try { for (var _iterator2 = entries[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) { const entry = _step2.value; let extraData; switch (entry.leaf.entry.entryType) { case _LogEntryType2.default.constants("x509_entry"): { if (getX509 === false) continue; extraData = []; var _iteratorNormalCompletion3 = true; var _didIteratorError3 = false; var _iteratorError3 = undefined; try { for (var _iterator3 = entry.extra_data[Symbol.iterator](), _step3; !(_iteratorNormalCompletion3 = (_step3 = _iterator3.next()).done); _iteratorNormalCompletion3 = true) { const certificate = _step3.value; 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 _pkijs.Certificate({ schema: asn1.result })); } } catch (err) { _didIteratorError3 = true; _iteratorError3 = err; } finally { try { if (!_iteratorNormalCompletion3 && _iterator3.return) { _iterator3.return(); } } finally { if (_didIteratorError3) { throw _iteratorError3; } } } } break; case _LogEntryType2.default.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 _pkijs.Certificate({ schema: asn1.result }); //endregion //region Get information about "precertificate_chain" array const preCertificateChain = []; var _iteratorNormalCompletion4 = true; var _didIteratorError4 = false; var _iteratorError4 = undefined; try { for (var _iterator4 = entry.extra_data.precertificate_chain[Symbol.iterator](), _step4; !(_iteratorNormalCompletion4 = (_step4 = _iterator4.next()).done); _iteratorNormalCompletion4 = true) { const preCertificateChainElement = _step4.value; 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 _pkijs.Certificate({ schema: asn1.result })); } //endregion } catch (err) { _didIteratorError4 = true; _iteratorError4 = err; } finally { try { if (!_iteratorNormalCompletion4 && _iterator4.return) { _iterator4.return(); } } finally { if (_didIteratorError4) { throw _iteratorError4; } } } extraData = { pre_certificate: preCertificate, precertificate_chain: preCertificateChain }; } break; default: } result.push({ leaf: entry.leaf, extra_data: extraData }); } } catch (err) { _didIteratorError2 = true; _iteratorError2 = err; } finally { try { if (!_iteratorNormalCompletion2 && _iterator2.return) { _iterator2.return(); } } finally { if (_didIteratorError2) { throw _iteratorError2; } } } 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>} */ get_leafs(start, end, request = true) { var _this8 = this; return _asyncToGenerator(function* () { //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 = yield _this8.fetch(`${_this8.url}/${api}?start=${start}&end=${end}`).then(handleResult(api), handleError(api)); //region Make major result array const result = Array.from(json.entries, function (element) { return new _MerkleTreeLeaf2.default({ stream: new _bytestreamjs.SeqStream({ buffer: (0, _pvutils.stringToArrayBuffer)((0, _pvutils.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 = yield _this8.get_proof_by_hash(result[result.length - 1], end); if (proof.leaf_index !== end) { const additionalEntries = yield _this8.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>>} */ get_roots() { var _this9 = this; return _asyncToGenerator(function* () { const api = "get-roots"; /** * @type {Object} * @property {Array} certificates An array of base64-encoded root certificates that are acceptable to the log */ const json = yield _this9.fetch(`${_this9.url}/${api}`).then(handleResult(api), handleError(api)); return Array.from(json.certificates, function (element) { const asn1 = asn1js.fromBER((0, _pvutils.stringToArrayBuffer)((0, _pvutils.fromBase64)(element))); if (asn1.offset === -1) throw new Error("Incorrect data returned after get-roots call"); return new _pkijs.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>} */ get_entry_and_proof(leaf_index, tree_size) { var _this10 = this; return _asyncToGenerator(function* () { //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 = yield _this10.fetch(`${_this10.url}/${api}?leaf_index=${leaf_index}&tree_size=${tree_size}`).then(handleResult(api), handleError(api)); //region Initial variables let extraData; const stream = new _bytestreamjs.SeqStream({ buffer: (0, _pvutils.stringToArrayBuffer)((0, _pvutils.fromBase64)(json.extra_data)) }); //endregion const merkleTreeLeaf = new _MerkleTreeLeaf2.default({ stream: new _bytestreamjs.SeqStream({ buffer: (0, _pvutils.stringToArrayBuffer)((0, _pvutils.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 _pkijs.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 _pkijs.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 _pkijs.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, function (element) { return (0, _pvutils.stringToArrayBuffer)((0, _pvutils.fromBase64)(element)); }) }; })(); } //********************************************************************************** } exports.default = LogV1; //************************************************************************************** //# sourceMappingURL=LogV1.js.map