JavaScript generic ASN.1 parser (mirror)

Compare changes

Choose any two refs to compare.

+220 -70
+40
.vscode/launch.json
··· 7 7 { 8 8 "type": "node", 9 9 "request": "launch", 10 + "name": "dumpASN1", 11 + "skipFiles": [ 12 + "<node_internals>/**" 13 + ], 14 + "program": "${workspaceFolder}/dumpASN1.js", 15 + "args": [ 16 + "examples/ed25519.cer" 17 + ] 18 + }, 19 + { 20 + "type": "node", 21 + "request": "launch", 10 22 "name": "parseRFC", 11 23 "skipFiles": [ 12 24 "<node_internals>/**" ··· 15 27 "args": [ 16 28 "rfc/rfc4511.txt", 17 29 "rfcdef.json" 30 + ] 31 + }, 32 + { 33 + "type": "node", 34 + "request": "launch", 35 + "name": "dumpASN1 cert", 36 + "skipFiles": [ 37 + "<node_internals>/**" 38 + ], 39 + "program": "${workspaceFolder}/dumpASN1.js", 40 + "args": [ 41 + "data:base64,MIG9AgEBMA0GCSqGSIb3DQEBCwUAMEExCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxUUjM0IFNhbXBsZXMxGzAZBgNVBAMTElRSMzQgU2FtcGxlIENBIEtESBcNMTAxMTAyMTczMzMwWhcNMTAxMjAyMTczMzMwWjBIMBYCBTQAAAAIFw0xMDExMDIxNzI4MTNaMBYCBTQAAAAKFw0xMDExMDIxNzMxNDZaMBYCBTQAAAALFw0xMDExMDIxNzMzMjVa", 42 + "1.3.6.1.5.5.7.0.18", 43 + "TBSCertList" 44 + ] 45 + }, 46 + { 47 + "type": "node", 48 + "request": "launch", 49 + "name": "dumpASN1 CMS", 50 + "skipFiles": [ 51 + "<node_internals>/**" 52 + ], 53 + "program": "${workspaceFolder}/dumpASN1.js", 54 + "args": [ 55 + "examples/cms-password.p7m", 56 + "1.2.840.113549.1.9.16.0.14", 57 + "ContentInfo" 18 58 ] 19 59 }, 20 60 {
+22 -16
asn1.js
··· 21 21 reTimeS = /^(\d\d)(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])([01]\d|2[0-3])(?:([0-5]\d)(?:([0-5]\d)(?:[.,](\d{1,3}))?)?)?(Z|(-(?:0\d|1[0-2])|[+](?:0\d|1[0-4]))([0-5]\d)?)?$/, 22 22 reTimeL = /^(\d\d\d\d)(0[1-9]|1[0-2])(0[1-9]|[12]\d|3[01])([01]\d|2[0-3])(?:([0-5]\d)(?:([0-5]\d)(?:[.,](\d{1,3}))?)?)?(Z|(-(?:0\d|1[0-2])|[+](?:0\d|1[0-4]))([0-5]\d)?)?$/, 23 23 hexDigits = '0123456789ABCDEF', 24 - b64Safe = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_', 24 + b64Std = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/', 25 + b64URL = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_', 25 26 tableT61 = [ 26 27 ['', ''], 27 28 ['AEIOUaeiou', 'ร€รˆรŒร’ร™ร รจรฌรฒรน'], // Grave ··· 58 59 59 60 /** Class to manage a stream of bytes, with a zero-copy approach. 60 61 * It uses an existing array or binary string and advances a position index. */ 61 - class Stream { 62 + export class Stream { 62 63 63 64 /** 64 65 * @param {Stream|array|string} enc data (will not be copied) ··· 98 99 /** Hexadecimal dump of a specified region of the stream. 99 100 * @param {number} start starting position (included) 100 101 * @param {number} end ending position (excluded) 101 - * @param {string} type 'raw', 'byte' or 'dump' */ 102 + * @param {string} type 'raw', 'byte' or 'dump' (default) */ 102 103 hexDump(start, end, type = 'dump') { 103 104 let s = ''; 104 105 for (let i = start; i < end; ++i) { ··· 114 115 } 115 116 return s; 116 117 } 117 - /** Base-64 dump of a specified region of the stream. 118 + /** Base64url dump of a specified region of the stream (according to RFC 4648 section 5). 118 119 * @param {number} start starting position (included) 119 - * @param {number} end ending position (excluded) */ 120 - b64Dump(start, end) { 120 + * @param {number} end ending position (excluded) 121 + * @param {string} type 'url' (default, section 5 without padding) or 'std' (section 4 with padding) */ 122 + b64Dump(start, end, type = 'url') { 123 + const b64 = type === 'url' ? b64URL : b64Std; 121 124 let extra = (end - start) % 3, 122 125 s = '', 123 126 i, c; 124 127 for (i = start; i + 2 < end; i += 3) { 125 128 c = this.get(i) << 16 | this.get(i + 1) << 8 | this.get(i + 2); 126 - s += b64Safe.charAt(c >> 18 & 0x3F); 127 - s += b64Safe.charAt(c >> 12 & 0x3F); 128 - s += b64Safe.charAt(c >> 6 & 0x3F); 129 - s += b64Safe.charAt(c & 0x3F); 129 + s += b64.charAt(c >> 18 & 0x3F); 130 + s += b64.charAt(c >> 12 & 0x3F); 131 + s += b64.charAt(c >> 6 & 0x3F); 132 + s += b64.charAt(c & 0x3F); 130 133 } 131 134 if (extra > 0) { 132 135 c = this.get(i) << 16; 133 136 if (extra > 1) c |= this.get(i + 1) << 8; 134 - s += b64Safe.charAt(c >> 18 & 0x3F); 135 - s += b64Safe.charAt(c >> 12 & 0x3F); 136 - if (extra == 2) s += b64Safe.charAt(c >> 6 & 0x3F); 137 + s += b64.charAt(c >> 18 & 0x3F); 138 + s += b64.charAt(c >> 12 & 0x3F); 139 + if (extra == 2) s += b64.charAt(c >> 6 & 0x3F); 140 + if (b64 === b64Std) s += '==='.slice(0, 3 - extra); 137 141 } 138 142 return s; 139 143 } ··· 557 561 toHexString(type = 'raw') { 558 562 return this.stream.hexDump(this.posStart(), this.posEnd(), type); 559 563 } 560 - /** Base64 dump of the node. */ 561 - toB64String() { 562 - return this.stream.b64Dump(this.posStart(), this.posEnd()); 564 + /** Base64url dump of the node (according to RFC 4648 section 5). 565 + * @param {string} type 'url' (default, section 5 without padding) or 'std' (section 4 with padding) 566 + */ 567 + toB64String(type = 'url') { 568 + return this.stream.b64Dump(this.posStart(), this.posEnd(), type); 563 569 } 564 570 static decodeLength(stream) { 565 571 let buf = stream.get(),
+6 -5
base64.js
··· 31 31 decoder[b64.charCodeAt(i)] = i; 32 32 for (i = 0; i < ignore.length; ++i) 33 33 decoder[ignore.charCodeAt(i)] = -1; 34 - // RFC 3548 URL & file safe encoding 34 + // also support decoding Base64url (RFC 4648 section 5) 35 35 decoder['-'.charCodeAt(0)] = decoder['+'.charCodeAt(0)]; 36 36 decoder['_'.charCodeAt(0)] = decoder['/'.charCodeAt(0)]; 37 37 } ··· 75 75 76 76 static pretty(str) { 77 77 // fix padding 78 - if (str.length % 4 > 0) 79 - str = (str + '===').slice(0, str.length + str.length % 4); 80 - // convert RFC 3548 to standard Base64 78 + let pad = 4 - str.length % 4; 79 + if (pad < 4) 80 + str += '==='.slice(0, pad); 81 + // convert Base64url (RFC 4648 section 5) to standard Base64 (RFC 4648 section 4) 81 82 str = str.replace(/-/g, '+').replace(/_/g, '/'); 82 83 // 80 column width 83 - return str.replace(/(.{80})/g, '$1\n'); 84 + return str.replace(/.{80}/g, '$&\n'); 84 85 } 85 86 86 87 static unarmor(a) {
+3 -1
index.css
··· 1 1 html { 2 2 --main-bg-color: #C0C0C0; 3 3 --main-text-color: #000000; 4 + --id-text-color: #7d5900; 4 5 --headline-text-color: #8be9fd; 5 6 --button-border-color: #767676; 6 7 --button-bg-color: #efefef; ··· 35 36 html[data-theme="dark"] { 36 37 --main-bg-color: #0d1116; 37 38 --main-text-color: #f8f8f2; 39 + --id-text-color: #9d7c2d; 38 40 --headline-text-color: #8be9fd; 39 41 --button-border-color: #505050; 40 42 --button-bg-color: #303030; ··· 186 188 color: var(--preview-border-color); 187 189 } 188 190 .name.id { 189 - color: var(--main-text-color); 191 + color: var(--id-text-color); 190 192 } 191 193 .value { 192 194 display: none;
+1 -1
index.html
··· 37 37 <div id="tree"></div> 38 38 </div> 39 39 <form> 40 - <textarea id="area" rows="8"></textarea> 40 + <textarea id="area" rows="8" placeholder="Paste hex or base64 or PEM encoded ASN.1 BER or DER structures here, or load a file."></textarea> 41 41 <br> 42 42 <br> 43 43 <label title="can be slow with big files"><input type="checkbox" id="wantHex" checked="checked"> with hex dump</label>
+13 -7
int10.js
··· 13 13 // ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF 14 14 // OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 15 15 16 - let max = 10000000000000; // biggest 10^n integer that can still fit 2^53 when multiplied by 256 16 + /** Biggest 10^n integer that can still fit 2^53 when multiplied by 256. */ 17 + const max = 10000000000000; 17 18 18 19 export class Int10 { 19 20 /** ··· 26 27 27 28 /** 28 29 * Multiply value by m and add c. 29 - * @param {number} m - multiplier, must be < =256 30 - * @param {number} c - value to add 30 + * @param {number} m - multiplier, must be 0<m<=256 31 + * @param {number} c - value to add, must be c>=0 31 32 */ 32 33 mulAdd(m, c) { 34 + // assert(m > 0) 33 35 // assert(m <= 256) 36 + // assert(c >= 0) 34 37 let b = this.buf, 35 38 l = b.length, 36 39 i, t; ··· 71 74 72 75 /** 73 76 * Convert to decimal string representation. 74 - * @param {*} base - optional value, only value accepted is 10 77 + * @param {number} [base=10] - optional value, only value accepted is 10 78 + * @returns {string} The decimal string representation. 75 79 */ 76 - toString(base) { 77 - if ((base || 10) != 10) 78 - throw 'only base 10 is supported'; 80 + toString(base = 10) { 81 + if (base != 10) 82 + throw new Error('only base 10 is supported'); 79 83 let b = this.buf, 80 84 s = b[b.length - 1].toString(); 81 85 for (let i = b.length - 2; i >= 0; --i) ··· 86 90 /** 87 91 * Convert to Number value representation. 88 92 * Will probably overflow 2^53 and thus become approximate. 93 + * @returns {number} The numeric value. 89 94 */ 90 95 valueOf() { 91 96 let b = this.buf, ··· 97 102 98 103 /** 99 104 * Return value as a simple Number (if it is <= 10000000000000), or return this. 105 + * @returns {number | Int10} The simplified value. 100 106 */ 101 107 simplify() { 102 108 let b = this.buf;
+2 -2
package.json
··· 1 1 { 2 2 "name": "@lapo/asn1js", 3 - "version": "2.0.5", 3 + "version": "2.0.6", 4 4 "description": "Generic ASN.1 parser/decoder that can decode any valid ASN.1 DER or BER structures.", 5 5 "type": "module", 6 6 "main": "asn1.js", ··· 18 18 "lint": "npx eslint asn1.js base64.js hex.js int10.js dom.js defs.js oids.js rfcdef.js tags.js context.js index.js parseRFC.js dumpASN1.js test.js testDefs.js vite.config.js theme.js", 19 19 "lint-action": "npx @action-validator/cli .github/workflows/node.js.yml", 20 20 "build": "vite build", 21 - "serve": "echo 'Connect to http://localhost:3000/' ; npx statik --port 3000 .", 21 + "serve": "npx -p local-web-server ws", 22 22 "test": "node test", 23 23 "testdefs": "node testDefs" 24 24 },
+1
tags.js
··· 1 1 export const tags = { 2 + "2.0.5": "2025-04-12", 2 3 "2.0.4": "2024-05-08", 3 4 "2.0.3": "2024-05-06", 4 5 "2.0.2": "2024-04-20",
+132 -38
test.js
··· 1 1 #!/usr/bin/env node 2 2 3 - import { ASN1 } from './asn1.js'; 3 + import { ASN1, Stream } from './asn1.js'; 4 4 import { Hex } from './hex.js'; 5 + import { Base64 } from './base64.js'; 6 + import { Int10 } from './int10.js'; 7 + 8 + const all = (process.argv[2] == 'all'); 5 9 6 - const 7 - all = (process.argv[2] == 'all'); 10 + /** @type {Array<Tests>} */ 11 + const tests = []; 12 + 13 + const stats = { 14 + run: 0, 15 + error: 0, 16 + }; 17 + 18 + /** 19 + * A class for managing and executing tests. 20 + */ 21 + class Tests { 22 + /** 23 + * An array to store test data. 24 + * @type {Array<unknown>} 25 + */ 26 + data; 27 + 28 + /** 29 + * Checks a row of test data. 30 + * @param {Function} t - How to test a row of data. 31 + */ 32 + checkRow; 33 + 34 + /** 35 + * Constructs a new Tests instance. 36 + * @param {Function} checkRow - A function to check each row of data. 37 + * @param {Array<unknown>} data - The test data to be processed. 38 + */ 39 + constructor(checkRow, data) { 40 + this.checkRow = checkRow; 41 + this.data = data; 42 + } 43 + 44 + /** 45 + * Executes the tests and checks their results for all rows. 46 + */ 47 + checkAll() { 48 + for (const t of this.data) 49 + this.checkRow(t); 50 + } 8 51 9 - const tests = [ 52 + /** 53 + * Prints the result of a test, indicating if it passed or failed. 54 + * @param {unknown} result The actual result of the test. 55 + * @param {unknown} expected The expected result of the test. 56 + * @param {string} comment A comment describing the test. 57 + */ 58 + checkResult(result, expected, comment) { 59 + ++stats.run; 60 + if (!result || result == expected) { 61 + if (all) console.log('\x1B[1m\x1B[32mOK \x1B[39m\x1B[22m ' + comment); 62 + } else { 63 + ++stats.error; 64 + console.log('\x1B[1m\x1B[31mERR\x1B[39m\x1B[22m ' + comment); 65 + console.log(' \x1B[1m\x1B[34mEXP\x1B[39m\x1B[22m ' + expected.toString().replace(/\n/g, '\n ')); 66 + console.log(' \x1B[1m\x1B[33mGOT\x1B[39m\x1B[22m ' + result.replace(/\n/g, '\n ')); 67 + } 68 + } 69 + } 70 + 71 + tests.push(new Tests(function (t) { 72 + const input = t[0], 73 + expected = t[1], 74 + comment = t[2]; 75 + let result; 76 + try { 77 + let node = ASN1.decode(Hex.decode(input)); 78 + if (typeof expected == 'function') 79 + result = expected(node); 80 + else 81 + result = node.content(); 82 + //TODO: check structure, not only first level content 83 + } catch (e) { 84 + result = 'Exception:\n' + e; 85 + } 86 + if (expected instanceof RegExp) 87 + result = expected.test(result) ? null : 'does not match'; 88 + this.checkResult(result, expected, comment); 89 + }, [ 10 90 // RSA Laboratories technical notes from https://luca.ntop.org/Teaching/Appunti/asn1.html 11 91 ['0304066E5DC0', '(18 bit)\n011011100101110111', 'ntop, bit string: DER encoding'], 12 92 ['0304066E5DE0', '(18 bit)\n011011100101110111', 'ntop, bit string: padded with "100000"'], ··· 88 168 ['181331393835313130363231303632372E332B3134', '1985-11-06 21:06:27.3 UTC+14:00', 'UTC offset +13 and +14'], // GitHub issue #54 89 169 ['032100171E83C1B251803F86DD01E9CFA886BE89A7316D8372649AC2231EC669F81A84', n => { if (n.sub != null) return 'Should not decode content: ' + n.sub[0].content(); }, 'Key that resembles an UTCTime'], // GitHub issue #79 90 170 ['171E83C1B251803F86DD01E9CFA886BE89A7316D8372649AC2231EC669F81A84', /^Exception:\nError: Unrecognized time: /, 'Invalid UTCTime'], // GitHub issue #79 91 - ]; 171 + ])); 92 172 93 - let 94 - run = 0, 95 - expErr = 0, 96 - error = 0; 97 - tests.forEach(function (t) { 98 - const input = t[0], 99 - expected = t[1], 100 - comment = t[2]; 101 - let result; 102 - try { 103 - let node = ASN1.decode(Hex.decode(input)); 104 - if (typeof expected == 'function') 105 - result = expected(node); 106 - else 107 - result = node.content(); 108 - //TODO: check structure, not only first level content 109 - } catch (e) { 110 - result = 'Exception:\n' + e; 111 - } 112 - if (expected instanceof RegExp) 113 - result = expected.test(result) ? null : 'does not match'; 114 - ++run; 115 - if (!result || result == expected) { 116 - if (all) console.log('\x1B[1m\x1B[32mOK \x1B[39m\x1B[22m ' + comment); 117 - } else { 118 - ++error; 119 - console.log('\x1B[1m\x1B[31mERR\x1B[39m\x1B[22m ' + comment); 120 - console.log(' \x1B[1m\x1B[34mEXP\x1B[39m\x1B[22m ' + expected.toString().replace(/\n/g, '\n ')); 121 - console.log(' \x1B[1m\x1B[33mGOT\x1B[39m\x1B[22m ' + result.replace(/\n/g, '\n ')); 122 - } 123 - }); 124 - console.log(run + ' tested, ' + expErr + ' expected, ' + error + ' errors.'); 125 - process.exit(error ? 1 : 0); 173 + tests.push(new Tests(function (t) { 174 + let bin = Base64.decode(t); 175 + let url = new Stream(bin, 0).b64Dump(0, bin.length); 176 + // check base64url encoding 177 + this.checkResult(url, t.replace(/\n/g, '').replace(/=*$/g, ''), 'Base64url: ' + bin.length + ' bytes'); 178 + // check conversion from base64url to base64 179 + let pretty = Base64.pretty(url); 180 + this.checkResult(pretty, t, 'Base64pretty: ' + bin.length + ' bytes'); 181 + let std = new Stream(bin, 0).b64Dump(0, bin.length, 'std'); 182 + // check direct base64 encoding 183 + this.checkResult(std, t.replace(/\n/g, ''), 'Base64: ' + bin.length + ' bytes'); 184 + }, [ 185 + 'AA==', 186 + 'ABA=', 187 + 'ABCD', 188 + 'ABCDEA==', 189 + 'ABCDEFE=', 190 + 'ABCDEFGH', 191 + 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQR\nSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456w==', 192 + ])); 193 + 194 + tests.push(new Tests(function (t) { 195 + this.row = (0|this.row) + 1; 196 + this.num = this.num || new Int10(); 197 + this.num.mulAdd(t[0], t[1]); 198 + this.checkResult(this.num.toString(), t[2], 'Int10 row ' + this.row); 199 + }, [ 200 + [0, 1000000000, '1000000000'], 201 + [256, 23, '256000000023'], 202 + [256, 23, '65536000005911'], 203 + [256, 23, '16777216001513239'], 204 + [256, 23, '4294967296387389207'], 205 + [256, 23, '1099511627875171637015'], 206 + [256, 23, '281474976736043939075863'], 207 + [253, 1, '71213169114219116586193340'], 208 + [253, 1, '18016931785897436496306915021'], 209 + [253, 1, '4558283741832051433565649500314'], 210 + [253, 1, '1153245786683509012692109323579443'], 211 + [253, 1, '291771184030927780211103658865599080'], 212 + [1, 0, '291771184030927780211103658865599080'], 213 + ])); 214 + 215 + for (const t of tests) 216 + t.checkAll(); 217 + 218 + console.log(stats.run + ' tested, ' + stats.error + ' errors.'); 219 + process.exit(stats.error ? 1 : 0);