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