···11+# ChangeLog
22+33+## 2.1.1 - 2025-10-24
44+55+### Changed
66+77+- update dev dependencies
88+- fix test suite that was reporting no error with empty responses
99+1010+### Added
1111+1212+- add content length check for BOOLEAN, INTEGER, OID ([GitHub #104](https://github.com/lapo-luchini/asn1js/pull/104))
1313+1414+## 2.1.0 - 2025-08-03
1515+1616+### Changed
1717+1818+- when fields are CHOICEs now both the field name and the choice name are shown (fixes [GitHub #102](https://github.com/lapo-luchini/asn1js/issues/102))
1919+- upgrade minimum NodeJS version supported from 12.20.0 to 14.6.0 due to usage of ?. and ?? operators in defs.js (ECMAScript 2020); older code is still linted against ECMAScript 2015 for now
2020+2121+### Added
2222+2323+- add tests to check expected decoding
2424+2525+## 2.0.6 - 2025-07-29
2626+2727+### Added
2828+2929+- add proper support for standard Base64 (we previously only supported Base64url) (fixes [GitHub #99](https://github.com/lapo-luchini/asn1js/pull/99))
3030+- improve test harness
3131+3232+## 2.0.5 - 2025-04-12
3333+3434+### Added
3535+3636+- add `index-local.html` for local `file://` usage without needing a web server
3737+- add definitions support for `LDAPMessage`
3838+- #TODO continue producing old ChangeLog entries
+1-1
LICENSE
···11ISC License
2233-Copyright (c) 2008-2024 Lapo Luchini <lapo@lapo.it>
33+Copyright (c) 2008-2025 Lapo Luchini <lapo@lapo.it>
4455Permission to use, copy, modify, and/or distribute this software for any
66purpose with or without fee is hereby granted, provided that the above
+20-5
README.md
···3344asn1js is a JavaScript generic ASN.1 parser/decoder that can decode any valid ASN.1 DER or BER structures.
5566-An example page that can decode Base64-encoded (raw base64, PEM armoring and `begin-base64` are recognized) or Hex-encoded (or local files with some browsers) is included and can be used both [online on the official website](https://lapo.it/asn1js/) or [offline (ZIP file)](https://lapo.it/asn1js/asn1js.zip).
66+An example page that can decode Base64-encoded (raw base64, PEM armoring and `begin-base64` are recognized) or Hex-encoded (or local files with some browsers) is included and can be used both [online on the official website](https://asn1js.eu/) or [offline (ZIP file)](https://lapo.it/asn1js/asn1js.zip) by opening `index-local.html`.
7788Usage with `nodejs`
99-------------------
···6464</script>
6565```
66666767+Local usage
6868+--------------------
6969+7070+Since unfortunately ESM modules are not working on `file:` protocol due to [CORS issues](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Modules#other_differences_between_modules_and_standard_scripts), there is a bundled [single-file version working locally](https://asn1js.eu/index-local.html). It doesn't work online (due to [CSP](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP) restrictions about inline content) but can be saved locally and opened in a browser.
7171+7272+Usage from CLI
7373+--------------------
7474+7575+You can dump an ASN.1 structure from the command line using the following command (no need to even install it):
7676+7777+```sh
7878+npx @lapo/asn1js ed25519.cer
7979+```
8080+6781ISC license
6882-----------
69837070-ASN.1 JavaScript decoder Copyright (c) 2008-2024 Lapo Luchini <lapo@lapo.it>
8484+ASN.1 JavaScript decoder Copyright (c) 2008-2025 Lapo Luchini <lapo@lapo.it>
71857286Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies.
7387···87101links
88102-----
891039090-- [official website](https://lapo.it/asn1js/)
9191-- [dedicated domain](https://asn1js.eu/)
104104+- [official website](https://asn1js.eu/)
105105+- [alternate website](https://lapo.it/asn1js/)
92106- [single-file version working locally](https://asn1js.eu/index-local.html) (just save this link)
9393-- [InDefero tracker](http://idf.lapo.it/p/asn1js/)
107107+- [InDefero tracker](http://idf.lapo.it/p/asn1js/) (currently offline)
94108- [GitHub mirror](https://github.com/lapo-luchini/asn1js)
109109+- [ChangeLog on GitHub](https://github.com/lapo-luchini/asn1js/blob/trunk/CHANGELOG.md)
95110- [Ohloh code stats](https://www.openhub.net/p/asn1js)
+307-75
asn1.js
···11// ASN.1 JavaScript decoder
22-// Copyright (c) 2008-2024 Lapo Luchini <lapo@lapo.it>
22+// Copyright (c) 2008 Lapo Luchini <lapo@lapo.it>
3344// Permission to use, copy, modify, and/or distribute this software for any
55// purpose with or without fee is hereby granted, provided that the above
66// copyright notice and this permission notice appear in all copies.
77-//
77+//
88// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
99// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
1010// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
···1313// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
1414// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
15151616-import { Int10 } from './int10.js';
1716import { oids } from './oids.js';
18171918const
···2120 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)?)?$/,
2221 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)?)?$/,
2322 hexDigits = '0123456789ABCDEF',
2424- b64Safe = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_',
2323+ b64Std = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/',
2424+ b64URL = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_',
2525 tableT61 = [
2626 ['', ''],
2727 ['AEIOUaeiou', 'รรรรรร รจรฌรฒรน'], // Grave
···4141 ['CDELNRSTZcdelnrstz', 'ฤฤฤฤฝลลล ลคลฝฤฤฤฤพลลลกลฅลพ'], // Caron
4242 ];
43434444+/**
4545+ * Truncates a string to a specified length and adds an ellipsis if needed.
4646+ * @param {string} str - The input string to truncate
4747+ * @param {number} len - The maximum length of the string
4848+ * @returns {string} The truncated string
4949+ */
4450function stringCut(str, len) {
4551 if (str.length > len)
4652 str = str.substring(0, len) + ellipsis;
4753 return str;
4854}
49555656+/**
5757+ * Checks if a string contains only printable characters (ASCII 32-126, plus tab, newline, carriage return)
5858+ * @param {string} s - The string to check
5959+ * @throws {Error} If an unprintable character is found
6060+ */
5061function checkPrintable(s) {
5162 let i, v;
5263 for (i = 0; i < s.length; ++i) {
···5667 }
5768}
58695959-class Stream {
7070+/**
7171+ * Class to manage a stream of bytes, with a zero-copy approach.
7272+ * It uses an existing array or binary string and advances a position index.
7373+ */
7474+export class Stream {
60757676+ /**
7777+ * Creates a new Stream object.
7878+ * @param {Stream|array|string} enc data (will not be copied)
7979+ * @param {?number} pos starting position (mandatory when `end` is not a Stream)
8080+ */
6181 constructor(enc, pos) {
6282 if (enc instanceof Stream) {
6383 this.enc = enc.enc;
6484 this.pos = enc.pos;
6585 } else {
6666- // enc should be an array or a binary string
6786 this.enc = enc;
6887 this.pos = pos;
6988 }
8989+ if (typeof this.pos != 'number')
9090+ throw new Error('"pos" must be a numeric value');
9191+ // Set up the raw byte access function based on the type of data
9292+ if (typeof this.enc == 'string')
9393+ this.getRaw = pos => this.enc.charCodeAt(pos);
9494+ else if (typeof this.enc[0] == 'number')
9595+ this.getRaw = pos => this.enc[pos];
9696+ else
9797+ throw new Error('"enc" must be a numeric array or a string');
7098 }
9999+100100+ /**
101101+ * Get the byte at current position (and increment it) or at a specified position (and avoid moving current position).
102102+ * @param {?number} pos read position if specified, else current position (and increment it)
103103+ * @returns {number} The byte value at the specified position
104104+ */
71105 get(pos) {
72106 if (pos === undefined)
73107 pos = this.pos++;
74108 if (pos >= this.enc.length)
75109 throw new Error('Requesting byte offset ' + pos + ' on a stream of length ' + this.enc.length);
7676- return (typeof this.enc == 'string') ? this.enc.charCodeAt(pos) : this.enc[pos];
110110+ return this.getRaw(pos);
77111 }
7878- hexByte(b) {
112112+113113+ /**
114114+ * Convert a single byte to a hexadecimal string (of length 2).
115115+ * @param {number} b - The byte to convert
116116+ * @returns {string} Hexadecimal representation of the byte
117117+ */
118118+ static hexByte(b) {
79119 return hexDigits.charAt((b >> 4) & 0xF) + hexDigits.charAt(b & 0xF);
80120 }
8181- /** Hexadecimal dump.
8282- * @param type 'raw', 'byte' or 'dump' */
121121+122122+ /**
123123+ * Hexadecimal dump of a specified region of the stream.
124124+ * @param {number} start - starting position (included)
125125+ * @param {number} end - ending position (excluded)
126126+ * @param {string} type - 'raw', 'byte' or 'dump' (default)
127127+ * @returns {string} Hexadecimal representation of the data
128128+ */
83129 hexDump(start, end, type = 'dump') {
84130 let s = '';
85131 for (let i = start; i < end; ++i) {
86132 if (type == 'byte' && i > start)
87133 s += ' ';
8888- s += this.hexByte(this.get(i));
134134+ s += Stream.hexByte(this.get(i));
89135 if (type == 'dump')
90136 switch (i & 0xF) {
91137 case 0x7: s += ' '; break;
···95141 }
96142 return s;
97143 }
9898- b64Dump(start, end) {
9999- let extra = (end - start) % 3,
100100- s = '',
144144+145145+ /**
146146+ * Base64url dump of a specified region of the stream (according to RFC 4648 section 5).
147147+ * @param {number} start - starting position (included)
148148+ * @param {number} end - ending position (excluded)
149149+ * @param {string} type - 'url' (default, section 5 without padding) or 'std' (section 4 with padding)
150150+ * @returns {string} Base64 encoded representation of the data
151151+ */
152152+ b64Dump(start, end, type = 'url') {
153153+ const b64 = type === 'url' ? b64URL : b64Std,
154154+ extra = (end - start) % 3;
155155+ let s = '',
101156 i, c;
102157 for (i = start; i + 2 < end; i += 3) {
103158 c = this.get(i) << 16 | this.get(i + 1) << 8 | this.get(i + 2);
104104- s += b64Safe.charAt(c >> 18 & 0x3F);
105105- s += b64Safe.charAt(c >> 12 & 0x3F);
106106- s += b64Safe.charAt(c >> 6 & 0x3F);
107107- s += b64Safe.charAt(c & 0x3F);
159159+ s += b64.charAt(c >> 18 & 0x3F);
160160+ s += b64.charAt(c >> 12 & 0x3F);
161161+ s += b64.charAt(c >> 6 & 0x3F);
162162+ s += b64.charAt(c & 0x3F);
108163 }
109164 if (extra > 0) {
110165 c = this.get(i) << 16;
111166 if (extra > 1) c |= this.get(i + 1) << 8;
112112- s += b64Safe.charAt(c >> 18 & 0x3F);
113113- s += b64Safe.charAt(c >> 12 & 0x3F);
114114- if (extra == 2) s += b64Safe.charAt(c >> 6 & 0x3F);
167167+ s += b64.charAt(c >> 18 & 0x3F);
168168+ s += b64.charAt(c >> 12 & 0x3F);
169169+ if (extra == 2) s += b64.charAt(c >> 6 & 0x3F);
170170+ if (b64 === b64Std) s += '==='.slice(0, 3 - extra);
115171 }
116172 return s;
117173 }
174174+175175+ /**
176176+ * Check if a region of the stream contains only ASCII characters (32-176)
177177+ * @param {number} start - starting position (included)
178178+ * @param {number} end - ending position (excluded)
179179+ * @returns {boolean} True if all characters are ASCII, false otherwise
180180+ */
118181 isASCII(start, end) {
119182 for (let i = start; i < end; ++i) {
120183 let c = this.get(i);
···123186 }
124187 return true;
125188 }
189189+190190+ /**
191191+ * Parse a region of the stream as an ISO string
192192+ * @param {number} start - starting position (included)
193193+ * @param {number} end - ending position (excluded)
194194+ * @param {number} maxLength - maximum length of the output string
195195+ * @returns {Object} Object with size and str properties
196196+ */
126197 parseStringISO(start, end, maxLength) {
127198 let s = '';
128199 for (let i = start; i < end; ++i)
129200 s += String.fromCharCode(this.get(i));
130201 return { size: s.length, str: stringCut(s, maxLength) };
131202 }
203203+204204+ /**
205205+ * Parse a region of the stream as a T.61 string
206206+ * @param {number} start - starting position (included)
207207+ * @param {number} end - ending position (excluded)
208208+ * @param {number} maxLength - maximum length of the output string
209209+ * @returns {Object} Object with size and str properties
210210+ */
132211 parseStringT61(start, end, maxLength) {
133212 // warning: this code is not very well tested so far
134213 function merge(c, d) {
135135- let t = tableT61[c - 0xC0];
136136- let i = t[0].indexOf(String.fromCharCode(d));
214214+ const t = tableT61[c - 0xC0];
215215+ const i = t[0].indexOf(String.fromCharCode(d));
137216 return (i < 0) ? '\0' : t[1].charAt(i);
138217 }
139218 let s = '', c;
···150229 }
151230 return { size: s.length, str: stringCut(s, maxLength) };
152231 }
232232+233233+ /**
234234+ * Parse a region of the stream as a UTF-8 string
235235+ * @param {number} start - starting position (included)
236236+ * @param {number} end - ending position (excluded)
237237+ * @param {number} maxLength - maximum length of the output string
238238+ * @returns {Object} Object with size and str properties
239239+ */
153240 parseStringUTF(start, end, maxLength) {
241241+ /**
242242+ * Helper function to process UTF-8 continuation bytes
243243+ * @param {number} c - The continuation byte
244244+ * @returns {number} The extracted data bits
245245+ */
154246 function ex(c) { // must be 10xxxxxx
155247 if ((c < 0x80) || (c >= 0xC0))
156248 throw new Error('Invalid UTF-8 continuation byte: ' + c);
157249 return (c & 0x3F);
158250 }
251251+ /**
252252+ * Helper function to convert a code point to a surrogate pair
253253+ * @param {number} cp - The code point to convert
254254+ * @returns {string} The surrogate pair as a string
255255+ */
159256 function surrogate(cp) {
160257 if (cp < 0x10000)
161258 throw new Error('UTF-8 overlong encoding, codepoint encoded in 4 bytes: ' + cp);
···165262 }
166263 let s = '';
167264 for (let i = start; i < end; ) {
168168- let c = this.get(i++);
265265+ const c = this.get(i++);
169266 if (c < 0x80) // 0xxxxxxx (7 bit)
170267 s += String.fromCharCode(c);
171268 else if (c < 0xC0)
···181278 }
182279 return { size: s.length, str: stringCut(s, maxLength) };
183280 }
281281+282282+ /**
283283+ * Parse a region of the stream as a BMP (Basic Multilingual Plane) string
284284+ * @param {number} start - starting position (included)
285285+ * @param {number} end - ending position (excluded)
286286+ * @param {number} maxLength - maximum length of the output string
287287+ * @returns {Object} Object with size and str properties
288288+ */
184289 parseStringBMP(start, end, maxLength) {
185290 let s = '', hi, lo;
186291 for (let i = start; i < end; ) {
···190295 }
191296 return { size: s.length, str: stringCut(s, maxLength) };
192297 }
298298+299299+ /**
300300+ * Parse a region of the stream as a time string
301301+ * @param {number} start - starting position (included)
302302+ * @param {number} end - ending position (excluded)
303303+ * @param {boolean} shortYear - Whether to parse as short year (2-digit)
304304+ * @returns {string} Formatted time string
305305+ */
193306 parseTime(start, end, shortYear) {
194307 let s = this.parseStringISO(start, end).str,
195308 m = (shortYear ? reTimeS : reTimeL).exec(s);
···217330 }
218331 return s;
219332 }
333333+334334+ /**
335335+ * Parse a region of the stream as an integer
336336+ * @param {number} start - starting position (included)
337337+ * @param {number} end - ending position (excluded)
338338+ * @returns {string} Formatted integer string
339339+ */
220340 parseInteger(start, end) {
221341 let v = this.get(start),
222222- neg = (v > 127),
223223- pad = neg ? 255 : 0,
224224- len,
225342 s = '';
343343+ const neg = (v > 127),
344344+ pad = neg ? 255 : 0;
226345 // skip unuseful bits (not allowed in DER)
227346 while (v == pad && ++start < end)
228347 v = this.get(start);
229229- len = end - start;
348348+ const len = end - start;
230349 if (len === 0)
231350 return neg ? '-1' : '0';
232351 // show bit length of huge integers
233352 if (len > 4) {
234234- s = v;
235235- len <<= 3;
236236- while (((s ^ pad) & 0x80) == 0) {
237237- s <<= 1;
238238- --len;
353353+ let v2 = v,
354354+ lenBit = len << 3;
355355+ while (((v2 ^ pad) & 0x80) == 0) {
356356+ v2 <<= 1;
357357+ --lenBit;
239358 }
240240- s = '(' + len + ' bit)\n';
359359+ s = '(' + lenBit + ' bit)\n';
241360 }
242361 // decode the integer
243362 if (neg) v = v - 256;
244244- let n = new Int10(v);
363363+ let n = BigInt(v);
245364 for (let i = start + 1; i < end; ++i)
246246- n.mulAdd(256, this.get(i));
247247- return s + n.toString();
365365+ n = (n << 8n) | BigInt(this.get(i));
366366+ return s + n;
248367 }
368368+369369+ /**
370370+ * Parse a region of the stream as a bit string.
371371+ * @param {number} start - starting position (included)
372372+ * @param {number} end - ending position (excluded)
373373+ * @param {number} maxLength - maximum length of the output string
374374+ * @returns {Object} Object with size and str properties
375375+ */
249376 parseBitString(start, end, maxLength) {
250250- let unusedBits = this.get(start);
377377+ const unusedBits = this.get(start);
251378 if (unusedBits > 7)
252379 throw new Error('Invalid BitString with unusedBits=' + unusedBits);
253253- let lenBit = ((end - start - 1) << 3) - unusedBits,
254254- s = '';
380380+ const lenBit = ((end - start - 1) << 3) - unusedBits;
381381+ let s = '';
255382 for (let i = start + 1; i < end; ++i) {
256383 let b = this.get(i),
257384 skip = (i == end - 1) ? unusedBits : 0;
···262389 }
263390 return { size: lenBit, str: s };
264391 }
392392+393393+ /**
394394+ * Parse a region of the stream as an octet string.
395395+ * @param {number} start - starting position (included)
396396+ * @param {number} end - ending position (excluded)
397397+ * @param {number} maxLength - maximum length of the output string
398398+ * @returns {Object} Object with size and str properties
399399+ */
265400 parseOctetString(start, end, maxLength) {
266266- let len = end - start,
267267- s;
268401 try {
269269- s = this.parseStringUTF(start, end, maxLength);
402402+ let s = this.parseStringUTF(start, end, maxLength);
270403 checkPrintable(s.str);
271404 return { size: end - start, str: s.str };
272272- } catch (e) {
273273- // ignore
405405+ } catch (ignore) {
406406+ // If UTF-8 parsing fails, fall back to hexadecimal dump
274407 }
408408+ const len = end - start;
275409 maxLength /= 2; // we work in bytes
276410 if (len > maxLength)
277411 end = start + maxLength;
278278- s = '';
412412+ let s = '';
279413 for (let i = start; i < end; ++i)
280280- s += this.hexByte(this.get(i));
414414+ s += Stream.hexByte(this.get(i));
281415 if (len > maxLength)
282416 s += ellipsis;
283417 return { size: len, str: s };
284418 }
419419+420420+ /**
421421+ * Parse a region of the stream as an OID (Object Identifier).
422422+ * @param {number} start - starting position (included)
423423+ * @param {number} end - ending position (excluded)
424424+ * @param {number} maxLength - maximum length of the output string
425425+ * @param {boolean} isRelative - Whether the OID is relative
426426+ * @returns {string} Formatted OID string
427427+ */
285428 parseOID(start, end, maxLength, isRelative) {
286429 let s = '',
287287- n = new Int10(),
430430+ n = 0n,
288431 bits = 0;
289432 for (let i = start; i < end; ++i) {
290433 let v = this.get(i);
291291- n.mulAdd(128, v & 0x7F);
434434+ // Shift bits and add the lower 7 bits of the byte
435435+ n = (n << 7n) | BigInt(v & 0x7F);
292436 bits += 7;
437437+ // If the most significant bit is 0, this is the last byte of the OID component
293438 if (!(v & 0x80)) { // finished
439439+ // If this is the first component, handle it specially
294440 if (s === '') {
295295- n = n.simplify();
296441 if (isRelative) {
297297- s = (n instanceof Int10) ? n.toString() : '' + n;
298298- } else if (n instanceof Int10) {
299299- n.sub(80);
300300- s = '2.' + n.toString();
442442+ s = n.toString();
301443 } else {
302302- let m = n < 80 ? n < 40 ? 0 : 1 : 2;
303303- s = m + '.' + (n - m * 40);
444444+ let m = n < 80 ? n < 40 ? 0n : 1n : 2n;
445445+ s = m + '.' + (n - m * 40n);
304446 }
305447 } else
306306- s += '.' + n.toString();
448448+ s += '.' + n;
307449 if (s.length > maxLength)
308450 return stringCut(s, maxLength);
309309- n = new Int10();
451451+ n = 0n;
310452 bits = 0;
311453 }
312454 }
313455 if (bits > 0)
314456 s += '.incomplete';
457457+ // If OIDs mapping is available and the OID is absolute, try to resolve it
315458 if (typeof oids === 'object' && !isRelative) {
316459 let oid = oids[s];
317460 if (oid) {
···322465 }
323466 return s;
324467 }
468468+469469+ /**
470470+ * Parse a region of the stream as a relative OID (Object Identifier).
471471+ * @param {number} start - starting position (included)
472472+ * @param {number} end - ending position (excluded)
473473+ * @param {number} maxLength - maximum length of the output string
474474+ * @returns {string} Formatted relative OID string
475475+ */
325476 parseRelativeOID(start, end, maxLength) {
326477 return this.parseOID(start, end, maxLength, true);
327478 }
···354505 this.tagConstructed = ((buf & 0x20) !== 0);
355506 this.tagNumber = buf & 0x1F;
356507 if (this.tagNumber == 0x1F) { // long tag
357357- let n = new Int10();
508508+ let n = 0n;
358509 do {
359510 buf = stream.get();
360360- n.mulAdd(128, buf & 0x7F);
511511+ n = (n << 7n) | BigInt(buf & 0x7F);
361512 } while (buf & 0x80);
362362- this.tagNumber = n.simplify();
513513+ this.tagNumber = n <= Number.MAX_SAFE_INTEGER ? Number(n) : n;
363514 }
364515 }
365516 isUniversal() {
···370521 }
371522}
372523524524+/**
525525+ * ASN1 class for parsing ASN.1 encoded data.
526526+ * Instances of this class represent an ASN.1 element and provides methods to parse and display its content.
527527+ */
373528export class ASN1 {
529529+ /**
530530+ * Creates an ASN1 parser object.
531531+ * @param {Stream} stream - The stream containing the ASN.1 data.
532532+ * @param {number} header - The header length.
533533+ * @param {number} length - The length of the data.
534534+ * @param {ASN1Tag} tag - The ASN.1 tag.
535535+ * @param {number} tagLen - The length of the tag.
536536+ * @param {Array} sub - The sub-elements.
537537+ */
374538 constructor(stream, header, length, tag, tagLen, sub) {
375539 if (!(tag instanceof ASN1Tag)) throw new Error('Invalid tag value.');
376540 this.stream = stream;
···380544 this.tagLen = tagLen;
381545 this.sub = sub;
382546 }
547547+548548+ /**
549549+ * Get the type name of the ASN.1 element.
550550+ * @returns {string} The type name.
551551+ */
383552 typeName() {
384553 switch (this.tag.tagClass) {
385554 case 0: // universal
···419588 case 3: return 'Private_' + this.tag.tagNumber.toString();
420589 }
421590 }
422422- /** A string preview of the content (intended for humans). */
591591+592592+ /**
593593+ * Get a string preview of the content (intended for humans).
594594+ * @param {number} maxLength - The maximum length of the content.
595595+ * @returns {string|null} The content preview or null if not supported.
596596+ */
423597 content(maxLength) {
424598 if (this.tag === undefined)
425599 return null;
426600 if (maxLength === undefined)
427601 maxLength = Infinity;
428428- let content = this.posContent(),
602602+ const content = this.posContent(),
429603 len = Math.abs(this.length);
430604 if (!this.tag.isUniversal()) {
431605 if (this.sub !== null)
···435609 }
436610 switch (this.tag.tagNumber) {
437611 case 0x01: // BOOLEAN
612612+ if (len != 1) return 'invalid length ' + len;
438613 return (this.stream.get(content) === 0) ? 'false' : 'true';
439614 case 0x02: // INTEGER
615615+ if (len < 1) return 'invalid length ' + len;
440616 return this.stream.parseInteger(content, content + len);
441617 case 0x03: { // BIT_STRING
442618 let d = recurse(this, 'parseBitString', maxLength);
···448624 }
449625 //case 0x05: // NULL
450626 case 0x06: // OBJECT_IDENTIFIER
627627+ if (len < 1) return 'invalid length ' + len; // pgut001's dumpasn1.c enforces a minimum lenght of 3
451628 return this.stream.parseOID(content, content + len, maxLength);
452629 //case 0x07: // ObjectDescriptor
453630 //case 0x08: // EXTERNAL
···484661 }
485662 return null;
486663 }
664664+665665+ /**
666666+ * Get a string representation of the ASN.1 element.
667667+ * @returns {string} The string representation.
668668+ */
487669 toString() {
488670 return this.typeName() + '@' + this.stream.pos + '[header:' + this.header + ',length:' + this.length + ',sub:' + ((this.sub === null) ? 'null' : this.sub.length) + ']';
489671 }
672672+673673+ /**
674674+ * Get a pretty string representation of the ASN.1 element.
675675+ * @param {string} indent - The indentation string.
676676+ * @returns {string} The pretty string representation.
677677+ */
490678 toPrettyString(indent) {
491679 if (indent === undefined) indent = '';
492680 let s = indent;
···517705 }
518706 return s;
519707 }
708708+709709+ /**
710710+ * Get the starting position of the element in the stream.
711711+ * @returns {number} The starting position.
712712+ */
520713 posStart() {
521714 return this.stream.pos;
522715 }
716716+717717+ /**
718718+ * Get the position of the content in the stream.
719719+ * @returns {number} The content position.
720720+ */
523721 posContent() {
524722 return this.stream.pos + this.header;
525723 }
724724+725725+ /**
726726+ * Get the ending position of the element in the stream.
727727+ * @returns {number} The ending position.
728728+ */
526729 posEnd() {
527730 return this.stream.pos + this.header + Math.abs(this.length);
528731 }
529529- /** Position of the length. */
732732+733733+ /**
734734+ * Get the position of the length in the stream.
735735+ * @returns {number} The length position.
736736+ */
530737 posLen() {
531738 return this.stream.pos + this.tagLen;
532739 }
533533- /** Hexadecimal dump of the node.
534534- * @param type 'raw', 'byte' or 'dump' */
740740+741741+ /**
742742+ * Get a hexadecimal dump of the node.
743743+ * @param {string} [type='raw'] - The dump type: 'raw', 'byte', or 'dump'.
744744+ * @returns {string} The hexadecimal dump.
745745+ */
535746 toHexString(type = 'raw') {
536747 return this.stream.hexDump(this.posStart(), this.posEnd(), type);
537748 }
538538- /** Base64 dump of the node. */
539539- toB64String() {
540540- return this.stream.b64Dump(this.posStart(), this.posEnd());
749749+750750+ /**
751751+ * Get a base64url dump of the node (according to RFC 4648 section 5).
752752+ * @param {string} [type='url'] - The dump type: 'url' (section 5 without padding) or 'std' (section 4 with padding).
753753+ * @returns {string} The base64 encoded representation.
754754+ */
755755+ toB64String(type = 'url') {
756756+ return this.stream.b64Dump(this.posStart(), this.posEnd(), type);
541757 }
758758+759759+ /**
760760+ * Decode the length field of an ASN.1 element.
761761+ * @param {Stream} stream - The stream to read from.
762762+ * @returns {number|null} The decoded length, or null for indefinite length.
763763+ * @throws {Error} If the length is invalid or exceeds 48 bits.
764764+ */
542765 static decodeLength(stream) {
543543- let buf = stream.get(),
766766+ const buf = stream.get(),
544767 len = buf & 0x7F;
545768 if (len == buf) // first bit was 0, short form
546769 return len;
547770 if (len === 0) // long form with length 0 is a special case
548771 return null; // undefined length
549549- if (len > 6) // no reason to use Int10, as it would be a huge buffer anyways
772772+ if (len > 6) // no reason to use BigInt, as it would be a huge buffer anyways
550773 throw new Error('Length over 48 bits not supported at position ' + (stream.pos - 1));
551551- buf = 0;
774774+ let value = 0;
552775 for (let i = 0; i < len; ++i)
553553- buf = (buf * 256) + stream.get();
554554- return buf;
776776+ value = (value << 8) | stream.get();
777777+ return value;
555778 }
779779+780780+ /**
781781+ * Decode an ASN.1 element from a stream.
782782+ * @param {Stream|array|string} stream - The input data.
783783+ * @param {number} [offset=0] - The offset to start decoding from.
784784+ * @param {Function} [type=ASN1] - The class to instantiate.
785785+ * @returns {ASN1} The decoded ASN.1 element.
786786+ * @throws {Error} If the decoding fails.
787787+ */
556788 static decode(stream, offset, type = ASN1) {
557789 if (!(type == ASN1 || type.prototype instanceof ASN1))
558790 throw new Error('Must pass a class that extends ASN1');
···610842 throw new Error('Unable to parse content: ' + e);
611843 }
612844 }
613613- } catch (e) {
845845+ } catch (ignore) {
614846 // but silently ignore when they don't
615847 sub = null;
616848 //DEBUG console.log('Could not decode structure at ' + start + ':', e);
+8-7
base64.js
···11// Base64 JavaScript decoder
22-// Copyright (c) 2008-2024 Lapo Luchini <lapo@lapo.it>
22+// Copyright (c) 2008 Lapo Luchini <lapo@lapo.it>
3344// Permission to use, copy, modify, and/or distribute this software for any
55// purpose with or without fee is hereby granted, provided that the above
66// copyright notice and this permission notice appear in all copies.
77-//
77+//
88// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
99// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
1010// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
···3131 decoder[b64.charCodeAt(i)] = i;
3232 for (i = 0; i < ignore.length; ++i)
3333 decoder[ignore.charCodeAt(i)] = -1;
3434- // RFC 3548 URL & file safe encoding
3434+ // also support decoding Base64url (RFC 4648 section 5)
3535 decoder['-'.charCodeAt(0)] = decoder['+'.charCodeAt(0)];
3636 decoder['_'.charCodeAt(0)] = decoder['/'.charCodeAt(0)];
3737 }
···75757676 static pretty(str) {
7777 // fix padding
7878- if (str.length % 4 > 0)
7979- str = (str + '===').slice(0, str.length + str.length % 4);
8080- // convert RFC 3548 to standard Base64
7878+ let pad = 4 - str.length % 4;
7979+ if (pad < 4)
8080+ str += '==='.slice(0, pad);
8181+ // convert Base64url (RFC 4648 section 5) to standard Base64 (RFC 4648 section 4)
8182 str = str.replace(/-/g, '+').replace(/_/g, '/');
8283 // 80 column width
8383- return str.replace(/(.{80})/g, '$1\n');
8484+ return str.replace(/.{80}/g, '$&\n');
8485 }
85868687 static unarmor(a) {
···11+CRL example from RFC5280 as found here:
22+https://csrc.nist.gov/projects/pki-testing/sample-certificates-and-crls
33+44+begin-base64 644 crl-rfc5280.der
55+MIIBYDCBygIBATANBgkqhkiG9w0BAQUFADBDMRMwEQYKCZImiZPyLGQBGRYDY29tMRcwFQYKCZIm
66+iZPyLGQBGRYHZXhhbXBsZTETMBEGA1UEAxMKRXhhbXBsZSBDQRcNMDUwMjA1MTIwMDAwWhcNMDUw
77+MjA2MTIwMDAwWjAiMCACARIXDTA0MTExOTE1NTcwM1owDDAKBgNVHRUEAwoBAaAvMC0wHwYDVR0j
88+BBgwFoAUCGivhTPIOUp6+IKTjnBqSiCELDIwCgYDVR0UBAMCAQwwDQYJKoZIhvcNAQEFBQADgYEA
99+ItwYffcIzsx10NBqm60Q9HYjtIFutW2+DvsVFGzIF20f7pAXom9g5L2qjFXejoRvkvifEBInr0rU
1010+L4XiNkR9qqNMJTgV/wD9Pn7uPSYS69jnK2LiK8NGgO94gtEVxtCccmrLznrtZ5mLbnCBfUNCdMGm
1111+r8FVF6IzTNYGmCuk/C4=
1212+====
+45
examples/crl-rfc5280.b64.dump
···11+CertificateList SEQUENCE @0+352 (constructed): (3 elem)
22+ tbsCertList TBSCertList SEQUENCE @4+202 (constructed): (7 elem)
33+ version Version INTEGER @7+1: 1
44+ signature AlgorithmIdentifier SEQUENCE @10+13 (constructed): (2 elem)
55+ algorithm OBJECT_IDENTIFIER @12+9: 1.2.840.113549.1.1.5|sha1WithRSAEncryption|PKCS #1
66+ parameters ANY NULL @23+0
77+ issuer rdnSequence Name SEQUENCE @25+67 (constructed): (3 elem)
88+ RelativeDistinguishedName SET @27+19 (constructed): (1 elem)
99+ AttributeTypeAndValue SEQUENCE @29+17 (constructed): (2 elem)
1010+ type AttributeType OBJECT_IDENTIFIER @31+10: 0.9.2342.19200300.100.1.25|domainComponent|Men are from Mars, this OID is from Pluto
1111+ value AttributeValue [?] IA5String @43+3: com
1212+ RelativeDistinguishedName SET @48+23 (constructed): (1 elem)
1313+ AttributeTypeAndValue SEQUENCE @50+21 (constructed): (2 elem)
1414+ type AttributeType OBJECT_IDENTIFIER @52+10: 0.9.2342.19200300.100.1.25|domainComponent|Men are from Mars, this OID is from Pluto
1515+ value AttributeValue [?] IA5String @64+7: example
1616+ RelativeDistinguishedName SET @73+19 (constructed): (1 elem)
1717+ AttributeTypeAndValue SEQUENCE @75+17 (constructed): (2 elem)
1818+ type AttributeType OBJECT_IDENTIFIER @77+3: 2.5.4.3|commonName|X.520 DN component
1919+ value AttributeValue [?] PrintableString @82+10: Example CA
2020+ thisUpdate utcTime Time UTCTime @94+13: 2005-02-05 12:00:00 UTC
2121+ nextUpdate utcTime Time UTCTime @109+13: 2005-02-06 12:00:00 UTC
2222+ revokedCertificates SEQUENCE @124+34 (constructed): (1 elem)
2323+ SEQUENCE @126+32 (constructed): (3 elem)
2424+ userCertificate CertificateSerialNumber INTEGER @128+1: 18
2525+ revocationDate utcTime Time UTCTime @131+13: 2004-11-19 15:57:03 UTC
2626+ crlEntryExtensions Extensions SEQUENCE @146+12 (constructed): (1 elem)
2727+ Extension SEQUENCE @148+10 (constructed): (2 elem)
2828+ extnID OBJECT_IDENTIFIER @150+3: 2.5.29.21|cRLReason|X.509 extension
2929+ extnValue OCTET_STRING @155+3 (encapsulates): (3 byte)|0A0101
3030+ ENUMERATED @157+1: 1
3131+ crlExtensions [0] @160+47 (constructed): (1 elem)
3232+ Extensions SEQUENCE @162+45 (constructed): (2 elem)
3333+ Extension SEQUENCE @164+31 (constructed): (2 elem)
3434+ extnID OBJECT_IDENTIFIER @166+3: 2.5.29.35|authorityKeyIdentifier|X.509 extension
3535+ extnValue OCTET_STRING @171+24 (encapsulates): (24 byte)|301680140868AF8533C8394A7AF882938E706A4A20842C32
3636+ SEQUENCE @173+22 (constructed): (1 elem)
3737+ [0] @175+20: (20 byte)|0868AF8533C8394A7AF882938E706A4A20842C32
3838+ Extension SEQUENCE @197+10 (constructed): (2 elem)
3939+ extnID OBJECT_IDENTIFIER @199+3: 2.5.29.20|cRLNumber|X.509 extension
4040+ extnValue OCTET_STRING @204+3 (encapsulates): (3 byte)|02010C
4141+ INTEGER @206+1: 12
4242+ signatureAlgorithm AlgorithmIdentifier SEQUENCE @209+13 (constructed): (2 elem)
4343+ algorithm OBJECT_IDENTIFIER @211+9: 1.2.840.113549.1.1.5|sha1WithRSAEncryption|PKCS #1
4444+ parameters ANY NULL @222+0
4545+ signature BIT_STRING @224+129: (1024 bit)|0010001011011100000110000111110111110111000010001100111011001100011101011101000011010000011010101001101110101101000100001111010001110110001000111011010010000001011011101011010101101101101111100000111011111011000101010001010001101100110010000001011101101101000111111110111010010000000101111010001001101111011000001110010010111101101010101000110001010101110111101000111010000100011011111001001011111000100111110001000000010010001001111010111101001010110101000010111110000101111000100011011001000100011111011010101010100011010011000010010100111000000101011111111100000000111111010011111001111110111011100011110100100110000100101110101111011000111001110010101101100010111000100010101111000011010001101000000011101111011110001000001011010001000101011100011011010000100111000111001001101010110010111100111001111010111011010110011110011001100010110110111001110000100000010111110101000011010000100111010011000001101001101010111111000001010101010001011110100010001100110100110011010110000001101001100000101011101001001111110000101110
···11+LDAPMessage example as found on ldap.com.
22+33+Original link:
44+https://ldap.com/ldapv3-wire-protocol-reference-ldap-message/
55+66+begin-base64 644 ldapmessage.der
77+MDUCAQVKEWRjPWV4YW1wbGUsZGM9Y29toB0wGwQWMS4yLjg0MC4xMTM1NTYuMS40LjgwNQEB/w==
88+====
···11// Hex JavaScript decoder
22-// Copyright (c) 2008-2024 Lapo Luchini <lapo@lapo.it>
22+// Copyright (c) 2008 Lapo Luchini <lapo@lapo.it>
3344// Permission to use, copy, modify, and/or distribute this software for any
55// purpose with or without fee is hereby granted, provided that the above
66// copyright notice and this permission notice appear in all copies.
77-//
77+//
88// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
99// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
1010// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
···11+import './theme.js';
12import { ASN1DOM } from './dom.js';
23import { Base64 } from './base64.js';
34import { Hex } from './hex.js';
···1516 area = id('area'),
1617 file = id('file'),
1718 examples = id('examples'),
1818- selectTheme = id('theme-select'),
1919 selectDefs = id('definitions'),
2020 selectTag = id('tags');
2121···4141function show(asn1) {
4242 tree.innerHTML = '';
4343 dump.innerHTML = '';
4444- tree.appendChild(asn1.toDOM());
4444+ let ul = document.createElement('ul');
4545+ ul.className = 'treecollapse';
4646+ tree.appendChild(ul);
4747+ ul.appendChild(asn1.toDOM());
4548 if (wantHex.checked) dump.appendChild(asn1.toHexDOM(undefined, trimHex.checked));
4649}
4750export function decode(der, offset) {
···8386 if (area.value === '') area.value = Base64.pretty(b64);
8487 try {
8588 window.location.hash = hash = '#' + b64;
8686- } catch (e) {
8989+ } catch (ignore) {
8790 // fails with "Access Denied" on IE with URLs longer than ~2048 chars
8891 window.location.hash = hash = '#';
8992 }
···119122 else if (Base64.re.test(str)) der = Base64.unarmor(str);
120123 else der = str;
121124 decode(der);
122122- } catch (e) {
125125+ } catch (ignore) {
123126 text(tree, 'Cannot decode file.');
124127 dump.innerHTML = '';
125128 }
···157160 let elem = id(name);
158161 if (elem)
159162 elem.onclick = onClick;
160160-}
161161-// set dark theme depending on OS settings
162162-function setTheme() {
163163- if (!selectTheme) {
164164- console.log('Themes are currently not working with single file version.');
165165- return;
166166- }
167167- let storedTheme = localStorage.getItem('theme');
168168- let theme = 'os';
169169- if (storedTheme)
170170- theme = storedTheme;
171171- selectTheme.value = theme;
172172- if (theme == 'os') {
173173- let prefersDarkScheme = window.matchMedia('(prefers-color-scheme: dark)');
174174- theme = prefersDarkScheme.matches ? 'dark': 'light';
175175- }
176176- if (theme == 'dark') {
177177- const css1 = id('theme-base');
178178- const css2 = css1.cloneNode();
179179- css2.id = 'theme-override';
180180- css2.href = 'index-' + theme + '.css';
181181- css1.parentElement.appendChild(css2);
182182- } else {
183183- const css2 = id('theme-override');
184184- if (css2) css2.remove();
185185- }
186186-}
187187-setTheme();
188188-if (selectTheme) {
189189- selectTheme.addEventListener('change', function () {
190190- localStorage.setItem('theme', selectTheme.value);
191191- setTheme();
192192- });
193163}
194164// this is only used if window.FileReader
195165function read(f) {
-106
int10.js
···11-// Big integer base-10 printing library
22-// Copyright (c) 2008-2024 Lapo Luchini <lapo@lapo.it>
33-44-// Permission to use, copy, modify, and/or distribute this software for any
55-// purpose with or without fee is hereby granted, provided that the above
66-// copyright notice and this permission notice appear in all copies.
77-//
88-// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
99-// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
1010-// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
1111-// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
1212-// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
1313-// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
1414-// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
1515-1616-let max = 10000000000000; // biggest 10^n integer that can still fit 2^53 when multiplied by 256
1717-1818-export class Int10 {
1919- /**
2020- * Arbitrary length base-10 value.
2121- * @param {number} value - Optional initial value (will be 0 otherwise).
2222- */
2323- constructor(value) {
2424- this.buf = [+value || 0];
2525- }
2626-2727- /**
2828- * Multiply value by m and add c.
2929- * @param {number} m - multiplier, must be < =256
3030- * @param {number} c - value to add
3131- */
3232- mulAdd(m, c) {
3333- // assert(m <= 256)
3434- let b = this.buf,
3535- l = b.length,
3636- i, t;
3737- for (i = 0; i < l; ++i) {
3838- t = b[i] * m + c;
3939- if (t < max)
4040- c = 0;
4141- else {
4242- c = 0|(t / max);
4343- t -= c * max;
4444- }
4545- b[i] = t;
4646- }
4747- if (c > 0)
4848- b[i] = c;
4949- }
5050-5151- /**
5252- * Subtract value.
5353- * @param {number} c - value to subtract
5454- */
5555- sub(c) {
5656- let b = this.buf,
5757- l = b.length,
5858- i, t;
5959- for (i = 0; i < l; ++i) {
6060- t = b[i] - c;
6161- if (t < 0) {
6262- t += max;
6363- c = 1;
6464- } else
6565- c = 0;
6666- b[i] = t;
6767- }
6868- while (b[b.length - 1] === 0)
6969- b.pop();
7070- }
7171-7272- /**
7373- * Convert to decimal string representation.
7474- * @param {*} base - optional value, only value accepted is 10
7575- */
7676- toString(base) {
7777- if ((base || 10) != 10)
7878- throw 'only base 10 is supported';
7979- let b = this.buf,
8080- s = b[b.length - 1].toString();
8181- for (let i = b.length - 2; i >= 0; --i)
8282- s += (max + b[i]).toString().substring(1);
8383- return s;
8484- }
8585-8686- /**
8787- * Convert to Number value representation.
8888- * Will probably overflow 2^53 and thus become approximate.
8989- */
9090- valueOf() {
9191- let b = this.buf,
9292- v = 0;
9393- for (let i = b.length - 1; i >= 0; --i)
9494- v = v * max + b[i];
9595- return v;
9696- }
9797-9898- /**
9999- * Return value as a simple Number (if it is <= 10000000000000), or return this.
100100- */
101101- simplify() {
102102- let b = this.buf;
103103- return (b.length == 1) ? b[0] : this;
104104- }
105105-106106-}