277 lines
7.2 KiB
JavaScript
277 lines
7.2 KiB
JavaScript
// 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');
|
|
|
|
/*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.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';
|
|
|
|
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) {
|
|
return (c.name.toUpperCase() + '=' + c.value);
|
|
}).join(', '));
|
|
};
|
|
|
|
/*
|
|
* 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 (c.value.match(NOT_IA5)) {
|
|
var v = new Buffer(c.value, 'utf8');
|
|
der.writeBuffer(v, asn1.Ber.Utf8String);
|
|
} else if (c.value.match(NOT_PRINTABLE)) {
|
|
der.writeString(c.value, asn1.Ber.IA5String);
|
|
} else {
|
|
der.writeString(c.value, asn1.Ber.PrintableString);
|
|
}
|
|
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 = dn.split(',');
|
|
var cmps = parts.map(function (c) {
|
|
c = c.trim();
|
|
var eqPos = c.indexOf('=');
|
|
var name = c.slice(0, eqPos).toLowerCase();
|
|
var value = c.slice(eqPos + 1);
|
|
return ({ name: name, value: value });
|
|
});
|
|
return (new Identity({ components: cmps }));
|
|
};
|
|
|
|
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, 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]);
|
|
};
|