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
JavaScript
;
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