Compare commits

..

No commits in common. "master" and "v1.0.1" have entirely different histories.

4 changed files with 100 additions and 159 deletions

View File

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

View File

@ -1,7 +1,3 @@
// 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) { ;(function (exports) {
'use strict'; 'use strict';
@ -17,23 +13,16 @@ var PEM = exports.PEM;
// Parser // Parser
// //
// Although I've only seen 9 max in https certificates themselves, ASN1.ELOOP = "uASN1.js Error: iterated over 15+ elements (probably a malformed file)";
// but each domain list could have up to 100 ASN1.EDEEP = "uASN1.js Error: element nested 20+ layers deep (probably a malformed file)";
ASN1.ELOOPN = 102; // Container Types are Sequence 0x30, Octect String 0x04, Array? (0xA0, 0xA1)
ASN1.ELOOP = "uASN1.js Error: iterated over " + ASN1.ELOOPN + "+ elements (probably a malformed file)"; // Value Types are Integer 0x02, Bit String 0x03, Null 0x05, Object ID 0x06,
// 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) // Sometimes Bit String is used as a container (RSA Pub Spki)
ASN1.CTYPES = [ 0x30, 0x31, 0xa0, 0xa1 ]; ASN1.CTYPES = [ 0x30, 0x31, 0xa0, 0xa1 ];
ASN1.VTYPES = [ 0x01, 0x02, 0x05, 0x06, 0x0c, 0x82 ]; ASN1.parse = function parseAsn1(buf, depth, ws) {
ASN1.parse = function parseAsn1Helper(buf) { if (!ws) { ws = ''; }
//var ws = ' '; if (!depth) { depth = 0; }
function parseAsn1(buf, depth, eager) { if (depth >= 20) { throw new Error(ASN1.EDEEP); }
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 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 asn1 = { type: buf[0], lengthSize: 0, length: buf[1] };
@ -61,16 +50,14 @@ ASN1.parse = function parseAsn1Helper(buf) {
} }
adjustedLen = asn1.length + adjust; adjustedLen = asn1.length + adjust;
//console.warn(depth.join(ws) + '0x' + Enc.numToHex(asn1.type), index, 'len:', asn1.length, asn1); //console.warn(ws + '0x' + Enc.numToHex(asn1.type), index, 'len:', asn1.length, asn1);
function parseChildren(eager) { function parseChildren(eager) {
asn1.children = []; asn1.children = [];
//console.warn('1 len:', (2 + asn1.lengthSize + asn1.length), 'idx:', index, 'clen:', 0); //console.warn('1 len:', (2 + asn1.lengthSize + asn1.length), 'idx:', index, 'clen:', 0);
while (iters < ASN1.ELOOPN && index < (2 + asn1.length + asn1.lengthSize)) { while (iters < 15 && index < (2 + asn1.length + asn1.lengthSize)) {
iters += 1; iters += 1;
depth.length += 1; child = ASN1.parse(buf.slice(index, index + adjustedLen), (depth || 0) + 1, ws + ' ');
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... // The numbers don't match up exactly and I don't remember why...
// probably something with adjustedLen or some such, but the tests pass // probably something with adjustedLen or some such, but the tests pass
index += (2 + child.lengthSize + child.length); index += (2 + child.lengthSize + child.length);
@ -82,41 +69,35 @@ ASN1.parse = function parseAsn1Helper(buf) {
+ " = " + asn1.length + " - " + index + ")"); + " = " + asn1.length + " - " + index + ")");
} }
asn1.children.push(child); asn1.children.push(child);
//console.warn(depth.join(ws) + '0x' + Enc.numToHex(asn1.type), index, 'len:', asn1.length, asn1); //console.warn(ws + '0x' + Enc.numToHex(asn1.type), index, 'len:', asn1.length, asn1);
} }
if (index !== (2 + asn1.lengthSize + asn1.length)) { if (index !== (2 + asn1.lengthSize + asn1.length)) {
//console.warn('index:', index, 'length:', (2 + asn1.lengthSize + asn1.length)); //console.warn('index:', index, 'length:', (2 + asn1.lengthSize + asn1.length));
throw new Error("premature end-of-file"); throw new Error("premature end-of-file");
} }
if (iters >= ASN1.ELOOPN) { throw new Error(ASN1.ELOOP); } if (iters >= 15) { throw new Error(ASN1.ELOOP); }
delete asn1.value; delete asn1.value;
return asn1; return asn1;
} }
// Recurse into types that are _always_ containers // We want to fail if we know for sure that it's bad
if (-1 !== ASN1.CTYPES.indexOf(asn1.type)) { return parseChildren(eager); } if (-1 !== ASN1.CTYPES.indexOf(asn1.type)) {
return parseChildren();
}
// Return types that are _always_ values
asn1.value = buf.slice(index, index + adjustedLen); asn1.value = buf.slice(index, index + adjustedLen);
if (-1 !== ASN1.VTYPES.indexOf(asn1.type)) { return asn1; } try {
return parseChildren(true);
// For ambigious / unknown types, recurse and return on failure } catch(e) {
// (and return child array size to zero) // leaving iterable array as a matter of convenience
try { return parseChildren(true); } asn1.children = [];
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; return asn1;
}
}; };
ASN1._replacer = function (k, v) { ASN1._replacer = function (k, v) {
if ('type' === k) { return '0x' + Enc.numToHex(v); } if ('type' === k) { return '0x' + Enc.numToHex(v); }
if (v && 'value' === k) { return '0x' + Enc.bufToHex(v.data || v); } if ('value' === k) { return '0x' + Enc.bufToHex(v.data || v); }
return v; return v;
}; };

View File

@ -12,55 +12,28 @@
<body> <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----- <textarea class="js-input" placeholder="Paste a PEM here">-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEIT1SWLxsacPiE5Z16jkopAn8/+85 MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEIT1SWLxsacPiE5Z16jkopAn8/+85
rMjgyCokrnjDft6Y/YnA4A50yZe7CnFsqeDcpnPbubP6cpYiVcnevNIYyg== rMjgyCokrnjDft6Y/YnA4A50yZe7CnFsqeDcpnPbubP6cpYiVcnevNIYyg==
-----END PUBLIC KEY-----</textarea> -----END PUBLIC KEY-----</textarea>
<h2>Hex</h2>
<pre><code class="js-hex"> </code></pre> <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> <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 src="./asn1-parser.js"></script>
<script> <script>
var $input = document.querySelector('.js-input'); 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() { function convert() {
console.log('keyup'); console.log('change');
var json;
try {
var pem = PEM.parseBlock(document.querySelector('.js-input').value); var pem = PEM.parseBlock(document.querySelector('.js-input').value);
var hex = Enc.bufToHex(pem.der); var hex = Enc.bufToHex(pem.der);
var arr = []; console.log(hex);
document.querySelector('.js-hex').innerText = hex document.querySelector('.js-hex').innerText = hex
.match(/.{2}/g).join(' ').match(/.{1,24}/g).join(' ').match(/.{1,50}/g).join('\n'); .match(/.{2}/g).join(' ').match(/.{1,24}/g).join(' ').match(/.{1,50}/g).join('\n');
json = ASN1.parse(pem.der); var 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-json').innerText = JSON.stringify(json, ASN1._replacer, 2);
document.querySelector('.js-array').innerText = JSON.stringify(toArray(json), null, 2);
} }
$input.addEventListener('keyup', convert); $input.addEventListener('keyup', convert);

View File

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