summaryrefslogtreecommitdiffstats
path: root/src/tests/t_mkey.py
blob: 3cecabffaf05f16d8d0f2b7ef15e8438a30b7656 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
#!/usr/bin/python
from k5test import *
import random
import re
import struct

# Convenience constants for use as expected enctypes.  defetype is the
# default enctype for master keys.
aes256 = 'aes256-cts-hmac-sha1-96'
aes128 = 'aes128-cts-hmac-sha1-96'
des3 = 'des3-cbc-sha1'
defetype = aes256

realm = K5Realm(create_host=False, start_kadmind=True)
realm.prep_kadmin()
stash_file = os.path.join(realm.testdir, 'stash')

# Count the number of principals in the realm.
nprincs = len(realm.run_kadminl('listprincs').splitlines()) - 1

# List the currently active mkeys and compare against expected
# results.  Each argument must be a sequence of four elements: an
# expected kvno, an expected enctype, whether the key is expected to
# have an activation time, and whether the key is expected to be
# currently active.
list_mkeys_re = re.compile(r'^KVNO: (\d+), Enctype: (\S+), '
                           '(Active on: [^\*]+|No activate time set)( \*)?$')
def check_mkey_list(*expected):
    # Split the output of kdb5_util list_mkeys into lines and ignore the first.
    outlines = realm.run([kdb5_util, 'list_mkeys']).splitlines()[1:]
    if len(outlines) != len(expected):
        fail('Unexpected number of list_mkeys output lines')
    for line, ex in zip(outlines, expected):
        m = list_mkeys_re.match(line)
        if not m:
            fail('Unrecognized list_mkeys output line')
        kvno, enctype, act_time, active = m.groups()
        exp_kvno, exp_enctype, exp_act_time_present, exp_active = ex
        if kvno != str(exp_kvno):
            fail('Unexpected master key version')
        if enctype != exp_enctype:
            fail('Unexpected master key enctype')
        if act_time.startswith('Active on: ') != exp_act_time_present:
            fail('Unexpected presence or absence of mkey activation time')
        if (active == ' *') != exp_active:
            fail('Master key unexpectedly active or inactive')


# Get the K/M principal.  Verify that it has the expected mkvno.  Each
# remaining argment must be a sequence of two elements: an expected
# key version and an expected enctype.
keyline_re = re.compile(r'^Key: vno (\d+), (\S+), ')
def check_master_dbent(expected_mkvno, *expected_keys):
    outlines = realm.run_kadminl('getprinc K/M').splitlines()
    mkeyline = [l for l in outlines if l.startswith('MKey: vno ')]
    if len(mkeyline) != 1 or mkeyline[0] != ('MKey: vno %d' % expected_mkvno):
        fail('Unexpected mkvno in K/M DB entry')
    keylines = [l for l in outlines if l.startswith('Key: vno ')]
    if len(keylines) != len(expected_keys):
        fail('Unexpected number of key lines in K/M DB entry')
    for line, ex in zip(keylines, expected_keys):
        m = keyline_re.match(line)
        if not m:
            fail('Unrecognized key line in K/M DB entry')
        kvno, enctype = m.groups()
        exp_kvno, exp_enctype = ex
        if kvno != str(exp_kvno):
            fail('Unexpected key version in K/M DB entry')
        if enctype != exp_enctype:
            fail('Unexpected enctype in K/M DB entry')


# Check the stash file.  Each argument must be a sequence of two
# elements: an expected key version and an expected enctype.
klist_re = re.compile(r'^\s*(\d+) K/M@KRBTEST.COM \((\S+)\)')
def check_stash(*expected):
    # Split the output of klist -e -k into lines and ignore the first three.
    outlines = realm.run([klist, '-e', '-k', stash_file]).splitlines()[3:]
    if len(outlines) != len(expected):
        fail('Unexpected number of lines in stash file klist')
    for line, ex in zip(outlines, expected):
        m = klist_re.match(line)
        if not m:
            fail('Unrecognized stash file klist line')
        kvno, enctype = m.groups()
        exp_kvno, exp_enctype = ex
        if kvno != str(exp_kvno):
            fail('Unexpected stash file klist kvno')
        if enctype != exp_enctype:
            fail('Unexpected stash file klist enctype')


# Verify that the user principal has the expected mkvno.
def check_mkvno(princ, expected_mkvno):
    out = realm.run_kadminl('getprinc ' + princ)
    if ('MKey: vno %d\n' % expected_mkvno) not in out:
        fail('Unexpected mkvno in user DB entry')


# Change the password using either kadmin.local or kadmin, then check
# the mkvno of the principal against expected_mkvno and verify that
# the running KDC can access the new key.
def change_password_check_mkvno(local, princ, password, expected_mkvno):
    cmd = 'cpw -pw %s %s' % (password, princ)
    out = local and realm.run_kadminl(cmd) or realm.run_kadmin(cmd)
    if 'changed.' not in out:
        fail('Failed to change password')
    check_mkvno(princ, expected_mkvno)
    realm.kinit(princ, password)


# Add a master key with the specified options and a random password.
def add_mkey(options):
    pw = ''.join(random.choice(string.ascii_uppercase) for x in range(5))
    realm.run([kdb5_util, 'add_mkey'] + options, input=(pw + '\n' + pw + '\n'))


# Run kdb5_util update_princ_encryption (with the dry-run option if
# specified) and verify the output against the expected mkvno, number
# of updated principals, and number of already-current principals.
mkvno_re = {False: re.compile(r'^Principals whose keys are being re-encrypted '
                              'to master key vno (\d+) if necessary:$'),
            True: re.compile(r'^Principals whose keys WOULD BE re-encrypted '
                             'to master key vno (\d+):$')}
count_re = {False: re.compile(r'^(\d+) principals processed: (\d+) updated, '
                              '(\d+) already current$'),
            True: re.compile(r'^(\d+) principals processed: (\d+) would be '
                             'updated, (\d+) already current$')}
def update_princ_encryption(dry_run, expected_mkvno, expected_updated,
                            expected_current):
    opts = ['-f', '-v']
    if dry_run:
        opts += ['-n']
    out = realm.run([kdb5_util, 'update_princ_encryption'] + opts)
    lines = out.splitlines()
    # Parse the first line to get the target mkvno.
    m = mkvno_re[dry_run].match(lines[0])
    if not m:
        fail('Unexpected first line of update_princ_encryption output')
    if m.group(1) != str(expected_mkvno):
        fail('Unexpected master key version in update_princ_encryption output')
    # Parse the last line to get the principal counts.
    m = count_re[dry_run].match(lines[-1])
    if not m:
        fail('Unexpected last line of update_princ_encryption output')
    total, updated, current = m.groups()
    if (total != str(expected_updated + expected_current) or
        updated != str(expected_updated) or current != str(expected_current)):
        fail('Unexpected counts from update_princ_encryption')


# Check the initial state of the realm.
check_mkey_list((1, defetype, True, True))
check_master_dbent(1, (1, defetype))
check_stash((1, defetype))
check_mkvno(realm.user_princ, 1)

# Check that stash will fail if a temp stash file is already present.
collisionfile = os.path.join(realm.testdir, 'stash_tmp')
f = open(collisionfile, 'w')
f.close()
output = realm.run([kdb5_util, 'stash'], expected_code=1)
if 'Temporary stash file already exists' not in output:
    fail('Did not detect temp stash file collision')
os.unlink(collisionfile)

# Add a new master key with no options.  Verify that:
# 1. The new key appears in list_mkeys but has no activation time and
#    is not active.
# 2. The new key appears in the K/M DB entry and is the current key to
#    encrypt that entry.
# 3. The stash file is not modified (since we did not pass -s).
# 4. The old key is used for password changes.
add_mkey([])
check_mkey_list((2, defetype, False, False), (1, defetype, True, True))
check_master_dbent(2, (2, defetype), (1, defetype))
change_password_check_mkvno(True, realm.user_princ, 'abcd', 1)
change_password_check_mkvno(False, realm.user_princ, 'user', 1)

# Verify that use_mkey won't make all master keys inactive.
out = realm.run([kdb5_util, 'use_mkey', '1', 'now+1day'], expected_code=1)
if 'there must be one master key currently active' not in out:
    fail('Unexpected error from use_mkey making all mkeys inactive')
check_mkey_list((2, defetype, False, False), (1, defetype, True, True))

# Make the new master key active.  Verify that:
# 1. The new key has an activation time in list_mkeys and is active.
# 2. The new key is used for password changes.
# 3. The running KDC can access the new key.
realm.run([kdb5_util, 'use_mkey', '2', 'now-1day'])
check_mkey_list((2, defetype, True, True), (1, defetype, True, False))
change_password_check_mkvno(True, realm.user_princ, 'abcd', 2)
change_password_check_mkvno(False, realm.user_princ, 'user', 2)

# Check purge_mkeys behavior with both master keys still in use.
out = realm.run([kdb5_util, 'purge_mkeys', '-f', '-v'])
if 'All keys in use, nothing purged.' not in out:
    fail('Unexpected output from purge_mkeys with both mkeys in use')

# Do an update_princ_encryption dry run and for real.  Verify that:
# 1. The target master key is 2 (the active mkvno).
# 2. nprincs - 2 principals were updated and one principal was
#    skipped (K/M is not included in the output and user was updated
#    above).
# 3. The dry run doesn't change user/admin's mkvno but the real update
#    does.
# 4. The old stashed master key is sufficient to access the DB (via
#    MKEY_AUX tl-data which keeps the current master key encrypted in
#    each of the old master keys).
update_princ_encryption(True, 2, nprincs - 2, 1)
check_mkvno(realm.admin_princ, 1)
update_princ_encryption(False, 2, nprincs - 2, 1)
check_mkvno(realm.admin_princ, 2)
realm.stop_kdc()
realm.start_kdc()
realm.kinit(realm.user_princ, 'user')

# Update all principals back to mkvno 1 and to mkvno 2 again, to
# verify that update_princ_encryption targets the active master key.
realm.run([kdb5_util, 'use_mkey', '2', 'now+1day'])
update_princ_encryption(False, 1, nprincs - 1, 0)
check_mkvno(realm.user_princ, 1)
realm.run([kdb5_util, 'use_mkey', '2', 'now-1day'])
update_princ_encryption(False, 2, nprincs - 1, 0)
check_mkvno(realm.user_princ, 2)

# Test the safety check for purging with an outdated stash file.
out = realm.run([kdb5_util, 'purge_mkeys', '-f'], expected_code=1)
if 'stash file needs updating' not in out:
    fail('Unexpected error from purge_mkeys safety check')

# Update the master stash file and check it.  Save a copy of the old
# one for a later test.
shutil.copy(stash_file, stash_file + '.old')
realm.run([kdb5_util, 'stash'])
check_stash((2, defetype), (1, defetype))

# Do a purge_mkeys dry run and for real.  Verify that:
# 1. Master key 1 is purged.
# 2. The dry run doesn't remove mkvno 1 but the real one does.
# 3. The old stash file is no longer sufficient to access the DB.
# 4. If the stash file is updated, it no longer contains mkvno 1.
# 5. use_mkey now gives an error if we refer to mkvno 1.
# 6. A second purge_mkeys gives the right message.
out = realm.run([kdb5_util, 'purge_mkeys', '-v', '-n', '-f'])
if 'KVNO: 1' not in out or '1 key(s) would be purged' not in out:
    fail('Unexpected output from purge_mkeys dry-run')
check_mkey_list((2, defetype, True, True), (1, defetype, True, False))
check_master_dbent(2, (2, defetype), (1, defetype))
out = realm.run([kdb5_util, 'purge_mkeys', '-v', '-f'])
check_mkey_list((2, defetype, True, True))
check_master_dbent(2, (2, defetype))
os.rename(stash_file, stash_file + '.save')
os.rename(stash_file + '.old', stash_file)
out = realm.run([kadmin_local, '-q', 'getprinc user'], expected_code=1)
if 'Unable to decrypt latest master key' not in out:
    fail('Unexpected error from kadmin.local with old stash file')
os.rename(stash_file + '.save', stash_file)
realm.run([kdb5_util, 'stash'])
check_stash((2, defetype))
out = realm.run([kdb5_util, 'use_mkey', '1'], expected_code=1)
if '1 is an invalid KVNO value' not in out:
    fail('Unexpected error from use_mkey with invalid kvno')
out = realm.run([kdb5_util, 'purge_mkeys', '-f', '-v'])
if 'There is only one master key which can not be purged.' not in out:
    fail('Unexpected output from purge_mkeys with one mkey')

# Add a third master key with a specified enctype.  Verify that:
# 1. The new master key receives the correct number.
# 2. The enctype argument is respected.
# 3. The new master key is stashed (by itself, at the moment).
# 4. We can roll over to the new master key and use it.
add_mkey(['-s', '-e', aes128])
check_mkey_list((3, aes128, False, False), (2, defetype, True, True))
check_master_dbent(3, (3, aes128), (2, defetype))
check_stash((3, aes128))
realm.run([kdb5_util, 'use_mkey', '3', 'now-1day'])
update_princ_encryption(False, 3, nprincs - 1, 0)
check_mkey_list((3, aes128, True, True), (2, defetype, True, False))
check_mkvno(realm.user_princ, 3)

realm.stop()

# Load a dump file created with krb5 1.6, before the master key
# rollover changes were introduced.  Write out an old-format stash
# file consistent with the dump's master password ("footes").  The K/M
# entry in this database will not have actkvno tl-data because it was
# created prior to master key rollover support.  Verify that:
# 1. We can access the database using the old-format stash file.
# 2. list_mkeys displays the same list as for a post-1.7 KDB.
dumpfile = os.path.join(srctop, 'tests', 'dumpfiles', 'dump.16')
os.remove(stash_file)
f = open(stash_file, 'w')
f.write(struct.pack('=HL24s', 16, 24,
                    '\xF8\x3E\xFB\xBA\x6D\x80\xD9\x54\xE5\x5D\xF2\xE0'
                    '\x94\xAD\x6D\x86\xB5\x16\x37\xEC\x7C\x8A\xBC\x86'))
f.close()
realm.run([kdb5_util, 'load', dumpfile])
nprincs = len(realm.run_kadminl('listprincs').splitlines()) - 1
check_mkvno('K/M', 1)
check_mkey_list((1, des3, True, True))

# Create a new master key and verify that, without actkvkno tl-data:
# 1. list_mkeys displays the same as for a post-1.7 KDB.
# 2. update_princ_encryption still targets mkvno 1.
# 3. libkadm5 still uses mkvno 1 for key changes.
# 4. use_mkey creates the same list as for a post-1.7 KDB.
add_mkey([])
check_mkey_list((2, defetype, False, False), (1, des3, True, True))
update_princ_encryption(False, 1, 0, nprincs - 1)
realm.run_kadminl('addprinc -randkey ' + realm.user_princ)
check_mkvno(realm.user_princ, 1)
realm.run([kdb5_util, 'use_mkey', '2', 'now-1day'])
check_mkey_list((2, defetype, True, True), (1, des3, True, False))

success('Master key rollover tests')