... | ... |
@@ -1,6 +1,6 @@ |
1 | 1 |
#!/usr/bin/env python3 |
2 | 2 |
|
3 |
-# Copyright © 2020 Xavier G. <xavier.yamltab@kindwolf.org> |
|
3 |
+# Copyright © 2020-2022 Xavier G. <xavier.yamltab@kindwolf.org> |
|
4 | 4 |
# This work is free. You can redistribute it and/or modify it under the |
5 | 5 |
# terms of the Do What The Fuck You Want To Public License, Version 2, |
6 | 6 |
# as published by Sam Hocevar. See the COPYING file for more details. |
... | ... |
@@ -430,7 +430,7 @@ def spn_to_principal(spn, name_type='KRB5_NT_PRINCIPAL'): |
430 | 430 |
raise KeytabComposingError('Cannot parse SPN %s into principal' % spn) |
431 | 431 |
principal['realm'] = rem.group('realm') |
432 | 432 |
principal['components'] = [] |
433 |
- for name, component in rem.groupdict(): |
|
433 |
+ for name, component in rem.groupdict().items(): |
|
434 | 434 |
if name.startswith('comp') and component is not None: |
435 | 435 |
principal['components'].append(component) |
436 | 436 |
return principal |
... | ... |
@@ -315,7 +315,15 @@ def keytab_to_yaml(buf, args): |
315 | 315 |
def output_data(data, args, exit=None): |
316 | 316 |
serialized_data = prepare_serialization(data) |
317 | 317 |
if args.output_format == 'yaml': |
318 |
- yaml.dump(serialized_data, sys.stdout, width=160, sort_keys=False) |
|
318 |
+ yaml_width = 160 |
|
319 |
+ try: |
|
320 |
+ yaml.dump(serialized_data, sys.stdout, width=yaml_width, sort_keys=False) |
|
321 |
+ except TypeError as error: |
|
322 |
+ # Handle lack of support for sort_keys: |
|
323 |
+ if 'sort_keys' in str(error): |
|
324 |
+ yaml.dump(serialized_data, sys.stdout, width=yaml_width) |
|
325 |
+ else: |
|
326 |
+ raise |
|
319 | 327 |
else: |
320 | 328 |
json.dump(serialized_data, sys.stdout, indent=4) |
321 | 329 |
if exit is not None: |
... | ... |
@@ -26,7 +26,7 @@ DATA_LAYOUT_FULL = 1 |
26 | 26 |
DATA_LAYOUT_SIMPLE = 2 |
27 | 27 |
DATA_LAYOUTS = {'raw': DATA_LAYOUT_RAW, 'full': DATA_LAYOUT_FULL, 'simple': DATA_LAYOUT_SIMPLE} |
28 | 28 |
|
29 |
-DATE_TIME_FORMAT = '%Y-%m-%dT%H:%M:%SZ' |
|
29 |
+DATE_TIME_FORMAT = '%Y-%m-%dT%H:%M:%S%z' |
|
30 | 30 |
|
31 | 31 |
KEYTAB_FIRST_BYTE = 0x05 |
32 | 32 |
# Default prefix for struct's format strings, defining big-endian byte order: |
... | ... |
@@ -231,7 +231,7 @@ def enrich_keytab(keytab): |
231 | 231 |
if 'entry' not in record: |
232 | 232 |
continue |
233 | 233 |
entry = record['entry'] |
234 |
- entry['date'] = datetime.utcfromtimestamp(entry['timestamp']).strftime(DATE_TIME_FORMAT) |
|
234 |
+ entry['date'] = datetime.fromtimestamp(entry['timestamp'], tz=timezone.utc).strftime(DATE_TIME_FORMAT) |
|
235 | 235 |
if 'name_type_raw' in entry['principal']: |
236 | 236 |
entry['principal']['name_type'] = int_to_name_type(entry['principal']['name_type_raw']) |
237 | 237 |
spn = principal_to_spn(entry['principal']) |
... | ... |
@@ -13,7 +13,7 @@ import json |
13 | 13 |
import yaml |
14 | 14 |
import struct |
15 | 15 |
import argparse |
16 |
-from datetime import datetime |
|
16 |
+from datetime import datetime, timezone |
|
17 | 17 |
from binascii import hexlify, unhexlify |
18 | 18 |
|
19 | 19 |
# Documents used as reference to implement the keytab format: |
... | ... |
@@ -37,6 +37,7 @@ DEFAULT_PREFIX=BIG_ENDIAN |
37 | 37 |
DEFAULT_ENCODING='ascii' |
38 | 38 |
VERBOSITY=1 |
39 | 39 |
CALCSIZE={} |
40 |
+now = None |
|
40 | 41 |
|
41 | 42 |
# The following table is based on [3]: |
42 | 43 |
NAME_TYPES = { |
... | ... |
@@ -474,6 +475,9 @@ def simple_timestamp_to_full(inentry, index, entry): |
474 | 475 |
elif 'date' in inentry: |
475 | 476 |
entry['date'] = inentry['date'] |
476 | 477 |
if inentry['date'] == 'now': |
478 |
+ global now |
|
479 |
+ if now is None: |
|
480 |
+ now = int(datetime.now(timezone.utc).timestamp()) |
|
477 | 481 |
entry['timestamp'] = now |
478 | 482 |
else: |
479 | 483 |
try: |
... | ... |
@@ -497,7 +501,6 @@ def simple_enctype_to_full(inentry, index, entry): |
497 | 501 |
return 2 |
498 | 502 |
|
499 | 503 |
def simple_keytab_to_full(indata): |
500 |
- now = int(datetime.now().timestamp()) |
|
501 | 504 |
data = { |
502 | 505 |
'version': indata.get('version', 2), |
503 | 506 |
'records': [], |
... | ... |
@@ -537,6 +537,8 @@ def parse_args(): |
537 | 537 |
parser.add_argument('--output-format', '-f', dest='output_format', choices=['json', 'yaml'], default='yaml', help='output format (keytab to YAML/JSON only)') |
538 | 538 |
parser.add_argument('--v1-endianness', '-e', dest='v1_endianness', choices=['native', 'little', 'big'], default='native', help='Enforce endianness (keytab v1 to YAML/JSON only)') |
539 | 539 |
parser.add_argument('--simple-to-full', dest='simple_to_full', action='store_true', default=False, help='Convert from simple to full layout (YAML/JSON input only)') |
540 |
+ parser.add_argument('--name-types', dest='list_name_types', action='store_true', default=False, help='List all known name types.') |
|
541 |
+ parser.add_argument('--enc-types', dest='list_enc_types', action='store_true', default=False, help='List all known encryption types.') |
|
540 | 542 |
parser.add_argument('input', nargs='?', type=argparse.FileType('rb'), default=sys.stdin.buffer, help='input file; defaults to standard input') |
541 | 543 |
args = parser.parse_args() |
542 | 544 |
return args |
... | ... |
@@ -545,6 +547,10 @@ def main(): |
545 | 547 |
args = parse_args() |
546 | 548 |
global VERBOSITY |
547 | 549 |
VERBOSITY=args.verbose |
550 |
+ if args.list_name_types: |
|
551 |
+ output_data(NAME_TYPES, args, 0) |
|
552 |
+ if args.list_enc_types: |
|
553 |
+ output_data(ENC_TYPES, args, 0) |
|
548 | 554 |
buf = args.input |
549 | 555 |
first_byte = buf.peek(1)[0] |
550 | 556 |
if first_byte == KEYTAB_FIRST_BYTE: |
... | ... |
@@ -446,7 +446,9 @@ def simple_principal_to_full(inentry, index, entry): |
446 | 446 |
message = 'Invalid or unknown name_type specified in entry #%d' |
447 | 447 |
message += '; use name_type_raw to enforce an arbitrary value' |
448 | 448 |
raise KeytabParsingError(message % index) |
449 |
- entry['spn'] = principal_to_spn(principal) |
|
449 |
+ spn = principal_to_spn(principal) |
|
450 |
+ if spn: |
|
451 |
+ entry['spn'] = spn |
|
450 | 452 |
elif 'spn' in inentry: |
451 | 453 |
entry['spn'] = inentry['spn'] |
452 | 454 |
entry['principal'] = spn_to_principal(inentry['spn']) |
... | ... |
@@ -320,6 +320,12 @@ def output_data(data, args, exit=None): |
320 | 320 |
if exit is not None: |
321 | 321 |
sys.exit(exit) |
322 | 322 |
|
323 |
+def len_data(data): |
|
324 |
+ return 2 + len(data) |
|
325 |
+ |
|
326 |
+def len_str(string, encoding=DEFAULT_ENCODING): |
|
327 |
+ return len_data(string.encode(encoding)) |
|
328 |
+ |
|
323 | 329 |
def pack_data(data): |
324 | 330 |
return struct.pack('>H', len(data)) + data |
325 | 331 |
|
... | ... |
@@ -420,6 +426,16 @@ def spn_to_principal(spn, name_type='KRB5_NT_PRINCIPAL'): |
420 | 426 |
principal['components'].append(component) |
421 | 427 |
return principal |
422 | 428 |
|
429 |
+def principal_length(principal): |
|
430 |
+ # component count: |
|
431 |
+ length = 2 |
|
432 |
+ length += len_str(principal.get('realm', '')) |
|
433 |
+ for component in principal.get('components', []): |
|
434 |
+ length += len_str(component) |
|
435 |
+ if principal.get('name_type_raw', principal.get('name_type')): |
|
436 |
+ length += 4 |
|
437 |
+ return length |
|
438 |
+ |
|
423 | 439 |
def simple_principal_to_full(inentry, index, entry): |
424 | 440 |
if 'principal' in inentry: |
425 | 441 |
principal = entry['principal'] = inentry['principal'] |
... | ... |
@@ -434,6 +450,7 @@ def simple_principal_to_full(inentry, index, entry): |
434 | 450 |
elif 'spn' in inentry: |
435 | 451 |
entry['spn'] = inentry['spn'] |
436 | 452 |
entry['principal'] = spn_to_principal(inentry['spn']) |
453 |
+ return principal_length(entry['principal']) |
|
437 | 454 |
|
438 | 455 |
def simple_kvno_to_full(inentry, index, entry, record): |
439 | 456 |
if 'kvno' in inentry: |
... | ... |
@@ -447,6 +464,7 @@ def simple_kvno_to_full(inentry, index, entry, record): |
447 | 464 |
message = 'Cannot store kvno > 255 without kvno_in_tail in entry #%d' |
448 | 465 |
raise KeytabComposingError(message % index) |
449 | 466 |
record['tail'] += to_bytes(inentry.get('extra_tail', b'')) |
467 |
+ return 1 |
|
450 | 468 |
|
451 | 469 |
def simple_timestamp_to_full(inentry, index, entry): |
452 | 470 |
if 'timestamp' in inentry: |
... | ... |
@@ -461,6 +479,7 @@ def simple_timestamp_to_full(inentry, index, entry): |
461 | 479 |
entry['timestamp'] = int(parsed_date.timestamp()) |
462 | 480 |
except: |
463 | 481 |
raise KeytabParsingError('Invalid date specified in entry #%d' % index) |
482 |
+ return 4 |
|
464 | 483 |
|
465 | 484 |
def simple_enctype_to_full(inentry, index, entry): |
466 | 485 |
if 'enctype_raw' in inentry: |
... | ... |
@@ -473,6 +492,7 @@ def simple_enctype_to_full(inentry, index, entry): |
473 | 492 |
message = 'Invalid or unknown enctype specified in entry #%d' |
474 | 493 |
message += '; use enctype_raw to enforce an arbitrary value' |
475 | 494 |
raise KeytabParsingError(message % index) |
495 |
+ return 2 |
|
476 | 496 |
|
477 | 497 |
def simple_keytab_to_full(indata): |
478 | 498 |
now = int(datetime.now().timestamp()) |
... | ... |
@@ -483,13 +503,17 @@ def simple_keytab_to_full(indata): |
483 | 503 |
for index, inentry in enumerate(indata.get('entries', [])): |
484 | 504 |
entry = {} |
485 | 505 |
record = {'type': 'record', 'entry': entry, 'tail': b''} |
486 |
- simple_principal_to_full(inentry, index, entry) |
|
487 |
- simple_kvno_to_full(inentry, index, entry, record) |
|
488 |
- simple_timestamp_to_full(inentry, index, entry) |
|
489 |
- simple_enctype_to_full(inentry, index, entry) |
|
506 |
+ length = 0 |
|
507 |
+ length += simple_principal_to_full(inentry, index, entry) |
|
508 |
+ length += simple_kvno_to_full(inentry, index, entry, record) |
|
509 |
+ length += simple_timestamp_to_full(inentry, index, entry) |
|
510 |
+ length += simple_enctype_to_full(inentry, index, entry) |
|
490 | 511 |
if 'key' in inentry: |
491 | 512 |
entry['key'] = inentry['key'] |
492 | 513 |
entry['key_length'] = len(to_bytes(inentry['key'])) |
514 |
+ length += 2 + entry['key_length'] |
|
515 |
+ length += len(to_bytes(record['tail'])) |
|
516 |
+ record['length'] = length |
|
493 | 517 |
data['records'].append(record) |
494 | 518 |
return data |
495 | 519 |
|
... | ... |
@@ -268,6 +268,11 @@ def simplify_keytab(keytab): |
268 | 268 |
simple_entry['kvno'] = entry.get('actual_kvno', entry['kvno']) |
269 | 269 |
if 'tail_kvno' in entry: |
270 | 270 |
simple_entry['kvno_in_tail'] = True |
271 |
+ if 'tail' in record: |
|
272 |
+ start = 4 if 'tail_kvno' in entry else 0 |
|
273 |
+ extra_tail = record['tail'][start:] |
|
274 |
+ if extra_tail: |
|
275 |
+ simple_entry['extra_tail'] = extra_tail |
|
271 | 276 |
for key in ('date', 'enctype', 'key'): |
272 | 277 |
if key in entry: |
273 | 278 |
simple_entry[key] = entry[key] |
... | ... |
@@ -441,6 +446,7 @@ def simple_kvno_to_full(inentry, index, entry, record): |
441 | 446 |
elif kvno_too_big: |
442 | 447 |
message = 'Cannot store kvno > 255 without kvno_in_tail in entry #%d' |
443 | 448 |
raise KeytabComposingError(message % index) |
449 |
+ record['tail'] += to_bytes(inentry.get('extra_tail', b'')) |
|
444 | 450 |
|
445 | 451 |
def simple_timestamp_to_full(inentry, index, entry): |
446 | 452 |
if 'timestamp' in inentry: |
... | ... |
@@ -274,6 +274,11 @@ def simplify_keytab(keytab): |
274 | 274 |
simplified['entries'].append(simple_entry) |
275 | 275 |
return simplified |
276 | 276 |
|
277 |
+def to_bytes(value): |
|
278 |
+ if type(value) is bytes: |
|
279 |
+ return value |
|
280 |
+ return unhexlify(value) |
|
281 |
+ |
|
277 | 282 |
def prepare_serialization(obj): |
278 | 283 |
""" |
279 | 284 |
Prepare keytab for serialization. |
... | ... |
@@ -343,7 +348,7 @@ def raw_entry_to_binary(entry, index): |
343 | 348 |
raise KeytabComposingError('invalid %s data in entry #%d' % (key, index)) |
344 | 349 |
data = raw_principal_to_binary(entry['principal'], index) |
345 | 350 |
try: |
346 |
- key_data = unhexlify(entry['key']) |
|
351 |
+ key_data = to_bytes(entry['key']) |
|
347 | 352 |
except: |
348 | 353 |
raise KeytabComposingError('invalid key data in entry #%d' % index) |
349 | 354 |
data += struct.pack('>IBHH', entry['timestamp'], entry['kvno'], entry['enctype_raw'], len(key_data)) |
... | ... |
@@ -357,7 +362,7 @@ def raw_record_to_binary(record, index): |
357 | 362 |
data += raw_entry_to_binary(record['entry'], index) |
358 | 363 |
if 'tail' in record: |
359 | 364 |
try: |
360 |
- data += unhexlify(record['tail']) |
|
365 |
+ data += to_bytes(record['tail']) |
|
361 | 366 |
except: |
362 | 367 |
raise KeytabComposingError('invalid tail data in record #%d' % index) |
363 | 368 |
return struct.pack('>i', len(data)) + data |
... | ... |
@@ -375,7 +380,7 @@ def raw_hole_to_binary(hole, index): |
375 | 380 |
raise KeytabComposingError('invalid length in record #%d' % index) |
376 | 381 |
if 'data' in record: |
377 | 382 |
try: |
378 |
- hole_data = unhexlify(record['data']) |
|
383 |
+ hole_data = to_bytes(record['data']) |
|
379 | 384 |
except: |
380 | 385 |
raise KeytabComposingError('invalid data in record #%d' % index) |
381 | 386 |
if len(hole_data) != length: |
... | ... |
@@ -432,7 +437,7 @@ def simple_kvno_to_full(inentry, index, entry, record): |
432 | 437 |
entry['kvno'] = 0 if kvno_too_big else inentry['kvno'] |
433 | 438 |
if inentry.get('kvno_in_tail', False): |
434 | 439 |
entry['tail_kvno'] = inentry['kvno'] |
435 |
- record['tail'] = hexlify(struct.pack('>I', inentry['kvno'])) |
|
440 |
+ record['tail'] = struct.pack('>I', inentry['kvno']) |
|
436 | 441 |
elif kvno_too_big: |
437 | 442 |
message = 'Cannot store kvno > 255 without kvno_in_tail in entry #%d' |
438 | 443 |
raise KeytabComposingError(message % index) |
... | ... |
@@ -478,7 +483,7 @@ def simple_keytab_to_full(indata): |
478 | 483 |
simple_enctype_to_full(inentry, index, entry) |
479 | 484 |
if 'key' in inentry: |
480 | 485 |
entry['key'] = inentry['key'] |
481 |
- entry['key_length'] = len(unhexlify(inentry['key'])) |
|
486 |
+ entry['key_length'] = len(to_bytes(inentry['key'])) |
|
482 | 487 |
data['records'].append(record) |
483 | 488 |
return data |
484 | 489 |
|
... | ... |
@@ -420,7 +420,9 @@ def simple_principal_to_full(inentry, index, entry): |
420 | 420 |
message = 'Invalid or unknown name_type specified in entry #%d' |
421 | 421 |
message += '; use name_type_raw to enforce an arbitrary value' |
422 | 422 |
raise KeytabParsingError(message % index) |
423 |
+ entry['spn'] = principal_to_spn(principal) |
|
423 | 424 |
elif 'spn' in inentry: |
425 |
+ entry['spn'] = inentry['spn'] |
|
424 | 426 |
entry['principal'] = spn_to_principal(inentry['spn']) |
425 | 427 |
|
426 | 428 |
def simple_kvno_to_full(inentry, index, entry, record): |
... | ... |
@@ -439,6 +441,7 @@ def simple_timestamp_to_full(inentry, index, entry): |
439 | 441 |
if 'timestamp' in inentry: |
440 | 442 |
entry['timestamp'] = inentry['timestamp'] |
441 | 443 |
elif 'date' in inentry: |
444 |
+ entry['date'] = inentry['date'] |
|
442 | 445 |
if inentry['date'] == 'now': |
443 | 446 |
entry['timestamp'] = now |
444 | 447 |
else: |
... | ... |
@@ -452,6 +455,7 @@ def simple_enctype_to_full(inentry, index, entry): |
452 | 455 |
if 'enctype_raw' in inentry: |
453 | 456 |
entry['enctype_raw'] = inentry['enctype_raw'] |
454 | 457 |
elif 'enctype' in inentry: |
458 |
+ entry['enctype'] = inentry['enctype'] |
|
455 | 459 |
try: |
456 | 460 |
entry['enctype_raw'] = ENC_TYPES[inentry['enctype']] |
457 | 461 |
except KeyError: |
... | ... |
@@ -467,13 +471,14 @@ def simple_keytab_to_full(indata): |
467 | 471 |
} |
468 | 472 |
for index, inentry in enumerate(indata.get('entries', [])): |
469 | 473 |
entry = {} |
470 |
- record = {'entry': entry} |
|
474 |
+ record = {'type': 'record', 'entry': entry, 'tail': b''} |
|
471 | 475 |
simple_principal_to_full(inentry, index, entry) |
472 | 476 |
simple_kvno_to_full(inentry, index, entry, record) |
473 | 477 |
simple_timestamp_to_full(inentry, index, entry) |
474 | 478 |
simple_enctype_to_full(inentry, index, entry) |
475 | 479 |
if 'key' in inentry: |
476 | 480 |
entry['key'] = inentry['key'] |
481 |
+ entry['key_length'] = len(unhexlify(inentry['key'])) |
|
477 | 482 |
data['records'].append(record) |
478 | 483 |
return data |
479 | 484 |
|
... | ... |
@@ -483,6 +483,8 @@ def yaml_to_keytab(buf, args): |
483 | 483 |
# If the provided structure exposes "entries" rather than "records", |
484 | 484 |
# then it very likely features the "simple" data layout. |
485 | 485 |
data = simple_keytab_to_full(data) |
486 |
+ if args.simple_to_full: |
|
487 |
+ output_data(data, args, 0) |
|
486 | 488 |
result = raw_keytab_to_binary(data) |
487 | 489 |
sys.stdout.buffer.write(result) |
488 | 490 |
|
... | ... |
@@ -492,6 +494,7 @@ def parse_args(): |
492 | 494 |
parser.add_argument('--data-layout', '-l', dest='data_layout', choices=DATA_LAYOUTS.keys(), default='simple', help='data layout (keytab to YAML/JSON only)') |
493 | 495 |
parser.add_argument('--output-format', '-f', dest='output_format', choices=['json', 'yaml'], default='yaml', help='output format (keytab to YAML/JSON only)') |
494 | 496 |
parser.add_argument('--v1-endianness', '-e', dest='v1_endianness', choices=['native', 'little', 'big'], default='native', help='Enforce endianness (keytab v1 to YAML/JSON only)') |
497 |
+ parser.add_argument('--simple-to-full', dest='simple_to_full', action='store_true', default=False, help='Convert from simple to full layout (YAML/JSON input only)') |
|
495 | 498 |
parser.add_argument('input', nargs='?', type=argparse.FileType('rb'), default=sys.stdin.buffer, help='input file; defaults to standard input') |
496 | 499 |
args = parser.parse_args() |
497 | 500 |
return args |
... | ... |
@@ -299,11 +299,16 @@ def keytab_data(buf, args): |
299 | 299 |
|
300 | 300 |
def keytab_to_yaml(buf, args): |
301 | 301 |
keytab = keytab_data(buf, args) |
302 |
- final_keytab = prepare_serialization(keytab) |
|
302 |
+ output_data(keytab, args, 0) |
|
303 |
+ |
|
304 |
+def output_data(data, args, exit=None): |
|
305 |
+ serialized_data = prepare_serialization(data) |
|
303 | 306 |
if args.output_format == 'yaml': |
304 |
- yaml.dump(final_keytab, sys.stdout, width=160, sort_keys=False) |
|
307 |
+ yaml.dump(serialized_data, sys.stdout, width=160, sort_keys=False) |
|
305 | 308 |
else: |
306 |
- json.dump(final_keytab, sys.stdout, indent=4) |
|
309 |
+ json.dump(serialized_data, sys.stdout, indent=4) |
|
310 |
+ if exit is not None: |
|
311 |
+ sys.exit(exit) |
|
307 | 312 |
|
308 | 313 |
def pack_data(data): |
309 | 314 |
return struct.pack('>H', len(data)) + data |
... | ... |
@@ -226,10 +226,7 @@ def enrich_keytab(keytab): |
226 | 226 |
""" |
227 | 227 |
Enrich records with extra information suitable for human readers. |
228 | 228 |
""" |
229 |
- # Reflect whether the keytab uses record tails to store 32-bits kvno: |
|
230 |
- records = keytab.pop('records') |
|
231 |
- keytab['kvno_in_tail'] = False |
|
232 |
- for record in records: |
|
229 |
+ for record in keytab['records']: |
|
233 | 230 |
if 'entry' not in record: |
234 | 231 |
continue |
235 | 232 |
entry = record['entry'] |
... | ... |
@@ -246,23 +243,17 @@ def enrich_keytab(keytab): |
246 | 243 |
# those 32 bits is not 0. If present, this key version supersedes the 8-bit key version. |
247 | 244 |
if len(record['tail']) >= 4: |
248 | 245 |
tail_kvno = struct.unpack('>I', record['tail'][0:4])[0] |
249 |
- if tail_kvno: |
|
250 |
- entry['tail_kvno'] = tail_kvno |
|
251 |
- # tail_kvno overrides kvno if non-zero: |
|
252 |
- if entry['tail_kvno']: |
|
253 |
- entry['actual_kvno'] = entry['tail_kvno'] |
|
254 |
- keytab['kvno_in_tail'] = True |
|
246 |
+ if tail_kvno or not entry['kvno']: |
|
247 |
+ entry['actual_kvno'] = entry['tail_kvno'] = tail_kvno |
|
255 | 248 |
if 'actual_kvno' not in entry: |
256 | 249 |
entry['actual_kvno'] = entry['kvno'] |
257 |
- # Reintroduce records after kvno_in_tail for convenience: |
|
258 |
- keytab['records'] = records |
|
259 | 250 |
return keytab |
260 | 251 |
|
261 | 252 |
def simplify_keytab(keytab): |
262 | 253 |
""" |
263 | 254 |
Simplify the keytab to make it suitable for edition. |
264 | 255 |
""" |
265 |
- simplified = {'version': keytab['version'], 'kvno_in_tail': keytab['kvno_in_tail'], 'entries': []} |
|
256 |
+ simplified = {'version': keytab['version'], 'entries': []} |
|
266 | 257 |
for record in keytab['records']: |
267 | 258 |
if 'entry' not in record: |
268 | 259 |
continue |
... | ... |
@@ -275,6 +266,8 @@ def simplify_keytab(keytab): |
275 | 266 |
if key in entry['principal']: |
276 | 267 |
simple_entry['principal'][key] = entry['principal'][key] |
277 | 268 |
simple_entry['kvno'] = entry.get('actual_kvno', entry['kvno']) |
269 |
+ if 'tail_kvno' in entry: |
|
270 |
+ simple_entry['kvno_in_tail'] = True |
|
278 | 271 |
for key in ('date', 'enctype', 'key'): |
279 | 272 |
if key in entry: |
280 | 273 |
simple_entry[key] = entry[key] |
... | ... |
@@ -425,12 +418,12 @@ def simple_principal_to_full(inentry, index, entry): |
425 | 418 |
elif 'spn' in inentry: |
426 | 419 |
entry['principal'] = spn_to_principal(inentry['spn']) |
427 | 420 |
|
428 |
-def simple_kvno_to_full(inentry, index, entry, record, kvno_in_tail=False): |
|
421 |
+def simple_kvno_to_full(inentry, index, entry, record): |
|
429 | 422 |
if 'kvno' in inentry: |
430 | 423 |
entry['actual_kvno'] = inentry['kvno'] |
431 | 424 |
kvno_too_big = inentry['kvno'] > 255 |
432 | 425 |
entry['kvno'] = 0 if kvno_too_big else inentry['kvno'] |
433 |
- if kvno_in_tail: |
|
426 |
+ if inentry.get('kvno_in_tail', False): |
|
434 | 427 |
entry['tail_kvno'] = inentry['kvno'] |
435 | 428 |
record['tail'] = hexlify(struct.pack('>I', inentry['kvno'])) |
436 | 429 |
elif kvno_too_big: |
... | ... |
@@ -465,14 +458,13 @@ def simple_keytab_to_full(indata): |
465 | 458 |
now = int(datetime.now().timestamp()) |
466 | 459 |
data = { |
467 | 460 |
'version': indata.get('version', 2), |
468 |
- 'kvno_in_tail': indata.get('kvno_in_tail', False), |
|
469 | 461 |
'records': [], |
470 | 462 |
} |
471 | 463 |
for index, inentry in enumerate(indata.get('entries', [])): |
472 | 464 |
entry = {} |
473 | 465 |
record = {'entry': entry} |
474 | 466 |
simple_principal_to_full(inentry, index, entry) |
475 |
- simple_kvno_to_full(inentry, index, entry, record, data['kvno_in_tail']) |
|
467 |
+ simple_kvno_to_full(inentry, index, entry, record) |
|
476 | 468 |
simple_timestamp_to_full(inentry, index, entry) |
477 | 469 |
simple_enctype_to_full(inentry, index, entry) |
478 | 470 |
if 'key' in inentry: |
The initial implementation used to support only raw and full YAML/JSON data
layout when converting YAML/JSON to keytab.
... | ... |
@@ -7,6 +7,7 @@ |
7 | 7 |
|
8 | 8 |
from io import BufferedReader, BytesIO |
9 | 9 |
import os |
10 |
+import re |
|
10 | 11 |
import sys |
11 | 12 |
import json |
12 | 13 |
import yaml |
... | ... |
@@ -398,8 +399,93 @@ def raw_keytab_to_binary(indata): |
398 | 399 |
raise KeytabComposingError('Unknown record type in record #%d' % index) |
399 | 400 |
return data |
400 | 401 |
|
402 |
+def spn_to_principal(spn, name_type='KRB5_NT_PRINCIPAL'): |
|
403 |
+ principal = {'name_type': name_type} |
|
404 |
+ principal['name_type_raw'] = NAME_TYPES[name_type] |
|
405 |
+ rem = re.match(r'((?P<comp1>[^/]+)/)?(?P<comp2>[^:]+)(?::(?P<comp3>[^@]+))?@(?P<realm>.+)', spn) |
|
406 |
+ if not rem: |
|
407 |
+ raise KeytabComposingError('Cannot parse SPN %s into principal' % spn) |
|
408 |
+ principal['realm'] = rem.group('realm') |
|
409 |
+ principal['components'] = [] |
|
410 |
+ for name, component in rem.groupdict(): |
|
411 |
+ if name.startswith('comp') and component is not None: |
|
412 |
+ principal['components'].append(component) |
|
413 |
+ return principal |
|
414 |
+ |
|
415 |
+def simple_principal_to_full(inentry, index, entry): |
|
416 |
+ if 'principal' in inentry: |
|
417 |
+ principal = entry['principal'] = inentry['principal'] |
|
418 |
+ if 'name_type_raw' not in principal: |
|
419 |
+ try: |
|
420 |
+ principal['name_type_raw'] = NAME_TYPES[principal['name_type']] |
|
421 |
+ except KeyError: |
|
422 |
+ message = 'Invalid or unknown name_type specified in entry #%d' |
|
423 |
+ message += '; use name_type_raw to enforce an arbitrary value' |
|
424 |
+ raise KeytabParsingError(message % index) |
|
425 |
+ elif 'spn' in inentry: |
|
426 |
+ entry['principal'] = spn_to_principal(inentry['spn']) |
|
427 |
+ |
|
428 |
+def simple_kvno_to_full(inentry, index, entry, record, kvno_in_tail=False): |
|
429 |
+ if 'kvno' in inentry: |
|
430 |
+ entry['actual_kvno'] = inentry['kvno'] |
|
431 |
+ kvno_too_big = inentry['kvno'] > 255 |
|
432 |
+ entry['kvno'] = 0 if kvno_too_big else inentry['kvno'] |
|
433 |
+ if kvno_in_tail: |
|
434 |
+ entry['tail_kvno'] = inentry['kvno'] |
|
435 |
+ record['tail'] = hexlify(struct.pack('>I', inentry['kvno'])) |
|
436 |
+ elif kvno_too_big: |
|
437 |
+ message = 'Cannot store kvno > 255 without kvno_in_tail in entry #%d' |
|
438 |
+ raise KeytabComposingError(message % index) |
|
439 |
+ |
|
440 |
+def simple_timestamp_to_full(inentry, index, entry): |
|
441 |
+ if 'timestamp' in inentry: |
|
442 |
+ entry['timestamp'] = inentry['timestamp'] |
|
443 |
+ elif 'date' in inentry: |
|
444 |
+ if inentry['date'] == 'now': |
|
445 |
+ entry['timestamp'] = now |
|
446 |
+ else: |
|
447 |
+ try: |
|
448 |
+ parsed_date = datetime.strptime(inentry['date'], DATE_TIME_FORMAT) |
|
449 |
+ entry['timestamp'] = int(parsed_date.timestamp()) |
|
450 |
+ except: |
|
451 |
+ raise KeytabParsingError('Invalid date specified in entry #%d' % index) |
|
452 |
+ |
|
453 |
+def simple_enctype_to_full(inentry, index, entry): |
|
454 |
+ if 'enctype_raw' in inentry: |
|
455 |
+ entry['enctype_raw'] = inentry['enctype_raw'] |
|
456 |
+ elif 'enctype' in inentry: |
|
457 |
+ try: |
|
458 |
+ entry['enctype_raw'] = ENC_TYPES[inentry['enctype']] |
|
459 |
+ except KeyError: |
|
460 |
+ message = 'Invalid or unknown enctype specified in entry #%d' |
|
461 |
+ message += '; use enctype_raw to enforce an arbitrary value' |
|
462 |
+ raise KeytabParsingError(message % index) |
|
463 |
+ |
|
464 |
+def simple_keytab_to_full(indata): |
|
465 |
+ now = int(datetime.now().timestamp()) |
|
466 |
+ data = { |
|
467 |
+ 'version': indata.get('version', 2), |
|
468 |
+ 'kvno_in_tail': indata.get('kvno_in_tail', False), |
|
469 |
+ 'records': [], |
|
470 |
+ } |
|
471 |
+ for index, inentry in enumerate(indata.get('entries', [])): |
|
472 |
+ entry = {} |
|
473 |
+ record = {'entry': entry} |
|
474 |
+ simple_principal_to_full(inentry, index, entry) |
|
475 |
+ simple_kvno_to_full(inentry, index, entry, record, data['kvno_in_tail']) |
|
476 |
+ simple_timestamp_to_full(inentry, index, entry) |
|
477 |
+ simple_enctype_to_full(inentry, index, entry) |
|
478 |
+ if 'key' in inentry: |
|
479 |
+ entry['key'] = inentry['key'] |
|
480 |
+ data['records'].append(record) |
|
481 |
+ return data |
|
482 |
+ |
|
401 | 483 |
def yaml_to_keytab(buf, args): |
402 | 484 |
data = yaml.load(buf, Loader=yaml.SafeLoader) |
485 |
+ if 'entries' in data: |
|
486 |
+ # If the provided structure exposes "entries" rather than "records", |
|
487 |
+ # then it very likely features the "simple" data layout. |
|
488 |
+ data = simple_keytab_to_full(data) |
|
403 | 489 |
result = raw_keytab_to_binary(data) |
404 | 490 |
sys.stdout.buffer.write(result) |
405 | 491 |
|
This extra property, shown only in full and simple modes, reflect whether
record tails are used to store 32-bit key version numbers.
... | ... |
@@ -225,7 +225,10 @@ def enrich_keytab(keytab): |
225 | 225 |
""" |
226 | 226 |
Enrich records with extra information suitable for human readers. |
227 | 227 |
""" |
228 |
- for record in keytab['records']: |
|
228 |
+ # Reflect whether the keytab uses record tails to store 32-bits kvno: |
|
229 |
+ records = keytab.pop('records') |
|
230 |
+ keytab['kvno_in_tail'] = False |
|
231 |
+ for record in records: |
|
229 | 232 |
if 'entry' not in record: |
230 | 233 |
continue |
231 | 234 |
entry = record['entry'] |
... | ... |
@@ -247,15 +250,18 @@ def enrich_keytab(keytab): |
247 | 250 |
# tail_kvno overrides kvno if non-zero: |
248 | 251 |
if entry['tail_kvno']: |
249 | 252 |
entry['actual_kvno'] = entry['tail_kvno'] |
253 |
+ keytab['kvno_in_tail'] = True |
|
250 | 254 |
if 'actual_kvno' not in entry: |
251 | 255 |
entry['actual_kvno'] = entry['kvno'] |
256 |
+ # Reintroduce records after kvno_in_tail for convenience: |
|
257 |
+ keytab['records'] = records |
|
252 | 258 |
return keytab |
253 | 259 |
|
254 | 260 |
def simplify_keytab(keytab): |
255 | 261 |
""" |
256 | 262 |
Simplify the keytab to make it suitable for edition. |
257 | 263 |
""" |
258 |
- simplified = {'version': keytab['version'], 'entries': []} |
|
264 |
+ simplified = {'version': keytab['version'], 'kvno_in_tail': keytab['kvno_in_tail'], 'entries': []} |
|
259 | 265 |
for record in keytab['records']: |
260 | 266 |
if 'entry' not in record: |
261 | 267 |
continue |
... | ... |
@@ -244,8 +244,8 @@ def enrich_keytab(keytab): |
244 | 244 |
tail_kvno = struct.unpack('>I', record['tail'][0:4])[0] |
245 | 245 |
if tail_kvno: |
246 | 246 |
entry['tail_kvno'] = tail_kvno |
247 |
- # If kvno is zero, assume the one found in the tail is the one that matters: |
|
248 |
- if not entry['kvno']: |
|
247 |
+ # tail_kvno overrides kvno if non-zero: |
|
248 |
+ if entry['tail_kvno']: |
|
249 | 249 |
entry['actual_kvno'] = entry['tail_kvno'] |
250 | 250 |
if 'actual_kvno' not in entry: |
251 | 251 |
entry['actual_kvno'] = entry['kvno'] |
This first implementation only supports raw and full YAML/JSON data lyaout.
... | ... |
@@ -13,7 +13,7 @@ import yaml |
13 | 13 |
import struct |
14 | 14 |
import argparse |
15 | 15 |
from datetime import datetime |
16 |
-from binascii import hexlify |
|
16 |
+from binascii import hexlify, unhexlify |
|
17 | 17 |
|
18 | 18 |
# Documents used as reference to implement the keytab format: |
19 | 19 |
# [1] https://web.mit.edu/kerberos/krb5-1.12/doc/formats/keytab_file_format.html |
... | ... |
@@ -91,6 +91,9 @@ class KeytabParsingError(Exception): |
91 | 91 |
def __str__(self): |
92 | 92 |
return __class__.MESSAGE.format(**self.__dict__, length=len(self.data)) |
93 | 93 |
|
94 |
+class KeytabComposingError(Exception): |
|
95 |
+ pass |
|
96 |
+ |
|
94 | 97 |
def lookup(lookup_value, dictionary, default): |
95 | 98 |
for name, value in dictionary.items(): |
96 | 99 |
if value == lookup_value: |
... | ... |
@@ -302,9 +305,97 @@ def keytab_to_yaml(buf, args): |
302 | 305 |
else: |
303 | 306 |
json.dump(final_keytab, sys.stdout, indent=4) |
304 | 307 |
|
305 |
-def yaml_to_keytab(fd): |
|
306 |
- data = yaml.load(fd.read(), Loader=yaml.SafeLoader) |
|
307 |
- print('YAML:', data) |
|
308 |
+def pack_data(data): |
|
309 |
+ return struct.pack('>H', len(data)) + data |
|
310 |
+ |
|
311 |
+def pack_str(string, encoding=DEFAULT_ENCODING): |
|
312 |
+ return pack_data(string.encode(encoding)) |
|
313 |
+ |
|
314 |
+def raw_principal_to_binary(principal, index): |
|
315 |
+ for key in ('realm', 'components'): |
|
316 |
+ if key not in principal: |
|
317 |
+ raise KeytabComposingError('Mandatory key "%s" not found in principal #%d' % (key, index)) |
|
318 |
+ name_type_raw = principal.get('name_type_raw', NAME_TYPES['KRB5_NT_PRINCIPAL']) |
|
319 |
+ try: |
|
320 |
+ _ = int(name_type_raw) |
|
321 |
+ except: |
|
322 |
+ raise KeytabComposingError('invalid name_type_raw value in principal #%d' % index) |
|
323 |
+ data = struct.pack('>H', len(principal['components'])) |
|
324 |
+ data += pack_str(principal['realm']) |
|
325 |
+ for component in principal['components']: |
|
326 |
+ data += pack_str(component) |
|
327 |
+ data += struct.pack('>i', name_type_raw) |
|
328 |
+ return data |
|
329 |
+ |
|
330 |
+def raw_entry_to_binary(entry, index): |
|
331 |
+ for key in ('principal', 'timestamp', 'kvno', 'enctype_raw', 'key'): |
|
332 |
+ if key not in entry: |
|
333 |
+ raise KeytabComposingError('Mandatory key "%s" not found in entry #%d' % (key, index)) |
|
334 |
+ for key in ('timestamp', 'kvno', 'enctype_raw'): |
|
335 |
+ try: |
|
336 |
+ assert(int(entry[key]) >= 0) |
|
337 |
+ except: |
|
338 |
+ raise KeytabComposingError('invalid %s data in entry #%d' % (key, index)) |
|
339 |
+ data = raw_principal_to_binary(entry['principal'], index) |
|
340 |
+ try: |
|
341 |
+ key_data = unhexlify(entry['key']) |
|
342 |
+ except: |
|
343 |
+ raise KeytabComposingError('invalid key data in entry #%d' % index) |
|
344 |
+ data += struct.pack('>IBHH', entry['timestamp'], entry['kvno'], entry['enctype_raw'], len(key_data)) |
|
345 |
+ data += key_data |
|
346 |
+ return data |
|
347 |
+ |
|
348 |
+def raw_record_to_binary(record, index): |
|
349 |
+ data = b'' |
|
350 |
+ if 'entry' not in record: |
|
351 |
+ raise KeytabComposingError('missing entry in record #%d' % index) |
|
352 |
+ data += raw_entry_to_binary(record['entry'], index) |
|
353 |
+ if 'tail' in record: |
|
354 |
+ try: |
|
355 |
+ data += unhexlify(record['tail']) |
|
356 |
+ except: |
|
357 |
+ raise KeytabComposingError('invalid tail data in record #%d' % index) |
|
358 |
+ return struct.pack('>i', len(data)) + data |
|
359 |
+ |
|
360 |
+def raw_hole_to_binary(hole, index): |
|
361 |
+ data = b'' |
|
362 |
+ if 'length' not in record: |
|
363 |
+ raise KeytabComposingError('missing length in record #%d' % index) |
|
364 |
+ try: |
|
365 |
+ length = abs(int(record['length'])) |
|
366 |
+ if not length: |
|
367 |
+ raise KeytabComposingError('illegal zero-length hole in record #%d' % index) |
|
368 |
+ data += struct.pack('>i', -length) |
|
369 |
+ except: |
|
370 |
+ raise KeytabComposingError('invalid length in record #%d' % index) |
|
371 |
+ if 'data' in record: |
|
372 |
+ try: |
|
373 |
+ hole_data = unhexlify(record['data']) |
|
374 |
+ except: |
|
375 |
+ raise KeytabComposingError('invalid data in record #%d' % index) |
|
376 |
+ if len(hole_data) != length: |
|
377 |
+ raise KeytabComposingError('length and data do not match in record #%d' % index) |
|
378 |
+ data += hole_data |
|
379 |
+ else: |
|
380 |
+ data += b'\x00' * length |
|
381 |
+ return data |
|
382 |
+ |
|
383 |
+def raw_keytab_to_binary(indata): |
|
384 |
+ data = bytes([KEYTAB_FIRST_BYTE, 2]) |
|
385 |
+ for index, record in enumerate(indata.get('records', [])): |
|
386 |
+ record_type = record.get('type', 'record') |
|
387 |
+ if record_type == 'hole': |
|
388 |
+ data += raw_hole_to_binary(record, index) |
|
389 |
+ elif record_type == 'record': |
|
390 |
+ data += raw_record_to_binary(record, index) |
|
391 |
+ else: |
|
392 |
+ raise KeytabComposingError('Unknown record type in record #%d' % index) |
|
393 |
+ return data |
|
394 |
+ |
|
395 |
+def yaml_to_keytab(buf, args): |
|
396 |
+ data = yaml.load(buf, Loader=yaml.SafeLoader) |
|
397 |
+ result = raw_keytab_to_binary(data) |
|
398 |
+ sys.stdout.buffer.write(result) |
|
308 | 399 |
|
309 | 400 |
def parse_args(): |
310 | 401 |
parser = argparse.ArgumentParser(description='Keytab <-> YAML/JSON convertor.') |
... | ... |
@@ -30,6 +30,8 @@ DATE_TIME_FORMAT = '%Y-%m-%dT%H:%M:%SZ' |
30 | 30 |
KEYTAB_FIRST_BYTE = 0x05 |
31 | 31 |
# Default prefix for struct's format strings, defining big-endian byte order: |
32 | 32 |
BIG_ENDIAN='>' |
33 |
+LITTLE_ENDIAN='<' |
|
34 |
+NATIVE_ENDIANNESS='=' |
|
33 | 35 |
DEFAULT_PREFIX=BIG_ENDIAN |
34 | 36 |
DEFAULT_ENCODING='ascii' |
35 | 37 |
VERBOSITY=1 |
... | ... |
@@ -102,7 +104,7 @@ def int_to_enc_type(lookup_value): |
102 | 104 |
return lookup(lookup_value, ENC_TYPES, 'ENCTYPE_UNKNOWN') |
103 | 105 |
|
104 | 106 |
def principal_to_spn(principal): |
105 |
- if principal['name_type_raw'] != 1: |
|
107 |
+ if principal.get('name_type_raw') != NAME_TYPES['KRB5_NT_PRINCIPAL']: |
|
106 | 108 |
return None |
107 | 109 |
components = principal['components'] |
108 | 110 |
count = len(components) |
... | ... |
@@ -145,29 +147,34 @@ def parse_data(buf, prefix=DEFAULT_PREFIX): |
145 | 147 |
def parse_str(buf, prefix=DEFAULT_PREFIX, encoding=DEFAULT_ENCODING): |
146 | 148 |
return parse_data(buf, prefix).decode(encoding) |
147 | 149 |
|
148 |
-def parse_principal(buf, prefix=DEFAULT_PREFIX): |
|
150 |
+def parse_principal(buf, prefix=DEFAULT_PREFIX, version=2): |
|
149 | 151 |
principal = {} |
150 | 152 |
# [1] states "count of components (32 bits)" but [2] says int16: |
151 | 153 |
component_count = unpack(buf, prefix, 'H') |
154 |
+ # [1] states "[includes realm in version 1]" |
|
155 |
+ if version == 1: |
|
156 |
+ component_count -= 1 |
|
152 | 157 |
principal['realm'] = parse_str(buf, prefix) |
153 | 158 |
components = [] |
154 | 159 |
for i in range(component_count): |
155 | 160 |
components.append(parse_str(buf, prefix)) |
156 | 161 |
principal['components'] = components |
157 |
- # [3] states int32: |
|
158 |
- principal['name_type_raw'] = unpack(buf, prefix, 'i') |
|
162 |
+ # [1] states "[omitted in version 1]" |
|
163 |
+ if version != 1: |
|
164 |
+ # [3] states int32: |
|
165 |
+ principal['name_type_raw'] = unpack(buf, prefix, 'i') |
|
159 | 166 |
return principal |
160 | 167 |
|
161 |
-def parse_entry(buf, prefix=DEFAULT_PREFIX): |
|
168 |
+def parse_entry(buf, prefix=DEFAULT_PREFIX, version=2): |
|
162 | 169 |
entry = {} |
163 |
- entry['principal'] = parse_principal(buf, prefix) |
|
170 |
+ entry['principal'] = parse_principal(buf, prefix, version) |
|
164 | 171 |
entry['timestamp'], entry['kvno'], entry['enctype_raw'], entry['key_length'] = unpack(buf, prefix, 'IBHH') |
165 | 172 |
entry['key'] = buf.read(entry['key_length']) |
166 | 173 |
return entry |
167 | 174 |
|
168 |
-def parse_record(buf, prefix=DEFAULT_PREFIX): |
|
175 |
+def parse_record(buf, prefix=DEFAULT_PREFIX, version=2): |
|
169 | 176 |
record = {'type': 'record'} |
170 |
- record['entry'] = parse_entry(buf, prefix) |
|
177 |
+ record['entry'] = parse_entry(buf, prefix, version) |
|
171 | 178 |
record['tail'] = buf.read() |
172 | 179 |
return record |
173 | 180 |
|
... | ... |
@@ -175,8 +182,12 @@ def parse_keytab(buf, args): |
175 | 182 |
second_byte = buf.read(2)[1] |
176 | 183 |
verbose(2, 'keytab v{}', second_byte) |
177 | 184 |
if second_byte == 1: |
178 |
- verbose(1, 'Keytab v1 not supported yet!') |
|
179 |
- sys.exit(1) |
|
185 |
+ # Version 1 uses native byte order: |
|
186 |
+ prefix = NATIVE_ENDIANNESS |
|
187 |
+ if args.v1_endianness == 'big': |
|
188 |
+ prefix = BIG_ENDIAN |
|
189 |
+ elif args.v1_endianness == 'little': |
|
190 |
+ prefix = LITTLE_ENDIAN |
|
180 | 191 |
elif second_byte == 2: |
181 | 192 |
# Version 2 always uses big-endian byte order: |
182 | 193 |
prefix = BIG_ENDIAN |
... | ... |
@@ -200,7 +211,7 @@ def parse_keytab(buf, args): |
200 | 211 |
verbose(3, 'Record #{} of length {}', len(keytab['records']) + 1, record_length) |
201 | 212 |
record = buf.read(abs(record_length)) |
202 | 213 |
if record_length > 0: |
203 |
- record = parse_record(BufferedReader(BytesIO(record)), prefix) |
|
214 |
+ record = parse_record(BufferedReader(BytesIO(record)), prefix, second_byte) |
|
204 | 215 |
else: |
205 | 216 |
record = {'type': 'hole', 'data': record} |
206 | 217 |
record['length'] = record_length |
... | ... |
@@ -300,6 +311,7 @@ def parse_args(): |
300 | 311 |
parser.add_argument('--verbose', '-v', dest='verbose', action='count', help='increase verbosity level', default=VERBOSITY) |
301 | 312 |
parser.add_argument('--data-layout', '-l', dest='data_layout', choices=DATA_LAYOUTS.keys(), default='simple', help='data layout (keytab to YAML/JSON only)') |
302 | 313 |
parser.add_argument('--output-format', '-f', dest='output_format', choices=['json', 'yaml'], default='yaml', help='output format (keytab to YAML/JSON only)') |
314 |
+ parser.add_argument('--v1-endianness', '-e', dest='v1_endianness', choices=['native', 'little', 'big'], default='native', help='Enforce endianness (keytab v1 to YAML/JSON only)') |
|
303 | 315 |
parser.add_argument('input', nargs='?', type=argparse.FileType('rb'), default=sys.stdin.buffer, help='input file; defaults to standard input') |
304 | 316 |
args = parser.parse_args() |
305 | 317 |
return args |
At this stage, yamltab can parse keytabs (version 2 format only) and drop their
contents as either YAML or JSON, with three possible data layouts.
1 | 1 |
new file mode 100755 |
... | ... |
@@ -0,0 +1,319 @@ |
1 |
+#!/usr/bin/env python3 |
|
2 |
+ |
|
3 |
+# Copyright © 2020 Xavier G. <xavier.yamltab@kindwolf.org> |
|
4 |
+# This work is free. You can redistribute it and/or modify it under the |
|
5 |
+# terms of the Do What The Fuck You Want To Public License, Version 2, |
|
6 |
+# as published by Sam Hocevar. See the COPYING file for more details. |
|
7 |
+ |
|
8 |
+from io import BufferedReader, BytesIO |
|
9 |
+import os |
|
10 |
+import sys |
|
11 |
+import json |
|
12 |
+import yaml |
|
13 |
+import struct |
|
14 |
+import argparse |
|
15 |
+from datetime import datetime |
|
16 |
+from binascii import hexlify |
|
17 |
+ |
|
18 |
+# Documents used as reference to implement the keytab format: |
|
19 |
+# [1] https://web.mit.edu/kerberos/krb5-1.12/doc/formats/keytab_file_format.html |
|
20 |
+# [2] https://github.com/krb5/krb5/blob/master/src/lib/krb5/keytab/kt_file.c#L892 |
|
21 |
+# [3] https://github.com/krb5/krb5/blob/master/src/include/krb5/krb5.hin#L230 |
|
22 |
+ |
|
23 |
+DATA_LAYOUT_RAW = 0 |
|
24 |
+DATA_LAYOUT_FULL = 1 |
|
25 |
+DATA_LAYOUT_SIMPLE = 2 |
|
26 |
+DATA_LAYOUTS = {'raw': DATA_LAYOUT_RAW, 'full': DATA_LAYOUT_FULL, 'simple': DATA_LAYOUT_SIMPLE} |
|
27 |
+ |
|
28 |
+DATE_TIME_FORMAT = '%Y-%m-%dT%H:%M:%SZ' |
|
29 |
+ |
|
30 |
+KEYTAB_FIRST_BYTE = 0x05 |
|
31 |
+# Default prefix for struct's format strings, defining big-endian byte order: |
|
32 |
+BIG_ENDIAN='>' |
|
33 |
+DEFAULT_PREFIX=BIG_ENDIAN |
|
34 |
+DEFAULT_ENCODING='ascii' |
|
35 |
+VERBOSITY=1 |
|
36 |
+CALCSIZE={} |
|
37 |
+ |
|
38 |
+# The following table is based on [3]: |
|
39 |
+NAME_TYPES = { |
|
40 |
+ 'KRB5_NT_UNKNOWN': 0, |
|
41 |
+ 'KRB5_NT_PRINCIPAL': 1, |
|
42 |
+ 'KRB5_NT_SRV_INST': 2, |
|
43 |
+ 'KRB5_NT_SRV_HST': 3, |
|
44 |
+ 'KRB5_NT_SRV_XHST': 4, |
|
45 |
+ 'KRB5_NT_UID': 5, |
|
46 |
+ 'KRB5_NT_X500_PRINCIPAL': 6, |
|
47 |
+ 'KRB5_NT_SMTP_NAME': 7, |
|
48 |
+ 'KRB5_NT_ENTERPRISE_PRINCIPAL': 10, |
|
49 |
+ 'KRB5_NT_WELLKNOWN': 11, |
|
50 |
+ 'KRB5_NT_MS_PRINCIPAL': -128, |
|
51 |
+ 'KRB5_NT_MS_PRINCIPAL_AND_ID': -129, |
|
52 |
+ 'KRB5_NT_ENT_PRINCIPAL_AND_ID': -130, |
|
53 |
+} |
|
54 |
+ |
|
55 |
+ENC_TYPES = { |
|
56 |
+ 'NULL': 0, |
|
57 |
+ 'DES_CBC_CRC': 1, |
|
58 |
+ 'DES_CBC_MD4': 2, |
|
59 |
+ 'DES_CBC_MD5': 3, |
|
60 |
+ 'DES_CBC_RAW': 4, |
|
61 |
+ 'DES3_CBC_SHA': 5, |
|
62 |
+ 'DES3_CBC_RAW': 6, |
|
63 |
+ 'DES_HMAC_SHA1': 8, |
|
64 |
+ 'DSA_SHA1_CMS': 9, |
|
65 |
+ 'MD5_RSA_CMS': 10, |
|
66 |
+ 'SHA1_RSA_CMS': 11, |
|
67 |
+ 'RC2_CBC_ENV': 12, |
|
68 |
+ 'RSA_ENV': 13, |
|
69 |
+ 'RSA_ES_OAEP_ENV': 14, |
|
70 |
+ 'DES3_CBC_ENV': 15, |
|
71 |
+ 'DES3_CBC_SHA1': 16, |
|
72 |
+ 'AES128_CTS_HMAC_SHA1_96': 17, |
|
73 |
+ 'AES256_CTS_HMAC_SHA1_96': 18, |
|
74 |
+ 'AES128_CTS_HMAC_SHA256_128': 19, |
|
75 |
+ 'AES256_CTS_HMAC_SHA384_192': 20, |
|
76 |
+ 'ARCFOUR_HMAC': 23, |
|
77 |
+ 'ARCFOUR_HMAC_EXP': 24, |
|
78 |
+ 'CAMELLIA128_CTS_CMAC': 25, |
|
79 |
+ 'CAMELLIA256_CTS_CMAC': 26, |
|
80 |
+ 'UNKNOWN': 511, |
|
81 |
+} |
|
82 |
+ |
|
83 |
+class KeytabParsingError(Exception): |
|
84 |
+ MESSAGE = 'Parsing eror: expected {size} bytes to unpack {format} but read {length} bytes instead: {data}' |
|
85 |
+ def __init__(self, data, size, frmt): |
|
86 |
+ self.data = data |
|
87 |
+ self.size = size |
|
88 |
+ self.format = frmt |
|
89 |
+ def __str__(self): |
|
90 |
+ return __class__.MESSAGE.format(**self.__dict__, length=len(self.data)) |
|
91 |
+ |
|
92 |
+def lookup(lookup_value, dictionary, default): |
|
93 |
+ for name, value in dictionary.items(): |
|
94 |
+ if value == lookup_value: |
|
95 |
+ return name |
|
96 |
+ return default |
|
97 |
+ |
|
98 |
+def int_to_name_type(lookup_value): |
|
99 |
+ return lookup(lookup_value, NAME_TYPES, 'KRB5_NT_UNKNOWN') |
|
100 |
+ |
|
101 |
+def int_to_enc_type(lookup_value): |
|
102 |
+ return lookup(lookup_value, ENC_TYPES, 'ENCTYPE_UNKNOWN') |
|
103 |
+ |
|
104 |
+def principal_to_spn(principal): |
|
105 |
+ if principal['name_type_raw'] != 1: |
|
106 |
+ return None |
|
107 |
+ components = principal['components'] |
|
108 |
+ count = len(components) |
|
109 |
+ if count < 1 or count > 3: |
|
110 |
+ return None |
|
111 |
+ for component in components: |
|
112 |
+ if not component: |
|
113 |
+ return None |
|
114 |
+ spn = components[0] |
|
115 |
+ if count >= 2: |
|
116 |
+ spn += '/' + components[1] |
|
117 |
+ if count == 3: |
|
118 |
+ spn += ':' + components[2] |
|
119 |
+ spn += '@' + principal['realm'] |
|
120 |
+ return spn |
|
121 |
+ |
|
122 |
+def verbose(level, msg, *args, **kwargs): |
|
123 |
+ if level <= VERBOSITY: |
|
124 |
+ message = msg.format(*args, **kwargs) |
|
125 |
+ sys.stderr.write(message + '\n') |
|
126 |
+ |
|
127 |
+def unpack(buf, prefix, format): |
|
128 |
+ """ |
|
129 |
+ Wrapper around read(), struct.unpack() and struct.calcsize(). |
|
130 |
+ """ |
|
131 |
+ actual_format = prefix + format |
|
132 |
+ size = CALCSIZE.get(actual_format) |
|
133 |
+ if size is None: |
|
134 |
+ size = CALCSIZE[actual_format] = struct.calcsize(actual_format) |
|
135 |
+ data = buf.read(size) |
|
136 |
+ if len(data) < size: |
|
137 |
+ raise KeytabParsingError(data, size, actual_format) |
|
138 |
+ results = struct.unpack(actual_format, data) |
|
139 |
+ return results[0] if len(results) == 1 else results |
|
140 |
+ |
|
141 |
+def parse_data(buf, prefix=DEFAULT_PREFIX): |
|
142 |
+ length = unpack(buf, prefix, 'H') |
|
143 |
+ return buf.read(length) |
|
144 |
+ |
|
145 |
+def parse_str(buf, prefix=DEFAULT_PREFIX, encoding=DEFAULT_ENCODING): |
|
146 |
+ return parse_data(buf, prefix).decode(encoding) |
|
147 |
+ |
|
148 |
+def parse_principal(buf, prefix=DEFAULT_PREFIX): |
|
149 |
+ principal = {} |
|
150 |
+ # [1] states "count of components (32 bits)" but [2] says int16: |
|
151 |
+ component_count = unpack(buf, prefix, 'H') |
|
152 |
+ principal['realm'] = parse_str(buf, prefix) |
|
153 |
+ components = [] |
|
154 |
+ for i in range(component_count): |
|
155 |
+ components.append(parse_str(buf, prefix)) |
|
156 |
+ principal['components'] = components |
|
157 |
+ # [3] states int32: |
|
158 |
+ principal['name_type_raw'] = unpack(buf, prefix, 'i') |
|
159 |
+ return principal |
|
160 |
+ |
|
161 |
+def parse_entry(buf, prefix=DEFAULT_PREFIX): |
|
162 |
+ entry = {} |
|
163 |
+ entry['principal'] = parse_principal(buf, prefix) |
|
164 |
+ entry['timestamp'], entry['kvno'], entry['enctype_raw'], entry['key_length'] = unpack(buf, prefix, 'IBHH') |
|
165 |
+ entry['key'] = buf.read(entry['key_length']) |
|
166 |
+ return entry |
|
167 |
+ |
|
168 |
+def parse_record(buf, prefix=DEFAULT_PREFIX): |
|
169 |
+ record = {'type': 'record'} |
|
170 |
+ record['entry'] = parse_entry(buf, prefix) |
|
171 |
+ record['tail'] = buf.read() |
|
172 |
+ return record |
|
173 |
+ |
|
174 |
+def parse_keytab(buf, args): |
|
175 |
+ second_byte = buf.read(2)[1] |
|
176 |
+ verbose(2, 'keytab v{}', second_byte) |
|
177 |
+ if second_byte == 1: |
|
178 |
+ verbose(1, 'Keytab v1 not supported yet!') |
|
179 |
+ sys.exit(1) |
|
180 |
+ elif second_byte == 2: |
|
181 |
+ # Version 2 always uses big-endian byte order: |
|
182 |
+ prefix = BIG_ENDIAN |
|
183 |
+ else: |
|
184 |
+ verbose(1, 'Unknown keytab version: v{}', second_byte) |
|
185 |
+ sys.exit(1) |
|
186 |
+ |
|
187 |
+ keytab = { |
|
188 |
+ 'version': second_byte, |
|
189 |
+ 'records': [], |
|
190 |
+ } |
|
191 |
+ while True: |
|
192 |
+ try: |
|
193 |
+ record_length = unpack(buf, prefix, 'i') |
|
194 |
+ except KeytabParsingError as kpe: |
|
195 |
+ if len(kpe.data): |
|
196 |
+ verbose(1, 'Premature end of file? Got {} as record length.', kpe.data) |
|
197 |
+ break |
|
198 |
+ if not record_length: |
|
199 |
+ break |
|
200 |
+ verbose(3, 'Record #{} of length {}', len(keytab['records']) + 1, record_length) |
|
201 |
+ record = buf.read(abs(record_length)) |
|
202 |
+ if record_length > 0: |
|
203 |
+ record = parse_record(BufferedReader(BytesIO(record)), prefix) |
|
204 |
+ else: |
|
205 |
+ record = {'type': 'hole', 'data': record} |
|
206 |
+ record['length'] = record_length |
|
207 |
+ keytab['records'].append(record) |
|
208 |
+ return keytab |
|
209 |
+ |
|
210 |
+def enrich_keytab(keytab): |
|
211 |
+ """ |
|
212 |
+ Enrich records with extra information suitable for human readers. |
|
213 |
+ """ |
|
214 |
+ for record in keytab['records']: |
|
215 |
+ if 'entry' not in record: |
|
216 |
+ continue |
|
217 |
+ entry = record['entry'] |
|
218 |
+ entry['date'] = datetime.utcfromtimestamp(entry['timestamp']).strftime(DATE_TIME_FORMAT) |
|
219 |
+ if 'name_type_raw' in entry['principal']: |
|
220 |
+ entry['principal']['name_type'] = int_to_name_type(entry['principal']['name_type_raw']) |
|
221 |
+ spn = principal_to_spn(entry['principal']) |
|
222 |
+ if spn: |
|
223 |
+ entry['spn'] = spn |
|
224 |
+ entry['enctype'] = int_to_enc_type(entry['enctype_raw']) |
|
225 |
+ if 'tail' in record: |
|
226 |
+ # [1] states: Some implementations of Kerberos recognize a 32-bit key version at the end of |
|
227 |
+ # an entry, if the record length is at least 4 bytes longer than the entry and the value of |
|
228 |
+ # those 32 bits is not 0. If present, this key version supersedes the 8-bit key version. |
|
229 |
+ if len(record['tail']) >= 4: |
|
230 |
+ tail_kvno = struct.unpack('>I', record['tail'][0:4])[0] |
|
231 |
+ if tail_kvno: |
|
232 |
+ entry['tail_kvno'] = tail_kvno |
|
233 |
+ # If kvno is zero, assume the one found in the tail is the one that matters: |
|
234 |
+ if not entry['kvno']: |
|
235 |
+ entry['actual_kvno'] = entry['tail_kvno'] |
|
236 |
+ if 'actual_kvno' not in entry: |
|
237 |
+ entry['actual_kvno'] = entry['kvno'] |
|
238 |
+ return keytab |
|
239 |
+ |
|
240 |
+def simplify_keytab(keytab): |
|
241 |
+ """ |
|
242 |
+ Simplify the keytab to make it suitable for edition. |
|
243 |
+ """ |
|
244 |
+ simplified = {'version': keytab['version'], 'entries': []} |
|
245 |
+ for record in keytab['records']: |
|
246 |
+ if 'entry' not in record: |
|
247 |
+ continue |
|
248 |
+ entry = record['entry'] |
|
249 |
+ simple_entry = {} |
|
250 |
+ if 'spn' in entry: |
|
251 |
+ simple_entry['spn'] = entry['spn'] |
|
252 |
+ simple_entry['principal'] = {} |
|
253 |
+ for key in ('name_type', 'components', 'realm'): |
|
254 |
+ if key in entry['principal']: |
|
255 |
+ simple_entry['principal'][key] = entry['principal'][key] |
|
256 |
+ simple_entry['kvno'] = entry.get('actual_kvno', entry['kvno']) |
|
257 |
+ for key in ('date', 'enctype', 'key'): |
|
258 |
+ if key in entry: |
|
259 |
+ simple_entry[key] = entry[key] |
|
260 |
+ simplified['entries'].append(simple_entry) |
|
261 |
+ return simplified |
|
262 |
+ |
|
263 |
+def prepare_serialization(obj): |
|
264 |
+ """ |
|
265 |
+ Prepare keytab for serialization. |
|
266 |
+ """ |
|
267 |
+ if type(obj) is dict: |
|
268 |
+ for key, value in obj.items(): |
|
269 |
+ obj[key] = prepare_serialization(value) |
|
270 |
+ elif type(obj) is list: |
|
271 |
+ for index, value in enumerate(obj): |
|
272 |
+ obj[index] = prepare_serialization(value) |
|
273 |
+ elif type(obj) is bytes: |
|
274 |
+ obj = hexlify(obj).decode(DEFAULT_ENCODING) |
|
275 |
+ return obj |
|
276 |
+ |
|
277 |
+def keytab_data(buf, args): |
|
278 |
+ keytab = parse_keytab(buf, args) |
|
279 |
+ layout = DATA_LAYOUTS.get(args.data_layout, DATA_LAYOUT_FULL) |
|
280 |
+ if layout >= DATA_LAYOUT_FULL: |
|
281 |
+ keytab = enrich_keytab(keytab) |
|
282 |
+ if layout >= DATA_LAYOUT_SIMPLE: |
|
283 |
+ keytab = simplify_keytab(keytab) |
|
284 |
+ return keytab |
|
285 |
+ |
|
286 |
+def keytab_to_yaml(buf, args): |
|
287 |
+ keytab = keytab_data(buf, args) |
|
288 |
+ final_keytab = prepare_serialization(keytab) |
|
289 |
+ if args.output_format == 'yaml': |
|
290 |
+ yaml.dump(final_keytab, sys.stdout, width=160, sort_keys=False) |
|
291 |
+ else: |
|
292 |
+ json.dump(final_keytab, sys.stdout, indent=4) |
|
293 |
+ |
|
294 |
+def yaml_to_keytab(fd): |
|
295 |
+ data = yaml.load(fd.read(), Loader=yaml.SafeLoader) |
|
296 |
+ print('YAML:', data) |
|
297 |
+ |
|
298 |
+def parse_args(): |
|
299 |
+ parser = argparse.ArgumentParser(description='Keytab <-> YAML/JSON convertor.') |
|
300 |
+ parser.add_argument('--verbose', '-v', dest='verbose', action='count', help='increase verbosity level', default=VERBOSITY) |
|
301 |
+ parser.add_argument('--data-layout', '-l', dest='data_layout', choices=DATA_LAYOUTS.keys(), default='simple', help='data layout (keytab to YAML/JSON only)') |
|
302 |
+ parser.add_argument('--output-format', '-f', dest='output_format', choices=['json', 'yaml'], default='yaml', help='output format (keytab to YAML/JSON only)') |
|
303 |
+ parser.add_argument('input', nargs='?', type=argparse.FileType('rb'), default=sys.stdin.buffer, help='input file; defaults to standard input') |
|
304 |
+ args = parser.parse_args() |
|
305 |
+ return args |
|
306 |
+ |
|
307 |
+def main(): |
|
308 |
+ args = parse_args() |
|
309 |
+ global VERBOSITY |
|
310 |
+ VERBOSITY=args.verbose |
|
311 |
+ buf = args.input |
|
312 |
+ first_byte = buf.peek(1)[0] |
|
313 |
+ if first_byte == KEYTAB_FIRST_BYTE: |
|
314 |
+ keytab_to_yaml(buf, args) |
|
315 |
+ else: |
|
316 |
+ yaml_to_keytab(buf, args) |
|
317 |
+ |
|
318 |
+if __name__ == '__main__': |
|
319 |
+ main() |