diff --git a/.gitignore b/.gitignore
index 144585f..be8362d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,5 @@
+*.gz
+
# ---> Node
# Logs
logs
diff --git a/README.md b/README.md
index 74b4ad1..9329dea 100644
--- a/README.md
+++ b/README.md
@@ -1,3 +1,412 @@
-# asn1.js
+# @root/asn1
-Lightweight, Zero-Dependency ASN.1 encoder and decoder in less than 200 lines of vanilla JavaScript
\ No newline at end of file
+Built by [The Root Company](https://therootcompany.com)
+for [Greenlock](https://greenlock.domains)
+and [ACME.js](https://git.rootprojects.org/root/acme.js)
+
+Lightweight, Zero-Dependency ASN.1 encoder and decoder for Node.js and Browsers,
+in less than 300 lines of vanilla JavaScript
+
+| 1.6k gzipped
+| 4.2k minified
+| 8.4k pretty
+|
+
+- [x] Zero External Dependencies
+- [x] Universal Support
+ - [x] Node.js
+ - [x] Browsers
+- [x] Vanilla JS
+
+This ASN.1 codec is built for simplicity. It encodes into DER format
+and decodes into a simple, classless Array of Arrays and values.
+
+Most people don't actually want to work with ANS.1 directly,
+but rather intend to work with pre-defined x509 schemas.
+
+If you're **most people**, you're actually looking for one or more of these:
+
+- [pem.js](https://git.rootprojects.org/root/pem.js)
+- [x509.js](https://git.rootprojects.org/root/x509.js)
+- [csr.js](https://git.rootprojects.org/root/csr.js)
+- [keypairs.js](https://git.rootprojects.org/root/keypairs.js)
+- [encoding.js](https://git.rootprojects.org/root/encoding.js)
+
+Want to [contribute](#contributions)?
+Need [commercial support](#commercial-support)?
+
+# Usage
+
+ASN.1 DER consists values which have
+
+- a type (2-bit class, 6-bit tag)
+- a coded length
+- zero or more values
+
+Common types include:
+
+```txt
+0x30 SEQUENCE
+0x02 INTEGER*
+0x03 BIT STRING**
+0x04 OCTET STRING
+0x05 NULL
+0x06 OBJECT IDENTIFIER
+0x0C UTF8String
+0x16 IA5String (ASCII)
+0x17 UTCTime
+0x31 SET
+0xA0 context-specific***
+0xA3 context-specific***
+```
+
+\* INTEGERS are always BigInt-encoded (a leading '00' for positive numbers with a 1 in the most-significant-bit position)
+
+\*\*BIT STRINGS have a leading "bit mask" which, for all practical purposes, is actually _always_ '00'
+
+\*\*\* See
+
+The core value in this library is that it:
+
+- correctly sums the byte length of children elements
+- correctly encodes BigInts
+
+## Parser Usage
+
+There are three options:
+
+- `der` (required) - the input bytes as a buffer
+- `json` (default) - returns hex strings for values, rather than buffers
+- `verbose` - returns a more human-friendly object that is useful for debugging
+
+```js
+ASN1.parse({ der: ``, json: true, verbose: true });
+```
+
+Default (hex) output:
+
+```js
+[
+ '30',
+ [
+ ['02', '01'],
+ ['04', '2c8996...'],
+ ['a0', [['06', '2a8648...']]],
+ ['a1', [['03', '04bdd8...']]]
+ ]
+];
+```
+
+Verbose output:
+
+```js
+{ type: 48,
+ lengthSize: 0,
+ length: 119,
+ children:
+ [ { type: 2, lengthSize: 0, length: 1, value: },
+ { type: 4,
+ lengthSize: 0,
+ length: 32,
+ value:
+ ,
+ children: [] },
+ { type: 160, lengthSize: 0, length: 10, children: [Array] },
+ { type: 161, lengthSize: 0, length: 68, children: [Array] } ] }
+```
+
+## Packer Usage
+
+You can use either of two syntaxes. One is much easier to read than the other.
+
+Ironically, hex strings are used in place of buffers for efficiency.
+
+```js
+ASN1.Any(hexType, hexBytes1, hexBytes2, ...);
+ASN1.UInt(hexBigInt);
+ASN1.BitStr(hexBitStream);
+```
+
+In practice, you'll be cascading the objects into a final hex string:
+
+```
+// result is a hex-encoded DER
+var der = hexToBuf(
+ ASN1.Any('30' // Sequence
+ , ASN1.UInt('01') // Integer (Version 1)
+ , ASN1.Any('04', '07CAD7...') // Octet String
+ , ASN1.Any('A0', '06082A...') // [0] Object ID (context-specific)
+ , ASN1.Any('A1', // [1] (context-specific value)
+ ASN1.BitStr('04BDD8...')
+ )
+ )
+);
+```
+
+Alternatively you can pack either the sparse array or verbose object, using hex strings or buffers:
+
+- `json` when set to true will return a hex-encoded DER rather than a DER buffer
+
+```js
+var buf = Uint8Array.from([0x01]);
+
+ASN1.pack(
+ [
+ '30',
+ [
+ ['02', buf],
+ ['04', '07CAD7...'],
+ ['A0', '06082A...'],
+ ['A1', ['03', '04BDD8...']]
+ ]
+ ],
+ { json: false }
+);
+```
+
+```js
+var buf = Uint8Array.from([0x01]);
+
+ASN1.pack(
+ {
+ type: 48,
+ children: [
+ { type: 2, value: '01' },
+ { type: 4, value: '2c 89 96 ...', children: [] },
+ { type: 160, children: [...] },
+ { type: 161, children: [...] }
+ ]
+ },
+ { json: false }
+);
+```
+
+# Install
+
+This package contains both node-specific and browser-specific code,
+and the `package.json#browser` field ensures that your package manager
+will automatically choose the correct code for your environment.
+
+## Node (and Webpack)
+
+```js
+npm install -g @root/asn1
+```
+
+```js
+var asn1 = require('@root/asn1');
+```
+
+```js
+// just the packer
+var asn1 = require('@root/asn1/packer');
+
+// just the parser
+var asn1 = require('@root/asn1/parser');
+```
+
+## Browsers (Vanilla JS)
+
+```html
+
+```
+
+```html
+
+```
+
+```js
+var ASN1 = window.ASN1;
+```
+
+# Examples
+
+## Decoding DER to JSON-ASN.1
+
+```js
+var PEM = require('@root/pem/packer');
+var Enc = require('@root/encoding');
+var ASN1 = require('@root/asn1/parser');
+```
+
+```js
+var pem = [
+ '-----BEGIN EC PRIVATE KEY-----',
+ 'MHcCAQEEICyJlsaqkx2z9yx0H6rHA0lM3/7jXjxqn/VOhExHDuR6oAoGCCqGSM49',
+ 'AwEHoUQDQgAEvdjQ3T6VBX82LIKDzepYgRsz3HgRwp83yPuonu6vqoshSQRe0Aye',
+ 'mmdXUDX2wTZsmFSjhY9uroRiBbGZrigbKA==',
+ '-----END EC PRIVATE KEY-----'
+].join('\n');
+```
+
+```js
+var der = PEM.parseBlock(pem).bytes;
+var asn1 = ASN1.parse({ der: der, json: true, verbose: false });
+```
+
+```json
+[
+ "30",
+ [
+ ["02", "01"],
+ [
+ "04",
+ "2c8996c6aa931db3f72c741faac703494cdffee35e3c6a9ff54e844c470ee47a"
+ ],
+ ["a0", [["06", "2a8648ce3d030107"]]],
+ [
+ "a1",
+ [
+ [
+ "03",
+ "04bdd8d0dd3e95057f362c8283cdea58811b33dc7811c29f37c8fba89eeeafaa8b2149045ed00c9e9a67575035f6c1366c9854a3858f6eae846205b199ae281b28"
+ ]
+ ]
+ ]
+ ]
+]
+```
+
+```json
+{
+ "type": 48,
+ "lengthSize": 0,
+ "length": 119,
+ "children": [
+ { "type": 2, "lengthSize": 0, "length": 1, "value": "01" },
+ {
+ "type": 4,
+ "lengthSize": 0,
+ "length": 32,
+ "value": "2c8996c6aa931db3f72c741faac703494cdffee35e3c6a9ff54e844c470ee47a",
+ "children": []
+ },
+ {
+ "type": 160,
+ "lengthSize": 0,
+ "length": 10,
+ "children": [
+ {
+ "type": 6,
+ "lengthSize": 0,
+ "length": 8,
+ "value": "2a8648ce3d030107"
+ }
+ ]
+ },
+ {
+ "type": 161,
+ "lengthSize": 0,
+ "length": 68,
+ "children": [
+ {
+ "type": 3,
+ "lengthSize": 0,
+ "length": 66,
+ "value": "04bdd8d0dd3e95057f362c8283cdea58811b33dc7811c29f37c8fba89eeeafaa8b2149045ed00c9e9a67575035f6c1366c9854a3858f6eae846205b199ae281b28",
+ "children": []
+ }
+ ]
+ }
+ ]
+}
+```
+
+## Encoding ASN.1 to DER
+
+Here's an example of an SEC1-encoded EC P-256 Public/Private Keypair:
+
+```js
+var ASN1 = require('@root/asn1/packer');
+var Enc = require('@root/encoding');
+var PEM = require('@root/pem/packer');
+```
+
+```js
+// 1.2.840.10045.3.1.7
+// prime256v1 (ANSI X9.62 named elliptic curve)
+var OBJ_ID_EC_256 = '06 08 2A8648CE3D030107';
+```
+
+```js
+var jwk = {
+ crv: 'P-256',
+ d: 'LImWxqqTHbP3LHQfqscDSUzf_uNePGqf9U6ETEcO5Ho',
+ kty: 'EC',
+ x: 'vdjQ3T6VBX82LIKDzepYgRsz3HgRwp83yPuonu6vqos',
+ y: 'IUkEXtAMnppnV1A19sE2bJhUo4WPbq6EYgWxma4oGyg',
+ kid: 'MnfJYyS9W5gUjrJLdn8ePMzik8ZJz2qc-VZmKOs_oCw'
+};
+var d = Enc.base64ToHex(jwk.d);
+var x = Enc.base64ToHex(jwk.x);
+var y = Enc.base64ToHex(jwk.y);
+```
+
+```
+var der = Enc.hexToBuf(
+ ASN1.Any('30' // Sequence
+ , ASN1.UInt('01') // Integer (Version 1)
+ , ASN1.Any('04', d) // Octet String
+ , ASN1.Any('A0', OBJ_ID_EC_256) // [0] Object ID
+ , ASN1.Any('A1', // [1] Embedded EC/ASN1 public key
+ ASN1.BitStr('04' + x + y)
+ )
+ )
+);
+
+var pem = PEM.packBlock({
+ type: 'EC PRIVATE KEY',
+ bytes: der
+});
+```
+
+# Disabiguation
+
+`ASN1.Any(typ, hexVal, ...)`
+
+There was once an actual ASN.1 type with the literal name 'Any'.
+It was deprecated in 1994 and the `Any` in this API simply means "give any value"
+
+# Contributions
+
+Did this project save you some time? Maybe make your day? Even save the day?
+
+Please say "thanks" via Paypal or Patreon:
+
+- Paypal: [\$5](https://paypal.me/rootprojects/5) | [\$10](https://paypal.me/rootprojects/10) | Any amount:
+- Patreon:
+
+Where does your contribution go?
+
+[Root](https://therootcompany.com) is a collection of experts
+who trust each other and enjoy working together on deep-tech,
+Indie Web projects.
+
+Our goal is to operate as a sustainable community.
+
+Your contributions - both in code and _especially_ monetarily -
+help to not just this project, but also our broader work
+of [projects](https://rootprojects.org) that fuel the **Indie Web**.
+
+Also, we chat on [Keybase](https://keybase.io)
+in [#rootprojects](https://keybase.io/team/rootprojects)
+
+# Commercial Support
+
+Do you need...
+
+- more features?
+- bugfixes, on _your_ timeline?
+- custom code, built by experts?
+- commercial support and licensing?
+
+Contact for support options.
+
+# Legal
+
+Copyright [AJ ONeal](https://coolaj86.com),
+[Root](https://therootcompany.com) 2018-2019
+
+MPL-2.0 |
+[Terms of Use](https://therootcompany.com/legal/#terms) |
+[Privacy Policy](https://therootcompany.com/legal/#privacy)
diff --git a/build.sh b/build.sh
new file mode 100644
index 0000000..5779634
--- /dev/null
+++ b/build.sh
@@ -0,0 +1,28 @@
+#!/bin/bash
+
+# TODO convert to JS
+cat parser.js packer.js > all.tmp.js
+sed -i '' '/use strict/d' all.tmp.js
+sed -i '' '/require/d' all.tmp.js
+sed -i '' '/exports/d' all.tmp.js
+
+echo ';(function () {' > dist/asn1.js
+echo "'use strict';" >> dist/asn1.js
+echo "var ASN1 = window.ASN1 = {};" >> dist/asn1.js
+echo "var Enc = window.Encoding;" >> dist/asn1.js
+cat all.tmp.js >> dist/asn1.js
+rm all.tmp.js
+echo '}());' >> dist/asn1.js
+
+rm dist/*.gz
+
+cat node_modules/@root/encoding/dist/encoding.all.js > all.js
+cat dist/asn1.js >> all.js
+uglifyjs dist/asn1.js > dist/asn1.min.js
+gzip dist/asn1.min.js
+uglifyjs dist/asn1.js > dist/asn1.min.js
+
+mv all.js dist/asn1.all.js
+uglifyjs dist/asn1.all.js > dist/asn1.all.min.js
+gzip dist/asn1.all.min.js
+uglifyjs dist/asn1.all.js > dist/asn1.all.min.js
diff --git a/dist/asn1.all.js b/dist/asn1.all.js
new file mode 100644
index 0000000..29a5bb2
--- /dev/null
+++ b/dist/asn1.all.js
@@ -0,0 +1,518 @@
+;(function () {
+'use strict';
+var Enc = window.Encoding = {};
+
+
+// To Base64
+
+Enc.bufToBase64 = function(u8) {
+ var bin = '';
+ u8.forEach(function(i) {
+ bin += String.fromCharCode(i);
+ });
+ return btoa(bin);
+};
+
+Enc.strToBase64 = function(str) {
+ return btoa(Enc.strToBin(str));
+};
+
+// From Base64
+
+function _base64ToBin(b64) {
+ return atob(Enc.urlBase64ToBase64(b64));
+}
+
+Enc._base64ToBin = _base64ToBin;
+
+Enc.base64ToBuf = function(b64) {
+ return Enc.binToBuf(_base64ToBin(b64));
+};
+
+Enc.base64ToStr = function(b64) {
+ return Enc.binToStr(_base64ToBin(b64));
+};
+
+// URL Safe Base64
+
+Enc.urlBase64ToBase64 = function(u64) {
+ var r = u64 % 4;
+ if (2 === r) {
+ u64 += '==';
+ } else if (3 === r) {
+ u64 += '=';
+ }
+ return u64.replace(/-/g, '+').replace(/_/g, '/');
+};
+
+Enc.base64ToUrlBase64 = function(b64) {
+ return b64
+ .replace(/\+/g, '-')
+ .replace(/\//g, '_')
+ .replace(/=/g, '');
+};
+
+Enc.bufToUrlBase64 = function(buf) {
+ return Enc.base64ToUrlBase64(Enc.bufToBase64(buf));
+};
+
+Enc.strToUrlBase64 = function(str) {
+ return Enc.bufToUrlBase64(Enc.strToBuf(str));
+};
+
+
+
+// To Hex
+
+Enc.bufToHex = function(u8) {
+ var hex = [];
+ var i, h;
+ var len = u8.byteLength || u8.length;
+
+ for (i = 0; i < len; i += 1) {
+ h = u8[i].toString(16);
+ if (2 !== h.length) {
+ h = '0' + h;
+ }
+ hex.push(h);
+ }
+
+ return hex.join('').toLowerCase();
+};
+
+Enc.numToHex = function(d) {
+ d = d.toString(16); // .padStart(2, '0');
+ if (d.length % 2) {
+ return '0' + d;
+ }
+ return d;
+};
+
+Enc.strToHex = function(str) {
+ return Enc._binToHex(Enc.strToBin(str));
+};
+
+Enc._binToHex = function(bin) {
+ return bin
+ .split('')
+ .map(function(ch) {
+ var h = ch.charCodeAt(0).toString(16);
+ if (2 !== h.length) {
+ h = '0' + h;
+ }
+ return h;
+ })
+ .join('');
+};
+
+// From Hex
+
+Enc.hexToBuf = function(hex) {
+ var arr = [];
+ hex.match(/.{2}/g).forEach(function(h) {
+ arr.push(parseInt(h, 16));
+ });
+ return 'undefined' !== typeof Uint8Array ? new Uint8Array(arr) : arr;
+};
+
+Enc.hexToStr = function(hex) {
+ return Enc.binToStr(_hexToBin(hex));
+};
+
+function _hexToBin(hex) {
+ return hex.replace(/([0-9A-F]{2})/gi, function(_, p1) {
+ return String.fromCharCode('0x' + p1);
+ });
+}
+
+Enc._hexToBin = _hexToBin;
+
+
+
+// to Binary String
+
+Enc.bufToBin = function(buf) {
+ var bin = '';
+ // cannot use .map() because Uint8Array would return only 0s
+ buf.forEach(function(ch) {
+ bin += String.fromCharCode(ch);
+ });
+ return bin;
+};
+
+Enc.strToBin = function(str) {
+ // Note: TextEncoder might be faster (or it might be slower, I don't know),
+ // but it doesn't solve the double-utf8 problem and MS Edge still has users without it
+ var escstr = encodeURIComponent(str);
+ // replaces any uri escape sequence, such as %0A,
+ // with binary escape, such as 0x0A
+ var binstr = escstr.replace(/%([0-9A-F]{2})/g, function(_, p1) {
+ return String.fromCharCode('0x' + p1);
+ });
+ return binstr;
+};
+
+// to Buffer
+
+Enc.binToBuf = function(bin) {
+ var arr = bin.split('').map(function(ch) {
+ return ch.charCodeAt(0);
+ });
+ return 'undefined' !== typeof Uint8Array ? new Uint8Array(arr) : arr;
+};
+
+Enc.strToBuf = function(str) {
+ return Enc.binToBuf(Enc.strToBin(str));
+};
+
+// to Unicode String
+
+Enc.binToStr = function(binstr) {
+ var escstr = binstr.replace(/(.)/g, function(m, p) {
+ var code = p
+ .charCodeAt(0)
+ .toString(16)
+ .toUpperCase();
+ if (code.length < 2) {
+ code = '0' + code;
+ }
+ return '%' + code;
+ });
+
+ return decodeURIComponent(escstr);
+};
+
+Enc.bufToStr = function(buf) {
+ return Enc.binToStr(Enc.bufToBin(buf));
+};
+
+// Base64 + Hex
+
+Enc.base64ToHex = function(b64) {
+ return Enc.bufToHex(Enc.base64ToBuf(b64));
+};
+
+Enc.hexToBase64 = function(hex) {
+ return btoa(Enc._hexToBin(hex));
+};
+
+}());
+;(function () {
+'use strict';
+var ASN1 = window.ASN1 = {};
+var Enc = window.Encoding;
+// Copyright 2018 AJ ONeal. All rights reserved
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+
+//
+// Parser
+//
+
+// Although I've only seen 9 max in https certificates themselves,
+// but each domain list could have up to 100
+ASN1.ELOOPN = 102;
+ASN1.ELOOP =
+ 'uASN1.js Error: iterated over ' +
+ ASN1.ELOOPN +
+ '+ elements (probably a malformed file)';
+// I've seen https certificates go 29 deep
+ASN1.EDEEPN = 60;
+ASN1.EDEEP =
+ 'uASN1.js Error: element nested ' +
+ ASN1.EDEEPN +
+ '+ layers deep (probably a malformed file)';
+// Container Types are Sequence 0x30, Container Array? (0xA0, 0xA1)
+// Value Types are Boolean 0x01, Integer 0x02, Null 0x05, Object ID 0x06, String 0x0C, 0x16, 0x13, 0x1e Value Array? (0x82)
+// Bit String (0x03) and Octet String (0x04) may be values or containers
+// Sometimes Bit String is used as a container (RSA Pub Spki)
+ASN1.CTYPES = [0x30, 0x31, 0xa0, 0xa1];
+ASN1.VTYPES = [0x01, 0x02, 0x05, 0x06, 0x0c, 0x82];
+ASN1.parseVerbose = function parseAsn1Helper(buf, opts) {
+ if (!opts) {
+ opts = {};
+ }
+ //var ws = ' ';
+ function parseAsn1(buf, depth, eager) {
+ if (depth.length >= ASN1.EDEEPN) {
+ throw new Error(ASN1.EDEEP);
+ }
+
+ var index = 2; // we know, at minimum, data starts after type (0) and lengthSize (1)
+ var asn1 = { type: buf[0], lengthSize: 0, length: buf[1] };
+ var child;
+ var iters = 0;
+ var adjust = 0;
+ var adjustedLen;
+
+ // Determine how many bytes the length uses, and what it is
+ if (0x80 & asn1.length) {
+ asn1.lengthSize = 0x7f & asn1.length;
+ // I think that buf->hex->int solves the problem of Endianness... not sure
+ asn1.length = parseInt(
+ Enc.bufToHex(buf.slice(index, index + asn1.lengthSize)),
+ 16
+ );
+ index += asn1.lengthSize;
+ }
+
+ // High-order bit Integers have a leading 0x00 to signify that they are positive.
+ // Bit Streams use the first byte to signify padding, which x.509 doesn't use.
+ if (0x00 === buf[index] && (0x02 === asn1.type || 0x03 === asn1.type)) {
+ // However, 0x00 on its own is a valid number
+ if (asn1.length > 1) {
+ index += 1;
+ adjust = -1;
+ }
+ }
+ adjustedLen = asn1.length + adjust;
+
+ //console.warn(depth.join(ws) + '0x' + Enc.numToHex(asn1.type), index, 'len:', asn1.length, asn1);
+
+ function parseChildren(eager) {
+ asn1.children = [];
+ //console.warn('1 len:', (2 + asn1.lengthSize + asn1.length), 'idx:', index, 'clen:', 0);
+ while (
+ iters < ASN1.ELOOPN &&
+ index < 2 + asn1.length + asn1.lengthSize
+ ) {
+ iters += 1;
+ depth.length += 1;
+ child = parseAsn1(
+ buf.slice(index, index + adjustedLen),
+ depth,
+ eager
+ );
+ depth.length -= 1;
+ // The numbers don't match up exactly and I don't remember why...
+ // probably something with adjustedLen or some such, but the tests pass
+ index += 2 + child.lengthSize + child.length;
+ //console.warn('2 len:', (2 + asn1.lengthSize + asn1.length), 'idx:', index, 'clen:', (2 + child.lengthSize + child.length));
+ if (index > 2 + asn1.lengthSize + asn1.length) {
+ if (!eager) {
+ console.error(JSON.stringify(asn1, ASN1._replacer, 2));
+ }
+ throw new Error(
+ 'Parse error: child value length (' +
+ child.length +
+ ') is greater than remaining parent length (' +
+ (asn1.length - index) +
+ ' = ' +
+ asn1.length +
+ ' - ' +
+ index +
+ ')'
+ );
+ }
+ asn1.children.push(child);
+ //console.warn(depth.join(ws) + '0x' + Enc.numToHex(asn1.type), index, 'len:', asn1.length, asn1);
+ }
+ if (index !== 2 + asn1.lengthSize + asn1.length) {
+ //console.warn('index:', index, 'length:', (2 + asn1.lengthSize + asn1.length));
+ throw new Error('premature end-of-file');
+ }
+ if (iters >= ASN1.ELOOPN) {
+ throw new Error(ASN1.ELOOP);
+ }
+
+ delete asn1.value;
+ return asn1;
+ }
+
+ // Recurse into types that are _always_ containers
+ if (-1 !== ASN1.CTYPES.indexOf(asn1.type)) {
+ return parseChildren(eager);
+ }
+
+ // Return types that are _always_ values
+ asn1.value = buf.slice(index, index + adjustedLen);
+ if (opts.json) {
+ asn1.value = Enc.bufToHex(asn1.value);
+ }
+ if (-1 !== ASN1.VTYPES.indexOf(asn1.type)) {
+ return asn1;
+ }
+
+ // For ambigious / unknown types, recurse and return on failure
+ // (and return child array size to zero)
+ try {
+ return parseChildren(true);
+ } catch (e) {
+ asn1.children.length = 0;
+ return asn1;
+ }
+ }
+
+ var asn1 = parseAsn1(buf, []);
+ var len = buf.byteLength || buf.length;
+ if (len !== 2 + asn1.lengthSize + asn1.length) {
+ throw new Error(
+ 'Length of buffer does not match length of ASN.1 sequence.'
+ );
+ }
+ return asn1;
+};
+ASN1._toArray = function toArray(next, opts) {
+ var typ = opts.json ? Enc.numToHex(next.type) : next.type;
+ var val = next.value;
+ if (val) {
+ if ('string' !== typeof val && opts.json) {
+ val = Enc.bufToHex(val);
+ }
+ return [typ, val];
+ }
+ return [
+ typ,
+ next.children.map(function(child) {
+ return toArray(child, opts);
+ })
+ ];
+};
+ASN1.parse = function(opts) {
+ var opts2 = { json: false !== opts.json };
+ var verbose = ASN1.parseVerbose(opts.der, opts2);
+ if (opts.verbose) {
+ return verbose;
+ }
+ return ASN1._toArray(verbose, opts2);
+};
+ASN1._replacer = function(k, v) {
+ if ('type' === k) {
+ return '0x' + Enc.numToHex(v);
+ }
+ if (v && 'value' === k) {
+ return '0x' + Enc.bufToHex(v.data || v);
+ }
+ return v;
+};
+
+
+//
+// Packer
+//
+
+// Almost every ASN.1 type that's important for CSR
+// can be represented generically with only a few rules.
+function Any(/*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;
+ if ('number' === typeof hex) {
+ hex = Enc.numToHex(hex);
+ }
+
+ // 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 += Enc.numToHex(0x80 + lenlen);
+ }
+ return hex + Enc.numToHex(str.length / 2) + str;
+}
+ASN1.Any = Any;
+
+// 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 Any('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 Any('03', '00' + str);
+};
+
+ASN1._toArray = function toArray(next, opts) {
+ var typ = opts.json ? Enc.numToHex(next.type) : next.type;
+ var val = next.value;
+ if (val) {
+ if ('string' !== typeof val && opts.json) {
+ val = Enc.bufToHex(val);
+ }
+ return [typ, val];
+ }
+ return [
+ typ,
+ next.children.map(function(child) {
+ return toArray(child, opts);
+ })
+ ];
+};
+
+ASN1._pack = function(arr) {
+ var typ = arr[0];
+ if ('number' === typeof arr[0]) {
+ typ = Enc.numToHex(arr[0]);
+ }
+ var str = '';
+ if (Array.isArray(arr[1])) {
+ arr[1].forEach(function(a) {
+ str += ASN1._pack(a);
+ });
+ } else if ('string' === typeof arr[1]) {
+ str = arr[1];
+ } else if (arr[1].byteLength) {
+ str = Enc.bufToHex(arr[1]);
+ } else {
+ throw new Error('unexpected array');
+ }
+ if ('03' === typ) {
+ return ASN1.BitStr(str);
+ } else if ('02' === typ) {
+ return ASN1.UInt(str);
+ } else {
+ return Any(typ, str);
+ }
+};
+
+// TODO should this return a buffer?
+ASN1.pack = function(asn1, opts) {
+ if (!opts) {
+ opts = {};
+ }
+ if (!Array.isArray(asn1)) {
+ asn1 = ASN1._toArray(asn1, { json: true });
+ }
+ var result = ASN1._pack(asn1);
+ if (opts.json) {
+ return result;
+ }
+ return Enc.hexToBuf(result);
+};
+}());
diff --git a/dist/asn1.all.min.js b/dist/asn1.all.min.js
new file mode 100644
index 0000000..6ea75f9
--- /dev/null
+++ b/dist/asn1.all.min.js
@@ -0,0 +1 @@
+(function(){"use strict";var Enc=window.Encoding={};Enc.bufToBase64=function(u8){var bin="";u8.forEach(function(i){bin+=String.fromCharCode(i)});return btoa(bin)};Enc.strToBase64=function(str){return btoa(Enc.strToBin(str))};function _base64ToBin(b64){return atob(Enc.urlBase64ToBase64(b64))}Enc._base64ToBin=_base64ToBin;Enc.base64ToBuf=function(b64){return Enc.binToBuf(_base64ToBin(b64))};Enc.base64ToStr=function(b64){return Enc.binToStr(_base64ToBin(b64))};Enc.urlBase64ToBase64=function(u64){var r=u64%4;if(2===r){u64+="=="}else if(3===r){u64+="="}return u64.replace(/-/g,"+").replace(/_/g,"/")};Enc.base64ToUrlBase64=function(b64){return b64.replace(/\+/g,"-").replace(/\//g,"_").replace(/=/g,"")};Enc.bufToUrlBase64=function(buf){return Enc.base64ToUrlBase64(Enc.bufToBase64(buf))};Enc.strToUrlBase64=function(str){return Enc.bufToUrlBase64(Enc.strToBuf(str))};Enc.bufToHex=function(u8){var hex=[];var i,h;var len=u8.byteLength||u8.length;for(i=0;i=ASN1.EDEEPN){throw new Error(ASN1.EDEEP)}var index=2;var asn1={type:buf[0],lengthSize:0,length:buf[1]};var child;var iters=0;var adjust=0;var adjustedLen;if(128&asn1.length){asn1.lengthSize=127&asn1.length;asn1.length=parseInt(Enc.bufToHex(buf.slice(index,index+asn1.lengthSize)),16);index+=asn1.lengthSize}if(0===buf[index]&&(2===asn1.type||3===asn1.type)){if(asn1.length>1){index+=1;adjust=-1}}adjustedLen=asn1.length+adjust;function parseChildren(eager){asn1.children=[];while(iters2+asn1.lengthSize+asn1.length){if(!eager){console.error(JSON.stringify(asn1,ASN1._replacer,2))}throw new Error("Parse error: child value length ("+child.length+") is greater than remaining parent length ("+(asn1.length-index)+" = "+asn1.length+" - "+index+")")}asn1.children.push(child)}if(index!==2+asn1.lengthSize+asn1.length){throw new Error("premature end-of-file")}if(iters>=ASN1.ELOOPN){throw new Error(ASN1.ELOOP)}delete asn1.value;return asn1}if(-1!==ASN1.CTYPES.indexOf(asn1.type)){return parseChildren(eager)}asn1.value=buf.slice(index,index+adjustedLen);if(opts.json){asn1.value=Enc.bufToHex(asn1.value)}if(-1!==ASN1.VTYPES.indexOf(asn1.type)){return asn1}try{return parseChildren(true)}catch(e){asn1.children.length=0;return asn1}}var asn1=parseAsn1(buf,[]);var len=buf.byteLength||buf.length;if(len!==2+asn1.lengthSize+asn1.length){throw new Error("Length of buffer does not match length of ASN.1 sequence.")}return asn1};ASN1._toArray=function toArray(next,opts){var typ=opts.json?Enc.numToHex(next.type):next.type;var val=next.value;if(val){if("string"!==typeof val&&opts.json){val=Enc.bufToHex(val)}return[typ,val]}return[typ,next.children.map(function(child){return toArray(child,opts)})]};ASN1.parse=function(opts){var opts2={json:false!==opts.json};var verbose=ASN1.parseVerbose(opts.der,opts2);if(opts.verbose){return verbose}return ASN1._toArray(verbose,opts2)};ASN1._replacer=function(k,v){if("type"===k){return"0x"+Enc.numToHex(v)}if(v&&"value"===k){return"0x"+Enc.bufToHex(v.data||v)}return v};function Any(){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;if("number"===typeof hex){hex=Enc.numToHex(hex)}if(len!==Math.round(len)){throw new Error("invalid hex")}if(len>127){lenlen+=1;while(len>255){lenlen+=1;len=len>>8}}if(lenlen){hex+=Enc.numToHex(128+lenlen)}return hex+Enc.numToHex(str.length/2)+str}ASN1.Any=Any;ASN1.UInt=function UINT(){var str=Array.prototype.slice.call(arguments).join("");var first=parseInt(str.slice(0,2),16);if(128&first){str="00"+str}return Any("02",str)};ASN1.BitStr=function BITSTR(){var str=Array.prototype.slice.call(arguments).join("");return Any("03","00"+str)};ASN1._toArray=function toArray(next,opts){var typ=opts.json?Enc.numToHex(next.type):next.type;var val=next.value;if(val){if("string"!==typeof val&&opts.json){val=Enc.bufToHex(val)}return[typ,val]}return[typ,next.children.map(function(child){return toArray(child,opts)})]};ASN1._pack=function(arr){var typ=arr[0];if("number"===typeof arr[0]){typ=Enc.numToHex(arr[0])}var str="";if(Array.isArray(arr[1])){arr[1].forEach(function(a){str+=ASN1._pack(a)})}else if("string"===typeof arr[1]){str=arr[1]}else if(arr[1].byteLength){str=Enc.bufToHex(arr[1])}else{throw new Error("unexpected array")}if("03"===typ){return ASN1.BitStr(str)}else if("02"===typ){return ASN1.UInt(str)}else{return Any(typ,str)}};ASN1.pack=function(asn1,opts){if(!opts){opts={}}if(!Array.isArray(asn1)){asn1=ASN1._toArray(asn1,{json:true})}var result=ASN1._pack(asn1);if(opts.json){return result}return Enc.hexToBuf(result)}})();
diff --git a/dist/asn1.js b/dist/asn1.js
new file mode 100644
index 0000000..49efad6
--- /dev/null
+++ b/dist/asn1.js
@@ -0,0 +1,319 @@
+;(function () {
+'use strict';
+var ASN1 = window.ASN1 = {};
+var Enc = window.Encoding;
+// Copyright 2018 AJ ONeal. All rights reserved
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+
+//
+// Parser
+//
+
+// Although I've only seen 9 max in https certificates themselves,
+// but each domain list could have up to 100
+ASN1.ELOOPN = 102;
+ASN1.ELOOP =
+ 'uASN1.js Error: iterated over ' +
+ ASN1.ELOOPN +
+ '+ elements (probably a malformed file)';
+// I've seen https certificates go 29 deep
+ASN1.EDEEPN = 60;
+ASN1.EDEEP =
+ 'uASN1.js Error: element nested ' +
+ ASN1.EDEEPN +
+ '+ layers deep (probably a malformed file)';
+// Container Types are Sequence 0x30, Container Array? (0xA0, 0xA1)
+// Value Types are Boolean 0x01, Integer 0x02, Null 0x05, Object ID 0x06, String 0x0C, 0x16, 0x13, 0x1e Value Array? (0x82)
+// Bit String (0x03) and Octet String (0x04) may be values or containers
+// Sometimes Bit String is used as a container (RSA Pub Spki)
+ASN1.CTYPES = [0x30, 0x31, 0xa0, 0xa1];
+ASN1.VTYPES = [0x01, 0x02, 0x05, 0x06, 0x0c, 0x82];
+ASN1.parseVerbose = function parseAsn1Helper(buf, opts) {
+ if (!opts) {
+ opts = {};
+ }
+ //var ws = ' ';
+ function parseAsn1(buf, depth, eager) {
+ if (depth.length >= ASN1.EDEEPN) {
+ throw new Error(ASN1.EDEEP);
+ }
+
+ var index = 2; // we know, at minimum, data starts after type (0) and lengthSize (1)
+ var asn1 = { type: buf[0], lengthSize: 0, length: buf[1] };
+ var child;
+ var iters = 0;
+ var adjust = 0;
+ var adjustedLen;
+
+ // Determine how many bytes the length uses, and what it is
+ if (0x80 & asn1.length) {
+ asn1.lengthSize = 0x7f & asn1.length;
+ // I think that buf->hex->int solves the problem of Endianness... not sure
+ asn1.length = parseInt(
+ Enc.bufToHex(buf.slice(index, index + asn1.lengthSize)),
+ 16
+ );
+ index += asn1.lengthSize;
+ }
+
+ // High-order bit Integers have a leading 0x00 to signify that they are positive.
+ // Bit Streams use the first byte to signify padding, which x.509 doesn't use.
+ if (0x00 === buf[index] && (0x02 === asn1.type || 0x03 === asn1.type)) {
+ // However, 0x00 on its own is a valid number
+ if (asn1.length > 1) {
+ index += 1;
+ adjust = -1;
+ }
+ }
+ adjustedLen = asn1.length + adjust;
+
+ //console.warn(depth.join(ws) + '0x' + Enc.numToHex(asn1.type), index, 'len:', asn1.length, asn1);
+
+ function parseChildren(eager) {
+ asn1.children = [];
+ //console.warn('1 len:', (2 + asn1.lengthSize + asn1.length), 'idx:', index, 'clen:', 0);
+ while (
+ iters < ASN1.ELOOPN &&
+ index < 2 + asn1.length + asn1.lengthSize
+ ) {
+ iters += 1;
+ depth.length += 1;
+ child = parseAsn1(
+ buf.slice(index, index + adjustedLen),
+ depth,
+ eager
+ );
+ depth.length -= 1;
+ // The numbers don't match up exactly and I don't remember why...
+ // probably something with adjustedLen or some such, but the tests pass
+ index += 2 + child.lengthSize + child.length;
+ //console.warn('2 len:', (2 + asn1.lengthSize + asn1.length), 'idx:', index, 'clen:', (2 + child.lengthSize + child.length));
+ if (index > 2 + asn1.lengthSize + asn1.length) {
+ if (!eager) {
+ console.error(JSON.stringify(asn1, ASN1._replacer, 2));
+ }
+ throw new Error(
+ 'Parse error: child value length (' +
+ child.length +
+ ') is greater than remaining parent length (' +
+ (asn1.length - index) +
+ ' = ' +
+ asn1.length +
+ ' - ' +
+ index +
+ ')'
+ );
+ }
+ asn1.children.push(child);
+ //console.warn(depth.join(ws) + '0x' + Enc.numToHex(asn1.type), index, 'len:', asn1.length, asn1);
+ }
+ if (index !== 2 + asn1.lengthSize + asn1.length) {
+ //console.warn('index:', index, 'length:', (2 + asn1.lengthSize + asn1.length));
+ throw new Error('premature end-of-file');
+ }
+ if (iters >= ASN1.ELOOPN) {
+ throw new Error(ASN1.ELOOP);
+ }
+
+ delete asn1.value;
+ return asn1;
+ }
+
+ // Recurse into types that are _always_ containers
+ if (-1 !== ASN1.CTYPES.indexOf(asn1.type)) {
+ return parseChildren(eager);
+ }
+
+ // Return types that are _always_ values
+ asn1.value = buf.slice(index, index + adjustedLen);
+ if (opts.json) {
+ asn1.value = Enc.bufToHex(asn1.value);
+ }
+ if (-1 !== ASN1.VTYPES.indexOf(asn1.type)) {
+ return asn1;
+ }
+
+ // For ambigious / unknown types, recurse and return on failure
+ // (and return child array size to zero)
+ try {
+ return parseChildren(true);
+ } catch (e) {
+ asn1.children.length = 0;
+ return asn1;
+ }
+ }
+
+ var asn1 = parseAsn1(buf, []);
+ var len = buf.byteLength || buf.length;
+ if (len !== 2 + asn1.lengthSize + asn1.length) {
+ throw new Error(
+ 'Length of buffer does not match length of ASN.1 sequence.'
+ );
+ }
+ return asn1;
+};
+ASN1._toArray = function toArray(next, opts) {
+ var typ = opts.json ? Enc.numToHex(next.type) : next.type;
+ var val = next.value;
+ if (val) {
+ if ('string' !== typeof val && opts.json) {
+ val = Enc.bufToHex(val);
+ }
+ return [typ, val];
+ }
+ return [
+ typ,
+ next.children.map(function(child) {
+ return toArray(child, opts);
+ })
+ ];
+};
+ASN1.parse = function(opts) {
+ var opts2 = { json: false !== opts.json };
+ var verbose = ASN1.parseVerbose(opts.der, opts2);
+ if (opts.verbose) {
+ return verbose;
+ }
+ return ASN1._toArray(verbose, opts2);
+};
+ASN1._replacer = function(k, v) {
+ if ('type' === k) {
+ return '0x' + Enc.numToHex(v);
+ }
+ if (v && 'value' === k) {
+ return '0x' + Enc.bufToHex(v.data || v);
+ }
+ return v;
+};
+
+
+//
+// Packer
+//
+
+// Almost every ASN.1 type that's important for CSR
+// can be represented generically with only a few rules.
+function Any(/*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;
+ if ('number' === typeof hex) {
+ hex = Enc.numToHex(hex);
+ }
+
+ // 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 += Enc.numToHex(0x80 + lenlen);
+ }
+ return hex + Enc.numToHex(str.length / 2) + str;
+}
+ASN1.Any = Any;
+
+// 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 Any('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 Any('03', '00' + str);
+};
+
+ASN1._toArray = function toArray(next, opts) {
+ var typ = opts.json ? Enc.numToHex(next.type) : next.type;
+ var val = next.value;
+ if (val) {
+ if ('string' !== typeof val && opts.json) {
+ val = Enc.bufToHex(val);
+ }
+ return [typ, val];
+ }
+ return [
+ typ,
+ next.children.map(function(child) {
+ return toArray(child, opts);
+ })
+ ];
+};
+
+ASN1._pack = function(arr) {
+ var typ = arr[0];
+ if ('number' === typeof arr[0]) {
+ typ = Enc.numToHex(arr[0]);
+ }
+ var str = '';
+ if (Array.isArray(arr[1])) {
+ arr[1].forEach(function(a) {
+ str += ASN1._pack(a);
+ });
+ } else if ('string' === typeof arr[1]) {
+ str = arr[1];
+ } else if (arr[1].byteLength) {
+ str = Enc.bufToHex(arr[1]);
+ } else {
+ throw new Error('unexpected array');
+ }
+ if ('03' === typ) {
+ return ASN1.BitStr(str);
+ } else if ('02' === typ) {
+ return ASN1.UInt(str);
+ } else {
+ return Any(typ, str);
+ }
+};
+
+// TODO should this return a buffer?
+ASN1.pack = function(asn1, opts) {
+ if (!opts) {
+ opts = {};
+ }
+ if (!Array.isArray(asn1)) {
+ asn1 = ASN1._toArray(asn1, { json: true });
+ }
+ var result = ASN1._pack(asn1);
+ if (opts.json) {
+ return result;
+ }
+ return Enc.hexToBuf(result);
+};
+}());
diff --git a/dist/asn1.min.js b/dist/asn1.min.js
new file mode 100644
index 0000000..5589cd1
--- /dev/null
+++ b/dist/asn1.min.js
@@ -0,0 +1 @@
+(function(){"use strict";var ASN1=window.ASN1={};var Enc=window.Encoding;ASN1.ELOOPN=102;ASN1.ELOOP="uASN1.js Error: iterated over "+ASN1.ELOOPN+"+ elements (probably a malformed file)";ASN1.EDEEPN=60;ASN1.EDEEP="uASN1.js Error: element nested "+ASN1.EDEEPN+"+ layers deep (probably a malformed file)";ASN1.CTYPES=[48,49,160,161];ASN1.VTYPES=[1,2,5,6,12,130];ASN1.parseVerbose=function parseAsn1Helper(buf,opts){if(!opts){opts={}}function parseAsn1(buf,depth,eager){if(depth.length>=ASN1.EDEEPN){throw new Error(ASN1.EDEEP)}var index=2;var asn1={type:buf[0],lengthSize:0,length:buf[1]};var child;var iters=0;var adjust=0;var adjustedLen;if(128&asn1.length){asn1.lengthSize=127&asn1.length;asn1.length=parseInt(Enc.bufToHex(buf.slice(index,index+asn1.lengthSize)),16);index+=asn1.lengthSize}if(0===buf[index]&&(2===asn1.type||3===asn1.type)){if(asn1.length>1){index+=1;adjust=-1}}adjustedLen=asn1.length+adjust;function parseChildren(eager){asn1.children=[];while(iters2+asn1.lengthSize+asn1.length){if(!eager){console.error(JSON.stringify(asn1,ASN1._replacer,2))}throw new Error("Parse error: child value length ("+child.length+") is greater than remaining parent length ("+(asn1.length-index)+" = "+asn1.length+" - "+index+")")}asn1.children.push(child)}if(index!==2+asn1.lengthSize+asn1.length){throw new Error("premature end-of-file")}if(iters>=ASN1.ELOOPN){throw new Error(ASN1.ELOOP)}delete asn1.value;return asn1}if(-1!==ASN1.CTYPES.indexOf(asn1.type)){return parseChildren(eager)}asn1.value=buf.slice(index,index+adjustedLen);if(opts.json){asn1.value=Enc.bufToHex(asn1.value)}if(-1!==ASN1.VTYPES.indexOf(asn1.type)){return asn1}try{return parseChildren(true)}catch(e){asn1.children.length=0;return asn1}}var asn1=parseAsn1(buf,[]);var len=buf.byteLength||buf.length;if(len!==2+asn1.lengthSize+asn1.length){throw new Error("Length of buffer does not match length of ASN.1 sequence.")}return asn1};ASN1._toArray=function toArray(next,opts){var typ=opts.json?Enc.numToHex(next.type):next.type;var val=next.value;if(val){if("string"!==typeof val&&opts.json){val=Enc.bufToHex(val)}return[typ,val]}return[typ,next.children.map(function(child){return toArray(child,opts)})]};ASN1.parse=function(opts){var opts2={json:false!==opts.json};var verbose=ASN1.parseVerbose(opts.der,opts2);if(opts.verbose){return verbose}return ASN1._toArray(verbose,opts2)};ASN1._replacer=function(k,v){if("type"===k){return"0x"+Enc.numToHex(v)}if(v&&"value"===k){return"0x"+Enc.bufToHex(v.data||v)}return v};function Any(){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;if("number"===typeof hex){hex=Enc.numToHex(hex)}if(len!==Math.round(len)){throw new Error("invalid hex")}if(len>127){lenlen+=1;while(len>255){lenlen+=1;len=len>>8}}if(lenlen){hex+=Enc.numToHex(128+lenlen)}return hex+Enc.numToHex(str.length/2)+str}ASN1.Any=Any;ASN1.UInt=function UINT(){var str=Array.prototype.slice.call(arguments).join("");var first=parseInt(str.slice(0,2),16);if(128&first){str="00"+str}return Any("02",str)};ASN1.BitStr=function BITSTR(){var str=Array.prototype.slice.call(arguments).join("");return Any("03","00"+str)};ASN1._toArray=function toArray(next,opts){var typ=opts.json?Enc.numToHex(next.type):next.type;var val=next.value;if(val){if("string"!==typeof val&&opts.json){val=Enc.bufToHex(val)}return[typ,val]}return[typ,next.children.map(function(child){return toArray(child,opts)})]};ASN1._pack=function(arr){var typ=arr[0];if("number"===typeof arr[0]){typ=Enc.numToHex(arr[0])}var str="";if(Array.isArray(arr[1])){arr[1].forEach(function(a){str+=ASN1._pack(a)})}else if("string"===typeof arr[1]){str=arr[1]}else if(arr[1].byteLength){str=Enc.bufToHex(arr[1])}else{throw new Error("unexpected array")}if("03"===typ){return ASN1.BitStr(str)}else if("02"===typ){return ASN1.UInt(str)}else{return Any(typ,str)}};ASN1.pack=function(asn1,opts){if(!opts){opts={}}if(!Array.isArray(asn1)){asn1=ASN1._toArray(asn1,{json:true})}var result=ASN1._pack(asn1);if(opts.json){return result}return Enc.hexToBuf(result)}})();
diff --git a/dist/index.html b/dist/index.html
new file mode 100644
index 0000000..3d70692
--- /dev/null
+++ b/dist/index.html
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
diff --git a/index.js b/index.js
new file mode 100644
index 0000000..a1765f2
--- /dev/null
+++ b/index.js
@@ -0,0 +1,11 @@
+'use strict';
+
+var ASN1 = module.exports;
+var packer = require('./packer.js');
+var parser = require('./parser.js');
+Object.keys(parser).forEach(function(key) {
+ ASN1[key] = parser[key];
+});
+Object.keys(packer).forEach(function(key) {
+ ASN1[key] = packer[key];
+});
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..1455d1e
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,19 @@
+{
+ "name": "@root/asn1",
+ "version": "1.0.0",
+ "lockfileVersion": 1,
+ "requires": true,
+ "dependencies": {
+ "@root/encoding": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@root/encoding/-/encoding-1.0.1.tgz",
+ "integrity": "sha512-OaEub02ufoU038gy6bsNHQOjIn8nUjGiLcaRmJ40IUykneJkIW5fxDqKxQx48cszuNflYldsJLPPXCrGfHs8yQ=="
+ },
+ "@root/pem": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/@root/pem/-/pem-1.0.3.tgz",
+ "integrity": "sha512-6iFwsbwm6YzWdfjogHzLTYkA1KWdeEkutVX2BBVfhyWoE9q0vp89G7mAcLIhi0QTRd199AMOacHWFq+gTyQkVA==",
+ "dev": true
+ }
+ }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..b983dc6
--- /dev/null
+++ b/package.json
@@ -0,0 +1,36 @@
+{
+ "name": "@root/asn1",
+ "version": "1.0.0",
+ "description": "VanillaJS, Lightweight, Zero-Dependency, ASN.1 encoder and decoder.",
+ "main": "index.js",
+ "browser": {
+ "./node/native.js": "./browser/native.js"
+ },
+ "files": [
+ "*.js",
+ "node",
+ "browser",
+ "dist"
+ ],
+ "scripts": {
+ "test": "node tests"
+ },
+ "repository": {
+ "type": "git",
+ "url": "https://git.rootprojects.org/root/asn1.js.git"
+ },
+ "keywords": [
+ "ASN.1",
+ "asn1",
+ "x509",
+ "PEM"
+ ],
+ "author": "AJ ONeal (https://coolaj86.com/)",
+ "license": "MPL-2.0",
+ "devDependencies": {
+ "@root/pem": "^1.0.3"
+ },
+ "dependencies": {
+ "@root/encoding": "^1.0.1"
+ }
+}
diff --git a/packer.js b/packer.js
new file mode 100644
index 0000000..b349d7d
--- /dev/null
+++ b/packer.js
@@ -0,0 +1,131 @@
+'use strict';
+
+var ASN1 = module.exports;
+var Enc = require('@root/encoding/hex');
+
+//
+// Packer
+//
+
+// Almost every ASN.1 type that's important for CSR
+// can be represented generically with only a few rules.
+function Any(/*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;
+ if ('number' === typeof hex) {
+ hex = Enc.numToHex(hex);
+ }
+
+ // 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 += Enc.numToHex(0x80 + lenlen);
+ }
+ return hex + Enc.numToHex(str.length / 2) + str;
+}
+ASN1.Any = Any;
+
+// 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 Any('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 Any('03', '00' + str);
+};
+
+ASN1._toArray = function toArray(next, opts) {
+ var typ = opts.json ? Enc.numToHex(next.type) : next.type;
+ var val = next.value;
+ if (val) {
+ if ('string' !== typeof val && opts.json) {
+ val = Enc.bufToHex(val);
+ }
+ return [typ, val];
+ }
+ return [
+ typ,
+ next.children.map(function(child) {
+ return toArray(child, opts);
+ })
+ ];
+};
+
+ASN1._pack = function(arr) {
+ var typ = arr[0];
+ if ('number' === typeof arr[0]) {
+ typ = Enc.numToHex(arr[0]);
+ }
+ var str = '';
+ if (Array.isArray(arr[1])) {
+ arr[1].forEach(function(a) {
+ str += ASN1._pack(a);
+ });
+ } else if ('string' === typeof arr[1]) {
+ str = arr[1];
+ } else if (arr[1].byteLength) {
+ str = Enc.bufToHex(arr[1]);
+ } else {
+ throw new Error('unexpected array');
+ }
+ if ('03' === typ) {
+ return ASN1.BitStr(str);
+ } else if ('02' === typ) {
+ return ASN1.UInt(str);
+ } else {
+ return Any(typ, str);
+ }
+};
+
+// TODO should this return a buffer?
+ASN1.pack = function(asn1, opts) {
+ if (!opts) {
+ opts = {};
+ }
+ if (!Array.isArray(asn1)) {
+ asn1 = ASN1._toArray(asn1, { json: true });
+ }
+ var result = ASN1._pack(asn1);
+ if (opts.json) {
+ return result;
+ }
+ return Enc.hexToBuf(result);
+};
diff --git a/parser.js b/parser.js
new file mode 100644
index 0000000..64d0682
--- /dev/null
+++ b/parser.js
@@ -0,0 +1,189 @@
+// Copyright 2018 AJ ONeal. All rights reserved
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+'use strict';
+
+var ASN1 = module.exports;
+var Enc = require('@root/encoding/hex');
+
+//
+// Parser
+//
+
+// Although I've only seen 9 max in https certificates themselves,
+// but each domain list could have up to 100
+ASN1.ELOOPN = 102;
+ASN1.ELOOP =
+ 'uASN1.js Error: iterated over ' +
+ ASN1.ELOOPN +
+ '+ elements (probably a malformed file)';
+// I've seen https certificates go 29 deep
+ASN1.EDEEPN = 60;
+ASN1.EDEEP =
+ 'uASN1.js Error: element nested ' +
+ ASN1.EDEEPN +
+ '+ layers deep (probably a malformed file)';
+// Container Types are Sequence 0x30, Container Array? (0xA0, 0xA1)
+// Value Types are Boolean 0x01, Integer 0x02, Null 0x05, Object ID 0x06, String 0x0C, 0x16, 0x13, 0x1e Value Array? (0x82)
+// Bit String (0x03) and Octet String (0x04) may be values or containers
+// Sometimes Bit String is used as a container (RSA Pub Spki)
+ASN1.CTYPES = [0x30, 0x31, 0xa0, 0xa1];
+ASN1.VTYPES = [0x01, 0x02, 0x05, 0x06, 0x0c, 0x82];
+ASN1.parseVerbose = function parseAsn1Helper(buf, opts) {
+ if (!opts) {
+ opts = {};
+ }
+ //var ws = ' ';
+ function parseAsn1(buf, depth, eager) {
+ if (depth.length >= ASN1.EDEEPN) {
+ throw new Error(ASN1.EDEEP);
+ }
+
+ var index = 2; // we know, at minimum, data starts after type (0) and lengthSize (1)
+ var asn1 = { type: buf[0], lengthSize: 0, length: buf[1] };
+ var child;
+ var iters = 0;
+ var adjust = 0;
+ var adjustedLen;
+
+ // Determine how many bytes the length uses, and what it is
+ if (0x80 & asn1.length) {
+ asn1.lengthSize = 0x7f & asn1.length;
+ // I think that buf->hex->int solves the problem of Endianness... not sure
+ asn1.length = parseInt(
+ Enc.bufToHex(buf.slice(index, index + asn1.lengthSize)),
+ 16
+ );
+ index += asn1.lengthSize;
+ }
+
+ // High-order bit Integers have a leading 0x00 to signify that they are positive.
+ // Bit Streams use the first byte to signify padding, which x.509 doesn't use.
+ if (0x00 === buf[index] && (0x02 === asn1.type || 0x03 === asn1.type)) {
+ // However, 0x00 on its own is a valid number
+ if (asn1.length > 1) {
+ index += 1;
+ adjust = -1;
+ }
+ }
+ adjustedLen = asn1.length + adjust;
+
+ //console.warn(depth.join(ws) + '0x' + Enc.numToHex(asn1.type), index, 'len:', asn1.length, asn1);
+
+ function parseChildren(eager) {
+ asn1.children = [];
+ //console.warn('1 len:', (2 + asn1.lengthSize + asn1.length), 'idx:', index, 'clen:', 0);
+ while (
+ iters < ASN1.ELOOPN &&
+ index < 2 + asn1.length + asn1.lengthSize
+ ) {
+ iters += 1;
+ depth.length += 1;
+ child = parseAsn1(
+ buf.slice(index, index + adjustedLen),
+ depth,
+ eager
+ );
+ depth.length -= 1;
+ // The numbers don't match up exactly and I don't remember why...
+ // probably something with adjustedLen or some such, but the tests pass
+ index += 2 + child.lengthSize + child.length;
+ //console.warn('2 len:', (2 + asn1.lengthSize + asn1.length), 'idx:', index, 'clen:', (2 + child.lengthSize + child.length));
+ if (index > 2 + asn1.lengthSize + asn1.length) {
+ if (!eager) {
+ console.error(JSON.stringify(asn1, ASN1._replacer, 2));
+ }
+ throw new Error(
+ 'Parse error: child value length (' +
+ child.length +
+ ') is greater than remaining parent length (' +
+ (asn1.length - index) +
+ ' = ' +
+ asn1.length +
+ ' - ' +
+ index +
+ ')'
+ );
+ }
+ asn1.children.push(child);
+ //console.warn(depth.join(ws) + '0x' + Enc.numToHex(asn1.type), index, 'len:', asn1.length, asn1);
+ }
+ if (index !== 2 + asn1.lengthSize + asn1.length) {
+ //console.warn('index:', index, 'length:', (2 + asn1.lengthSize + asn1.length));
+ throw new Error('premature end-of-file');
+ }
+ if (iters >= ASN1.ELOOPN) {
+ throw new Error(ASN1.ELOOP);
+ }
+
+ delete asn1.value;
+ return asn1;
+ }
+
+ // Recurse into types that are _always_ containers
+ if (-1 !== ASN1.CTYPES.indexOf(asn1.type)) {
+ return parseChildren(eager);
+ }
+
+ // Return types that are _always_ values
+ asn1.value = buf.slice(index, index + adjustedLen);
+ if (opts.json) {
+ asn1.value = Enc.bufToHex(asn1.value);
+ }
+ if (-1 !== ASN1.VTYPES.indexOf(asn1.type)) {
+ return asn1;
+ }
+
+ // For ambigious / unknown types, recurse and return on failure
+ // (and return child array size to zero)
+ try {
+ return parseChildren(true);
+ } catch (e) {
+ asn1.children.length = 0;
+ return asn1;
+ }
+ }
+
+ var asn1 = parseAsn1(buf, []);
+ var len = buf.byteLength || buf.length;
+ if (len !== 2 + asn1.lengthSize + asn1.length) {
+ throw new Error(
+ 'Length of buffer does not match length of ASN.1 sequence.'
+ );
+ }
+ return asn1;
+};
+ASN1._toArray = function toArray(next, opts) {
+ var typ = opts.json ? Enc.numToHex(next.type) : next.type;
+ var val = next.value;
+ if (val) {
+ if ('string' !== typeof val && opts.json) {
+ val = Enc.bufToHex(val);
+ }
+ return [typ, val];
+ }
+ return [
+ typ,
+ next.children.map(function(child) {
+ return toArray(child, opts);
+ })
+ ];
+};
+ASN1.parse = function(opts) {
+ var opts2 = { json: false !== opts.json };
+ var verbose = ASN1.parseVerbose(opts.der, opts2);
+ if (opts.verbose) {
+ return verbose;
+ }
+ return ASN1._toArray(verbose, opts2);
+};
+ASN1._replacer = function(k, v) {
+ if ('type' === k) {
+ return '0x' + Enc.numToHex(v);
+ }
+ if (v && 'value' === k) {
+ return '0x' + Enc.bufToHex(v.data || v);
+ }
+ return v;
+};
diff --git a/tests/index.js b/tests/index.js
new file mode 100644
index 0000000..c8a91a9
--- /dev/null
+++ b/tests/index.js
@@ -0,0 +1,123 @@
+'use strict';
+
+var ASN1 = require('../');
+var Enc = require('@root/encoding');
+var PEM = require('@root/pem');
+
+// 1.2.840.10045.3.1.7
+// prime256v1 (ANSI X9.62 named elliptic curve)
+var OBJ_ID_EC_256 = '06 08 2A8648CE3D030107';
+
+var jwk = {
+ crv: 'P-256',
+ d: 'LImWxqqTHbP3LHQfqscDSUzf_uNePGqf9U6ETEcO5Ho',
+ kty: 'EC',
+ x: 'vdjQ3T6VBX82LIKDzepYgRsz3HgRwp83yPuonu6vqos',
+ y: 'IUkEXtAMnppnV1A19sE2bJhUo4WPbq6EYgWxma4oGyg',
+ kid: 'MnfJYyS9W5gUjrJLdn8ePMzik8ZJz2qc-VZmKOs_oCw'
+};
+var d = Enc.base64ToHex(jwk.d);
+var x = Enc.base64ToHex(jwk.x);
+var y = Enc.base64ToHex(jwk.y);
+
+var der = Enc.hexToBuf(
+ ASN1.Any(
+ '30', // Sequence
+ ASN1.UInt('01'), // Integer (Version 1)
+ ASN1.Any('04', d), // Octet String
+ ASN1.Any('A0', OBJ_ID_EC_256), // [0] Object ID
+ ASN1.Any(
+ 'A1', // [1] Embedded EC/ASN1 public key
+ ASN1.BitStr('04' + x + y)
+ )
+ )
+);
+
+var pem1 = PEM.packBlock({
+ type: 'EC PRIVATE KEY',
+ bytes: der
+});
+
+var expected = [
+ '-----BEGIN EC PRIVATE KEY-----',
+ 'MHcCAQEEICyJlsaqkx2z9yx0H6rHA0lM3/7jXjxqn/VOhExHDuR6oAoGCCqGSM49',
+ 'AwEHoUQDQgAEvdjQ3T6VBX82LIKDzepYgRsz3HgRwp83yPuonu6vqoshSQRe0Aye',
+ 'mmdXUDX2wTZsmFSjhY9uroRiBbGZrigbKA==',
+ '-----END EC PRIVATE KEY-----'
+].join('\n');
+
+if (pem1 !== expected) {
+ throw new Error('Did not correctly Cascade pack EC P-256 JWK to DER');
+} else {
+ console.info('PASS: packed cascaded ASN1');
+}
+
+// Mix and match hex ints, hex strings, and byte arrays
+var asn1Arr = [
+ '30', // Sequence
+ [
+ [0x02, '01'], // Integer (Version 1)
+ [0x04, Buffer.from(d, 'hex')], // Octet String
+ ['a0', OBJ_ID_EC_256], // [0] Object ID
+ [
+ 0xa1, // [1] Embedded EC/ASN1 public key
+ ASN1.BitStr('04' + x + y)
+ ]
+ ]
+];
+
+var der2 = ASN1.pack(asn1Arr);
+var pem2 = PEM.packBlock({
+ type: 'EC PRIVATE KEY',
+ bytes: der2
+});
+
+if (pem2 !== expected) {
+ console.log(pem2);
+ console.log(expected);
+ throw new Error('Did not correctly Array pack EC P-256 JWK to DER');
+} else {
+ console.info('PASS: packed array-style ASN1');
+}
+
+var block = PEM.parseBlock(expected);
+var arr1 = ASN1.parse({ der: block.bytes });
+//console.log(JSON.stringify(arr1));
+var arr2 = ASN1.parse({ der: block.bytes, verbose: false, json: false });
+var obj3 = ASN1.parse({ der: block.bytes, verbose: true, json: true });
+//console.log(obj3);
+
+function eq(b1, b2) {
+ if (b1.byteLength !== b2.byteLength) {
+ return false;
+ }
+ return b1.every(function(b, i) {
+ return b === b2[i];
+ });
+}
+
+if (!eq(block.bytes, ASN1.pack(arr1))) {
+ throw new Error('packing hex array resulted in different bytes');
+} else {
+ console.log('PASS: packs parsed (hex) array');
+}
+
+if (!eq(block.bytes, ASN1.pack(arr2))) {
+ throw new Error('packing array with bytes resulted in different bytes');
+} else {
+ console.log('PASS: packs parsed array (with bytes)');
+}
+
+if (!eq(block.bytes, ASN1.pack(obj3))) {
+ console.log(block.bytes.toString('hex'));
+ console.log(ASN1.pack(obj3));
+ throw new Error('packing verbose object resulted in different bytes');
+} else {
+ console.log('PASS: packs parsed verbose object');
+}
+
+if (block.bytes.toString('hex') !== ASN1.pack(obj3, { json: true })) {
+ throw new Error('packing to hex resulted in different bytes');
+} else {
+ console.log('PASS: packs as hex when json: true');
+}