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