UNPKG

ctutils

Version:

Utilities for interacting with Certificate Transparency logs

518 lines (441 loc) 14.7 kB
/** * Certificate Transparency Utilities * CTLog class * * By Fotis Loukos <me@fotisl.com> * @module ctutils */ import * as pkijs from 'pkijs'; import * as asn1js from 'asn1js'; import * as pvutils from 'pvutils'; import MerkleTreeLeaf from './MerkleTreeLeaf'; import SignedCertificateTimestamp from './SignedCertificateTimestamp'; import SignedTreeHead from './SignedTreeHead'; import { Version, LogEntryType } from './Enums'; import { paramsToQueryString } from './Common'; import { getFetch } from './Engines'; /** * An audit proof. * @typedef {Object} AuditProof * @property {number} index - The index of the leaf in the tree. * @property {Array<ArrayBuffer>} auditPath - The audit path. */ /** * An entry in the log. * @typedef {Object} LogEntry * @property {MerkleTreeLeaf} leaf - The merkle tree leaf. * @property {ArrayBuffer} extraData - The data pertaining to the entry. */ /** * An entry in the log and the audit proof. * @typedef {Object} LogEntryAndProof * @property {MerkleTreeLeaf} leaf - The merkle tree leaf. * @property {ArrayBuffer} extraData - The data pertaining to the entry. * @property {Array<ArrayBuffer>} auditPath - The audit path. */ /** * CTLog class */ export default class CTLog { /** * Construct a CTLog object. * @param {string} url - The url of the log. * @param {ArrayBuffer} pubKey - The public key of the log. * @param {number} version - The version of the log. * @param {ArrayBuffer} logId - The log id. * @param {number} maximumMergeDelay - The maximum merge delay. * @param {string} description - The description of the log. * @param {Array.<string>} operators - The operators of the log. */ constructor(url, pubKey, version = Version.v1, logId = null, maximumMergeDelay = 0, description = null, operators = null) { if(version !== Version.v1) throw new Error('Unsupported CT version'); /** * @type string * @description The url of the log. */ this.url = url; /** * @type ArrayBuffer * @description The public key of the log. */ this.pubKey = pubKey; /** * @type number * @description The version of the log. */ this.version = version; /** * @type ArrayBuffer * @description The log id. */ this.logId = logId; /** * @type number * @description The maximum merge delay. */ this.maximumMergeDelay = maximumMergeDelay; /** * @type string * @description The description of the log. */ this.description = description; /** * @type Array<string> * @description The operators of the log. */ this.operators = operators; } /** * Generate the log id from the public key. * @param {string} algorithmOID - The OID of the algorithm used for signing. * If this is null, then a heuristic method based on the key size will * be used. * @return {Promise.<Boolean>} The result of the generation. This will * normally be true, and it's used to notify that the calculation has * finished. */ generateId(algorithmOID = null) { let algorithmIdentifier; if(algorithmOID === null) { if(this.pubKey.byteLength === 91) { algorithmIdentifier = new pkijs.AlgorithmIdentifier({ algorithmId: '1.2.840.10045.2.1' }); } else if(this.pubKey.byteLength === 294) { algorithmIdentifier = new pkijs.AlgorithmIdentifier({ algorithmId: '1.2.840.113549.1.1.1' }); } else { return Promise.reject(new Error('Cannot identify algorithm')); } } else { algorithmIdentifier = new pkijs.AlgorithmIdentifier({ algorithmId: algorithmOID }); } const pubKeyInfo = new pkijs.PublicKeyInfo({ algorithm: algorithmIdentifier, subjectPublicKey: new asn1js.BitString({ valueHex: this.pubKey }) }); return pkijs.getEngine().subtle.digest({ name: 'SHA-256' }, pubKeyInfo.subjectPublicKey.valueBlock.valueHex).then(id => { this.logId = id; return true; }); } /** * Get the base url under which all calls are made. * @return {string} The base url */ getBaseUrl() { let url; if(this.url.startsWith('https://')) url = this.url; else url = 'https://' + this.url; while(url.endsWith('/')) url = url.substr(0, url.length - 1); if(this.version === Version.v1) url = url + '/ct/v1'; return url; } /** * Add a certificate. * @param {Array.<pkijs.Certificate>} certs - A list of certificates. The * first certificate is the end-entity certificate to be added, the second * chains to the first and so on (please check RFC6962 section 4.1). * @return {Promise.<SignedCertificateTimestamp>} A promise that is resolved * with the SCT. */ addCertChain(certs) { const encCerts = []; certs.forEach(cert => { const schema = cert.toSchema().toBER(false); encCerts.push(pvutils.toBase64(pvutils.arrayBufferToString(schema))); }); let sequence = getFetch()(this.getBaseUrl() + '/add-chain', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ chain: encCerts }) }); sequence = sequence.then(res => { if(!res.ok) return Promise.reject(new Error(`Error: ${res.statusText}`)); return res.json(); }); sequence = sequence.then(res => { const logId = pvutils.stringToArrayBuffer(pvutils.fromBase64(res.id)); const extensions = pvutils.stringToArrayBuffer( pvutils.fromBase64(res.extensions)); const signature = pvutils.stringToArrayBuffer( pvutils.fromBase64(res.signature)); return new SignedCertificateTimestamp(res.sct_version, logId, res.timestamp, extensions, signature, LogEntryType.x509_entry, certs[0].toSchema().toBER(false)); }); return sequence; } /** * Add a precertificate. * @param {Array.<pkijs.Certificate>} precerts - A list of certificates. The * first should be the precertificate to be added, the second chains to * the first and so on (please check RFC6962 section 4.1). * @return {Promise.<SignedCertificateTimestamp>} A promise that is resolved * with the SCT. */ addPreCertChain(certs) { const encCerts = []; certs.forEach(cert => { const schema = cert.toSchema().toBER(false); encCerts.push(pvutils.toBase64(pvutils.arrayBufferToString(schema))); }); let sequence = getFetch()(this.getBaseUrl() + '/add-pre-chain', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ chain: encCerts }) }); sequence = sequence.then(res => { if(!res.ok) return Promise.reject(new Error(`Error: ${res.statusText}`)); return res.json(); }); sequence = sequence.then(res => { const logId = pvutils.stringToArrayBuffer(pvutils.fromBase64(res.id)); const extensions = pvutils.stringToArrayBuffer( pvutils.fromBase64(res.extensions)); const signature = pvutils.stringToArrayBuffer( pvutils.fromBase64(res.signature)); return new SignedCertificateTimestamp(res.sct_version, logId, res.timestamp, extensions, signature, LogEntryType.precert_entry, certs[0].toSchema().toBER(false)); }); return sequence; } /** * Get the SignedTreeHead. * @return {Promise.<SignedTreeHead>} A promise that is resolved with the * SignedTreeHead. */ getSTH() { let sequence = getFetch()(this.getBaseUrl() + '/get-sth'); sequence = sequence.then(res => { if(!res.ok) return Promise.reject(new Error(`Error: ${res.statusText}`)); return res.json(); }); sequence = sequence.then(res => { const rootHash = pvutils.stringToArrayBuffer( pvutils.fromBase64(res.sha256_root_hash)); const signature = pvutils.stringToArrayBuffer( pvutils.fromBase64(res.tree_head_signature)); return new SignedTreeHead(res.tree_size, res.timestamp, rootHash, signature, Version.v1); }); return sequence; } /** * Get the consistency proof between two signed tree heads. * @param {(number|SignedTreeHead)} first - The first signed tree head or its * size. * @param {(number|SignedTreeHead)} second - The second signed tree head or * its size. * @return {Promise.<Array<ArrayBuffer>>} A Promise than is resolved with an * array of ArrayBuffers containing the proofs. */ getSTHConsistency(first, second) { let firstSize, secondSize; if(first instanceof SignedTreeHead) { firstSize = first.treeSize; } else if(typeof first === 'number') { firstSize = first; } else { return Promise.reject(new Error('Unknown first head type')); } if(second instanceof SignedTreeHead) { secondSize = second.treeSize; } else if(typeof second === 'number') { secondSize = second; } else { return Promise.reject(new Error('Unknown second head type')); } const params = { first: firstSize, second: secondSize }; const url = this.getBaseUrl() + '/get-sth-consistency?' + paramsToQueryString(params); let sequence = getFetch()(url).then(res => { if(!res.ok) return Promise.reject(new Error(`Error: ${res.statusText}`)); return res.json(); }); sequence = sequence.then(res => { const cons = []; for(let proof of res.consistency) cons.push(pvutils.stringToArrayBuffer(pvutils.fromBase64(proof))); return cons; }); return sequence; } /** * Get merkle audit proof by leaf hash. * @param {(number|SignedTreeHead)} sth - The signed tree head or the tree * size on which to base the proof. * @param {ArrayBuffer} hash - The leaf hash. * @return {Promise.<AuditProof>} A promise that is resolved with the audit * proof. */ getProofByHash(sth, hash) { let treeSize; if(sth instanceof SignedTreeHead) { treeSize = sth.treeSize; } else if(typeof sth === 'number') { treeSize = sth; } else { return Promise.reject(new Error('Unknown signed tree head type')); } const params = { tree_size: treeSize, hash: pvutils.toBase64(pvutils.arrayBufferToString(hash)) }; const url = this.getBaseUrl() + '/get-proof-by-hash?' + paramsToQueryString(params); let sequence = getFetch()(url).then(res => { if(!res.ok) return Promise.reject(new Error(`Error: ${res.statusText}`)); return res.json(); }); sequence = sequence.then(res => { const auditPath = []; res.audit_path.forEach(p => { auditPath.push(pvutils.stringToArrayBuffer(pvutils.fromBase64(p))); }); return { index: res.leaf_index, auditPath }; }); return sequence; } /** * Get merkle audit proof by leaf hash. * @param {(number|SignedTreeHead)} sth - The signed tree head or the tree * size on which to base the proof. * @param {MerkleTreeLeaf} leaf - The merkle tree leaf. * @return {Promise.<AuditProof>} A promise that is resolved with the audit * proof. */ getProofByLeaf(sth, leaf) { return leaf.getHash().then(h => this.getProofByHash(sth, h)); } /** * Get entries from the log. * @param {number} start - The index of the first entry. * @param {number} end - The index of the last entry. * @return {Promise.<Array<LogEntry>>} A promise that is resolved with an * array of MerkleTreeLeaf structures. */ getEntries(start, end) { const params = { start, end }; const url = this.getBaseUrl() + '/get-entries?' + paramsToQueryString(params); let sequence = getFetch()(url).then(res => { if(!res.ok) return Promise.reject(new Error(`Error: ${res.statusText}`)); return res.json(); }); sequence = sequence.then(res => { const entries = []; res.entries.forEach(entry => { const leafData = pvutils.stringToArrayBuffer(pvutils.fromBase64( entry.leaf_input)); const extraData = pvutils.stringToArrayBuffer(pvutils.fromBase64( entry.extra_data)); entries.push({ leaf: MerkleTreeLeaf.fromBinary(leafData), extraData }); }); return entries; }); return sequence; } /** * Get accepted roots. * @return {Promise.<Array<pkijs.Certificate>>} An array of certificates. */ getRoots() { let sequence = getFetch()(this.getBaseUrl() + '/get-roots'); sequence = sequence.then(res => { if(!res.ok) return Promise.reject(new Error(`Error: ${res.statusText}`)); return res.json(); }); sequence = sequence.then(res => { const certs = []; res.certificates.forEach(cert => { const certData = pvutils.stringToArrayBuffer(pvutils.fromBase64(cert)); const asn1 = asn1js.fromBER(certData); certs.push(new pkijs.Certificate({ schema: asn1.result })); }); return certs; }); return sequence; } /** * Get an entry from the log and the audit path. * @param {(number|SignedTreeHead)} sth - The signed tree head or the tree * size on which to base the proof. * @param {number} index - The index of the entry. * @return {Promise.<LogEntryAndProof>} The log entry with the audit path. */ getEntryAndProof(sth, index) { let treeSize; if(sth instanceof SignedTreeHead) { treeSize = sth.treeSize; } else if(typeof sth === 'number') { treeSize = sth; } else { return Promise.reject(new Error('Unknown signed tree head type')); } const params = { leaf_index: index, tree_size: treeSize }; const url = this.getBaseUrl() + '/get-entry-and-proof?' + paramsToQueryString(params); let sequence = getFetch()(url).then(res => { if(!res.ok) return Promise.reject(new Error(`Error: ${res.statusText}`)); return res.json(); }); sequence = sequence.then(res => { const leafData = pvutils.stringToArrayBuffer(pvutils.fromBase64( res.leaf_input)); const extraData = pvutils.stringToArrayBuffer(pvutils.fromBase64( res.extra_data)); const auditPath = []; res.audit_path.forEach(p => { auditPath.push(pvutils.stringToArrayBuffer(pvutils.fromBase64(p))); }); return { leaf: MerkleTreeLeaf.fromBinary(leafData), extraData, auditPath }; }); return sequence; } }