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