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'); +}