411 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
		
		
			
		
	
	
			411 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
|  | // 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]); | ||
|  | }; |