/*!
 * 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);
