123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373 |
- // Copyright 2017 Joyent, Inc.
-
- module.exports = Identity;
-
- var assert = require('assert-plus');
- var algs = require('./algs');
- var crypto = require('crypto');
- var Fingerprint = require('./fingerprint');
- var Signature = require('./signature');
- var errs = require('./errors');
- var util = require('util');
- var utils = require('./utils');
- var asn1 = require('asn1');
- var Buffer = require('safer-buffer').Buffer;
-
- /*JSSTYLED*/
- var DNS_NAME_RE = /^([*]|[a-z0-9][a-z0-9\-]{0,62})(?:\.([*]|[a-z0-9][a-z0-9\-]{0,62}))*$/i;
-
- var oids = {};
- oids.cn = '2.5.4.3';
- oids.o = '2.5.4.10';
- oids.ou = '2.5.4.11';
- oids.l = '2.5.4.7';
- oids.s = '2.5.4.8';
- oids.c = '2.5.4.6';
- oids.sn = '2.5.4.4';
- oids.postalCode = '2.5.4.17';
- oids.serialNumber = '2.5.4.5';
- oids.street = '2.5.4.9';
- oids.x500UniqueIdentifier = '2.5.4.45';
- oids.role = '2.5.4.72';
- oids.telephoneNumber = '2.5.4.20';
- oids.description = '2.5.4.13';
- oids.dc = '0.9.2342.19200300.100.1.25';
- oids.uid = '0.9.2342.19200300.100.1.1';
- oids.mail = '0.9.2342.19200300.100.1.3';
- oids.title = '2.5.4.12';
- oids.gn = '2.5.4.42';
- oids.initials = '2.5.4.43';
- oids.pseudonym = '2.5.4.65';
- oids.emailAddress = '1.2.840.113549.1.9.1';
-
- var unoids = {};
- Object.keys(oids).forEach(function (k) {
- unoids[oids[k]] = k;
- });
-
- function Identity(opts) {
- var self = this;
- assert.object(opts, 'options');
- assert.arrayOfObject(opts.components, 'options.components');
- this.components = opts.components;
- this.componentLookup = {};
- this.components.forEach(function (c) {
- if (c.name && !c.oid)
- c.oid = oids[c.name];
- if (c.oid && !c.name)
- c.name = unoids[c.oid];
- if (self.componentLookup[c.name] === undefined)
- self.componentLookup[c.name] = [];
- self.componentLookup[c.name].push(c);
- });
- if (this.componentLookup.cn && this.componentLookup.cn.length > 0) {
- this.cn = this.componentLookup.cn[0].value;
- }
- assert.optionalString(opts.type, 'options.type');
- if (opts.type === undefined) {
- if (this.components.length === 1 &&
- this.componentLookup.cn &&
- this.componentLookup.cn.length === 1 &&
- this.componentLookup.cn[0].value.match(DNS_NAME_RE)) {
- this.type = 'host';
- this.hostname = this.componentLookup.cn[0].value;
-
- } else if (this.componentLookup.dc &&
- this.components.length === this.componentLookup.dc.length) {
- this.type = 'host';
- this.hostname = this.componentLookup.dc.map(
- function (c) {
- return (c.value);
- }).join('.');
-
- } else if (this.componentLookup.uid &&
- this.components.length ===
- this.componentLookup.uid.length) {
- this.type = 'user';
- this.uid = this.componentLookup.uid[0].value;
-
- } else if (this.componentLookup.cn &&
- this.componentLookup.cn.length === 1 &&
- this.componentLookup.cn[0].value.match(DNS_NAME_RE)) {
- this.type = 'host';
- this.hostname = this.componentLookup.cn[0].value;
-
- } else if (this.componentLookup.uid &&
- this.componentLookup.uid.length === 1) {
- this.type = 'user';
- this.uid = this.componentLookup.uid[0].value;
-
- } else if (this.componentLookup.mail &&
- this.componentLookup.mail.length === 1) {
- this.type = 'email';
- this.email = this.componentLookup.mail[0].value;
-
- } else if (this.componentLookup.cn &&
- this.componentLookup.cn.length === 1) {
- this.type = 'user';
- this.uid = this.componentLookup.cn[0].value;
-
- } else {
- this.type = 'unknown';
- }
- } else {
- this.type = opts.type;
- if (this.type === 'host')
- this.hostname = opts.hostname;
- else if (this.type === 'user')
- this.uid = opts.uid;
- else if (this.type === 'email')
- this.email = opts.email;
- else
- throw (new Error('Unknown type ' + this.type));
- }
- }
-
- Identity.prototype.toString = function () {
- return (this.components.map(function (c) {
- var n = c.name.toUpperCase();
- /*JSSTYLED*/
- n = n.replace(/=/g, '\\=');
- var v = c.value;
- /*JSSTYLED*/
- v = v.replace(/,/g, '\\,');
- return (n + '=' + v);
- }).join(', '));
- };
-
- Identity.prototype.get = function (name, asArray) {
- assert.string(name, 'name');
- var arr = this.componentLookup[name];
- if (arr === undefined || arr.length === 0)
- return (undefined);
- if (!asArray && arr.length > 1)
- throw (new Error('Multiple values for attribute ' + name));
- if (!asArray)
- return (arr[0].value);
- return (arr.map(function (c) {
- return (c.value);
- }));
- };
-
- Identity.prototype.toArray = function (idx) {
- return (this.components.map(function (c) {
- return ({
- name: c.name,
- value: c.value
- });
- }));
- };
-
- /*
- * These are from X.680 -- PrintableString allowed chars are in section 37.4
- * table 8. Spec for IA5Strings is "1,6 + SPACE + DEL" where 1 refers to
- * ISO IR #001 (standard ASCII control characters) and 6 refers to ISO IR #006
- * (the basic ASCII character set).
- */
- /* JSSTYLED */
- var NOT_PRINTABLE = /[^a-zA-Z0-9 '(),+.\/:=?-]/;
- /* JSSTYLED */
- var NOT_IA5 = /[^\x00-\x7f]/;
-
- Identity.prototype.toAsn1 = function (der, tag) {
- der.startSequence(tag);
- this.components.forEach(function (c) {
- der.startSequence(asn1.Ber.Constructor | asn1.Ber.Set);
- der.startSequence();
- der.writeOID(c.oid);
- /*
- * If we fit in a PrintableString, use that. Otherwise use an
- * IA5String or UTF8String.
- *
- * If this identity was parsed from a DN, use the ASN.1 types
- * from the original representation (otherwise this might not
- * be a full match for the original in some validators).
- */
- if (c.asn1type === asn1.Ber.Utf8String ||
- c.value.match(NOT_IA5)) {
- var v = Buffer.from(c.value, 'utf8');
- der.writeBuffer(v, asn1.Ber.Utf8String);
-
- } else if (c.asn1type === asn1.Ber.IA5String ||
- c.value.match(NOT_PRINTABLE)) {
- der.writeString(c.value, asn1.Ber.IA5String);
-
- } else {
- var type = asn1.Ber.PrintableString;
- if (c.asn1type !== undefined)
- type = c.asn1type;
- der.writeString(c.value, type);
- }
- der.endSequence();
- der.endSequence();
- });
- der.endSequence();
- };
-
- function globMatch(a, b) {
- if (a === '**' || b === '**')
- return (true);
- var aParts = a.split('.');
- var bParts = b.split('.');
- if (aParts.length !== bParts.length)
- return (false);
- for (var i = 0; i < aParts.length; ++i) {
- if (aParts[i] === '*' || bParts[i] === '*')
- continue;
- if (aParts[i] !== bParts[i])
- return (false);
- }
- return (true);
- }
-
- Identity.prototype.equals = function (other) {
- if (!Identity.isIdentity(other, [1, 0]))
- return (false);
- if (other.components.length !== this.components.length)
- return (false);
- for (var i = 0; i < this.components.length; ++i) {
- if (this.components[i].oid !== other.components[i].oid)
- return (false);
- if (!globMatch(this.components[i].value,
- other.components[i].value)) {
- return (false);
- }
- }
- return (true);
- };
-
- Identity.forHost = function (hostname) {
- assert.string(hostname, 'hostname');
- return (new Identity({
- type: 'host',
- hostname: hostname,
- components: [ { name: 'cn', value: hostname } ]
- }));
- };
-
- Identity.forUser = function (uid) {
- assert.string(uid, 'uid');
- return (new Identity({
- type: 'user',
- uid: uid,
- components: [ { name: 'uid', value: uid } ]
- }));
- };
-
- Identity.forEmail = function (email) {
- assert.string(email, 'email');
- return (new Identity({
- type: 'email',
- email: email,
- components: [ { name: 'mail', value: email } ]
- }));
- };
-
- Identity.parseDN = function (dn) {
- assert.string(dn, 'dn');
- var parts = [''];
- var idx = 0;
- var rem = dn;
- while (rem.length > 0) {
- var m;
- /*JSSTYLED*/
- if ((m = /^,/.exec(rem)) !== null) {
- parts[++idx] = '';
- rem = rem.slice(m[0].length);
- /*JSSTYLED*/
- } else if ((m = /^\\,/.exec(rem)) !== null) {
- parts[idx] += ',';
- rem = rem.slice(m[0].length);
- /*JSSTYLED*/
- } else if ((m = /^\\./.exec(rem)) !== null) {
- parts[idx] += m[0];
- rem = rem.slice(m[0].length);
- /*JSSTYLED*/
- } else if ((m = /^[^\\,]+/.exec(rem)) !== null) {
- parts[idx] += m[0];
- rem = rem.slice(m[0].length);
- } else {
- throw (new Error('Failed to parse DN'));
- }
- }
- var cmps = parts.map(function (c) {
- c = c.trim();
- var eqPos = c.indexOf('=');
- while (eqPos > 0 && c.charAt(eqPos - 1) === '\\')
- eqPos = c.indexOf('=', eqPos + 1);
- if (eqPos === -1) {
- throw (new Error('Failed to parse DN'));
- }
- /*JSSTYLED*/
- var name = c.slice(0, eqPos).toLowerCase().replace(/\\=/g, '=');
- var value = c.slice(eqPos + 1);
- return ({ name: name, value: value });
- });
- return (new Identity({ components: cmps }));
- };
-
- Identity.fromArray = function (components) {
- assert.arrayOfObject(components, 'components');
- components.forEach(function (cmp) {
- assert.object(cmp, 'component');
- assert.string(cmp.name, 'component.name');
- if (!Buffer.isBuffer(cmp.value) &&
- !(typeof (cmp.value) === 'string')) {
- throw (new Error('Invalid component value'));
- }
- });
- return (new Identity({ components: components }));
- };
-
- Identity.parseAsn1 = function (der, top) {
- var components = [];
- der.readSequence(top);
- var end = der.offset + der.length;
- while (der.offset < end) {
- der.readSequence(asn1.Ber.Constructor | asn1.Ber.Set);
- var after = der.offset + der.length;
- der.readSequence();
- var oid = der.readOID();
- var type = der.peek();
- var value;
- switch (type) {
- case asn1.Ber.PrintableString:
- case asn1.Ber.IA5String:
- case asn1.Ber.OctetString:
- case asn1.Ber.T61String:
- value = der.readString(type);
- break;
- case asn1.Ber.Utf8String:
- value = der.readString(type, true);
- value = value.toString('utf8');
- break;
- case asn1.Ber.CharacterString:
- case asn1.Ber.BMPString:
- value = der.readString(type, true);
- value = value.toString('utf16le');
- break;
- default:
- throw (new Error('Unknown asn1 type ' + type));
- }
- components.push({ oid: oid, asn1type: type, value: value });
- der._offset = after;
- }
- der._offset = end;
- return (new Identity({
- components: components
- }));
- };
-
- Identity.isIdentity = function (obj, ver) {
- return (utils.isCompatible(obj, Identity, ver));
- };
-
- /*
- * API versions for Identity:
- * [1,0] -- initial ver
- */
- Identity.prototype._sshpkApiVersion = [1, 0];
-
- Identity._oldVersionDetect = function (obj) {
- return ([1, 0]);
- };
|