v0.7.0: initial commit

This commit is contained in:
AJ ONeal 2018-11-20 00:49:30 -07:00
commit 64e8b54253
5 changed files with 354 additions and 0 deletions

137
README.md Normal file
View File

@ -0,0 +1,137 @@
# μASN1.js
An insanely minimal ASN.1 builder for X.509 common schemas,
specifically SEC1/X9.62 PKCS#8, SPKI/PKIX, PKCS#1 and CSR.
Created for [ECDSA-CSR](https://git.coolaj86.com/coolaj86/ecdsa-csr.js)
and [eckles.js](https://git.coolaj86.com/coolaj86/eckles.js) (PEM-to-JWK and JWK-to-PEM).
Optimal for the times you want lightweight ASN.1 support
and it's reasonable to build concise specific functions for
a bounded number of supported schemas rather than a generic
parser that supports _all_ schemas.
Works exclusively in hexidecimal for simplicity and ease-of-use.
```js
var ASN1 = require('uasn1');
```
# API
The ASN.1 standard is actually pretty simple and fairly consistent,
but it's a little tedius to construct due to how sizes are calculated
with nested structures.
There are only 3 methods needed to support all of the X.509 schemas
that most of us care about, and so that's all this library has:
* ASN1(type, hex1, hex2, ...)
* ASN1.UInt(hex1, hex2, ...)
* ASN1.BitStr(hex1, hex2, ...)
* (helper) ASN1.numToHex(num)
Most ASN.1 types follow the same rules:
* Type byte goes first
* Length Info byte goes next
* for numbers < 128 length info is read as the length
* for numbers > 128 length info is size of the length (and the next bytes are the length)
* 128 is a special case which essentially means "read to the end of the file"
* The value bytes go next
The tedius part is just cascading the lengths.
Integer values are different.
They must have a leading '0' if the first byte is > 127,
if the number is positive (otherwise it will be considered negative).
Bit Strings are also different.
The first byte is used to tell how many of the next bytes are used for alignment.
For the purposes of all X509 schemas I've seen, that means it's just '0'.
As far as I've been able to tell, that's all that matters.
# Examples
* EC SEC1/X9.62
* EC PKCS#8
* EC SPKI/PKIX
First, some CONSTANTs:
```js
// 1.2.840.10045.3.1.7
// prime256v1 (ANSI X9.62 named elliptic curve)
var OBJ_ID_EC_256 = '06 08 2A8648CE3D030107'.replace(/\s+/g, '').toLowerCase();
// 1.3.132.0.34
// secp384r1 (SECG (Certicom) named elliptic curve)
var OBJ_ID_EC_384 = '06 05 2B81040022'.replace(/\s+/g, '').toLowerCase();
// 1.2.840.10045.2.1
// ecPublicKey (ANSI X9.62 public key type)
var OBJ_ID_EC_PUB = '06 07 2A8648CE3D0201'.replace(/\s+/g, '').toLowerCase();
```
## EC sec1
```js
function packEcSec1(jwk) {
var d = toHex(base64ToUint8(urlBase64ToBase64(jwk.d)));
var x = toHex(base64ToUint8(urlBase64ToBase64(jwk.x)));
var y = toHex(base64ToUint8(urlBase64ToBase64(jwk.y)));
var objId = ('P-256' === jwk.crv) ? OBJ_ID_EC_256 : OBJ_ID_EC_384;
return hexToUint8(
ASN1('30' // Sequence
, ASN1.UInt('01') // Integer (Version 1)
, ASN1('04', d) // Octet String
, ASN1('A0', objId) // [0] Object ID
, ASN1('A1', ASN1.BitStr('04' + x + y))) // [1] Embedded EC/ASN1 public key
);
}
```
## EC pkcs8
```js
function packEcPkcs8(jwk) {
var d = toHex(base64ToUint8(urlBase64ToBase64(jwk.d)));
var x = toHex(base64ToUint8(urlBase64ToBase64(jwk.x)));
var y = toHex(base64ToUint8(urlBase64ToBase64(jwk.y)));
var objId = ('P-256' === jwk.crv) ? OBJ_ID_EC_256 : OBJ_ID_EC_384;
return hexToUint8(
ASN1('30'
, ASN1.UInt('00')
, ASN1('30'
, OBJ_ID_EC_PUB
, objId
)
, ASN1('04'
, ASN1('30'
, ASN1.UInt('01')
, ASN1('04', d)
, ASN1('A1', ASN1.BitStr('04' + x + y)))))
);
}
```
## EC spki/pkix
```js
function packEcSpki(jwk) {
var x = toHex(base64ToUint8(urlBase64ToBase64(jwk.x)));
var y = toHex(base64ToUint8(urlBase64ToBase64(jwk.y)));
var objId = ('P-256' === jwk.crv) ? OBJ_ID_EC_256 : OBJ_ID_EC_384;
return hexToUint8(
ASN1('30'
, ASN1('30'
, OBJ_ID_EC_PUB
, objId
)
, ASN1.BitStr('04' + x + y))
);
}
var packPkix = packSpki;
```

3
index.js Normal file
View File

@ -0,0 +1,3 @@
'use strict';
module.exports = require('./lib/uasn1.js');

111
lib/telemetry.js Normal file
View File

@ -0,0 +1,111 @@
'use strict';
// We believe in a proactive approach to sustainable open source.
// As part of that we make it easy for you to opt-in to following our progress
// and we also stay up-to-date on telemetry such as operating system and node
// version so that we can focus our efforts where they'll have the greatest impact.
//
// Want to learn more about our Terms, Privacy Policy, and Mission?
// Check out https://therootcompany.com/legal/
var os = require('os');
var crypto = require('crypto');
var https = require('https');
var pkg = require('../package.json');
// to help focus our efforts in the right places
var data = {
package: pkg.name
, version: pkg.version
, node: process.version
, arch: process.arch || os.arch()
, platform: process.platform || os.platform()
, release: os.release()
};
function addCommunityMember(opts) {
setTimeout(function () {
var req = https.request({
hostname: 'api.therootcompany.com'
, port: 443
, path: '/api/therootcompany.com/public/community'
, method: 'POST'
, headers: { 'Content-Type': 'application/json' }
}, function (resp) {
// let the data flow, so we can ignore it
resp.on('data', function () {});
//resp.on('data', function (chunk) { console.log(chunk.toString()); });
resp.on('error', function () { /*ignore*/ });
//resp.on('error', function (err) { console.error(err); });
});
var obj = JSON.parse(JSON.stringify(data));
obj.action = 'updates';
try {
obj.ppid = ppid(obj.action);
} catch(e) {
// ignore
//console.error(e);
}
obj.name = opts.name || undefined;
obj.address = opts.email;
obj.community = 'node.js@therootcompany.com';
req.write(JSON.stringify(obj, 2, null));
req.end();
req.on('error', function () { /*ignore*/ });
//req.on('error', function (err) { console.error(err); });
}, 50);
}
function ping(action) {
setTimeout(function () {
var req = https.request({
hostname: 'api.therootcompany.com'
, port: 443
, path: '/api/therootcompany.com/public/ping'
, method: 'POST'
, headers: { 'Content-Type': 'application/json' }
}, function (resp) {
// let the data flow, so we can ignore it
resp.on('data', function () { });
//resp.on('data', function (chunk) { console.log(chunk.toString()); });
resp.on('error', function () { /*ignore*/ });
//resp.on('error', function (err) { console.error(err); });
});
var obj = JSON.parse(JSON.stringify(data));
obj.action = action;
try {
obj.ppid = ppid(obj.action);
} catch(e) {
// ignore
//console.error(e);
}
req.write(JSON.stringify(obj, 2, null));
req.end();
req.on('error', function (/*e*/) { /*console.error('req.error', e);*/ });
}, 50);
}
// to help identify unique installs without getting
// the personally identifiable info that we don't want
function ppid(action) {
var parts = [ action, data.package, data.version, data.node, data.arch, data.platform, data.release ];
var ifaces = os.networkInterfaces();
Object.keys(ifaces).forEach(function (ifname) {
if (/^en/.test(ifname) || /^eth/.test(ifname) || /^wl/.test(ifname)) {
if (ifaces[ifname] && ifaces[ifname].length) {
parts.push(ifaces[ifname][0].mac);
}
}
});
return crypto.createHash('sha1').update(parts.join(',')).digest('base64');
}
module.exports.ping = ping;
module.exports.joinCommunity = addCommunityMember;
if (require.main === module) {
ping('install');
//addCommunityMember({ name: "AJ ONeal", email: 'coolaj86@gmail.com' });
}

67
lib/uasn1.js Normal file
View File

@ -0,0 +1,67 @@
'use strict';
//
// A dumbed-down, minimal ASN.1 packer
//
// Almost every ASN.1 type that's important for CSR
// can be represented generically with only a few rules.
var ASN1 = function ASN1(/*type, hexstrings...*/) {
var args = Array.prototype.slice.call(arguments);
var typ = args.shift();
var str = args.join('').replace(/\s+/g, '').toLowerCase();
var len = (str.length/2);
var lenlen = 0;
var hex = typ;
// We can't have an odd number of hex chars
if (len !== Math.round(len)) {
throw new Error("invalid hex");
}
// The first byte of any ASN.1 sequence is the type (Sequence, Integer, etc)
// The second byte is either the size of the value, or the size of its size
// 1. If the second byte is < 0x80 (128) it is considered the size
// 2. If it is > 0x80 then it describes the number of bytes of the size
// ex: 0x82 means the next 2 bytes describe the size of the value
// 3. The special case of exactly 0x80 is "indefinite" length (to end-of-file)
if (len > 127) {
lenlen += 1;
while (len > 255) {
lenlen += 1;
len = len >> 8;
}
}
if (lenlen) { hex += ASN1.numToHex(0x80 + lenlen); }
return hex + ASN1.numToHex(str.length/2) + str;
};
// The Integer type has some special rules
ASN1.UInt = function UINT() {
var str = Array.prototype.slice.call(arguments).join('');
var first = parseInt(str.slice(0, 2), 16);
// If the first byte is 0x80 or greater, the number is considered negative
// Therefore we add a '00' prefix if the 0x80 bit is set
if (0x80 & first) { str = '00' + str; }
return ASN1('02', str);
};
// The Bit String type also has a special rule
ASN1.BitStr = function BITSTR() {
var str = Array.prototype.slice.call(arguments).join('');
// '00' is a mask of how many bits of the next byte to ignore
return ASN1('03', '00' + str);
};
ASN1.numToHex = function (d) {
d = d.toString(16);
if (d.length % 2) {
return '0' + d;
}
return d;
};

36
package.json Normal file
View File

@ -0,0 +1,36 @@
{
"name": "uasn1",
"version": "0.7.0",
"description": "An insanely minimal ASN.1 builder for X.509 common schemas, specifically SEC1/X9.62 PKCS#8, SPKI/PKIX, PKCS#1 and CSR.",
"homepage": "https://git.coolaj86.com/coolaj86/uasn1.js",
"main": "index.js",
"files": [
"lib"
],
"directories": {
"lib": "lib"
},
"scripts": {
"postinstall": "node lib/telemetry.js event:install",
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "https://git.coolaj86.com/coolaj86/uasn1.js"
},
"keywords": [
"ASN.1",
"SEC1",
"PKCS#8",
"PKCS#1",
"SPKI",
"PKIX",
"X.509",
"asn1",
"pkcs8",
"pkcs1",
"x509"
],
"author": "AJ ONeal <coolaj86@gmail.com> (https://coolaj86.com/)",
"license": "MPL-2.0"
}