krb5 commit: Add master key rollover tests in k5test framework

Greg Hudson ghudson at MIT.EDU
Fri Oct 25 11:42:17 EDT 2013


https://github.com/krb5/krb5/commit/3c56f1f8584e4e170b3bc19f077d18c5c5acfd38
commit 3c56f1f8584e4e170b3bc19f077d18c5c5acfd38
Author: Greg Hudson <ghudson at mit.edu>
Date:   Wed Oct 23 11:17:00 2013 -0400

    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.

 src/tests/Makefile.in       |    1 +
 src/tests/dumpfiles/dump.16 |    7 +
 src/tests/t_mkey.py         |  307 +++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 315 insertions(+), 0 deletions(-)

diff --git a/src/tests/Makefile.in b/src/tests/Makefile.in
index d50d513..ac944a9 100644
--- a/src/tests/Makefile.in
+++ b/src/tests/Makefile.in
@@ -114,6 +114,7 @@ check-pytests:: t_init_creds t_localauth
 	$(RUNPYTEST) $(srcdir)/t_kadmin_acl.py $(PYTESTFLAGS)
 	$(RUNPYTEST) $(srcdir)/t_kdb.py $(PYTESTFLAGS)
 	$(RUNPYTEST) $(srcdir)/t_keydata.py $(PYTESTFLAGS)
+	$(RUNPYTEST) $(srcdir)/t_mkey.py $(PYTESTFLAGS)
 	$(RUNPYTEST) $(srcdir)/t_cve-2012-1014.py $(PYTESTFLAGS)
 	$(RUNPYTEST) $(srcdir)/t_cve-2012-1015.py $(PYTESTFLAGS)
 	$(RUNPYTEST) $(srcdir)/t_cve-2013-1416.py $(PYTESTFLAGS)
diff --git a/src/tests/dumpfiles/dump.16 b/src/tests/dumpfiles/dump.16
new file mode 100644
index 0000000..bea2ff6
--- /dev/null
+++ b/src/tests/dumpfiles/dump.16
@@ -0,0 +1,7 @@
+kdb5_util load_dump version 5
+princ	38	15	1	1	0	K/M at 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 at 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 at 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 at 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 at 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 at KRBTEST.COM	0	36000	604800	0	0	0	0	0	2	28	18de675264625f6372656174696f6e404b5242544553542e434f4d00	1	1	16	54	1800010572e45515fc5ee028a59a48bc86896ab5014c265304b2340d37a46c0185312e475e9245e70df0f6874c9348a2ca4389c7168f	1	1	1	38	0800d8b88190a5e7f49cdca68c0e018bafd971f1606f9af2f2e3d9e31c69071556896443ade2	-1;
diff --git a/src/tests/t_mkey.py b/src/tests/t_mkey.py
new file mode 100644
index 0000000..35d14f7
--- /dev/null
+++ b/src/tests/t_mkey.py
@@ -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 at 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')


More information about the cvs-krb5 mailing list