identity.js 7.6 KB

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