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