• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// Copyright 2017 Joyent, Inc.
2
3module.exports = Identity;
4
5var assert = require('assert-plus');
6var algs = require('./algs');
7var crypto = require('crypto');
8var Fingerprint = require('./fingerprint');
9var Signature = require('./signature');
10var errs = require('./errors');
11var util = require('util');
12var utils = require('./utils');
13var asn1 = require('asn1');
14var Buffer = require('safer-buffer').Buffer;
15
16/*JSSTYLED*/
17var DNS_NAME_RE = /^([*]|[a-z0-9][a-z0-9\-]{0,62})(?:\.([*]|[a-z0-9][a-z0-9\-]{0,62}))*$/i;
18
19var oids = {};
20oids.cn = '2.5.4.3';
21oids.o = '2.5.4.10';
22oids.ou = '2.5.4.11';
23oids.l = '2.5.4.7';
24oids.s = '2.5.4.8';
25oids.c = '2.5.4.6';
26oids.sn = '2.5.4.4';
27oids.dc = '0.9.2342.19200300.100.1.25';
28oids.uid = '0.9.2342.19200300.100.1.1';
29oids.mail = '0.9.2342.19200300.100.1.3';
30
31var unoids = {};
32Object.keys(oids).forEach(function (k) {
33	unoids[oids[k]] = k;
34});
35
36function Identity(opts) {
37	var self = this;
38	assert.object(opts, 'options');
39	assert.arrayOfObject(opts.components, 'options.components');
40	this.components = opts.components;
41	this.componentLookup = {};
42	this.components.forEach(function (c) {
43		if (c.name && !c.oid)
44			c.oid = oids[c.name];
45		if (c.oid && !c.name)
46			c.name = unoids[c.oid];
47		if (self.componentLookup[c.name] === undefined)
48			self.componentLookup[c.name] = [];
49		self.componentLookup[c.name].push(c);
50	});
51	if (this.componentLookup.cn && this.componentLookup.cn.length > 0) {
52		this.cn = this.componentLookup.cn[0].value;
53	}
54	assert.optionalString(opts.type, 'options.type');
55	if (opts.type === undefined) {
56		if (this.components.length === 1 &&
57		    this.componentLookup.cn &&
58		    this.componentLookup.cn.length === 1 &&
59		    this.componentLookup.cn[0].value.match(DNS_NAME_RE)) {
60			this.type = 'host';
61			this.hostname = this.componentLookup.cn[0].value;
62
63		} else if (this.componentLookup.dc &&
64		    this.components.length === this.componentLookup.dc.length) {
65			this.type = 'host';
66			this.hostname = this.componentLookup.dc.map(
67			    function (c) {
68				return (c.value);
69			}).join('.');
70
71		} else if (this.componentLookup.uid &&
72		    this.components.length ===
73		    this.componentLookup.uid.length) {
74			this.type = 'user';
75			this.uid = this.componentLookup.uid[0].value;
76
77		} else if (this.componentLookup.cn &&
78		    this.componentLookup.cn.length === 1 &&
79		    this.componentLookup.cn[0].value.match(DNS_NAME_RE)) {
80			this.type = 'host';
81			this.hostname = this.componentLookup.cn[0].value;
82
83		} else if (this.componentLookup.uid &&
84		    this.componentLookup.uid.length === 1) {
85			this.type = 'user';
86			this.uid = this.componentLookup.uid[0].value;
87
88		} else if (this.componentLookup.mail &&
89		    this.componentLookup.mail.length === 1) {
90			this.type = 'email';
91			this.email = this.componentLookup.mail[0].value;
92
93		} else if (this.componentLookup.cn &&
94		    this.componentLookup.cn.length === 1) {
95			this.type = 'user';
96			this.uid = this.componentLookup.cn[0].value;
97
98		} else {
99			this.type = 'unknown';
100		}
101	} else {
102		this.type = opts.type;
103		if (this.type === 'host')
104			this.hostname = opts.hostname;
105		else if (this.type === 'user')
106			this.uid = opts.uid;
107		else if (this.type === 'email')
108			this.email = opts.email;
109		else
110			throw (new Error('Unknown type ' + this.type));
111	}
112}
113
114Identity.prototype.toString = function () {
115	return (this.components.map(function (c) {
116		return (c.name.toUpperCase() + '=' + c.value);
117	}).join(', '));
118};
119
120/*
121 * These are from X.680 -- PrintableString allowed chars are in section 37.4
122 * table 8. Spec for IA5Strings is "1,6 + SPACE + DEL" where 1 refers to
123 * ISO IR #001 (standard ASCII control characters) and 6 refers to ISO IR #006
124 * (the basic ASCII character set).
125 */
126/* JSSTYLED */
127var NOT_PRINTABLE = /[^a-zA-Z0-9 '(),+.\/:=?-]/;
128/* JSSTYLED */
129var NOT_IA5 = /[^\x00-\x7f]/;
130
131Identity.prototype.toAsn1 = function (der, tag) {
132	der.startSequence(tag);
133	this.components.forEach(function (c) {
134		der.startSequence(asn1.Ber.Constructor | asn1.Ber.Set);
135		der.startSequence();
136		der.writeOID(c.oid);
137		/*
138		 * If we fit in a PrintableString, use that. Otherwise use an
139		 * IA5String or UTF8String.
140		 *
141		 * If this identity was parsed from a DN, use the ASN.1 types
142		 * from the original representation (otherwise this might not
143		 * be a full match for the original in some validators).
144		 */
145		if (c.asn1type === asn1.Ber.Utf8String ||
146		    c.value.match(NOT_IA5)) {
147			var v = Buffer.from(c.value, 'utf8');
148			der.writeBuffer(v, asn1.Ber.Utf8String);
149
150		} else if (c.asn1type === asn1.Ber.IA5String ||
151		    c.value.match(NOT_PRINTABLE)) {
152			der.writeString(c.value, asn1.Ber.IA5String);
153
154		} else {
155			var type = asn1.Ber.PrintableString;
156			if (c.asn1type !== undefined)
157				type = c.asn1type;
158			der.writeString(c.value, type);
159		}
160		der.endSequence();
161		der.endSequence();
162	});
163	der.endSequence();
164};
165
166function globMatch(a, b) {
167	if (a === '**' || b === '**')
168		return (true);
169	var aParts = a.split('.');
170	var bParts = b.split('.');
171	if (aParts.length !== bParts.length)
172		return (false);
173	for (var i = 0; i < aParts.length; ++i) {
174		if (aParts[i] === '*' || bParts[i] === '*')
175			continue;
176		if (aParts[i] !== bParts[i])
177			return (false);
178	}
179	return (true);
180}
181
182Identity.prototype.equals = function (other) {
183	if (!Identity.isIdentity(other, [1, 0]))
184		return (false);
185	if (other.components.length !== this.components.length)
186		return (false);
187	for (var i = 0; i < this.components.length; ++i) {
188		if (this.components[i].oid !== other.components[i].oid)
189			return (false);
190		if (!globMatch(this.components[i].value,
191		    other.components[i].value)) {
192			return (false);
193		}
194	}
195	return (true);
196};
197
198Identity.forHost = function (hostname) {
199	assert.string(hostname, 'hostname');
200	return (new Identity({
201		type: 'host',
202		hostname: hostname,
203		components: [ { name: 'cn', value: hostname } ]
204	}));
205};
206
207Identity.forUser = function (uid) {
208	assert.string(uid, 'uid');
209	return (new Identity({
210		type: 'user',
211		uid: uid,
212		components: [ { name: 'uid', value: uid } ]
213	}));
214};
215
216Identity.forEmail = function (email) {
217	assert.string(email, 'email');
218	return (new Identity({
219		type: 'email',
220		email: email,
221		components: [ { name: 'mail', value: email } ]
222	}));
223};
224
225Identity.parseDN = function (dn) {
226	assert.string(dn, 'dn');
227	var parts = dn.split(',');
228	var cmps = parts.map(function (c) {
229		c = c.trim();
230		var eqPos = c.indexOf('=');
231		var name = c.slice(0, eqPos).toLowerCase();
232		var value = c.slice(eqPos + 1);
233		return ({ name: name, value: value });
234	});
235	return (new Identity({ components: cmps }));
236};
237
238Identity.parseAsn1 = function (der, top) {
239	var components = [];
240	der.readSequence(top);
241	var end = der.offset + der.length;
242	while (der.offset < end) {
243		der.readSequence(asn1.Ber.Constructor | asn1.Ber.Set);
244		var after = der.offset + der.length;
245		der.readSequence();
246		var oid = der.readOID();
247		var type = der.peek();
248		var value;
249		switch (type) {
250		case asn1.Ber.PrintableString:
251		case asn1.Ber.IA5String:
252		case asn1.Ber.OctetString:
253		case asn1.Ber.T61String:
254			value = der.readString(type);
255			break;
256		case asn1.Ber.Utf8String:
257			value = der.readString(type, true);
258			value = value.toString('utf8');
259			break;
260		case asn1.Ber.CharacterString:
261		case asn1.Ber.BMPString:
262			value = der.readString(type, true);
263			value = value.toString('utf16le');
264			break;
265		default:
266			throw (new Error('Unknown asn1 type ' + type));
267		}
268		components.push({ oid: oid, asn1type: type, value: value });
269		der._offset = after;
270	}
271	der._offset = end;
272	return (new Identity({
273		components: components
274	}));
275};
276
277Identity.isIdentity = function (obj, ver) {
278	return (utils.isCompatible(obj, Identity, ver));
279};
280
281/*
282 * API versions for Identity:
283 * [1,0] -- initial ver
284 */
285Identity.prototype._sshpkApiVersion = [1, 0];
286
287Identity._oldVersionDetect = function (obj) {
288	return ([1, 0]);
289};
290