v0.7.0: initial commit
This commit is contained in:
commit
64e8b54253
137
README.md
Normal file
137
README.md
Normal 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
3
index.js
Normal file
@ -0,0 +1,3 @@
|
||||
'use strict';
|
||||
|
||||
module.exports = require('./lib/uasn1.js');
|
111
lib/telemetry.js
Normal file
111
lib/telemetry.js
Normal 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
67
lib/uasn1.js
Normal 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
36
package.json
Normal 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"
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user