Skip to content

Commit

Permalink
Add k5test.py facilities for PKINIT
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
greghudson committed Jan 27, 2022
1 parent 872bed1 commit 7276270
Show file tree
Hide file tree
Showing 4 changed files with 76 additions and 90 deletions.
12 changes: 2 additions & 10 deletions src/tests/t_authdata.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,19 +51,11 @@

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'])

Expand Down
57 changes: 20 additions & 37 deletions src/tests/t_certauth.py
Original file line number Diff line number Diff line change
@@ -1,82 +1,65 @@
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
# again. Authentication should succeed only if another module accepts.
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")
62 changes: 21 additions & 41 deletions src/tests/t_pkinit.py
Original file line number Diff line number Diff line change
@@ -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' }}}
Expand All @@ -41,7 +35,6 @@
'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',
Expand All @@ -67,8 +60,7 @@
':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')
Expand Down Expand Up @@ -104,8 +96,7 @@
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')
Expand Down Expand Up @@ -177,9 +168,7 @@
'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])

Expand All @@ -192,11 +181,9 @@

# 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
Expand All @@ -217,25 +204,18 @@
'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'])

Expand Down
35 changes: 33 additions & 2 deletions src/util/k5test.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@
- $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)
- ...
Expand Down Expand Up @@ -121,6 +122,8 @@
* 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
Expand Down Expand Up @@ -197,6 +200,11 @@
* 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
Expand Down Expand Up @@ -302,6 +310,10 @@
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
Expand Down Expand Up @@ -881,7 +893,7 @@ def __init__(self, realm='KRBTEST.COM', portbase=61000, testdir='testdir',
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

Expand All @@ -898,11 +910,15 @@ def __init__(self, realm='KRBTEST.COM', portbase=61000, testdir='testdir',
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
Expand Down Expand Up @@ -979,6 +995,7 @@ def _subst_cfg_value(self, value):
buildtop=buildtop,
srctop=srctop,
plugins=plugins,
certs=pkinit_certs,
hostname=hostname,
port0=self.portbase,
port1=self.portbase + 1,
Expand Down Expand Up @@ -1120,6 +1137,12 @@ def kinit(self, princname, password=None, flags=[], **keywords):
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
Expand Down Expand Up @@ -1302,6 +1325,12 @@ def cross_realms(num, xtgts=None, args=None, **keywords):
'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.
Expand Down Expand Up @@ -1370,6 +1399,8 @@ def cross_realms(num, xtgts=None, args=None, **keywords):
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')

Expand Down

0 comments on commit 7276270

Please sign in to comment.