ctutils
Version:
Utilities for interacting with Certificate Transparency logs
223 lines (190 loc) • 7.82 kB
JavaScript
/**
* Certificate Transparency Utilities
* CertHelper class
*
* By Fotis Loukos <me@fotisl.com>
* @module ctutils
*/
import * as asn1js from 'asn1js';
import * as pkijs from 'pkijs';
import CTLogHelper from './CTLogHelper';
import TimestampedEntry from './TimestampedEntry';
import MerkleTreeLeaf from './MerkleTreeLeaf';
import SignedCertificateTimestamp from './SignedCertificateTimestamp';
import PreCert from './PreCert';
import { LogEntryType, MerkleLeafType } from './Enums';
/**
* CertHelper class
*/
export default class CertHelper {
/**
* Generate a key hash from a certificate.
* This is the key hash as defined in RFC6962. Please note that you do not
* always need the certificate of the final issuing CA since a precertificate
* can be signed by a certificate with the CT EKU.
* @param {pkijs.Certificate} cert - The certificate whose key hash will be
* generated.
* @return {Promise.<ArrayBuffer>} A promise that is resolved with the key
* hash.
*/
static getKeyHash(cert) {
const webcrypto = pkijs.getEngine();
return webcrypto.subtle.digest({
name: 'SHA-256'
}, cert.subjectPublicKeyInfo.toSchema().toBER(false));
}
/**
* Extract a list of SCTs from a certificate.
* @param {pkijs.Certificate} cert - The certificate to extract the SCTs from.
* @param {ArrayBuffer} issuerKeyHash - The issuer key hash. For more
* information, please see the getKeyHash() documentation. If this is null,
* then the SCTs will be extracted but the cert field will not be populated.
* This is useful if you want to extract the information from the certificate
* but you will not be able to perform any kind of validation.
* @return {Array.<SignedCertificateTimestamp>} An array of
* SignedCertificateTimestamp objects or null if no SCTs exist in the
* certificate.
*/
static extractSCTFromCert(cert, issuerKeyHash = null) {
let sctExt = null;
cert.extensions.forEach(ext => {
if(ext.extnID === '1.3.6.1.4.1.11129.2.4.2')
sctExt = ext;
});
if(sctExt === null)
return null;
const asn1 = asn1js.fromBER(sctExt.extnValue.valueBlock.valueHex);
if((asn1.offset === -1) || !(asn1.result instanceof asn1js.OctetString))
return null;
const preCert = CertHelper.certToPreCert(cert, issuerKeyHash);
const sctList = [];
let parseBlock = asn1.result.valueBlock.valueHex;
let parseBlockView = new Uint8Array(parseBlock);
const totalLen = (parseBlockView[0] << 8) + parseBlockView[1];
let parsedLen = 0;
parseBlock = parseBlock.slice(2);
while(parsedLen < totalLen) {
parseBlockView = new Uint8Array(parseBlock);
const sctLen = (parseBlockView[0] << 8) + parseBlockView[1];
sctList.push(SignedCertificateTimestamp.fromBinary(parseBlock.slice(2,
sctLen), LogEntryType.precert_entry, preCert));
parseBlock = parseBlock.slice(2 + sctLen);
parsedLen += (2 + sctLen);
}
return sctList;
}
/**
* Generate a PreCert from a certificate.
* This will not generate a precertificate but a PreCert structure.
* If there is an SCT list extension in the certificate, then this will be
* removed.
* @param {pkijs.Certificate} cert - The certificate from which the PreCert
* will be generated.
* @param {ArrayBuffer} issuerKeyHash - The issuer key hash. For more
* information, please see the getKeyHash() documentation.
* @return {PreCert} A PreCert object.
*/
static certToPreCert(cert, issuerKeyHash) {
/* First make a copy of the cert */
let asn1 = asn1js.fromBER(cert.toSchema().toBER(false));
const workingCert = new pkijs.Certificate({ schema: asn1.result });
/* Remove SCT list */
const sctListIndex = workingCert.extensions.findIndex(ext =>
ext.extnID === '1.3.6.1.4.1.11129.2.4.2');
if(sctListIndex !== -1)
workingCert.extensions.splice(sctListIndex, 1);
return new PreCert(issuerKeyHash, workingCert.encodeTBS().toBER(false));
}
/**
* Validate the SCTs from a certificate.
* Note that this validates the SCTs that were generated from a precertificate
* and are embedded in a certificate. To validate an SCT that was generated
* for a certificate, create a SignedCertificateTimestamp object and use the
* verify() method.
* @param {pkijs.Certificate} cert - The certificate that will be validated.
* @param {ArrayBuffer} issuerKeyHash - The issuer key hash. For more
* information, please see the getKeyHash() documentation.
* @param {(Array.<CTLog>|CTLogHelper)} ctLogs - An array of CTLog objects or
* a CTLogHelper object. The only logs that need to be included are the logs
* that issued the SCTs. Furthermore, the only fields used are the logId and
* the public key of the log.
* @return {Promise.<Boolean>} A promise that is resolved with the result of
* the validation. If there are no SCTs in the certificate, then the result
* of the validation will be true. If an SCT belongs to a log that is not
* included in ctLogs, then the result will be false;
*/
static validateCertSCT(cert, issuerKeyHash, ctLogs) {
const sctList = CertHelper.extractSCTFromCert(cert, issuerKeyHash);
let logHelper;
if(ctLogs instanceof CTLogHelper) {
logHelper = ctLogs;
} else if(ctLogs instanceof Array) {
logHelper = new CTLogHelper(ctLogs);
} else {
return Promise.reject(new Error('Unkown ctlogs type'));
}
if(sctList.length === 0)
return Promise.resolve(true);
/*
* We first check if all public keys exist and then we validate the
* SCTs. This requires double work in finding the correct log from the
* list, but we avoid starting a number of verifications if a single
* log is missing.
*/
for(const sct of sctList)
if(logHelper.findById(sct.logId) == null)
return Promise.resolve(false);
const validations = [];
sctList.forEach(sct => {
const log = logHelper.findById(sct.logId);
validations.push(sct.verify(log.pubKey));
});
return Promise.all(validations).then(res => {
let ret = true;
res.forEach(r => {
ret &= r;
});
return ret;
});
}
/**
* Verify the inclusion of an SCT in a log.
* @param {SignedCertificateTimestamp} sct - The SCT that will be checked.
* Even if it is not required for other operations, the type and cert fields
* must be set.
* @param {(Array.<CTLog>|CTLogHelper)} ctLogs - An array of CTLog objects or
* a CTLogHelper object. The only log that needs to be included is the log
* that issued the SCT. Furthermore, the only fields used are the logId and
* the public key of the log.
* @return {Promise.<Boolean>} A promise that is resolved with the result of
* the verification.
*/
static verifySCTInclusion(sct, ctLogs) {
let logHelper;
if(ctLogs instanceof CTLogHelper) {
logHelper = ctLogs;
} else if(ctLogs instanceof Array) {
logHelper = new CTLogHelper(ctLogs);
} else {
return Promise.reject(new Error('Unkown ctlogs type'));
}
const log = logHelper.findById(sct.logId);
if(log === null)
return Promise.resolve(false);
const timestampedEntry = new TimestampedEntry(sct.timestamp, sct.type,
sct.cert, sct.extensions);
const merkleTreeLeaf = new MerkleTreeLeaf(sct.version,
MerkleLeafType.timestamped_entry, timestampedEntry);
let sequence = Promise.resolve();
sequence = sequence.then(() => log.getSTH());
sequence = sequence.then(sth =>
log.getProofByLeaf(sth.treeSize, merkleTreeLeaf));
sequence = sequence.then(res => {
return true;
});
sequence = sequence.catch(e => {
return false;
});
return sequence;
}
}