You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
410 lines
11 KiB
410 lines
11 KiB
// 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]); |
|
};
|
|
|