From 727627036ccba5f1c4c2b9ce6949fdc3172fc684 Mon Sep 17 00:00:00 2001 From: Greg Hudson Date: Mon, 24 Jan 2022 11:20:12 -0500 Subject: Add k5test.py facilities for PKINIT Add the global variables pkinit_enabled and pkinit_certs. Add the realm flag pkinit=True. Add the realm method pkinit(). Use these facilities in t_pkinit.py, t_certauth.py, and t_authdata.py. --- src/tests/t_authdata.py | 12 ++-------- src/tests/t_certauth.py | 57 ++++++++++++++++----------------------------- src/tests/t_pkinit.py | 62 +++++++++++++++++-------------------------------- src/util/k5test.py | 35 ++++++++++++++++++++++++++-- 4 files changed, 76 insertions(+), 90 deletions(-) diff --git a/src/tests/t_authdata.py b/src/tests/t_authdata.py index fef8a79..cea5007 100644 --- a/src/tests/t_authdata.py +++ b/src/tests/t_authdata.py @@ -51,19 +51,11 @@ if '128:' not in out or '^-42: Hello' not in out or ' -3: test' not in out: realm.stop() -if not os.path.exists(os.path.join(plugins, 'preauth', 'pkinit.so')): +if not pkinit_enabled: skipped('anonymous ticket authdata tests', 'PKINIT not built') else: # Set up a realm with PKINIT support and get anonymous tickets. - certs = os.path.join(srctop, 'tests', 'pkinit-certs') - ca_pem = os.path.join(certs, 'ca.pem') - kdc_pem = os.path.join(certs, 'kdc.pem') - privkey_pem = os.path.join(certs, 'privkey.pem') - pkinit_conf = {'realms': {'$realm': { - 'pkinit_anchors': 'FILE:%s' % ca_pem, - 'pkinit_identity': 'FILE:%s,%s' % (kdc_pem, privkey_pem)}}} - conf.update(pkinit_conf) - realm = K5Realm(krb5_conf=conf, get_creds=False) + realm = K5Realm(krb5_conf=conf, get_creds=False, pkinit=True) realm.addprinc('WELLKNOWN/ANONYMOUS') realm.kinit('@%s' % realm.realm, flags=['-n']) diff --git a/src/tests/t_certauth.py b/src/tests/t_certauth.py index 06a5399..82a98a8 100644 --- a/src/tests/t_certauth.py +++ b/src/tests/t_certauth.py @@ -1,72 +1,55 @@ from k5test import * # Skip this test if pkinit wasn't built. -if not os.path.exists(os.path.join(plugins, 'preauth', 'pkinit.so')): +if not pkinit_enabled: skip_rest('certauth tests', 'PKINIT module not built') -certs = os.path.join(srctop, 'tests', 'pkinit-certs') -ca_pem = os.path.join(certs, 'ca.pem') -kdc_pem = os.path.join(certs, 'kdc.pem') -privkey_pem = os.path.join(certs, 'privkey.pem') -user_pem = os.path.join(certs, 'user.pem') - modpath = os.path.join(buildtop, 'plugins', 'certauth', 'test', 'certauth_test.so') -pkinit_krb5_conf = {'realms': {'$realm': { - 'pkinit_anchors': 'FILE:%s' % ca_pem}}, - 'plugins': {'certauth': {'module': ['test1:' + modpath, - 'test2:' + modpath, - 'test3:' + modpath], - 'enable_only': ['test1', 'test2', - 'test3']}}} -pkinit_kdc_conf = {'realms': {'$realm': { - 'default_principal_flags': '+preauth', - 'pkinit_eku_checking': 'none', - 'pkinit_identity': 'FILE:%s,%s' % (kdc_pem, privkey_pem), - 'pkinit_indicator': ['indpkinit1', 'indpkinit2']}}} - -file_identity = 'FILE:%s,%s' % (user_pem, privkey_pem) +krb5_conf = {'plugins': {'certauth': { + 'module': ['test1:' + modpath, 'test2:' + modpath, 'test3:' + modpath], + 'enable_only': ['test1', 'test2', 'test3']}}} +kdc_conf = {'realms': {'$realm': { + 'default_principal_flags': '+preauth', + 'pkinit_indicator': ['indpkinit1', 'indpkinit2']}}} -realm = K5Realm(krb5_conf=pkinit_krb5_conf, kdc_conf=pkinit_kdc_conf, - get_creds=False) +realm = K5Realm(krb5_conf=krb5_conf, kdc_conf=kdc_conf, get_creds=False, + pkinit=True) realm.addprinc('nocert') -def pkinit(princ, **kw): - realm.kinit(princ, flags=['-X', 'X509_user_identity=%s' % file_identity], - **kw) - def check_indicators(inds): msg = '+97: [%s]' % inds realm.run(['./adata', realm.host_princ], expected_msg=msg) # Test that authentication fails if no module accepts. -pkinit('nocert', expected_code=1, expected_msg='Client name mismatch') +realm.pkinit('nocert', expected_code=1, expected_msg='Client name mismatch') # Let the test2 module match user to CN=user, with indicators. -pkinit(realm.user_princ) +realm.pkinit(realm.user_princ) realm.klist(realm.user_princ) check_indicators('test1, test2, user, indpkinit1, indpkinit2') # Let the test2 module mismatch with user2 to CN=user. realm.addprinc('user2@KRBTEST.COM') -pkinit('user2', expected_code=1, expected_msg='kinit: Certificate mismatch') +realm.pkinit('user2', expected_code=1, expected_msg='Certificate mismatch') # Test the KRB5_CERTAUTH_HWAUTH return code. mark('hw-authent flag tests') # First test +requires_hwauth without causing the hw-authent ticket # flag to be set. This currently results in a preauth loop. realm.run([kadminl, 'modprinc', '+requires_hwauth', realm.user_princ]) -pkinit(realm.user_princ, expected_code=1, expected_msg='Looping detected') +realm.pkinit(realm.user_princ, expected_code=1, + expected_msg='Looping detected') # Cause the test3 module to return KRB5_CERTAUTH_HWAUTH and try again. # Authentication should succeed whether or not another module accepts, # but not if another module rejects. realm.run([kadminl, 'setstr', realm.user_princ, 'hwauth', 'ok']) realm.run([kadminl, 'setstr', 'user2', 'hwauth', 'ok']) realm.run([kadminl, 'setstr', 'nocert', 'hwauth', 'ok']) -pkinit(realm.user_princ) +realm.pkinit(realm.user_princ) check_indicators('test1, test2, user, hwauth:ok, indpkinit1, indpkinit2') -pkinit('user2', expected_code=1, expected_msg='kinit: Certificate mismatch') -pkinit('nocert') +realm.pkinit('user2', expected_code=1, expected_msg='Certificate mismatch') +realm.pkinit('nocert') check_indicators('test1, hwauth:ok, indpkinit1, indpkinit2') # Cause the test3 module to return KRB5_CERTAUTH_HWAUTH_PASS and try @@ -74,9 +57,9 @@ check_indicators('test1, hwauth:ok, indpkinit1, indpkinit2') realm.run([kadminl, 'setstr', realm.user_princ, 'hwauth', 'pass']) realm.run([kadminl, 'setstr', 'user2', 'hwauth', 'pass']) realm.run([kadminl, 'setstr', 'nocert', 'hwauth', 'pass']) -pkinit(realm.user_princ) +realm.pkinit(realm.user_princ) check_indicators('test1, test2, user, hwauth:pass, indpkinit1, indpkinit2') -pkinit('user2', expected_code=1, expected_msg='kinit: Certificate mismatch') -pkinit('nocert', expected_code=1, expected_msg='kinit: Client name mismatch') +realm.pkinit('user2', expected_code=1, expected_msg='Certificate mismatch') +realm.pkinit('nocert', expected_code=1, expected_msg='Client name mismatch') success("certauth tests") diff --git a/src/tests/t_pkinit.py b/src/tests/t_pkinit.py index 3d90e76..ec2356e 100755 --- a/src/tests/t_pkinit.py +++ b/src/tests/t_pkinit.py @@ -1,33 +1,27 @@ from k5test import * # Skip this test if pkinit wasn't built. -if not os.path.exists(os.path.join(plugins, 'preauth', 'pkinit.so')): +if not pkinit_enabled: skip_rest('PKINIT tests', 'PKINIT module not built') soft_pkcs11 = os.path.join(buildtop, 'tests', 'softpkcs11', 'softpkcs11.so') # Construct a krb5.conf fragment configuring pkinit. -certs = os.path.join(srctop, 'tests', 'pkinit-certs') -ca_pem = os.path.join(certs, 'ca.pem') -kdc_pem = os.path.join(certs, 'kdc.pem') -user_pem = os.path.join(certs, 'user.pem') -privkey_pem = os.path.join(certs, 'privkey.pem') -privkey_enc_pem = os.path.join(certs, 'privkey-enc.pem') -user_p12 = os.path.join(certs, 'user.p12') -user_enc_p12 = os.path.join(certs, 'user-enc.p12') -user_upn_p12 = os.path.join(certs, 'user-upn.p12') -user_upn2_p12 = os.path.join(certs, 'user-upn2.p12') -user_upn3_p12 = os.path.join(certs, 'user-upn3.p12') -generic_p12 = os.path.join(certs, 'generic.p12') +user_pem = os.path.join(pkinit_certs, 'user.pem') +privkey_pem = os.path.join(pkinit_certs, 'privkey.pem') +privkey_enc_pem = os.path.join(pkinit_certs, 'privkey-enc.pem') +user_p12 = os.path.join(pkinit_certs, 'user.p12') +user_enc_p12 = os.path.join(pkinit_certs, 'user-enc.p12') +user_upn_p12 = os.path.join(pkinit_certs, 'user-upn.p12') +user_upn2_p12 = os.path.join(pkinit_certs, 'user-upn2.p12') +user_upn3_p12 = os.path.join(pkinit_certs, 'user-upn3.p12') +generic_p12 = os.path.join(pkinit_certs, 'generic.p12') path = os.path.join(os.getcwd(), 'testdir', 'tmp-pkinit-certs') path_enc = os.path.join(os.getcwd(), 'testdir', 'tmp-pkinit-certs-enc') -pkinit_krb5_conf = {'realms': {'$realm': { - 'pkinit_anchors': 'FILE:%s' % ca_pem}}} pkinit_kdc_conf = {'realms': {'$realm': { 'default_principal_flags': '+preauth', 'pkinit_eku_checking': 'none', - 'pkinit_identity': 'FILE:%s,%s' % (kdc_pem, privkey_pem), 'pkinit_indicator': ['indpkinit1', 'indpkinit2']}}} restrictive_kdc_conf = {'realms': {'$realm': { 'restrict_anonymous_to_tgt': 'true' }}} @@ -41,7 +35,6 @@ alias_kdc_conf = {'realms': {'$realm': { 'default_principal_flags': '+preauth', 'pkinit_eku_checking': 'none', 'pkinit_allow_upn': 'true', - 'pkinit_identity': 'FILE:%s,%s' % (kdc_pem, privkey_pem), 'database_module': 'test'}}, 'dbmodules': {'test': { 'db_library': 'test', @@ -67,8 +60,7 @@ p11_token_identity = ('PKCS11:module_name=' + soft_pkcs11 + ':slotid=1:token=SoftToken (token)') # Start a realm with the test kdb module for the following UPN SAN tests. -realm = K5Realm(krb5_conf=pkinit_krb5_conf, kdc_conf=alias_kdc_conf, - create_kdb=False) +realm = K5Realm(kdc_conf=alias_kdc_conf, create_kdb=False, pkinit=True) realm.start_kdc() mark('UPN SANs') @@ -104,8 +96,7 @@ realm.run([kinit, '-X', 'X509_user_identity=%s' % p12_upn2_identity, 'user2'], expected_code=1, expected_msg=msg) realm.stop() -realm = K5Realm(krb5_conf=pkinit_krb5_conf, kdc_conf=pkinit_kdc_conf, - get_creds=False) +realm = K5Realm(kdc_conf=pkinit_kdc_conf, get_creds=False, pkinit=True) # Sanity check - password-based preauth should still work. mark('password preauth sanity check') @@ -177,9 +168,7 @@ msgs = ('Sending unauthenticated request', 'PKINIT client verified DH reply', 'PKINIT client found id-pkinit-san in KDC cert', 'PKINIT client matched KDC principal krbtgt/') -realm.kinit(realm.user_princ, - flags=['-X', 'X509_user_identity=%s' % file_identity], - expected_trace=msgs) +realm.pkinit(realm.user_princ, expected_trace=msgs) realm.klist(realm.user_princ) realm.run([kvno, realm.host_princ]) @@ -192,11 +181,9 @@ realm.kinit(realm.user_princ, expected_trace=msgs, env=id_env) # Try again using RSA instead of DH. mark('FILE identity, no password, RSA') -realm.kinit(realm.user_princ, - flags=['-X', 'X509_user_identity=%s' % file_identity, - '-X', 'flag_RSA_PROTOCOL=yes'], - expected_trace=('PKINIT client making RSA request', - 'PKINIT client verified RSA reply')) +realm.pkinit(realm.user_princ, flags=['-X', 'flag_RSA_PROTOCOL=yes'], + expected_trace=('PKINIT client making RSA request', + 'PKINIT client verified RSA reply')) realm.klist(realm.user_princ) # Test a DH parameter renegotiation by temporarily setting a 4096-bit @@ -217,25 +204,18 @@ msgs = ('Sending unauthenticated request', 'trying again with KDC-provided parameters', 'Preauth module pkinit (16) tryagain returned: 0/Success', ' preauth for next request: PA-PK-AS-REQ (16), PA-FX-COOKIE (133)') -realm.kinit(realm.user_princ, - flags=['-X', 'X509_user_identity=%s' % file_identity], - expected_trace=msgs) +realm.pkinit(realm.user_princ, expected_trace=msgs) # Test enforcement of required freshness tokens. (We can leave # freshness tokens required after this test.) mark('freshness token enforcement') -realm.kinit(realm.user_princ, - flags=['-X', 'X509_user_identity=%s' % file_identity, - '-X', 'disable_freshness=yes']) +realm.pkinit(realm.user_princ, flags=['-X', 'disable_freshness=yes']) f_env = realm.special_env('freshness', True, kdc_conf=freshness_kdc_conf) realm.stop_kdc() realm.start_kdc(env=f_env) -realm.kinit(realm.user_princ, - flags=['-X', 'X509_user_identity=%s' % file_identity]) -realm.kinit(realm.user_princ, - flags=['-X', 'X509_user_identity=%s' % file_identity, - '-X', 'disable_freshness=yes'], - expected_code=1, expected_msg='Preauthentication failed') +realm.pkinit(realm.user_princ) +realm.pkinit(realm.user_princ, flags=['-X', 'disable_freshness=yes'], + expected_code=1, expected_msg='Preauthentication failed') # Anonymous should never require a freshness token. realm.kinit('@%s' % realm.realm, flags=['-n', '-X', 'disable_freshness=yes']) diff --git a/src/util/k5test.py b/src/util/k5test.py index 1917bbc..bd71a45 100644 --- a/src/util/k5test.py +++ b/src/util/k5test.py @@ -81,6 +81,7 @@ keyword arguments: - $buildtop: The root of the build directory - $srctop: The root of the source directory - $plugins: The plugin directory in the build tree + - $certs: The PKINIT certificate directory in the source tree - $hostname: The FQDN of the host - $port0: The first listener port (portbase) - ... @@ -121,6 +122,8 @@ keyword arguments: * bdb_only=True: Use the DB2 KDB module even if K5TEST_LMDB is set in the environment. +* pkinit=True: Configure a PKINIT anchor and KDC certificate. + Scripts may use the following functions and variables: * fail(message): Display message (plus leading marker and trailing @@ -197,6 +200,11 @@ Scripts may use the following functions and variables: * plugins: The plugin directory in the build tree (absolute path). +* pkinit_enabled: True if the PKINIT plugin module is present in the + build directory. + +* pkinit_certs: The directory containing test PKINIT certificates. + * hostname: The local hostname as it will initially appear in krb5_sname_to_principal() results. (Shortname qualification is turned off in the test environment to make this value easy to @@ -302,6 +310,10 @@ Scripts may use the following realm methods and attributes: flags must cause kinit not to need a password (e.g. by specifying a keytab). +* realm.pkinit(princ, **keywords): Acquire credentials for princ, + supplying a PKINIT identity of the basic user test certificate + (matching user@KRBTEST.COM). + * realm.klist(client_princ, service_princ=None, ccache=None): Using klist, list the credentials cache ccache (must be a filename; self.ccache if not specified) and verify that the output shows @@ -881,7 +893,7 @@ class K5Realm(object): krb5_conf=None, kdc_conf=None, create_kdb=True, krbtgt_keysalt=None, create_user=True, get_creds=True, create_host=True, start_kdc=True, start_kadmind=False, - start_kpropd=False, bdb_only=False): + start_kpropd=False, bdb_only=False, pkinit=False): global hostname, _default_krb5_conf, _default_kdc_conf global _lmdb_kdc_conf, _current_db @@ -898,11 +910,15 @@ class K5Realm(object): self.ccache = os.path.join(self.testdir, 'ccache') self.gss_mech_config = os.path.join(self.testdir, 'mech.conf') self.kadmin_ccache = os.path.join(self.testdir, 'kadmin_ccache') - self._krb5_conf = _cfg_merge(_default_krb5_conf, krb5_conf) + base_krb5_conf = _default_krb5_conf base_kdc_conf = _default_kdc_conf if (os.getenv('K5TEST_LMDB') is not None and not bdb_only and not _current_db): base_kdc_conf = _cfg_merge(base_kdc_conf, _lmdb_kdc_conf) + if pkinit: + base_krb5_conf = _cfg_merge(base_krb5_conf, _pkinit_krb5_conf) + base_kdc_conf = _cfg_merge(base_kdc_conf, _pkinit_kdc_conf) + self._krb5_conf = _cfg_merge(base_krb5_conf, krb5_conf) self._kdc_conf = _cfg_merge(base_kdc_conf, kdc_conf) self._kdc_proc = None self._kadmind_proc = None @@ -979,6 +995,7 @@ class K5Realm(object): buildtop=buildtop, srctop=srctop, plugins=plugins, + certs=pkinit_certs, hostname=hostname, port0=self.portbase, port1=self.portbase + 1, @@ -1120,6 +1137,12 @@ class K5Realm(object): input = None return self.run([kinit] + flags + [princname], input=input, **keywords) + def pkinit(self, princ, flags=[], **kw): + id = 'FILE:%s,%s' % (os.path.join(pkinit_certs, 'user.pem'), + os.path.join(pkinit_certs, 'privkey.pem')) + flags = flags + ['-X', 'X509_user_identity=%s' % id] + self.kinit(princ, flags=flags, **kw) + def klist(self, client_princ, service_princ=None, ccache=None, **keywords): if service_princ is None: service_princ = self.krbtgt_princ @@ -1302,6 +1325,12 @@ _lmdb_kdc_conf = {'dbmodules': {'db': {'db_library': 'klmdb', 'nosync': 'true'}}} +_pkinit_krb5_conf = {'realms': {'$realm': { + 'pkinit_anchors': 'FILE:$certs/ca.pem'}}} +_pkinit_kdc_conf = {'realms': {'$realm': { + 'pkinit_identity': 'FILE:$certs/kdc.pem,$certs/privkey.pem'}}} + + # A pass is a tuple of: name, krbtgt_keysalt, krb5_conf, kdc_conf. _passes = [ # No special settings; exercises AES256. @@ -1370,6 +1399,8 @@ _last_cmd_output = None buildtop = _find_buildtop() srctop = _find_srctop() plugins = os.path.join(buildtop, 'plugins') +pkinit_enabled = os.path.exists(os.path.join(plugins, 'preauth', 'pkinit.so')) +pkinit_certs = os.path.join(srctop, 'tests', 'pkinit-certs') hostname = socket.gethostname().lower() null_input = open(os.devnull, 'r') -- cgit v1.1