/*! * x509.js - x509 handling for bcoin * Copyright (c) 2016-2017, Christopher Jeffrey (MIT License). * https://github.com/bcoin-org/bcoin */ 'use strict'; var assert = require('assert'); var ASN1 = require('../utils/asn1'); var PEM = require('../utils/pem'); var util = require('../utils/util'); var crypto = require('../crypto/crypto'); var pk = require('./pk'); var certs = require('./certs'); /** * @exports bip70/x509 */ var x509 = exports; /** * Map of trusted root certs. * @type {Object} */ x509.trusted = {}; /** * Whether to allow untrusted root * certs during verification. * @type {Boolean} */ x509.allowUntrusted = false; /** * OID to algorithm map for PKI. * @const {Object} * @see https://www.ietf.org/rfc/rfc2459.txt * @see https://tools.ietf.org/html/rfc3279 * @see http://oid-info.com/get/1.2.840.10040.4 * @see http://oid-info.com/get/1.2.840.113549.1.1 * @see http://oid-info.com/get/1.2.840.10045.4.3 */ x509.oid = { '1.2.840.10040.4.1' : { key: 'dsa', hash: null }, '1.2.840.10040.4.2' : { key: 'dsa', hash: null }, '1.2.840.10040.4.3' : { key: 'dsa', hash: 'sha1' }, '1.2.840.113549.1.1.1' : { key: 'rsa', hash: null }, '1.2.840.113549.1.1.2' : { key: 'rsa', hash: 'md2' }, '1.2.840.113549.1.1.3' : { key: 'rsa', hash: 'md4' }, '1.2.840.113549.1.1.4' : { key: 'rsa', hash: 'md5' }, '1.2.840.113549.1.1.5' : { key: 'rsa', hash: 'sha1' }, '1.2.840.113549.1.1.11': { key: 'rsa', hash: 'sha256' }, '1.2.840.113549.1.1.12': { key: 'rsa', hash: 'sha384' }, '1.2.840.113549.1.1.13': { key: 'rsa', hash: 'sha512' }, '1.2.840.113549.1.1.14': { key: 'rsa', hash: 'sha224' }, '1.2.840.10045.2.1' : { key: 'ecdsa', hash: null }, '1.2.840.10045.4.1' : { key: 'ecdsa', hash: 'sha1' }, '1.2.840.10045.4.3.1' : { key: 'ecdsa', hash: 'sha224' }, '1.2.840.10045.4.3.2' : { key: 'ecdsa', hash: 'sha256' }, '1.2.840.10045.4.3.3' : { key: 'ecdsa', hash: 'sha384' }, '1.2.840.10045.4.3.4' : { key: 'ecdsa', hash: 'sha512' } }; /** * OID to curve name map for ECDSA. * @type {Object} */ x509.curves = { '1.3.132.0.33': 'p224', '1.2.840.10045.3.1.7': 'p256', '1.3.132.0.34': 'p384', '1.3.132.0.35': 'p521' }; /** * Retrieve cert value by OID. * @param {Object} cert * @param {String} oid * @returns {String} */ x509.getSubjectOID = function getSubjectOID(cert, oid) { var subject = cert.tbs.subject; var i, entry; for (i = 0; i < subject.length; i++) { entry = subject[i]; if (entry.type === oid) return entry.value; } }; /** * Try to retrieve CA name by checking * for a few different OIDs. * @param {Object} cert * @returns {String} */ x509.getCAName = function getCAName(cert) { // This seems to work the best in practice // for getting a human-readable and // descriptive name for the CA. // See: // http://oid-info.com/get/2.5.4 // Precedence: // (3) commonName // (11) organizationUnitName // (10) organizationName return x509.getSubjectOID(cert, '2.5.4.3') || x509.getSubjectOID(cert, '2.5.4.11') || x509.getSubjectOID(cert, '2.5.4.10') || 'Unknown'; }; /** * Test whether a cert is trusted by hashing * and looking it up in the trusted map. * @param {Object} cert * @returns {Buffer} */ x509.isTrusted = function isTrusted(cert) { var fingerprint = crypto.sha256(cert.raw); var hash = fingerprint.toString('hex'); return x509.trusted[hash] === true; }; /** * Add root certificates to the trusted map. * @param {Buffer[]} certs */ x509.setTrust = function setTrust(certs) { var i, cert, pem, hash; assert(Array.isArray(certs), 'Certs must be an array.'); for (i = 0; i < certs.length; i++) { cert = certs[i]; if (typeof cert === 'string') { pem = PEM.decode(cert); assert(pem.type === 'certificate', 'Must add certificates to trust.'); cert = pem.data; } assert(Buffer.isBuffer(cert), 'Certificates must be PEM or DER.'); cert = x509.parse(cert); hash = crypto.sha256(cert.raw); hash = hash.toString('hex'); x509.trusted[hash] = true; } }; /** * Add root certificate fingerprints to the trusted map. * @param {Hash[]} hashes */ x509.setFingerprints = function setFingerprints(hashes) { var i, hash; assert(Array.isArray(hashes), 'Certs must be an array.'); for (i = 0; i < hashes.length; i++) { hash = hashes[i]; if (typeof hash === 'string') hash = new Buffer(hash, 'hex'); assert(Buffer.isBuffer(hash), 'Fingerprint must be a buffer.'); assert(hash.length === 32, 'Fingerprint must be a sha256 hash.'); hash = hash.toString('hex'); x509.trusted[hash] = true; } }; /** * Retrieve key algorithm from cert. * @param {Object} cert * @returns {Object} */ x509.getKeyAlgorithm = function getKeyAlgorithm(cert) { var oid = cert.tbs.pubkey.alg.alg; var alg = x509.oid[oid]; if (!alg) throw new Error('Unknown key algorithm: ' + oid); return alg; }; /** * Retrieve signature algorithm from cert. * @param {Object} cert * @returns {Object} */ x509.getSigAlgorithm = function getSigAlgorithm(cert) { var oid = cert.sigAlg.alg; var alg = x509.oid[oid]; if (!alg || !alg.hash) throw new Error('Unknown signature algorithm: ' + oid); return alg; }; /** * Lookup curve based on key parameters. * @param {Buffer} params * @returns {Object} */ x509.getCurve = function getCurve(params) { var oid, curve; try { oid = ASN1.parseOID(params); } catch (e) { throw new Error('Could not parse curve OID.'); } curve = x509.curves[oid]; if (!curve) throw new Error('Unknown ECDSA curve: ' + oid); return curve; }; /** * Parse a DER formatted cert. * @param {Buffer} der * @returns {Object|null} */ x509.parse = function parse(der) { try { return ASN1.parseCert(der); } catch (e) { throw new Error('Could not parse DER certificate.'); } }; /** * Get cert public key. * @param {Object} cert * @returns {Object|null} */ x509.getPublicKey = function getPublicKey(cert) { var alg = x509.getKeyAlgorithm(cert); var key, params, curve; key = cert.tbs.pubkey.pubkey; params = cert.tbs.pubkey.alg.params; if (alg.key === 'ecdsa') { if (!params) throw new Error('No curve selected for ECDSA (cert).'); curve = x509.getCurve(params); } return { alg: alg.key, data: key, params: params, curve: curve }; }; /** * Verify cert expiration time. * @param {Object} cert * @returns {Boolean} */ x509.verifyTime = function verifyTime(cert) { var time = cert.tbs.validity; var now = util.now(); return now > time.notBefore && now < time.notAfter; }; /** * Get signature key info from cert chain. * @param {Buffer} key * @param {Buffer[]} chain * @returns {Object} */ x509.getSigningKey = function getSigningKey(key, chain) { var cert, pub, curve; assert(chain.length !== 0, 'No chain available.'); if (typeof key === 'string') { key = PEM.decode(key); if (key.alg === 'ecdsa') { if (!key.params) throw new Error('No curve selected for ECDSA (key).'); curve = x509.getCurve(key.params); } key = { alg: key.alg, data: key.data, params: key.params, curve: curve }; } else { cert = x509.parse(chain[0]); pub = x509.getPublicKey(cert); key = { alg: pub.alg, data: key, params: pub.params, curve: pub.curve }; } return key; }; /** * Sign a hash with the chain signing key. * @param {String} hash * @param {Buffer} msg * @param {Buffer} key * @param {Buffer[]} chain * @returns {Buffer} */ x509.signSubject = function signSubject(hash, msg, key, chain) { var priv = x509.getSigningKey(key, chain); return pk.sign(hash, msg, priv); }; /** * Get chain verification key. * @param {Buffer[]} chain * @returns {Object|null} */ x509.getVerifyKey = function getVerifyKey(chain) { var cert, key; if (chain.length === 0) throw new Error('No verify key available (cert chain).'); cert = x509.parse(chain[0]); key = x509.getPublicKey(cert); return key; }; /** * Verify a sighash against chain verification key. * @param {String} hash * @param {Buffer} msg * @param {Buffer} sig * @param {Buffer[]} chain * @returns {Boolean} */ x509.verifySubject = function verifySubject(hash, msg, sig, chain) { var key = x509.getVerifyKey(chain); return pk.verify(hash, msg, sig, key); }; /** * Parse certificate chain. * @param {Buffer[]} chain * @returns {Object[]} */ x509.parseChain = function parseChain(chain) { var certs = []; var i, cert; for (i = 0; i < chain.length; i++) { cert = x509.parse(chain[i]); certs.push(cert); } return certs; }; /** * Verify all expiration times in a certificate chain. * @param {Object[]} chain * @returns {Boolean} */ x509.verifyTimes = function verifyTimes(chain) { var i, cert; for (i = 0; i < chain.length; i++) { cert = chain[i]; if (!x509.verifyTime(cert)) return false; } return true; }; /** * Verify that at least one parent * cert in the chain is trusted. * @param {Object[]} chain * @returns {Boolean} */ x509.verifyTrust = function verifyTrust(chain) { var i, cert; // If trust hasn't been // setup, just return. if (x509.allowUntrusted) return true; // Make sure we trust one // of the certs in the chain. for (i = 0; i < chain.length; i++) { cert = chain[i]; // If any certificate in the chain // is trusted, assume we also trust // the parent. if (x509.isTrusted(cert)) return true; } // No trusted certs present. return false; }; /** * Verify certificate chain. * @param {Object[]} certs */ x509.verifyChain = function verifyChain(certs) { var chain = x509.parseChain(certs); var i, child, parent, alg, key, sig, msg; // Parse certificates and // check validity time. if (!x509.verifyTimes(chain)) throw new Error('Invalid certificate times.'); // Verify signatures. for (i = 1; i < chain.length; i++) { child = chain[i - 1]; parent = chain[i]; alg = x509.getSigAlgorithm(child); key = x509.getPublicKey(parent); msg = child.tbs.raw; sig = child.sig; if (!pk.verify(alg.hash, msg, sig, key)) throw new Error(alg.key + ' verification failed for chain.'); } // Make sure we trust one // of the certs in the chain. if (!x509.verifyTrust(chain)) throw new Error('Certificate chain is untrusted.'); return true; }; /* * Load trusted certs. */ x509.setFingerprints(certs);