// Copyright 2016 Joyent, Inc.

module.exports = Certificate;

var assert = require('assert-plus'); var Buffer = require('safer-buffer').Buffer; var algs = require('./algs'); var crypto = require('crypto'); var Fingerprint = require('./fingerprint'); var Signature = require('./signature'); var errs = require('./errors'); var util = require('util'); var utils = require('./utils'); var Key = require('./key'); var PrivateKey = require('./private-key'); var Identity = require('./identity');

var formats = {}; formats = require('./formats/openssh-cert'); formats = require('./formats/x509'); formats = require('./formats/x509-pem');

var CertificateParseError = errs.CertificateParseError; var InvalidAlgorithmError = errs.InvalidAlgorithmError;

function Certificate(opts) {

assert.object(opts, 'options');
assert.arrayOfObject(opts.subjects, 'options.subjects');
utils.assertCompatible(opts.subjects[0], Identity, [1, 0],
    'options.subjects');
utils.assertCompatible(opts.subjectKey, Key, [1, 0],
    'options.subjectKey');
utils.assertCompatible(opts.issuer, Identity, [1, 0], 'options.issuer');
if (opts.issuerKey !== undefined) {
        utils.assertCompatible(opts.issuerKey, Key, [1, 0],
            'options.issuerKey');
}
assert.object(opts.signatures, 'options.signatures');
assert.buffer(opts.serial, 'options.serial');
assert.date(opts.validFrom, 'options.validFrom');
assert.date(opts.validUntil, 'optons.validUntil');

assert.optionalArrayOfString(opts.purposes, 'options.purposes');

this._hashCache = {};

this.subjects = opts.subjects;
this.issuer = opts.issuer;
this.subjectKey = opts.subjectKey;
this.issuerKey = opts.issuerKey;
this.signatures = opts.signatures;
this.serial = opts.serial;
this.validFrom = opts.validFrom;
this.validUntil = opts.validUntil;
this.purposes = opts.purposes;

}

Certificate.formats = formats;

Certificate.prototype.toBuffer = function (format, options) {

if (format === undefined)
        format = 'x509';
assert.string(format, 'format');
assert.object(formats[format], 'formats[format]');
assert.optionalObject(options, 'options');

return (formats[format].write(this, options));

};

Certificate.prototype.toString = function (format, options) {

if (format === undefined)
        format = 'pem';
return (this.toBuffer(format, options).toString());

};

Certificate.prototype.fingerprint = function (algo) {

if (algo === undefined)
        algo = 'sha256';
assert.string(algo, 'algorithm');
var opts = {
        type: 'certificate',
        hash: this.hash(algo),
        algorithm: algo
};
return (new Fingerprint(opts));

};

Certificate.prototype.hash = function (algo) {

assert.string(algo, 'algorithm');
algo = algo.toLowerCase();
if (algs.hashAlgs[algo] === undefined)
        throw (new InvalidAlgorithmError(algo));

if (this._hashCache[algo])
        return (this._hashCache[algo]);

var hash = crypto.createHash(algo).
    update(this.toBuffer('x509')).digest();
this._hashCache[algo] = hash;
return (hash);

};

Certificate.prototype.isExpired = function (when) {

if (when === undefined)
        when = new Date();
return (!((when.getTime() >= this.validFrom.getTime()) &&
        (when.getTime() < this.validUntil.getTime())));

};

Certificate.prototype.isSignedBy = function (issuerCert) {

utils.assertCompatible(issuerCert, Certificate, [1, 0], 'issuer');

if (!this.issuer.equals(issuerCert.subjects[0]))
        return (false);
if (this.issuer.purposes && this.issuer.purposes.length > 0 &&
    this.issuer.purposes.indexOf('ca') === -1) {
        return (false);
}

return (this.isSignedByKey(issuerCert.subjectKey));

};

Certificate.prototype.getExtension = function (keyOrOid) {

assert.string(keyOrOid, 'keyOrOid');
var ext = this.getExtensions().filter(function (maybeExt) {
        if (maybeExt.format === 'x509')
                return (maybeExt.oid === keyOrOid);
        if (maybeExt.format === 'openssh')
                return (maybeExt.name === keyOrOid);
        return (false);
})[0];
return (ext);

};

Certificate.prototype.getExtensions = function () {

var exts = [];
var x509 = this.signatures.x509;
if (x509 && x509.extras && x509.extras.exts) {
        x509.extras.exts.forEach(function (ext) {
                ext.format = 'x509';
                exts.push(ext);
        });
}
var openssh = this.signatures.openssh;
if (openssh && openssh.exts) {
        openssh.exts.forEach(function (ext) {
                ext.format = 'openssh';
                exts.push(ext);
        });
}
return (exts);

};

Certificate.prototype.isSignedByKey = function (issuerKey) {

utils.assertCompatible(issuerKey, Key, [1, 2], 'issuerKey');

if (this.issuerKey !== undefined) {
        return (this.issuerKey.
            fingerprint('sha512').matches(issuerKey));
}

var fmt = Object.keys(this.signatures)[0];
var valid = formats[fmt].verify(this, issuerKey);
if (valid)
        this.issuerKey = issuerKey;
return (valid);

};

Certificate.prototype.signWith = function (key) {

utils.assertCompatible(key, PrivateKey, [1, 2], 'key');
var fmts = Object.keys(formats);
var didOne = false;
for (var i = 0; i < fmts.length; ++i) {
        if (fmts[i] !== 'pem') {
                var ret = formats[fmts[i]].sign(this, key);
                if (ret === true)
                        didOne = true;
        }
}
if (!didOne) {
        throw (new Error('Failed to sign the certificate for any ' +
            'available certificate formats'));
}

};

Certificate.createSelfSigned = function (subjectOrSubjects, key, options) {

var subjects;
if (Array.isArray(subjectOrSubjects))
        subjects = subjectOrSubjects;
else
        subjects = [subjectOrSubjects];

assert.arrayOfObject(subjects);
subjects.forEach(function (subject) {
        utils.assertCompatible(subject, Identity, [1, 0], 'subject');
});

utils.assertCompatible(key, PrivateKey, [1, 2], 'private key');

assert.optionalObject(options, 'options');
if (options === undefined)
        options = {};
assert.optionalObject(options.validFrom, 'options.validFrom');
assert.optionalObject(options.validUntil, 'options.validUntil');
var validFrom = options.validFrom;
var validUntil = options.validUntil;
if (validFrom === undefined)
        validFrom = new Date();
if (validUntil === undefined) {
        assert.optionalNumber(options.lifetime, 'options.lifetime');
        var lifetime = options.lifetime;
        if (lifetime === undefined)
                lifetime = 10*365*24*3600;
        validUntil = new Date();
        validUntil.setTime(validUntil.getTime() + lifetime*1000);
}
assert.optionalBuffer(options.serial, 'options.serial');
var serial = options.serial;
if (serial === undefined)
        serial = Buffer.from('0000000000000001', 'hex');

var purposes = options.purposes;
if (purposes === undefined)
        purposes = [];

if (purposes.indexOf('signature') === -1)
        purposes.push('signature');

/* Self-signed certs are always CAs. */
if (purposes.indexOf('ca') === -1)
        purposes.push('ca');
if (purposes.indexOf('crl') === -1)
        purposes.push('crl');

/*
 * If we weren't explicitly given any other purposes, do the sensible
 * thing and add some basic ones depending on the subject type.
 */
if (purposes.length <= 3) {
        var hostSubjects = subjects.filter(function (subject) {
                return (subject.type === 'host');
        });
        var userSubjects = subjects.filter(function (subject) {
                return (subject.type === 'user');
        });
        if (hostSubjects.length > 0) {
                if (purposes.indexOf('serverAuth') === -1)
                        purposes.push('serverAuth');
        }
        if (userSubjects.length > 0) {
                if (purposes.indexOf('clientAuth') === -1)
                        purposes.push('clientAuth');
        }
        if (userSubjects.length > 0 || hostSubjects.length > 0) {
                if (purposes.indexOf('keyAgreement') === -1)
                        purposes.push('keyAgreement');
                if (key.type === 'rsa' &&
                    purposes.indexOf('encryption') === -1)
                        purposes.push('encryption');
        }
}

var cert = new Certificate({
        subjects: subjects,
        issuer: subjects[0],
        subjectKey: key.toPublic(),
        issuerKey: key.toPublic(),
        signatures: {},
        serial: serial,
        validFrom: validFrom,
        validUntil: validUntil,
        purposes: purposes
});
cert.signWith(key);

return (cert);

};

Certificate.create =

function (subjectOrSubjects, key, issuer, issuerKey, options) {
    var subjects;
    if (Array.isArray(subjectOrSubjects))
            subjects = subjectOrSubjects;
    else
            subjects = [subjectOrSubjects];

    assert.arrayOfObject(subjects);
    subjects.forEach(function (subject) {
            utils.assertCompatible(subject, Identity, [1, 0], 'subject');
    });

    utils.assertCompatible(key, Key, [1, 0], 'key');
    if (PrivateKey.isPrivateKey(key))
            key = key.toPublic();
    utils.assertCompatible(issuer, Identity, [1, 0], 'issuer');
    utils.assertCompatible(issuerKey, PrivateKey, [1, 2], 'issuer key');

    assert.optionalObject(options, 'options');
    if (options === undefined)
            options = {};
    assert.optionalObject(options.validFrom, 'options.validFrom');
    assert.optionalObject(options.validUntil, 'options.validUntil');
    var validFrom = options.validFrom;
    var validUntil = options.validUntil;
    if (validFrom === undefined)
            validFrom = new Date();
    if (validUntil === undefined) {
            assert.optionalNumber(options.lifetime, 'options.lifetime');
            var lifetime = options.lifetime;
            if (lifetime === undefined)
                    lifetime = 10*365*24*3600;
            validUntil = new Date();
            validUntil.setTime(validUntil.getTime() + lifetime*1000);
    }
    assert.optionalBuffer(options.serial, 'options.serial');
    var serial = options.serial;
    if (serial === undefined)
            serial = Buffer.from('0000000000000001', 'hex');

    var purposes = options.purposes;
    if (purposes === undefined)
            purposes = [];

    if (purposes.indexOf('signature') === -1)
            purposes.push('signature');

    if (options.ca === true) {
            if (purposes.indexOf('ca') === -1)
                    purposes.push('ca');
            if (purposes.indexOf('crl') === -1)
                    purposes.push('crl');
    }

    var hostSubjects = subjects.filter(function (subject) {
            return (subject.type === 'host');
    });
    var userSubjects = subjects.filter(function (subject) {
            return (subject.type === 'user');
    });
    if (hostSubjects.length > 0) {
            if (purposes.indexOf('serverAuth') === -1)
                    purposes.push('serverAuth');
    }
    if (userSubjects.length > 0) {
            if (purposes.indexOf('clientAuth') === -1)
                    purposes.push('clientAuth');
    }
    if (userSubjects.length > 0 || hostSubjects.length > 0) {
            if (purposes.indexOf('keyAgreement') === -1)
                    purposes.push('keyAgreement');
            if (key.type === 'rsa' &&
                purposes.indexOf('encryption') === -1)
                    purposes.push('encryption');
    }

    var cert = new Certificate({
            subjects: subjects,
            issuer: issuer,
            subjectKey: key,
            issuerKey: issuerKey.toPublic(),
            signatures: {},
            serial: serial,
            validFrom: validFrom,
            validUntil: validUntil,
            purposes: purposes
    });
    cert.signWith(issuerKey);

    return (cert);

};

Certificate.parse = function (data, format, options) {

if (typeof (data) !== 'string')
        assert.buffer(data, 'data');
if (format === undefined)
        format = 'auto';
assert.string(format, 'format');
if (typeof (options) === 'string')
        options = { filename: options };
assert.optionalObject(options, 'options');
if (options === undefined)
        options = {};
assert.optionalString(options.filename, 'options.filename');
if (options.filename === undefined)
        options.filename = '(unnamed)';

assert.object(formats[format], 'formats[format]');

try {
        var k = formats[format].read(data, options);
        return (k);
} catch (e) {
        throw (new CertificateParseError(options.filename, format, e));
}

};

Certificate.isCertificate = function (obj, ver) {

return (utils.isCompatible(obj, Certificate, ver));

};

/*

* API versions for Certificate:
* [1,0] -- initial ver
* [1,1] -- openssh format now unpacks extensions
*/

Certificate.prototype._sshpkApiVersion = [1, 1];

Certificate._oldVersionDetect = function (obj) {

return ([1, 0]);

};