JavaScript generic ASN.1 parser (mirror)
at github-104 22 kB view raw
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.');