identity.js 7.6 KB

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