ctutils
Version:
Utilities for interacting with Certificate Transparency logs
349 lines (288 loc) • 9.57 kB
JavaScript
/**
* Certificate Transparency Utilities
* SignedTreeHead class
*
* By Fotis Loukos <me@fotisl.com>
* @module ctutils
*/
import * as pkijs from 'pkijs';
import * as asn1js from 'asn1js';
import CTLog from './CTLog';
import { SignatureType } from './Enums';
import { uint64ToArrayBuffer } from './Common';
/**
* SignedTreeHead class
*/
export default class SignedTreeHead {
/**
* Construct a SignedTreeHead.
* @param {number} treeSize - The size of the tree.
* @param {number} timestamp - The timestamp.
* @param {ArrayBuffer} rootHash - The Merkle Tree Hash.
* @param {ArrayBuffer} signature - The signature.
* @param {number} version - The version.
*/
constructor(treeSize, timestamp, rootHash, signature, version) {
/**
* @type {number}
* @description The size of the tree.
*/
this.treeSize = treeSize;
/**
* @type {number}
* @description The timestamp.
*/
this.timestamp = timestamp;
/**
* @type {ArrayBuffer}
* @description The Merkle Tree Hash.
*/
this.rootHash = rootHash;
/**
* @type {ArrayBuffer}
* @description The signature.
*/
this.signature = signature;
/**
* @type {number}
* @description The version.
*/
this.version = version;
}
/**
* Verify the signature of an SignedTreeHead.
* @param {(ArrayBuffer|CTLog)} log - The public key of the log as an
* ArrayBuffer, or a CTLog object.
* @return {Promise.<Boolean>} A promise that is resolved with the result
* of the verification.
*/
verify(log) {
let pubKey;
if(log instanceof CTLog) {
pubKey = log.pubKey;
} else if(log instanceof ArrayBuffer) {
pubKey = log;
} else {
return Promise.reject(new Error('Unknown key type'));
}
let sequence = Promise.resolve();
const signatureView = new Uint8Array(this.signature);
const dataStruct = new ArrayBuffer(50);
const dataStructView = new Uint8Array(dataStruct);
/*
* Prepare the struct with the data that was signed.
*/
dataStructView[0] = this.version;
dataStructView[1] = SignatureType.tree_hash;
dataStructView.set(new Uint8Array(uint64ToArrayBuffer(this.timestamp)), 2);
dataStructView.set(new Uint8Array(uint64ToArrayBuffer(this.treeSize)), 10);
dataStructView.set(new Uint8Array(this.rootHash), 18);
/*
* Per RFC6962 all signatures are either ECDSA with the NIST P-256 curve
* or RSA (RSASSA-PKCS1-V1_5) with SHA-256.
*/
const isECDSA = signatureView[1] === 3;
const pubKeyView = new Uint8Array(pubKey);
const webcrypto = pkijs.getEngine();
sequence = sequence.then(() => {
let opts;
if(isECDSA) {
opts = {
name: 'ECDSA',
namedCurve: 'P-256'
};
} else {
opts = {
name: 'RSASSA-PKCS1-v1_5',
hash: {
name: 'SHA-256'
}
};
}
return webcrypto.subtle.importKey('spki', pubKeyView, opts, false,
['verify']);
});
sequence = sequence.then(publicKey => {
let opts;
if(isECDSA) {
opts = {
name: 'ECDSA',
hash: {
name: 'SHA-256'
}
};
} else {
opts = {
name: 'RSASSA-PKCS1-v1_5'
};
}
if(isECDSA) {
/*
* Convert from a CMS signature to a webcrypto compatible one.
*/
const asn1 = asn1js.fromBER(this.signature.slice(4));
const ecdsaSig = pkijs.createECDSASignatureFromCMS(asn1.result);
return webcrypto.subtle.verify(opts, publicKey, ecdsaSig, dataStruct);
} else {
return webcrypto.subtle.verify(opts, publicKey, this.signature.slice(4),
dataStruct);
}
});
return sequence;
}
/**
* Verify consistency between two Signed Tree Heads.
* @param {SignedTreeHead} second - The second SignedTreeHead.
* @param {Array.<ArrayBuffer>} proofs - The consistency proofs.
* @return {Promise.<Boolean>} A promise that is resolved with the
* result of the consistency verification.
*/
verifyConsistency(second, proofs) {
/**
* Both functions return an array whose first item is the old hash
* and the second item is the new hash. This helps in creating
* the chain during the verification.
*/
const hashRightChild = async (oldHash, newHash, node) => {
const oldHashView = new Uint8Array(oldHash);
const newHashView = new Uint8Array(newHash);
const nodeView = new Uint8Array(node);
const data = new ArrayBuffer(oldHashView.length + nodeView.length + 1);
const dataView = new Uint8Array(data);
const webcrypto = pkijs.getEngine();
dataView[0] = 0x01;
dataView.set(nodeView, 1);
dataView.set(oldHashView, 1 + nodeView.length);
oldHash = await webcrypto.subtle.digest({ name: 'SHA-256' }, data);
dataView.set(newHashView, 1 + nodeView.length);
newHash = await webcrypto.subtle.digest({ name: 'SHA-256' }, data);
return [ oldHash, newHash ];
};
const hashLeftChild = async (oldHash, newHash, node) => {
const newHashView = new Uint8Array(newHash);
const nodeView = new Uint8Array(node);
const data = new ArrayBuffer(newHashView.length + nodeView.length + 1);
const dataView = new Uint8Array(data);
const webcrypto = pkijs.getEngine();
dataView[0] = 0x01;
dataView.set(newHashView, 1);
dataView.set(nodeView, 1 + newHashView.length);
newHash = await webcrypto.subtle.digest({ name: 'SHA-256' }, data);
return [ oldHash, newHash ];
};
let oldSTH, newSTH;
if(this.timestamp <= second.timestamp) {
oldSTH = this;
newSTH = second;
} else {
oldSTH = second;
newSTH = this;
}
if(oldSTH.treeSize > newSTH.treeSize)
return Promise.reject(new Error('Older tree is bigger than first'));
/**
* If the old tree is empty or has the same number of elements with the
* new we assume it's valid.
*/
if(oldSTH.treeSize === 0)
return Promise.resolve(true);
if(oldSTH.treeSize === newSTH.treeSize) {
const oldRootHashView = new Uint8Array(oldSTH.rootHash);
const newRootHashView = new Uint8Array(newSTH.rootHash);
if(oldRootHashView.length !== newRootHashView.length)
return Promise.resolve(false);
for(let i = 0; i < oldRootHashView; i++)
if(oldRootHashView[i] !== newRootHashView[i])
return Promise.resolve(false);
return Promise.resolve(true);
}
/* Calculate the expected size of the proof */
let length = 0;
let b = 0;
let m = oldSTH.treeSize;
let n = newSTH.treeSize;
while(m !== n) {
length++;
const k = 2 ** Math.floor(Math.log2(n - 1));
if(m <= k) {
n = k;
} else {
m -= k;
n -= k;
b = 1;
}
}
length += b;
if(proofs.length !== length)
return Promise.reject(new Error('Proof size wrong'));
/* Start verification */
let node = oldSTH.treeSize - 1;
let lastNode = newSTH.treeSize - 1;
while((node % 2) > 0) {
node = Math.floor(node / 2);
lastNode = Math.floor(lastNode / 2);
}
const proofArray = proofs.slice();
let sequence;
/**
* Sequence is resolved with old hash and new hash in order to be ready
* for input to the chain of calls below.
*/
if(node > 0) {
const h = proofArray.shift();
sequence = Promise.resolve([h, h]);
} else {
sequence = Promise.resolve([oldSTH.rootHash, oldSTH.rootHash]);
}
/**
* The following chain of calls to hashRightChild and hashLeftChild works
* because both callbacks for success expect an array with the old hash
* and the new one, and return such an array.
*/
while(node > 0) {
if((node % 2) > 0) {
sequence = sequence.then((args) => {
const oldHash = args[0];
const newHash = args[1];
return hashRightChild(oldHash, newHash, proofArray.shift())
});
} else if(node < lastNode) {
sequence = sequence.then((args) => {
const oldHash = args[0];
const newHash = args[1];
return hashLeftChild(oldHash, newHash, proofArray.shift())
});
}
node = Math.floor(node / 2);
lastNode = Math.floor(lastNode / 2);
}
while(lastNode > 0) {
sequence = sequence.then((args) => {
const oldHash = args[0];
const newHash = args[1];
return hashLeftChild(oldHash, newHash, proofArray.shift())
});
lastNode = Math.floor(lastNode / 2);
}
/* Finally compare calculated root hashes against the actual ones */
sequence = sequence.then((args) => {
const oldHash = args[0];
const newHash = args[1];
const oldHashView = new Uint8Array(oldHash);
const newHashView = new Uint8Array(newHash);
const oldRootView = new Uint8Array(oldSTH.rootHash);
const newRootView = new Uint8Array(newSTH.rootHash);
if((oldHashView.length !== oldRootView.length) ||
(newHashView.length !== newRootView.length))
return false;
for(let i = 0; i < oldHashView.length; i++)
if(oldHashView[i] !== oldRootView[i])
return false;
for(let i = 0; i < newHashView.length; i++)
if(newHashView[i] !== newRootView[i])
return false;
return true;
});
return sequence;
}
}