mirror of
				https://github.com/1disk/edp445.git
				synced 2024-08-14 22:47:02 +00:00 
			
		
		
		
	
		
			
				
	
	
		
			373 lines
		
	
	
	
		
			9.8 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			373 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]);
 | |
| };
 |