Browse code

Add first lumps of code.

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.

Xavier G authored on25/04/2020 15:10:02
Showing3 changed files

1 1
new file mode 100644
... ...
@@ -0,0 +1,14 @@
1
+            DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
2
+                    Version 2, December 2004
3
+
4
+ Copyright (C) 2004 Sam Hocevar <sam@hocevar.net>
5
+
6
+ Everyone is permitted to copy and distribute verbatim or modified
7
+ copies of this license document, and changing it is allowed as long
8
+ as the name is changed.
9
+
10
+            DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
11
+   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
12
+
13
+  0. You just DO WHAT THE FUCK YOU WANT TO.
14
+
0 15
new file mode 100644
... ...
@@ -0,0 +1 @@
1
+yamltab converts keytabs to YAML/JSON.
0 2
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()