Skip to content

Commit

Permalink
#38 want support for more obscure DN OIDs
Browse files Browse the repository at this point in the history
Reviewed by: Cody Peter Mello <cody.mello@joyent.com>
Approved by: Cody Peter Mello <cody.mello@joyent.com>
  • Loading branch information
arekinath committed Oct 10, 2018
1 parent 1cc4c99 commit 6ec6f9d
Show file tree
Hide file tree
Showing 7 changed files with 240 additions and 5 deletions.
72 changes: 71 additions & 1 deletion README.md
Expand Up @@ -614,6 +614,44 @@ Parameters

Returns an Identity instance.

### `identityFromArray(arr)`

Constructs an Identity from an array of DN components (see `Identity#toArray()`
for the format).

Parameters

- `arr` -- an Array of Objects, DN components with `name` and `value`

Returns an Identity instance.


Supported attributes in DNs:

| Attribute name | OID |
| -------------- | --- |
| `cn` | `2.5.4.3` |
| `o` | `2.5.4.10` |
| `ou` | `2.5.4.11` |
| `l` | `2.5.4.7` |
| `s` | `2.5.4.8` |
| `c` | `2.5.4.6` |
| `sn` | `2.5.4.4` |
| `postalCode` | `2.5.4.17` |
| `serialNumber` | `2.5.4.5` |
| `street` | `2.5.4.9` |
| `x500UniqueIdentifier` | `2.5.4.45` |
| `role` | `2.5.4.72` |
| `telephoneNumber` | `2.5.4.20` |
| `description` | `2.5.4.13` |
| `dc` | `0.9.2342.19200300.100.1.25` |
| `uid` | `0.9.2342.19200300.100.1.1` |
| `mail` | `0.9.2342.19200300.100.1.3` |
| `title` | `2.5.4.12` |
| `gn` | `2.5.4.42` |
| `initials` | `2.5.4.43` |
| `pseudonym` | `2.5.4.65` |

### `Identity#toString()`

Returns the identity as an LDAP-style DN string.
Expand All @@ -631,7 +669,39 @@ Set when `type` is `'host'`, `'user'`, or `'email'`, respectively. Strings.

### `Identity#cn`

The value of the first `CN=` in the DN, if any.
The value of the first `CN=` in the DN, if any. It's probably better to use
the `#get()` method instead of this property.

### `Identity#get(name[, asArray])`

Returns the value of a named attribute in the Identity DN. If there is no
attribute of the given name, returns `undefined`. If multiple components
of the DN contain an attribute of this name, an exception is thrown unless
the `asArray` argument is given as `true` -- then they will be returned as
an Array in the same order they appear in the DN.

Parameters

- `name` -- a String
- `asArray` -- an optional Boolean

### `Identity#toArray()`

Returns the Identity as an Array of DN component objects. This looks like:

```js
[ {
"name": "cn",
"value": "Joe Bloggs"
},
{
"name": "o",
"value": "Organisation Ltd"
} ]
```

Each object has a `name` and a `value` property. The returned objects may be
safely modified.

Errors
------
Expand Down
90 changes: 87 additions & 3 deletions lib/identity.js
Expand Up @@ -24,9 +24,21 @@ 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) {
Expand Down Expand Up @@ -113,10 +125,39 @@ function Identity(opts) {

Identity.prototype.toString = function () {
return (this.components.map(function (c) {
return (c.name.toUpperCase() + '=' + c.value);
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
Expand Down Expand Up @@ -224,17 +265,60 @@ Identity.forEmail = function (email) {

Identity.parseDN = function (dn) {
assert.string(dn, 'dn');
var parts = dn.split(',');
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('=');
var name = c.slice(0, eqPos).toLowerCase();
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);
Expand Down
1 change: 1 addition & 0 deletions lib/index.js
Expand Up @@ -28,6 +28,7 @@ module.exports = {
identityForHost: Identity.forHost,
identityForUser: Identity.forUser,
identityForEmail: Identity.forEmail,
identityFromArray: Identity.fromArray,

/* errors */
FingerprintFormatError: errs.FingerprintFormatError,
Expand Down
2 changes: 1 addition & 1 deletion package.json
@@ -1,6 +1,6 @@
{
"name": "sshpk",
"version": "1.14.2",
"version": "1.15.0",
"description": "A library for finding and using SSH public keys",
"main": "lib/index.js",
"scripts": {
Expand Down
21 changes: 21 additions & 0 deletions test/assets/double-title-cert.pem
@@ -0,0 +1,21 @@
-----BEGIN CERTIFICATE-----
MIIDhzCCAm+gAwIBAgIHWAIFM3wFkDANBgkqhkiG9w0BAQsFADCBrTERMA8GA1UE
DAwIY2VydC5rZXkxLTArBgNVBAMMJDJkYzc5ZDM2LWVhMDEtYzg1NS1lYWI1LWUy
YzdhMjRhYmJmNDEOMAwGA1UEKgwFem9uZTExNDAyBgoJkiaJk/IsZAEBDCRmZTAz
ZmY0MC1kYjEyLTExZTctYjQ4Yi02M2QxNTNhZjY5ODUxEjAQBgNVBAsMCWluc3Rh
bmNlczEPMA0GA1UECgwGdHJpdG9uMB4XDTE3MTIxNTIzMDUyMFoXDTE3MTIxNTIz
MDYyMFowgbYxFDASBgNVBAwMC2luLXpvbmUua2V5MS0wKwYDVQQDDCQyZGM3OWQz
Ni1lYTAxLWM4NTUtZWFiNS1lMmM3YTI0YWJiZjQxNDAyBgoJkiaJk/IsZAEBDCRm
ZTAzZmY0MC1kYjEyLTExZTctYjQ4Yi02M2QxNTNhZjY5ODUxEjAQBgNVBAsMCWRl
bGVnYXRlZDEPMA0GA1UECgwGdHJpdG9uMRQwEgYDVQQMDAtpbi16b25lLmtleTBZ
MBMGByqGSM49AgEGCCqGSM49AwEHA0IABMu/RgdeZ0/V9JqV3ByWW4t7/ncxcEnA
nsq1CBPwPIfb0aUhVy+FrJTYV+m+gStGJW9ZwE8uuU61rmxKdO+WWVSjbDBqMA8G
A1UdEwEB/wQFMAMBAQAwDgYDVR0PAQH/BAQDAgGIMBYGA1UdJQEB/wQMMAoGCCsG
AQUFBwMBMC8GA1UdEQQoMCaCJDJkYzc5ZDM2LWVhMDEtYzg1NS1lYWI1LWUyYzdh
MjRhYmJmNDANBgkqhkiG9w0BAQsFAAOCAQEAcRmBTD0bnI58nEjWXTTBPHYzBL8c
V6QBzfYOCNE6iXl1zzH3oR/x3+yIMRhTrw1tHv4A0lunvKZsz9fogy/8uTTQtOaT
xoXmjhvY7m1VZqb7W4+aG7DbJ2oZxIaZABXGEp+huKG5LPsmn0BGqJ2q8NxX3NKw
PPjyTZdwjVaxwSomkzIaGqjdWmRKeqxbkwL19SN9Rk9ZoytG1pjTrzWQW4yaYa+s
aycLMrrRMZE51qBPqFxf8YvXDHXk1gugQqJSRgmo4q/tFqloTwM+0D3tElzmAPCc
jPLGIkJ5/y+pQ81vTRl0HJmzndlg0ucbpiTRyZt9cpOe17KjvB8+tgy58w==
-----END CERTIFICATE-----
30 changes: 30 additions & 0 deletions test/certs.js
Expand Up @@ -229,6 +229,17 @@ test('napoleon cert (generalizedtime) (x509)', function (t) {
console.log(cert.validFrom.getTime());
console.log(cert.validUntil.getTime());
t.ok(!cert.isExpired(new Date('1775-03-01T00:00Z')));
t.deepEqual(cert.subjects[0].toArray(), [
{ name: 'c', value: 'FR' },
{ name: 's', value: 'Île-de-France' },
{ name: 'l', value: 'Paris' },
{ name: 'o', value: 'Hereditary Monarchy' },
{ name: 'ou', value: 'Head of State' },
{ name: 'emailAddress', value: 'nappi@greatfrenchempire.fr' },
{ name: 'cn', value: 'Emperor Napoleon I' },
{ name: 'sn', value: 'Bonaparte' },
{ name: 'gn', value: 'Napoleon' }
]);
t.end();
});

Expand All @@ -250,6 +261,8 @@ test('example cert: digicert (x509)', function (t) {
t.strictEqual(cert.subjects[0].hostname, 'www.digicert.com');
t.strictEqual(cert.issuer.cn,
'DigiCert SHA2 Extended Validation Server CA');
t.strictEqual(cert.issuer.get('c'), 'US');
t.strictEqual(cert.issuer.get('o'), 'DigiCert Inc');

var cacert = sshpk.parseCertificate(
fs.readFileSync(path.join(testDir, 'digicert-ca.crt')), 'x509');
Expand Down Expand Up @@ -356,3 +369,20 @@ test('example cert: ed25519 cert from curdle-pkix-04', function (t) {

t.end();
});

test('cert with doubled-up DN attribute', function (t) {
var cert = sshpk.parseCertificate(
fs.readFileSync(path.join(testDir, 'double-title-cert.pem')),
'pem');

var id = cert.subjects[0];
t.throws(function () {
id.get('title');
});
t.deepEqual(id.get('title', true), ['in-zone.key', 'in-zone.key']);

t.strictEqual(id.get('ou'), 'delegated');
t.strictEqual(id.get('cn'), '2dc79d36-ea01-c855-eab5-e2c7a24abbf4');

t.end();
});
29 changes: 29 additions & 0 deletions test/identity.js
Expand Up @@ -14,3 +14,32 @@ test('parsedn', function (t) {
t.strictEqual(id.cn, 'Blah Corp');
t.end();
});

test('parsedn escapes', function (t) {
var id = sshpk.Identity.parseDN('cn=what\\,something,o=b==a,c=\\US');
t.strictEqual(id.get('cn'), 'what,something');
t.strictEqual(id.get('o'), 'b==a');
t.strictEqual(id.get('c'), '\\US');
id = sshpk.Identity.parseDN('cn\\=foo=bar');
t.strictEqual(id.get('cn=foo'), 'bar');
t.throws(function () {
sshpk.Identity.parseDN('cn\\\\=foo');
});
t.end();
});

test('fromarray', function (t) {
var arr = [
{ name: 'ou', value: 'foo' },
{ name: 'ou', value: 'bar' },
{ name: 'cn', value: 'foobar,g=' }
];
var id = sshpk.identityFromArray(arr);
t.throws(function () {
id.get('ou');
});
t.deepEqual(id.get('ou', true), ['foo', 'bar']);
t.strictEqual(id.get('cn'), 'foobar,g=');
t.strictEqual(id.toString(), 'OU=foo, OU=bar, CN=foobar\\,g=');
t.end();
});

0 comments on commit 6ec6f9d

Please sign in to comment.