374 lines
		
	
	
		
			9.8 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
		
		
			
		
	
	
			374 lines
		
	
	
		
			9.8 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
|  | // Copyright 2017 Joyent, Inc.
 | ||
|  | 
 | ||
|  | module.exports = Identity; | ||
|  | 
 | ||
|  | var assert = require('assert-plus'); | ||
|  | 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 asn1 = require('asn1'); | ||
|  | var Buffer = require('safer-buffer').Buffer; | ||
|  | 
 | ||
|  | /*JSSTYLED*/ | ||
|  | var DNS_NAME_RE = /^([*]|[a-z0-9][a-z0-9\-]{0,62})(?:\.([*]|[a-z0-9][a-z0-9\-]{0,62}))*$/i; | ||
|  | 
 | ||
|  | var oids = {}; | ||
|  | oids.cn = '2.5.4.3'; | ||
|  | oids.o = '2.5.4.10'; | ||
|  | oids.ou = '2.5.4.11'; | ||
|  | oids.l = '2.5.4.7'; | ||
|  | oids.s = '2.5.4.8'; | ||
|  | oids.c = '2.5.4.6'; | ||
|  | oids.sn = '2.5.4.4'; | ||
|  | oids.postalCode = '2.5.4.17'; | ||
|  | oids.serialNumber = '2.5.4.5'; | ||
|  | oids.street = '2.5.4.9'; | ||
|  | oids.x500UniqueIdentifier = '2.5.4.45'; | ||
|  | oids.role = '2.5.4.72'; | ||
|  | oids.telephoneNumber = '2.5.4.20'; | ||
|  | oids.description = '2.5.4.13'; | ||
|  | oids.dc = '0.9.2342.19200300.100.1.25'; | ||
|  | oids.uid = '0.9.2342.19200300.100.1.1'; | ||
|  | oids.mail = '0.9.2342.19200300.100.1.3'; | ||
|  | oids.title = '2.5.4.12'; | ||
|  | oids.gn = '2.5.4.42'; | ||
|  | oids.initials = '2.5.4.43'; | ||
|  | oids.pseudonym = '2.5.4.65'; | ||
|  | oids.emailAddress = '1.2.840.113549.1.9.1'; | ||
|  | 
 | ||
|  | var unoids = {}; | ||
|  | Object.keys(oids).forEach(function (k) { | ||
|  | 	unoids[oids[k]] = k; | ||
|  | }); | ||
|  | 
 | ||
|  | function Identity(opts) { | ||
|  | 	var self = this; | ||
|  | 	assert.object(opts, 'options'); | ||
|  | 	assert.arrayOfObject(opts.components, 'options.components'); | ||
|  | 	this.components = opts.components; | ||
|  | 	this.componentLookup = {}; | ||
|  | 	this.components.forEach(function (c) { | ||
|  | 		if (c.name && !c.oid) | ||
|  | 			c.oid = oids[c.name]; | ||
|  | 		if (c.oid && !c.name) | ||
|  | 			c.name = unoids[c.oid]; | ||
|  | 		if (self.componentLookup[c.name] === undefined) | ||
|  | 			self.componentLookup[c.name] = []; | ||
|  | 		self.componentLookup[c.name].push(c); | ||
|  | 	}); | ||
|  | 	if (this.componentLookup.cn && this.componentLookup.cn.length > 0) { | ||
|  | 		this.cn = this.componentLookup.cn[0].value; | ||
|  | 	} | ||
|  | 	assert.optionalString(opts.type, 'options.type'); | ||
|  | 	if (opts.type === undefined) { | ||
|  | 		if (this.components.length === 1 && | ||
|  | 		    this.componentLookup.cn && | ||
|  | 		    this.componentLookup.cn.length === 1 && | ||
|  | 		    this.componentLookup.cn[0].value.match(DNS_NAME_RE)) { | ||
|  | 			this.type = 'host'; | ||
|  | 			this.hostname = this.componentLookup.cn[0].value; | ||
|  | 
 | ||
|  | 		} else if (this.componentLookup.dc && | ||
|  | 		    this.components.length === this.componentLookup.dc.length) { | ||
|  | 			this.type = 'host'; | ||
|  | 			this.hostname = this.componentLookup.dc.map( | ||
|  | 			    function (c) { | ||
|  | 				return (c.value); | ||
|  | 			}).join('.'); | ||
|  | 
 | ||
|  | 		} else if (this.componentLookup.uid && | ||
|  | 		    this.components.length === | ||
|  | 		    this.componentLookup.uid.length) { | ||
|  | 			this.type = 'user'; | ||
|  | 			this.uid = this.componentLookup.uid[0].value; | ||
|  | 
 | ||
|  | 		} else if (this.componentLookup.cn && | ||
|  | 		    this.componentLookup.cn.length === 1 && | ||
|  | 		    this.componentLookup.cn[0].value.match(DNS_NAME_RE)) { | ||
|  | 			this.type = 'host'; | ||
|  | 			this.hostname = this.componentLookup.cn[0].value; | ||
|  | 
 | ||
|  | 		} else if (this.componentLookup.uid && | ||
|  | 		    this.componentLookup.uid.length === 1) { | ||
|  | 			this.type = 'user'; | ||
|  | 			this.uid = this.componentLookup.uid[0].value; | ||
|  | 
 | ||
|  | 		} else if (this.componentLookup.mail && | ||
|  | 		    this.componentLookup.mail.length === 1) { | ||
|  | 			this.type = 'email'; | ||
|  | 			this.email = this.componentLookup.mail[0].value; | ||
|  | 
 | ||
|  | 		} else if (this.componentLookup.cn && | ||
|  | 		    this.componentLookup.cn.length === 1) { | ||
|  | 			this.type = 'user'; | ||
|  | 			this.uid = this.componentLookup.cn[0].value; | ||
|  | 
 | ||
|  | 		} else { | ||
|  | 			this.type = 'unknown'; | ||
|  | 		} | ||
|  | 	} else { | ||
|  | 		this.type = opts.type; | ||
|  | 		if (this.type === 'host') | ||
|  | 			this.hostname = opts.hostname; | ||
|  | 		else if (this.type === 'user') | ||
|  | 			this.uid = opts.uid; | ||
|  | 		else if (this.type === 'email') | ||
|  | 			this.email = opts.email; | ||
|  | 		else | ||
|  | 			throw (new Error('Unknown type ' + this.type)); | ||
|  | 	} | ||
|  | } | ||
|  | 
 | ||
|  | Identity.prototype.toString = function () { | ||
|  | 	return (this.components.map(function (c) { | ||
|  | 		var n = c.name.toUpperCase(); | ||
|  | 		/*JSSTYLED*/ | ||
|  | 		n = n.replace(/=/g, '\\='); | ||
|  | 		var v = c.value; | ||
|  | 		/*JSSTYLED*/ | ||
|  | 		v = v.replace(/,/g, '\\,'); | ||
|  | 		return (n + '=' + v); | ||
|  | 	}).join(', ')); | ||
|  | }; | ||
|  | 
 | ||
|  | Identity.prototype.get = function (name, asArray) { | ||
|  | 	assert.string(name, 'name'); | ||
|  | 	var arr = this.componentLookup[name]; | ||
|  | 	if (arr === undefined || arr.length === 0) | ||
|  | 		return (undefined); | ||
|  | 	if (!asArray && arr.length > 1) | ||
|  | 		throw (new Error('Multiple values for attribute ' + name)); | ||
|  | 	if (!asArray) | ||
|  | 		return (arr[0].value); | ||
|  | 	return (arr.map(function (c) { | ||
|  | 		return (c.value); | ||
|  | 	})); | ||
|  | }; | ||
|  | 
 | ||
|  | Identity.prototype.toArray = function (idx) { | ||
|  | 	return (this.components.map(function (c) { | ||
|  | 		return ({ | ||
|  | 			name: c.name, | ||
|  | 			value: c.value | ||
|  | 		}); | ||
|  | 	})); | ||
|  | }; | ||
|  | 
 | ||
|  | /* | ||
|  |  * These are from X.680 -- PrintableString allowed chars are in section 37.4 | ||
|  |  * table 8. Spec for IA5Strings is "1,6 + SPACE + DEL" where 1 refers to | ||
|  |  * ISO IR #001 (standard ASCII control characters) and 6 refers to ISO IR #006 | ||
|  |  * (the basic ASCII character set). | ||
|  |  */ | ||
|  | /* JSSTYLED */ | ||
|  | var NOT_PRINTABLE = /[^a-zA-Z0-9 '(),+.\/:=?-]/; | ||
|  | /* JSSTYLED */ | ||
|  | var NOT_IA5 = /[^\x00-\x7f]/; | ||
|  | 
 | ||
|  | Identity.prototype.toAsn1 = function (der, tag) { | ||
|  | 	der.startSequence(tag); | ||
|  | 	this.components.forEach(function (c) { | ||
|  | 		der.startSequence(asn1.Ber.Constructor | asn1.Ber.Set); | ||
|  | 		der.startSequence(); | ||
|  | 		der.writeOID(c.oid); | ||
|  | 		/* | ||
|  | 		 * If we fit in a PrintableString, use that. Otherwise use an | ||
|  | 		 * IA5String or UTF8String. | ||
|  | 		 * | ||
|  | 		 * If this identity was parsed from a DN, use the ASN.1 types | ||
|  | 		 * from the original representation (otherwise this might not | ||
|  | 		 * be a full match for the original in some validators). | ||
|  | 		 */ | ||
|  | 		if (c.asn1type === asn1.Ber.Utf8String || | ||
|  | 		    c.value.match(NOT_IA5)) { | ||
|  | 			var v = Buffer.from(c.value, 'utf8'); | ||
|  | 			der.writeBuffer(v, asn1.Ber.Utf8String); | ||
|  | 
 | ||
|  | 		} else if (c.asn1type === asn1.Ber.IA5String || | ||
|  | 		    c.value.match(NOT_PRINTABLE)) { | ||
|  | 			der.writeString(c.value, asn1.Ber.IA5String); | ||
|  | 
 | ||
|  | 		} else { | ||
|  | 			var type = asn1.Ber.PrintableString; | ||
|  | 			if (c.asn1type !== undefined) | ||
|  | 				type = c.asn1type; | ||
|  | 			der.writeString(c.value, type); | ||
|  | 		} | ||
|  | 		der.endSequence(); | ||
|  | 		der.endSequence(); | ||
|  | 	}); | ||
|  | 	der.endSequence(); | ||
|  | }; | ||
|  | 
 | ||
|  | function globMatch(a, b) { | ||
|  | 	if (a === '**' || b === '**') | ||
|  | 		return (true); | ||
|  | 	var aParts = a.split('.'); | ||
|  | 	var bParts = b.split('.'); | ||
|  | 	if (aParts.length !== bParts.length) | ||
|  | 		return (false); | ||
|  | 	for (var i = 0; i < aParts.length; ++i) { | ||
|  | 		if (aParts[i] === '*' || bParts[i] === '*') | ||
|  | 			continue; | ||
|  | 		if (aParts[i] !== bParts[i]) | ||
|  | 			return (false); | ||
|  | 	} | ||
|  | 	return (true); | ||
|  | } | ||
|  | 
 | ||
|  | Identity.prototype.equals = function (other) { | ||
|  | 	if (!Identity.isIdentity(other, [1, 0])) | ||
|  | 		return (false); | ||
|  | 	if (other.components.length !== this.components.length) | ||
|  | 		return (false); | ||
|  | 	for (var i = 0; i < this.components.length; ++i) { | ||
|  | 		if (this.components[i].oid !== other.components[i].oid) | ||
|  | 			return (false); | ||
|  | 		if (!globMatch(this.components[i].value, | ||
|  | 		    other.components[i].value)) { | ||
|  | 			return (false); | ||
|  | 		} | ||
|  | 	} | ||
|  | 	return (true); | ||
|  | }; | ||
|  | 
 | ||
|  | Identity.forHost = function (hostname) { | ||
|  | 	assert.string(hostname, 'hostname'); | ||
|  | 	return (new Identity({ | ||
|  | 		type: 'host', | ||
|  | 		hostname: hostname, | ||
|  | 		components: [ { name: 'cn', value: hostname } ] | ||
|  | 	})); | ||
|  | }; | ||
|  | 
 | ||
|  | Identity.forUser = function (uid) { | ||
|  | 	assert.string(uid, 'uid'); | ||
|  | 	return (new Identity({ | ||
|  | 		type: 'user', | ||
|  | 		uid: uid, | ||
|  | 		components: [ { name: 'uid', value: uid } ] | ||
|  | 	})); | ||
|  | }; | ||
|  | 
 | ||
|  | Identity.forEmail = function (email) { | ||
|  | 	assert.string(email, 'email'); | ||
|  | 	return (new Identity({ | ||
|  | 		type: 'email', | ||
|  | 		email: email, | ||
|  | 		components: [ { name: 'mail', value: email } ] | ||
|  | 	})); | ||
|  | }; | ||
|  | 
 | ||
|  | Identity.parseDN = function (dn) { | ||
|  | 	assert.string(dn, 'dn'); | ||
|  | 	var parts = ['']; | ||
|  | 	var idx = 0; | ||
|  | 	var rem = dn; | ||
|  | 	while (rem.length > 0) { | ||
|  | 		var m; | ||
|  | 		/*JSSTYLED*/ | ||
|  | 		if ((m = /^,/.exec(rem)) !== null) { | ||
|  | 			parts[++idx] = ''; | ||
|  | 			rem = rem.slice(m[0].length); | ||
|  | 		/*JSSTYLED*/ | ||
|  | 		} else if ((m = /^\\,/.exec(rem)) !== null) { | ||
|  | 			parts[idx] += ','; | ||
|  | 			rem = rem.slice(m[0].length); | ||
|  | 		/*JSSTYLED*/ | ||
|  | 		} else if ((m = /^\\./.exec(rem)) !== null) { | ||
|  | 			parts[idx] += m[0]; | ||
|  | 			rem = rem.slice(m[0].length); | ||
|  | 		/*JSSTYLED*/ | ||
|  | 		} else if ((m = /^[^\\,]+/.exec(rem)) !== null) { | ||
|  | 			parts[idx] += m[0]; | ||
|  | 			rem = rem.slice(m[0].length); | ||
|  | 		} else { | ||
|  | 			throw (new Error('Failed to parse DN')); | ||
|  | 		} | ||
|  | 	} | ||
|  | 	var cmps = parts.map(function (c) { | ||
|  | 		c = c.trim(); | ||
|  | 		var eqPos = c.indexOf('='); | ||
|  | 		while (eqPos > 0 && c.charAt(eqPos - 1) === '\\') | ||
|  | 			eqPos = c.indexOf('=', eqPos + 1); | ||
|  | 		if (eqPos === -1) { | ||
|  | 			throw (new Error('Failed to parse DN')); | ||
|  | 		} | ||
|  | 		/*JSSTYLED*/ | ||
|  | 		var name = c.slice(0, eqPos).toLowerCase().replace(/\\=/g, '='); | ||
|  | 		var value = c.slice(eqPos + 1); | ||
|  | 		return ({ name: name, value: value }); | ||
|  | 	}); | ||
|  | 	return (new Identity({ components: cmps })); | ||
|  | }; | ||
|  | 
 | ||
|  | Identity.fromArray = function (components) { | ||
|  | 	assert.arrayOfObject(components, 'components'); | ||
|  | 	components.forEach(function (cmp) { | ||
|  | 		assert.object(cmp, 'component'); | ||
|  | 		assert.string(cmp.name, 'component.name'); | ||
|  | 		if (!Buffer.isBuffer(cmp.value) && | ||
|  | 		    !(typeof (cmp.value) === 'string')) { | ||
|  | 			throw (new Error('Invalid component value')); | ||
|  | 		} | ||
|  | 	}); | ||
|  | 	return (new Identity({ components: components })); | ||
|  | }; | ||
|  | 
 | ||
|  | Identity.parseAsn1 = function (der, top) { | ||
|  | 	var components = []; | ||
|  | 	der.readSequence(top); | ||
|  | 	var end = der.offset + der.length; | ||
|  | 	while (der.offset < end) { | ||
|  | 		der.readSequence(asn1.Ber.Constructor | asn1.Ber.Set); | ||
|  | 		var after = der.offset + der.length; | ||
|  | 		der.readSequence(); | ||
|  | 		var oid = der.readOID(); | ||
|  | 		var type = der.peek(); | ||
|  | 		var value; | ||
|  | 		switch (type) { | ||
|  | 		case asn1.Ber.PrintableString: | ||
|  | 		case asn1.Ber.IA5String: | ||
|  | 		case asn1.Ber.OctetString: | ||
|  | 		case asn1.Ber.T61String: | ||
|  | 			value = der.readString(type); | ||
|  | 			break; | ||
|  | 		case asn1.Ber.Utf8String: | ||
|  | 			value = der.readString(type, true); | ||
|  | 			value = value.toString('utf8'); | ||
|  | 			break; | ||
|  | 		case asn1.Ber.CharacterString: | ||
|  | 		case asn1.Ber.BMPString: | ||
|  | 			value = der.readString(type, true); | ||
|  | 			value = value.toString('utf16le'); | ||
|  | 			break; | ||
|  | 		default: | ||
|  | 			throw (new Error('Unknown asn1 type ' + type)); | ||
|  | 		} | ||
|  | 		components.push({ oid: oid, asn1type: type, value: value }); | ||
|  | 		der._offset = after; | ||
|  | 	} | ||
|  | 	der._offset = end; | ||
|  | 	return (new Identity({ | ||
|  | 		components: components | ||
|  | 	})); | ||
|  | }; | ||
|  | 
 | ||
|  | Identity.isIdentity = function (obj, ver) { | ||
|  | 	return (utils.isCompatible(obj, Identity, ver)); | ||
|  | }; | ||
|  | 
 | ||
|  | /* | ||
|  |  * API versions for Identity: | ||
|  |  * [1,0] -- initial ver | ||
|  |  */ | ||
|  | Identity.prototype._sshpkApiVersion = [1, 0]; | ||
|  | 
 | ||
|  | Identity._oldVersionDetect = function (obj) { | ||
|  | 	return ([1, 0]); | ||
|  | }; |