1#! /usr/bin/env node
2
3// RFC ASN.1 definition parser
4// Copyright (c) 2021 Lapo Luchini <lapo@lapo.it>
5
6// Permission to use, copy, modify, and/or distribute this software for any
7// purpose with or without fee is hereby granted, provided that the above
8// copyright notice and this permission notice appear in all copies.
9//
10// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
11// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
12// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
13// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
14// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
15// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
16// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
17
18import * as fs from 'node:fs';
19
20const
21 patches = { // to fix some known RFCs' ASN.1 syntax errors
22 0: [
23 [ /\n\n[A-Z].*\n\f\n[A-Z].*\n\n/g, '' ], // page change
24 ],
25 2459: [ // currently unsupported
26 [ 'videotex (8) } (0..ub-integer-options)', 'videotex (8) }' ],
27 [ /OBJECT IDENTIFIER \( id-qt-cps \| id-qt-unotice \)/g, 'OBJECT IDENTIFIER' ],
28 [ /SIGNED \{ (SEQUENCE \{[^}]+\})\s*\}/g, 'SEQUENCE { toBeSigned $1, algorithm AlgorithmIdentifier, signature BIT STRING }' ],
29 [ /EXTENSION\.&[^,]+/g, 'OBJECT IDENTIFIER'],
30 ],
31 2986: [ // currently unsupported
32 [ /FROM (InformationFramework|AuthenticationFramework) [a-zA-Z]+/g, 'FROM $1 {joint-iso-itu-t(2) ds(5) module(1) usefulDefinitions(0) 3}' ],
33 [ /[(]v1,[^)]+[)]/g, '' ],
34 [ /[{][{][^}]+[}][}]/g, '' ],
35 [ 'SubjectPublicKeyInfo {ALGORITHM: IOSet}', 'SubjectPublicKeyInfo' ],
36 [ /PKInfoAlgorithms ALGORITHM ::=[^}]+[}]/g, '' ],
37 [ /(Attributes?) [{] ATTRIBUTE:IOSet [}]/g, '$1' ],
38 [ /CRIAttributes +ATTRIBUTE +::=[^}]+[}]/g, '' ],
39 [ /[A-Z]+[.]&id[(][{]IOSet[}][)]/g, 'OBJECT IDENTIFIER' ],
40 [ /[A-Z]+[.]&Type[(][{]IOSet[}][{]@[a-z]+[}][)]/g, 'ANY' ],
41 [ /(AlgorithmIdentifier) [{]ALGORITHM:IOSet [}]/g, '$1' ],
42 [ /SignatureAlgorithms ALGORITHM ::=[^}]+[}]/g, '' ],
43 ],
44 3161: [ // actual syntax errors
45 [ /--.*}/g, '}' ],
46 [ /^( +)--.*\n(?:\1 .*\n)+/mg, '' ],
47 [ /addInfoNotAvailable \(17\)/g, '$&,' ],
48 ],
49 5208: [ // currently unsupported
50 [ 'FROM InformationFramework informationFramework', 'FROM InformationFramework {joint-iso-itu-t(2) ds(5) module(1) usefulDefinitions(0) 3}' ],
51 [ ' {{PrivateKeyAlgorithms}}', '' ],
52 [ 'Version ::= INTEGER {v1(0)} (v1,...)', 'Version ::= INTEGER {v1(0)}' ],
53 [ ' {{KeyEncryptionAlgorithms}}', '' ],
54 [ /\.\.\. -- For local profiles/g, '' ],
55 ],
56 5280: [ // currently unsupported
57 [ 'videotex (8) } (0..ub-integer-options)', 'videotex (8) }' ],
58 [ /OBJECT IDENTIFIER \( id-qt-cps \| id-qt-unotice \)/g, 'OBJECT IDENTIFIER' ],
59 ],
60 4210: [
61 [ /^\s+-- .*\r?\n/mg, '' ], // comments
62 ],
63 8017: [ // this RFC uses a lot of currently unsupported syntax
64 [ /ALGORITHM-IDENTIFIER ::= CLASS[^-]+--/, '--' ],
65 [ /\n +\S+ +ALGORITHM-IDENTIFIER[^\n]+(\n {6}[^\n]+)+\n {3}[}]/g, '' ],
66 [ /AlgorithmIdentifier [{] ALGORITHM-IDENTIFIER:InfoObjectSet [}] ::=(\n {6}[^\n]+)+\n {3}[}]/, 'AlgorithmIdentifier ::= ANY'],
67 [ /algorithm +id-[^,\n]+,/g, 'algorithm ANY,' ],
68 [ / (sha1 {4}HashAlgorithm|mgf1SHA1 {4}MaskGenAlgorithm|pSpecifiedEmpty {4}PSourceAlgorithm|rSAES-OAEP-Default-Identifier {4}RSAES-AlgorithmIdentifier|rSASSA-PSS-Default-Identifier {4}RSASSA-AlgorithmIdentifier) ::= [{](\n( {6}[^\n]+)?)+\n {3}[}]/g, '' ],
69 [ / ::= AlgorithmIdentifier [{]\s+[{][^}]+[}]\s+[}]/g, ' ::= AlgorithmIdentifier' ],
70 [ /OCTET STRING[(]SIZE[(]0..MAX[)][)]/g, 'OCTET STRING' ],
71 [ /emptyString {4}EncodingParameters ::= ''H/g, '' ],
72 [ /[(]CONSTRAINED BY[^)]+[)]/g, '' ],
73 ],
74 4511: [
75 [ /^\s+-- .*\r?\n/mg, '' ], // comments
76 [ 'EXTENSIBILITY IMPLIED', '' ],
77 [ /\.\.\.(,| {2})/g, '' ],
78 [ /value AttributeValue/g, 'AttributeValue' ],
79 [ /control Control/g, 'Control' ],
80 [ /Attribute ::= PartialAttribute\(WITH COMPONENTS \{[^}]+\}\)/g, 'PartialAttribute ::= SEQUENCE { type AttributeDescription, vals SET SIZE (1..MAX) OF AttributeValue }' ],
81 [ /,\s+\}/g, '}' ],
82 [ /SaslCredentials,/g, 'SaslCredentials' ],
83 [ /(BindResponse|ExtendedResponse) ::= \[APPLICATION [0-9]+\] SEQUENCE \{[^}]+\}/g, '$1 ::= ANY' ],
84 [ /selector LDAPString/g, 'LDAPString' ],
85 [ /filter Filter/g, 'Filter' ],
86 [ /MatchingRuleAssertion,/g, 'MatchingRuleAssertion' ],
87 [ /OF substring CHOICE/g, 'OF CHOICE' ],
88 [ /partialAttribute PartialAttribute/g, 'PartialAttribute' ],
89 [ /uri URI/g, 'URI' ],
90 [ /OF change SEQUENCE/g, 'OF SEQUENCE' ],
91 [ /attribute Attribute/g, 'Attribute' ],
92 ],
93 };
94
95// const reWhitespace = /(?:\s|--(?:[}-]?[^\n}-])*(?:\n|--))*/y;
96const reWhitespace = /(?:\s|--(?:-?[^\n-])*(?:\n|--))*/my;
97const reIdentifier = /[a-zA-Z](?:[-]?[a-zA-Z0-9])*/y;
98const reNumber = /0|[1-9][0-9]*/y;
99const reToken = /[(){},[\];]|::=|OPTIONAL|DEFAULT|NULL|TRUE|FALSE|\.\.|OF|SIZE|MIN|MAX|DEFINED BY|DEFINITIONS|TAGS|BEGIN|EXPORTS|IMPORTS|FROM|END/y;
100const reType = /ANY|NULL|BOOLEAN|INTEGER|(?:BIT|OCTET)\s+STRING|OBJECT\s+IDENTIFIER|SEQUENCE|SET|CHOICE|ENUMERATED|(?:Generalized|UTC)Time|(?:BMP|General|Graphic|IA5|ISO64|Numeric|Printable|Teletex|T61|Universal|UTF8|Videotex|Visible)String/y;
101const reTagClass = /UNIVERSAL|APPLICATION|PRIVATE|/y;
102const reTagType = /IMPLICIT|EXPLICIT|/y;
103const reTagDefault = /(AUTOMATIC|IMPLICIT|EXPLICIT) TAGS|/y;
104
105let asn1;
106let currentMod;
107
108function searchImportedValue(id) {
109 for (let imp of Object.values(currentMod.imports))
110 for (let name of imp.types)
111 if (name == id) {
112 if (!(imp.oid in asn1))
113 throw new Error('Cannot find module: ' + imp.oid + ' ' + id);
114 if (id in asn1[imp.oid].values)
115 return asn1[imp.oid].values[id];
116 throw new Error('Cannot find imported value: ' + imp.oid + ' ' + id);
117 }
118 throw new Error('Cannot find imported value in any module: ' + id);
119}
120
121class Parser {
122 constructor(enc, pos) {
123 this.enc = enc;
124 this.pos = pos;
125 this.start = pos;
126 }
127 getChar(pos) {
128 if (pos === undefined)
129 pos = this.pos++;
130 if (pos >= this.enc.length)
131 throw 'Requesting byte offset ' + pos + ' on a stream of length ' + this.enc.length;
132 return this.enc.charAt(pos);
133 }
134 exception(s) {
135 const pos = this.pos;
136 let from = Math.max(pos - 30, this.start);
137 let to = Math.min(pos + 30, this.enc.length);
138 let ctx = '';
139 let arrow = '';
140 let i = from;
141 for (; i < pos; ++i) {
142 ctx += this.getChar(i);
143 arrow += ' ';
144 }
145 ctx += this.getChar(i++);
146 arrow += '^';
147 for (; i < to; ++i)
148 ctx += this.getChar(i);
149 // calculate line/column
150 let line = 1;
151 let lastLF = 0;
152 for (let i = 0; i < pos; ++i)
153 if (this.enc.charAt(i) == '\n') {
154 ++line;
155 lastLF = i;
156 }
157 let column = pos - lastLF;
158 throw new Error('[position ' + pos + ', line ' + line + ':' + column + '] ' + s + '\n' + ctx.replace(/\s/g, ' ') + '\n' + arrow);
159 }
160 peek() {
161 return this.enc.charCodeAt(this.pos);
162 }
163 peekChar() {
164 return this.enc.charAt(this.pos);
165 }
166 isWhitespace() {
167 let c = this.peekChar();
168 return c == ' ' || c == '\n';
169 }
170 isDigit() {
171 let c = this.peekChar();
172 return c >= '0' && c <= '9';
173 }
174 skipWhitespace() {
175 reWhitespace.lastIndex = this.pos;
176 let s = reWhitespace.exec(this.enc);
177 if (s)
178 this.pos = reWhitespace.lastIndex;
179 }
180 // DefStream.prototype.eat = function (str) {
181 // for (let i = 0; i < str.length; ++i) {
182 // let c = this.getChar();
183 // if (c != str.charAt(i))
184 // throw new Error("Found '" + c + "', was expecting '" + str.charAt(i) + "'");
185 // }
186 // };
187 getRegEx(type, re) {
188 this.skipWhitespace();
189 re.lastIndex = this.pos;
190 let s = re.exec(this.enc); //TODO: does not work with typed arrays
191 if (!s)
192 this.exception("Found '" + this.peekChar() + "', was expecting a " + type);
193 s = s[0];
194 // console.log('[debug] getRexEx@' + this.pos + ' = ' + s);
195 this.pos = re.lastIndex;
196 this.skipWhitespace();
197 return s;
198 }
199 parseIdentifier() {
200 let id = this.getRegEx('identifier', reIdentifier);
201 // console.log('[debug] parseIdentifier = ' + id);
202 return id;
203 }
204 parseNumber() {
205 let id = this.getRegEx('number', reNumber);
206 // console.log('[debug] parseNumber = ' + id);
207 return id;
208 }
209 parseToken() {
210 let tok = this.getRegEx('token', reToken);
211 return tok;
212 }
213 tryToken(expect) {
214 let p = this.pos;
215 let t;
216 try { t = this.parseToken(); } catch (ignore) { /*ignore*/ }
217 // console.log('[debug] tryToken(' + expect + ') = ' + t);
218 if (t == expect)
219 return true;
220 else {
221 this.pos = p;
222 return false;
223 }
224 }
225 expectToken(expect) {
226 let p = this.pos;
227 let t;
228 try { t = this.parseToken(); }
229 catch (e) { console.log('[debug] expectToken', e); }
230 // console.log('[debug] expectToken(' + expect + ') = ' + t);
231 if (t != expect) {
232 this.pos = p;
233 this.exception("Found '" + t + "', was expecting '" + expect + "'");
234 }
235 }
236 parseNumberOrValue() {
237 if (this.isDigit())
238 return +this.parseNumber();
239 return this.parseIdentifier();
240 }
241 parseRange() {
242 let min = this.tryToken('MIN') ? 'MIN' : this.parseNumberOrValue();
243 if (this.tryToken('..')) {
244 let max = this.tryToken('MAX') ? 'MAX' : this.parseNumberOrValue();
245 return [min, max];
246 }
247 return min;
248 }
249 parseBuiltinType() {
250 let x = {
251 name: this.getRegEx('type', reType),
252 type: 'builtin',
253 };
254 // console.log('[debug] parseType = ' + x.name);
255 try {
256 switch (x.name) {
257 case 'ANY':
258 if (this.tryToken('DEFINED BY'))
259 x.definedBy = this.parseIdentifier();
260 break;
261 case 'NULL':
262 case 'BOOLEAN':
263 case 'OCTET STRING':
264 case 'OBJECT IDENTIFIER':
265 break;
266 case 'CHOICE':
267 x.content = this.parseElementTypeList();
268 break;
269 case 'SEQUENCE':
270 case 'SET':
271 if (this.peekChar() == '{') {
272 x.content = this.parseElementTypeList();
273 } else {
274 x.typeOf = 1;
275 if (this.tryToken('SIZE')) {
276 this.expectToken('(');
277 x.size = this.parseRange();
278 this.expectToken(')');
279 }
280 this.expectToken('OF');
281 x.content = [this.parseType()];
282 }
283 break;
284 case 'INTEGER':
285 if (this.tryToken('(')) {
286 x.range = this.parseRange();
287 this.expectToken(')');
288 }
289 // falls through
290 case 'ENUMERATED':
291 case 'BIT STRING':
292 if (this.tryToken('{')) {
293 x.content = {};
294 do {
295 let id = this.parseIdentifier();
296 this.expectToken('(');
297 let val = this.parseNumber(); //TODO: signed
298 this.expectToken(')');
299 x.content[id] = +val;
300 } while (this.tryToken(','));
301 this.expectToken('}');
302 }
303 break;
304 case 'BMPString':
305 case 'GeneralString':
306 case 'GraphicString':
307 case 'IA5String':
308 case 'ISO646String':
309 case 'NumericString':
310 case 'PrintableString':
311 case 'TeletexString':
312 case 'T61String':
313 case 'UniversalString':
314 case 'UTF8String':
315 case 'VideotexString':
316 case 'VisibleString':
317 if (this.tryToken('(')) {
318 if (this.tryToken('SIZE')) {
319 this.expectToken('(');
320 x.size = this.parseRange();
321 this.expectToken(')');
322 }
323 this.expectToken(')');
324 }
325 break;
326 case 'UTCTime':
327 case 'GeneralizedTime':
328 break;
329 default:
330 x.warning = 'type unknown';
331 }
332 } catch (e) {
333 console.log('[debug] parseBuiltinType content', e);
334 x.warning = 'type exception';
335 }
336 return x;
337 }
338 parseTaggedType() {
339 this.expectToken('[');
340 let tagClass = this.getRegEx('class', reTagClass) || 'CONTEXT'; //TODO: use module defaults
341 let t = this.parseNumber();
342 this.expectToken(']');
343 let plicit = this.getRegEx('explicit/implicit', reTagType);
344 if (plicit == '') plicit = currentMod.tagDefault;
345 let x = this.parseType();
346 let name;
347 switch (tagClass) { // keep in sync with ASN1.typeName
348 case 'APPLICATION':
349 name = 'Application ' + t;
350 break;
351 case 'PRIVATE':
352 name = 'Private ' + t;
353 break;
354 case 'CONTEXT':
355 // fall through
356 default:
357 name = '[' + t + ']';
358 break;
359 }
360 return {
361 name,
362 type: 'tag',
363 'class': tagClass,
364 explicit: (plicit == 'EXPLICIT'),
365 content: [{ name: '', type: x }],
366 };
367 }
368 parseType() {
369 if (this.peekChar() == '[')
370 return this.parseTaggedType();
371 let p = this.pos;
372 try {
373 return this.parseBuiltinType();
374 } catch (ignore) {
375 // console.log('[debug] parseAssignment failed on parseType', e);
376 this.pos = p;
377 let x = {
378 name: this.parseIdentifier(),
379 type: 'defined',
380 };
381 // let from = searchImportedType(x.name);
382 // if (from)
383 // x.module = from;
384 return x;
385 //TODO "restricted string type"
386 }
387 }
388 parseValueBoolean() {
389 let p = this.pos;
390 let t = this.parseToken();
391 if (t == 'TRUE')
392 return true;
393 if (t == 'FALSE')
394 return false;
395 this.pos = p;
396 this.exception("Found '" + t + "', was expecting a boolean");
397 }
398 parseValueOID() {
399 this.expectToken('{');
400 let v = '';
401 while (!this.tryToken('}')) {
402 let p = this.pos;
403 let val;
404 if (this.isDigit())
405 val = this.parseNumber();
406 else {
407 this.pos = p;
408 let id = this.parseIdentifier();
409 if (this.tryToken('(')) {
410 val = this.parseNumber();
411 this.expectToken(')');
412 } else {
413 if (id in currentMod.values) // defined in local module
414 val = currentMod.values[id].value;
415 else try {
416 val = searchImportedValue(id);
417 } catch (e) {
418 this.exception(e.message);
419 }
420 }
421 }
422 if (v.length) v += '.';
423 v += val;
424 }
425 return v;
426 }
427 parseValue() {
428 let c = this.peekChar();
429 if (c == '{')
430 return this.parseValueOID();
431 if (c >= '0' && c <= '9')
432 return +this.parseNumber();
433 if (c == '-')
434 return -this.parseNumber();
435 let p = this.pos;
436 try {
437 switch (this.parseToken()) {
438 case 'TRUE':
439 return true;
440 case 'FALSE':
441 return false;
442 case 'NULL':
443 return null;
444 }
445 } catch (ignore) {
446 this.pos = p;
447 }
448 p = this.pos;
449 try {
450 return this.parseIdentifier();
451 } catch (ignore) {
452 this.pos = p;
453 }
454 this.exception('Unknown value type.');
455 }
456 /*DefStream.prototype.parseValue = function (type) {
457 console.log('[debug] parseValue type:', type);
458 if (type.type == 'defined') {
459 if (!(type.name in types))
460 this.exception("Missing type: " + type.name);
461 type = types[type.name];
462 }
463 switch (type.name) {
464 case 'BOOLEAN':
465 return this.parseValueBoolean();
466 case 'OBJECT IDENTIFIER':
467 return this.parseValueOID();
468 default:
469 console.log('[debug] parseValue unknown:', type);
470 return 'TODO:value';
471 }
472 }*/
473 parseElementType() {
474 let x = Object.assign({ id: this.parseIdentifier() }, this.parseType());
475 // console.log('[debug] parseElementType 1:', x);
476 if (this.tryToken('OPTIONAL'))
477 x.optional = true;
478 if (this.tryToken('DEFAULT'))
479 x.default = this.parseValue(x.type);
480 // console.log('[debug] parseElementType 2:', x);
481 return x;
482 }
483 parseElementTypeList() {
484 let v = [];
485 this.expectToken('{');
486 do {
487 v.push(this.parseElementType());
488 } while (this.tryToken(','));
489 this.expectToken('}');
490 return v;
491 }
492 parseAssignment() {
493 let name = this.parseIdentifier();
494 if (this.tryToken('::=')) { // type assignment
495 // console.log('type name', name);
496 let type = this.parseType();
497 currentMod.types[name] = { name, type };
498 return currentMod.types[name];
499 } else { // value assignment
500 // console.log('value name', name);
501 let type = this.parseType();
502 // console.log('[debug] parseAssignment type:', type);
503 this.expectToken('::=');
504 let value = this.parseValue(type);
505 currentMod.values[name] = { name, type, value };
506 return currentMod.values[name];
507 }
508 }
509 parseModuleIdentifier() {
510 return {
511 name: this.parseIdentifier(),
512 oid: this.parseValueOID(),
513 };
514 }
515 parseSymbolsImported() {
516 let imports = {};
517 do {
518 let l = [];
519 do {
520 l.push(this.parseIdentifier());
521 } while (this.tryToken(','));
522 this.expectToken('FROM');
523 let mod = this.parseModuleIdentifier();
524 mod.types = l;
525 imports[mod.oid] = mod;
526 } while (this.peekChar() != ';');
527 return imports;
528 }
529 parseModuleDefinition(file) {
530 let mod = this.parseModuleIdentifier();
531 currentMod = mod; // for deeply nested parsers
532 mod.source = file;
533 this.expectToken('DEFINITIONS');
534 mod.tagDefault = this.getRegEx('tag default', reTagDefault).split(' ')[0];
535 this.expectToken('::=');
536 this.expectToken('BEGIN');
537 //TODO this.tryToken('EXPORTS')
538 if (this.tryToken('IMPORTS')) {
539 mod.imports = this.parseSymbolsImported();
540 this.expectToken(';');
541 }
542 mod.values = {};
543 mod.types = {};
544 while (!this.tryToken('END'))
545 this.parseAssignment();
546 return mod;
547 }
548}
549
550let s = fs.readFileSync(process.argv[2], 'utf8');
551let num = /^Request for Comments: ([0-9]+)/m.exec(s)[1];
552console.log('RFC:', num);
553for (let p of patches[0])
554 s = s.replace(p[0], p[1]);
555if (num in patches)
556 for (let p of patches[num])
557 s = s.replace(p[0], p[1]);
558fs.writeFileSync(process.argv[2].replace(/[.]txt$/, '_patched.txt'), s, 'utf8');
559// console.log(s);
560asn1 = JSON.parse(fs.readFileSync(process.argv[3], 'utf8'));
561const reModuleDefinition = /\s[A-Z](?:[-]?[a-zA-Z0-9])*\s*\{[^}]+\}\s*(^--.*|\n)*DEFINITIONS/gm;
562let m;
563while ((m = reModuleDefinition.exec(s))) {
564 new Parser(s, m.index).parseModuleDefinition(process.argv[2]);
565 console.log('Module:', currentMod.name);
566 // fs.writeFileSync('rfc' + num + '.json', JSON.stringify(currentMod, null, 2) + '\n', 'utf8');
567 asn1[currentMod.oid] = currentMod;
568}
569/*asn1 = Object.keys(asn1).sort().reduce(
570 (obj, key) => {
571 obj[key] = asn1[key];
572 return obj;
573 },
574 {}
575);*/
576fs.writeFileSync(process.argv[3], JSON.stringify(asn1, null, 2) + '\n', 'utf8');
577// console.log('Module:', mod);
578/*while ((idx = s.indexOf('::=', idx + 1)) >= 0) {
579 let line = s.lastIndexOf('\n', idx) + 1;
580 // console.log('[line] ' + s.slice(line, line+30));
581 try {
582 let a = new DefStream(s, line).parseAssignment();
583 // console.log('[assignment]', util.inspect(a, {showHidden: false, depth: null, colors: true}));
584 } catch (e) {
585 console.log('Error:', e);
586 }
587}*/
588console.log('Done.');