You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

373 lines
9.8 KiB

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