-
Notifications
You must be signed in to change notification settings - Fork 366
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add master key rollover tests in k5test framework
Add a new script t_mkey.py using the k5test framework. Test the fixes for #6507, #7685, and #7686 as well as basic functionality and old-stashfile compatibility. dump.16 was created by running "kdb5_util create -s -P footes" and "kdb5_util dump dumpfile" with krb5 1.6. The key from the resulting stash file was extracted and placed in the struct.pack() call in the new test script.
- Loading branch information
1 parent
ec560fa
commit 3c56f1f
Showing
3 changed files
with
315 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
kdb5_util load_dump version 5 | ||
princ 38 15 1 1 0 K/M@KRBTEST.COM 64 36000 604800 0 0 0 0 0 2 28 18de675264625f6372656174696f6e404b5242544553542e434f4d00 1 1 16 54 180001361a6c58b3a273e484574c6c5739fb114ffe7d2298e767b545f332e3bda573021c97728028e7ec4942708f23f4445d4419f4ad -1; | ||
princ 38 24 3 2 0 kadmin/admin@KRBTEST.COM 4 10800 604800 0 0 0 0 0 3 24 12345c010000000000000000000000000000000200000000 2 26 18de67526b6462355f7574696c404b5242544553542e434f4d00 1 4 18de6752 1 2 16 54 1800555ff1892cdb7dd7c62b659f9659981205ebb4f9fd4446e5243f58dfc0b99a4f096080d435702e052793a90c66aea062c8b8964e 1 2 1 38 0800ca62598d281381a22fc6d5bdf25653002b6afeb8f24af6ea01c9301359527304a08bb8f5 -1; | ||
princ 38 27 3 2 0 kadmin/changepw@KRBTEST.COM 8196 300 604800 0 0 0 0 0 3 24 12345c010000000000000000000000000000000200000000 2 26 18de67526b6462355f7574696c404b5242544553542e434f4d00 1 4 18de6752 1 2 16 54 1800237a5df1fc3295e08ba18986f51f553c2d0dfff3fb8e17d19ac7777a1cd516713e5496521ba362261ab61c063090705ecf7bca01 1 2 1 38 0800685d1f82188b712b2947b63a259269b1fe53bf383bb05cdc802e2cb9d680631e512af4d4 -1; | ||
princ 38 38 3 2 0 kadmin/equal-rites.mit.edu@KRBTEST.COM 4 10800 604800 0 0 0 0 0 3 24 12345c010000000000000000000000000000000200000000 2 26 18de67526b6462355f7574696c404b5242544553542e434f4d00 1 4 18de6752 1 2 16 54 1800ed9ad2e91940a41abc9b05d7dfb736c353e3ff18272b1d3bc31e5ebc3204e15d5fd9c3caa0c57be4736831b6f03c1741d5423ff1 1 2 1 38 0800eb82c30aa8e1f5a10f0f099372e2ff3385cb5437b41abee02491673cf45c79b2c4466364 -1; | ||
princ 38 26 3 1 0 kadmin/history@KRBTEST.COM 0 64 604800 0 0 0 0 0 3 24 12345c010000000000000000000000000000000200000000 2 26 18de67526b6462355f7574696c404b5242544553542e434f4d00 1 4 18de6752 1 2 16 54 180070b4f132c63aa7d8b91f1323daa8d59bf2e04663b9b8931bcbe9a953c4fab6dda7c820db0cf9922af5ebfb7bd09101849e81f054 -1; | ||
princ 38 30 1 2 0 krbtgt/KRBTEST.COM@KRBTEST.COM 0 36000 604800 0 0 0 0 0 2 28 18de675264625f6372656174696f6e404b5242544553542e434f4d00 1 1 16 54 1800010572e45515fc5ee028a59a48bc86896ab5014c265304b2340d37a46c0185312e475e9245e70df0f6874c9348a2ca4389c7168f 1 1 1 38 0800d8b88190a5e7f49cdca68c0e018bafd971f1606f9af2f2e3d9e31c69071556896443ade2 -1; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,307 @@ | ||
#!/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) | ||
|
||
# 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') |