From 64e8b54253f1a6b7256313e2e090f25ff0e82733 Mon Sep 17 00:00:00 2001 From: AJ ONeal Date: Tue, 20 Nov 2018 00:49:30 -0700 Subject: [PATCH] v0.7.0: initial commit --- README.md | 137 +++++++++++++++++++++++++++++++++++++++++++++++ index.js | 3 ++ lib/telemetry.js | 111 ++++++++++++++++++++++++++++++++++++++ lib/uasn1.js | 67 +++++++++++++++++++++++ package.json | 36 +++++++++++++ 5 files changed, 354 insertions(+) create mode 100644 README.md create mode 100644 index.js create mode 100644 lib/telemetry.js create mode 100644 lib/uasn1.js create mode 100644 package.json diff --git a/README.md b/README.md new file mode 100644 index 0000000..9cc6f70 --- /dev/null +++ b/README.md @@ -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; +``` diff --git a/index.js b/index.js new file mode 100644 index 0000000..c00bb6d --- /dev/null +++ b/index.js @@ -0,0 +1,3 @@ +'use strict'; + +module.exports = require('./lib/uasn1.js'); diff --git a/lib/telemetry.js b/lib/telemetry.js new file mode 100644 index 0000000..c628a2d --- /dev/null +++ b/lib/telemetry.js @@ -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' }); +} diff --git a/lib/uasn1.js b/lib/uasn1.js new file mode 100644 index 0000000..cbfc2ed --- /dev/null +++ b/lib/uasn1.js @@ -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; +}; diff --git a/package.json b/package.json new file mode 100644 index 0000000..fe554f5 --- /dev/null +++ b/package.json @@ -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 (https://coolaj86.com/)", + "license": "MPL-2.0" +}