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