| // 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['openssh'] = require('./formats/openssh-cert'); |
| formats['x509'] = require('./formats/x509'); |
| formats['pem'] = 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]); |
| }; |