Compare commits

...

13 Commits

4 changed files with 127 additions and 76 deletions

View File

@ -1,6 +1,6 @@
# Bluecrypt ASN.1 Parser # Bluecrypt ASN.1 Parser
An ASN.1 parser in less than 100 lines of Vanilla JavaScript, An ASN.1 decoder 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>
@ -17,6 +17,10 @@ part of the Bluecrypt suite.
* [x] Online [Demo](https://coolaj86.com/demos/asn1-parser/) * [x] Online [Demo](https://coolaj86.com/demos/asn1-parser/)
* [ ] Node.js (built, publishing soon) * [ ] Node.js (built, publishing soon)
### Need an ASN.1 Builder too?
Check out https://git.coolaj86.com/coolaj86/asn1-packer.js/
# Demo # Demo
<https://coolaj86.com/demos/asn1-parser/> <https://coolaj86.com/demos/asn1-parser/>

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) { ;(function (exports) {
'use strict'; 'use strict';
@ -13,87 +17,102 @@ var PEM = exports.PEM;
// Parser // Parser
// //
ASN1.ELOOP = "uASN1.js Error: iterated over 15+ elements (probably a malformed file)"; // Although I've only seen 9 max in https certificates themselves,
ASN1.EDEEP = "uASN1.js Error: element nested 20+ layers deep (probably a malformed file)"; // 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) // Container Types are Sequence 0x30, Container Array? (0xA0, 0xA1)
// Value Types are Integer 0x02, Null 0x05, Object ID 0x06, Value Array? (0x82) // 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 // 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 = [ 0x02, 0x05, 0x06, 0x82 ]; 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] };
var child; var child;
var iters = 0; var iters = 0;
var adjust = 0; var adjust = 0;
var adjustedLen; var adjustedLen;
// Determine how many bytes the length uses, and what it is // Determine how many bytes the length uses, and what it is
if (0x80 & asn1.length) { if (0x80 & asn1.length) {
asn1.lengthSize = 0x7f & asn1.length; asn1.lengthSize = 0x7f & asn1.length;
// I think that buf->hex->int solves the problem of Endianness... not sure // 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); asn1.length = parseInt(Enc.bufToHex(buf.slice(index, index + asn1.lengthSize)), 16);
index += asn1.lengthSize; 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(ws + '0x' + Enc.numToHex(asn1.type), index, 'len:', asn1.length, asn1); // 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.
function parseChildren(eager) { if (0x00 === buf[index] && (0x02 === asn1.type || 0x03 === asn1.type)) {
asn1.children = []; // However, 0x00 on its own is a valid number
//console.warn('1 len:', (2 + asn1.lengthSize + asn1.length), 'idx:', index, 'clen:', 0); if (asn1.length > 1) {
while (iters < 15 && index < (2 + asn1.length + asn1.lengthSize)) { index += 1;
iters += 1; adjust = -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 + ")");
} }
asn1.children.push(child);
//console.warn(ws + '0x' + Enc.numToHex(asn1.type), index, 'len:', asn1.length, asn1);
} }
if (index !== (2 + asn1.lengthSize + asn1.length)) { adjustedLen = asn1.length + adjust;
//console.warn('index:', index, 'length:', (2 + asn1.lengthSize + asn1.length));
throw new Error("premature end-of-file");
}
if (iters >= 15) { throw new Error(ASN1.ELOOP); }
delete asn1.value; //console.warn(depth.join(ws) + '0x' + Enc.numToHex(asn1.type), index, 'len:', asn1.length, asn1);
return 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 (-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; }
} }
// Recurse into types that are _always_ containers var asn1 = parseAsn1(buf, []);
if (-1 !== ASN1.CTYPES.indexOf(asn1.type)) { return parseChildren(); } var len = buf.byteLength || buf.length;
if (len !== 2 + asn1.lengthSize + asn1.length) {
// Return types that are _always_ values throw new Error("Length of buffer does not match length of ASN.1 sequence.");
asn1.value = buf.slice(index, index + adjustedLen); }
if (-1 !== ASN1.VTYPES.indexOf(asn1.type)) { return asn1; } 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; }
}; };
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); }

View File

@ -12,30 +12,55 @@
<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> <br>
<p>Made with <a href="https://git.coolaj86.com/coolaj86/asn1-parser/">asn1-parser.js</a></p> <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('keyup');
var pem = PEM.parseBlock(document.querySelector('.js-input').value); var json;
var hex = Enc.bufToHex(pem.der);
document.querySelector('.js-hex').innerText = hex try {
.match(/.{2}/g).join(' ').match(/.{1,24}/g).join(' ').match(/.{1,50}/g).join('\n'); var pem = PEM.parseBlock(document.querySelector('.js-input').value);
var json = ASN1.parse(pem.der); 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-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,9 +1,12 @@
{ {
"name": "asn1-parser", "name": "asn1-parser",
"version": "1.1.1", "version": "1.1.8",
"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"
}, },