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]);
 | |
| };
 |