Compare commits

...

17 Commits

4 changed files with 160 additions and 101 deletions

View File

@ -1,29 +1,31 @@
# BlueCrypt ASN.1 Parser
# Bluecrypt ASN.1 Parser
An ASN.1 parser in less than 100 lines of Vanilla JavaScript,
part of the BlueCrypt suite.
An ASN.1 decoder in less than 100 lines of Vanilla JavaScript,
part of the Bluecrypt suite.
<br>
<small>(< 150 with newlines and comments)</small>
| < 100 lines of code | 1.1k gzipped | 2.5k minified | 4.7k with comments |
# Features
| <100 lines of code | 1.1k gzipped | 2.5k minified | 4.7k with comments |
* [x] Complete ASN.1 parser
* [x] Parses x.509 certificates
* [x] PEM (base64-encoded DER)
* [x] VanillaJS, Zero Dependencies
* [x] Browsers (back to ES5.1)
* [x] Browsers (even old ones)
* [x] Online [Demo](https://coolaj86.com/demos/asn1-parser/)
* [ ] Node.js (built, publishing soon)
* [ ] Online Demo (built, publishing soon)
![](https://i.imgur.com/gV7w7bM.png)
### Need an ASN.1 Builder too?
Check out https://git.coolaj86.com/coolaj86/asn1-packer.js/
<!--
# Demo
<https://coolaj86.com/demos/asn1-parser/>
-->
<img border="1" src="https://i.imgur.com/gV7w7bM.png" />
# Usage
@ -32,7 +34,7 @@ part of the BlueCrypt suite.
```
```js
'use strict";
'use strict';
var ASN1 = window.ASN1 // 62 lines
var Enc = window.Enc // 27 lines
@ -51,13 +53,13 @@ var json = ASN1.parse(der);
console.log(json);
```
```json
```js
{ "type": 48 /*0x30*/, "lengthSize": 0, "length": 89
, "children": [
{ "type": 48 /*0x30*/, "lengthSize": 0, "length": 19
, "children": [
{ "type": 6, "lengthSize": 0, "length": 7, "value": <0x2a8648ce3d0201> },
{ "type": 6, "lengthSize": 0, "length": 8, "value": <0x2a8648ce3d030107> }
{ "type": 6, "lengthSize": 0, "length": 7, "value": "<0x2a8648ce3d0201>" },
{ "type": 6, "lengthSize": 0, "length": 8, "value": "<0x2a8648ce3d030107>" }
]
},
{ "type": 3, "lengthSize": 0, "length": 66,
@ -71,14 +73,22 @@ Note: `value` will be a `Uint8Array`, not a hex string.
### Optimistic Parsing
This is a dumbed-down, minimal ASN1 parser.
This is a dumbed-down, minimal ASN1 parser
(though quite clever in its simplicity).
Rather than incorporating knowledge of each possible x509 schema
to know whether to traverse deeper into a value container,
it always tries to dive in (and backs out when parsing fails).
There are some ASN.1 types (at least Bit String and Octet String,
possibly others) that can be treated either as primitive values or
as container types base on the schema being used.
It is possible that it will produce false positives, but not likely
in real-world scenarios (PEM, x509, CSR, etc).
Rather than incorporating knowledge of each possible x509 schema,
this parser will return values for types that are _always_ values,
it recurse on types that are _always_ containers and, for ambigiuous
types, it will first try to recurse and, on error, will fall back to
returning a value.
In theory, it is possible that it will produce false positives,
but that would be highly unlikely in real-world scenarios
(PEM, x509, PKCS#1, SEC1, PKCS#8, SPKI, PKIX, CSR, etc).
I'd be interested to hear if you encounter such a case.
@ -92,5 +102,5 @@ to produce a small, focused file that does exactly what it needs to do.
# Legal
[BlueCrypt VanillaJS ASN.1 Parser](https://git.coolaj86.com/coolaj86/asn1-parser.js) |
[Bluecrypt VanillaJS ASN.1 Parser](https://git.coolaj86.com/coolaj86/asn1-parser.js) |
MPL-2.0

View File

@ -1,3 +1,7 @@
// 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/. */
;(function (exports) {
'use strict';
@ -13,91 +17,106 @@ var PEM = exports.PEM;
// Parser
//
ASN1.ELOOP = "uASN1.js Error: iterated over 15+ elements (probably a malformed file)";
ASN1.EDEEP = "uASN1.js Error: element nested 20+ layers deep (probably a malformed file)";
// Container Types are Sequence 0x30, Octect String 0x04, Array? (0xA0, 0xA1)
// Value Types are Integer 0x02, Bit String 0x03, Null 0x05, Object ID 0x06,
// 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.parse = function parseAsn1(buf, depth, ws) {
if (!ws) { ws = ''; }
if (!depth) { depth = 0; }
if (depth >= 20) { throw new Error(ASN1.EDEEP); }
ASN1.VTYPES = [ 0x01, 0x02, 0x05, 0x06, 0x0c, 0x82 ];
ASN1.parse = function parseAsn1Helper(buf) {
//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;
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;
// 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;
}
}
adjustedLen = asn1.length + adjust;
//console.warn(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 < 15 && index < (2 + asn1.length + asn1.lengthSize)) {
iters += 1;
child = ASN1.parse(buf.slice(index, index + adjustedLen), (depth || 0) + 1, ws + ' ');
// 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 + ")");
// 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;
}
asn1.children.push(child);
//console.warn(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");
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;
}
if (iters >= 15) { 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 (-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; }
}
// We want to fail if we know for sure that it's bad
if (-1 !== ASN1.CTYPES.indexOf(asn1.type)) {
return parseChildren();
}
asn1.value = buf.slice(index, index + adjustedLen);
try {
return parseChildren(true);
} catch(e) {
// leaving iterable array as a matter of convenience
asn1.children = [];
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._replacer = function (k, v) {
if ('type' === k) { return '0x' + Enc.numToHex(v); }
if ('value' === k) { return '0x' + Enc.bufToHex(v.data || v); }
if (v && 'value' === k) { return '0x' + Enc.bufToHex(v.data || v); }
return v;
};

View File

@ -1,7 +1,7 @@
<!DOCTYPE html>
<html>
<head>
<title>ASN.1 Parser - BlueCrypt</title>
<title>ASN.1 Parser - Bluecrypt</title>
<style>
textarea {
width: 42em;
@ -10,30 +10,57 @@
</style>
</head>
<body>
<h1>BlueCrypt ASN.1 Parser</h1>
<h1>Bluecrypt ASN.1 Parser</h1>
<h2>PEM (base64-encoded DER)</h2>
<textarea class="js-input" placeholder="Paste a PEM here">-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEIT1SWLxsacPiE5Z16jkopAn8/+85
rMjgyCokrnjDft6Y/YnA4A50yZe7CnFsqeDcpnPbubP6cpYiVcnevNIYyg==
-----END PUBLIC KEY-----</textarea>
<h2>Hex</h2>
<pre><code class="js-hex"> </code></pre>
<h2>ASN.1 Array</h2>
<pre><code class="js-array"> </code></pre>
<h2>ASN.1 Object</h2>
<pre><code class="js-json"> </code></pre>
<br>
<p>Made with <a href="https://git.coolaj86.com/coolaj86/asn1-parser.js/">asn1-parser.js</a></p>
<script src="./asn1-parser.js"></script>
<script>
var $input = document.querySelector('.js-input');
function toArray(next) {
console.log(next);
if (next.value) {
return [next.type, Enc.bufToHex(next.value)];
}
return [next.type, next.children.map(function (child) {
return toArray(child);
})];
}
function convert() {
console.log('change');
var pem = PEM.parseBlock(document.querySelector('.js-input').value);
var hex = Enc.bufToHex(pem.der);
console.log(hex);
document.querySelector('.js-hex').innerText = hex
.match(/.{2}/g).join(' ').match(/.{1,24}/g).join(' ').match(/.{1,50}/g).join('\n');
var json = ASN1.parse(pem.der);
console.log('keyup');
var json;
try {
var pem = PEM.parseBlock(document.querySelector('.js-input').value);
var hex = Enc.bufToHex(pem.der);
var arr = [];
document.querySelector('.js-hex').innerText = hex
.match(/.{2}/g).join(' ').match(/.{1,24}/g).join(' ').match(/.{1,50}/g).join('\n');
json = ASN1.parse(pem.der);
} catch(e) {
json = { error: { message: e.message } };
}
document.querySelector('.js-json').innerText = JSON.stringify(json, ASN1._replacer, 2);
document.querySelector('.js-array').innerText = JSON.stringify(toArray(json), null, 2);
}
$input.addEventListener('keyup', convert);

View File

@ -1,9 +1,12 @@
{
"name": "asn1-parser",
"version": "1.0.0",
"description": "An ASN.1 parser in less than 100 lines of Vanilla JavaScript, part of the BlueCrypt suite.",
"version": "1.1.8",
"description": "An ASN.1 parser in less than 100 lines of Vanilla JavaScript, part of the Bluecrypt suite.",
"homepage": "https://git.coolaj86.com/coolaj86/asn1-parser.js",
"main": "asn1-parser.js",
"scripts": {
"prepare": "uglifyjs asn1-parser.js > asn1-parser.min.js"
},
"directories": {
"lib": "lib"
},