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