1from k5test import *
2
3# Skip this test if pkinit wasn't built.
4if not os.path.exists(os.path.join(plugins, 'preauth', 'pkinit.so')):
5    skip_rest('PKINIT tests', 'PKINIT module not built')
6
7soft_pkcs11 = os.path.join(buildtop, 'tests', 'softpkcs11', 'softpkcs11.so')
8
9# Construct a krb5.conf fragment configuring pkinit.
10certs = os.path.join(srctop, 'tests', 'dejagnu', 'pkinit-certs')
11ca_pem = os.path.join(certs, 'ca.pem')
12kdc_pem = os.path.join(certs, 'kdc.pem')
13user_pem = os.path.join(certs, 'user.pem')
14privkey_pem = os.path.join(certs, 'privkey.pem')
15privkey_enc_pem = os.path.join(certs, 'privkey-enc.pem')
16user_p12 = os.path.join(certs, 'user.p12')
17user_enc_p12 = os.path.join(certs, 'user-enc.p12')
18user_upn_p12 = os.path.join(certs, 'user-upn.p12')
19user_upn2_p12 = os.path.join(certs, 'user-upn2.p12')
20user_upn3_p12 = os.path.join(certs, 'user-upn3.p12')
21generic_p12 = os.path.join(certs, 'generic.p12')
22path = os.path.join(os.getcwd(), 'testdir', 'tmp-pkinit-certs')
23path_enc = os.path.join(os.getcwd(), 'testdir', 'tmp-pkinit-certs-enc')
24
25pkinit_krb5_conf = {'realms': {'$realm': {
26            'pkinit_anchors': 'FILE:%s' % ca_pem}}}
27pkinit_kdc_conf = {'realms': {'$realm': {
28            'default_principal_flags': '+preauth',
29            'pkinit_eku_checking': 'none',
30            'pkinit_identity': 'FILE:%s,%s' % (kdc_pem, privkey_pem),
31            'pkinit_indicator': ['indpkinit1', 'indpkinit2']}}}
32restrictive_kdc_conf = {'realms': {'$realm': {
33            'restrict_anonymous_to_tgt': 'true' }}}
34freshness_kdc_conf = {'realms': {'$realm': {
35            'pkinit_require_freshness': 'true'}}}
36
37testprincs = {'krbtgt/KRBTEST.COM': {'keys': 'aes128-cts'},
38              'user': {'keys': 'aes128-cts', 'flags': '+preauth'},
39              'user2': {'keys': 'aes128-cts', 'flags': '+preauth'}}
40alias_kdc_conf = {'realms': {'$realm': {
41            'default_principal_flags': '+preauth',
42            'pkinit_eku_checking': 'none',
43            'pkinit_allow_upn': 'true',
44            'pkinit_identity': 'FILE:%s,%s' % (kdc_pem, privkey_pem),
45            'database_module': 'test'}},
46                  'dbmodules': {'test': {
47                      'db_library': 'test',
48                      'alias': {'user@krbtest.com': 'user'},
49                      'princs': testprincs}}}
50
51file_identity = 'FILE:%s,%s' % (user_pem, privkey_pem)
52file_enc_identity = 'FILE:%s,%s' % (user_pem, privkey_enc_pem)
53dir_identity = 'DIR:%s' % path
54dir_enc_identity = 'DIR:%s' % path_enc
55dir_file_identity = 'FILE:%s,%s' % (os.path.join(path, 'user.crt'),
56                                    os.path.join(path, 'user.key'))
57dir_file_enc_identity = 'FILE:%s,%s' % (os.path.join(path_enc, 'user.crt'),
58                                        os.path.join(path_enc, 'user.key'))
59p12_identity = 'PKCS12:%s' % user_p12
60p12_upn_identity = 'PKCS12:%s' % user_upn_p12
61p12_upn2_identity = 'PKCS12:%s' % user_upn2_p12
62p12_upn3_identity = 'PKCS12:%s' % user_upn3_p12
63p12_generic_identity = 'PKCS12:%s' % generic_p12
64p12_enc_identity = 'PKCS12:%s' % user_enc_p12
65p11_identity = 'PKCS11:' + soft_pkcs11
66p11_token_identity = ('PKCS11:module_name=' + soft_pkcs11 +
67                      ':slotid=1:token=SoftToken (token)')
68
69# Start a realm with the test kdb module for the following UPN SAN tests.
70realm = K5Realm(krb5_conf=pkinit_krb5_conf, kdc_conf=alias_kdc_conf,
71                create_kdb=False)
72realm.start_kdc()
73
74mark('UPN SANs')
75
76# Compatibility check: cert contains UPN "user", which matches the
77# request principal user@KRBTEST.COM if parsed as a normal principal.
78realm.kinit(realm.user_princ,
79            flags=['-X', 'X509_user_identity=%s' % p12_upn2_identity])
80
81# Compatibility check: cert contains UPN "user@KRBTEST.COM", which matches
82# the request principal user@KRBTEST.COM if parsed as a normal principal.
83realm.kinit(realm.user_princ,
84            flags=['-X', 'X509_user_identity=%s' % p12_upn3_identity])
85
86# Cert contains UPN "user@krbtest.com" which is aliased to the request
87# principal.
88realm.kinit(realm.user_princ,
89            flags=['-X', 'X509_user_identity=%s' % p12_upn_identity])
90
91# Test an id-pkinit-san match to a post-canonical principal.
92realm.kinit('user@krbtest.com',
93            flags=['-E', '-X', 'X509_user_identity=%s' % p12_identity])
94
95# Test a UPN match to a post-canonical principal.  (This only works
96# for the cert with the UPN containing just "user", as we don't allow
97# UPN reparsing when comparing to the canonicalized client principal.)
98realm.kinit('user@krbtest.com',
99            flags=['-E', '-X', 'X509_user_identity=%s' % p12_upn2_identity])
100
101# Test a mismatch.
102msg = 'kinit: Client name mismatch while getting initial credentials'
103realm.run([kinit, '-X', 'X509_user_identity=%s' % p12_upn2_identity, 'user2'],
104          expected_code=1, expected_msg=msg)
105realm.stop()
106
107realm = K5Realm(krb5_conf=pkinit_krb5_conf, kdc_conf=pkinit_kdc_conf,
108                get_creds=False)
109
110# Sanity check - password-based preauth should still work.
111mark('password preauth sanity check')
112realm.run(['./responder', '-r', 'password=%s' % password('user'),
113           realm.user_princ])
114realm.kinit(realm.user_princ, password=password('user'))
115realm.klist(realm.user_princ)
116realm.run([kvno, realm.host_princ])
117
118# Having tested password preauth, remove the keys for better error
119# reporting.
120realm.run([kadminl, 'purgekeys', '-all', realm.user_princ])
121
122# Test anonymous PKINIT.
123mark('anonymous')
124realm.kinit('@%s' % realm.realm, flags=['-n'], expected_code=1,
125            expected_msg='not found in Kerberos database')
126realm.addprinc('WELLKNOWN/ANONYMOUS')
127realm.kinit('@%s' % realm.realm, flags=['-n'])
128realm.klist('WELLKNOWN/ANONYMOUS@WELLKNOWN:ANONYMOUS')
129realm.run([kvno, realm.host_princ])
130out = realm.run(['./adata', realm.host_princ])
131if '97:' in out:
132    fail('auth indicators seen in anonymous PKINIT ticket')
133# Verify start_realm setting and test referrals TGS request.
134realm.run([klist, '-C'], expected_msg='start_realm = KRBTEST.COM')
135realm.run([kvno, '-S', 'host', hostname])
136
137# Test anonymous kadmin.
138mark('anonymous kadmin')
139f = open(os.path.join(realm.testdir, 'acl'), 'a')
140f.write('WELLKNOWN/ANONYMOUS@WELLKNOWN:ANONYMOUS a *')
141f.close()
142realm.start_kadmind()
143realm.run([kadmin, '-n', 'addprinc', '-pw', 'test', 'testadd'])
144realm.run([kadmin, '-n', 'getprinc', 'testadd'], expected_code=1,
145          expected_msg="Operation requires ``get'' privilege")
146realm.stop_kadmind()
147
148# Test with anonymous restricted; FAST should work but kvno should fail.
149mark('anonymous restricted')
150r_env = realm.special_env('restrict', True, kdc_conf=restrictive_kdc_conf)
151realm.stop_kdc()
152realm.start_kdc(env=r_env)
153realm.kinit('@%s' % realm.realm, flags=['-n'])
154realm.kinit('@%s' % realm.realm, flags=['-n', '-T', realm.ccache])
155realm.run([kvno, realm.host_princ], expected_code=1,
156          expected_msg='KDC policy rejects request')
157
158# Regression test for #8458: S4U2Self requests crash the KDC if
159# anonymous is restricted.
160mark('#8458 regression test')
161realm.kinit(realm.host_princ, flags=['-k'])
162realm.run([kvno, '-U', 'user', realm.host_princ])
163
164# Go back to the normal KDC environment.
165realm.stop_kdc()
166realm.start_kdc()
167
168# Run the basic test - PKINIT with FILE: identity, with no password on the key.
169mark('FILE identity, no password')
170msgs = ('Sending unauthenticated request',
171        '/Additional pre-authentication required',
172        'Preauthenticating using KDC method data',
173        'PKINIT client received freshness token from KDC',
174        'PKINIT loading CA certs and CRLs from FILE',
175        'PKINIT client making DH request',
176        ' preauth for next request: PA-FX-COOKIE (133), PA-PK-AS-REQ (16)',
177        'PKINIT client verified DH reply',
178        'PKINIT client found id-pkinit-san in KDC cert',
179        'PKINIT client matched KDC principal krbtgt/')
180realm.kinit(realm.user_princ,
181            flags=['-X', 'X509_user_identity=%s' % file_identity],
182            expected_trace=msgs)
183realm.klist(realm.user_princ)
184realm.run([kvno, realm.host_princ])
185
186# Try using multiple configured pkinit_identities, to make sure we
187# fall back to the second one when the first one cannot be read.
188id_conf = {'realms': {'$realm': {'pkinit_identities': [file_identity + 'X',
189                                                       file_identity]}}}
190id_env = realm.special_env('idconf', False, krb5_conf=id_conf)
191realm.kinit(realm.user_princ, expected_trace=msgs, env=id_env)
192
193# Try again using RSA instead of DH.
194mark('FILE identity, no password, RSA')
195realm.kinit(realm.user_princ,
196            flags=['-X', 'X509_user_identity=%s' % file_identity,
197                   '-X', 'flag_RSA_PROTOCOL=yes'],
198            expected_trace=('PKINIT client making RSA request',
199                            'PKINIT client verified RSA reply'))
200realm.klist(realm.user_princ)
201
202# Test a DH parameter renegotiation by temporarily setting a 4096-bit
203# minimum on the KDC.  (Preauth type 16 is PKINIT PA_PK_AS_REQ;
204# 109 is PKINIT TD_DH_PARAMETERS; 133 is FAST PA-FX-COOKIE.)
205mark('DH parameter renegotiation')
206minbits_kdc_conf = {'realms': {'$realm': {'pkinit_dh_min_bits': '4096'}}}
207minbits_env = realm.special_env('restrict', True, kdc_conf=minbits_kdc_conf)
208realm.stop_kdc()
209realm.start_kdc(env=minbits_env)
210msgs = ('Sending unauthenticated request',
211        '/Additional pre-authentication required',
212        'Preauthenticating using KDC method data',
213        'Preauth module pkinit (16) (real) returned: 0/Success',
214        ' preauth for next request: PA-FX-COOKIE (133), PA-PK-AS-REQ (16)',
215        '/Key parameters not accepted',
216        'Preauth tryagain input types (16): 109, PA-FX-COOKIE (133)',
217        'trying again with KDC-provided parameters',
218        'Preauth module pkinit (16) tryagain returned: 0/Success',
219        ' preauth for next request: PA-PK-AS-REQ (16), PA-FX-COOKIE (133)')
220realm.kinit(realm.user_princ,
221            flags=['-X', 'X509_user_identity=%s' % file_identity],
222            expected_trace=msgs)
223
224# Test enforcement of required freshness tokens.  (We can leave
225# freshness tokens required after this test.)
226mark('freshness token enforcement')
227realm.kinit(realm.user_princ,
228            flags=['-X', 'X509_user_identity=%s' % file_identity,
229                   '-X', 'disable_freshness=yes'])
230f_env = realm.special_env('freshness', True, kdc_conf=freshness_kdc_conf)
231realm.stop_kdc()
232realm.start_kdc(env=f_env)
233realm.kinit(realm.user_princ,
234            flags=['-X', 'X509_user_identity=%s' % file_identity])
235realm.kinit(realm.user_princ,
236            flags=['-X', 'X509_user_identity=%s' % file_identity,
237                   '-X', 'disable_freshness=yes'],
238            expected_code=1, expected_msg='Preauthentication failed')
239# Anonymous should never require a freshness token.
240realm.kinit('@%s' % realm.realm, flags=['-n', '-X', 'disable_freshness=yes'])
241
242# Run the basic test - PKINIT with FILE: identity, with a password on the key,
243# supplied by the prompter.
244# Expect failure if the responder does nothing, and we have no prompter.
245mark('FILE identity, password on key (prompter)')
246realm.run(['./responder', '-x', 'pkinit={"%s": 0}' % file_enc_identity,
247          '-X', 'X509_user_identity=%s' % file_enc_identity, realm.user_princ],
248          expected_code=2)
249realm.kinit(realm.user_princ,
250            flags=['-X', 'X509_user_identity=%s' % file_enc_identity],
251            password='encrypted')
252realm.klist(realm.user_princ)
253realm.run([kvno, realm.host_princ])
254realm.run(['./adata', realm.host_princ],
255          expected_msg='+97: [indpkinit1, indpkinit2]')
256
257# Run the basic test - PKINIT with FILE: identity, with a password on the key,
258# supplied by the responder.
259# Supply the response in raw form.
260mark('FILE identity, password on key (responder)')
261out = realm.run(['./responder', '-x', 'pkinit={"%s": 0}' % file_enc_identity,
262                 '-r', 'pkinit={"%s": "encrypted"}' % file_enc_identity,
263                 '-X', 'X509_user_identity=%s' % file_enc_identity,
264                 realm.user_princ])
265# Regression test for #8885 (password question asked twice).
266if out.count('OK: ') != 1:
267    fail('Wrong number of responder calls')
268# Supply the response through the convenience API.
269realm.run(['./responder', '-X', 'X509_user_identity=%s' % file_enc_identity,
270           '-p', '%s=%s' % (file_enc_identity, 'encrypted'), realm.user_princ])
271realm.klist(realm.user_princ)
272realm.run([kvno, realm.host_princ])
273
274# PKINIT with DIR: identity, with no password on the key.
275mark('DIR identity, no password')
276os.mkdir(path)
277os.mkdir(path_enc)
278shutil.copy(privkey_pem, os.path.join(path, 'user.key'))
279shutil.copy(privkey_enc_pem, os.path.join(path_enc, 'user.key'))
280shutil.copy(user_pem, os.path.join(path, 'user.crt'))
281shutil.copy(user_pem, os.path.join(path_enc, 'user.crt'))
282realm.kinit(realm.user_princ,
283            flags=['-X', 'X509_user_identity=%s' % dir_identity])
284realm.klist(realm.user_princ)
285realm.run([kvno, realm.host_princ])
286
287# PKINIT with DIR: identity, with a password on the key, supplied by the
288# prompter.
289# Expect failure if the responder does nothing, and we have no prompter.
290mark('DIR identity, password on key (prompter)')
291realm.run(['./responder', '-x', 'pkinit={"%s": 0}' % dir_file_enc_identity,
292           '-X', 'X509_user_identity=%s' % dir_enc_identity, realm.user_princ],
293           expected_code=2)
294realm.kinit(realm.user_princ,
295            flags=['-X', 'X509_user_identity=%s' % dir_enc_identity],
296            password='encrypted')
297realm.klist(realm.user_princ)
298realm.run([kvno, realm.host_princ])
299
300# PKINIT with DIR: identity, with a password on the key, supplied by the
301# responder.
302# Supply the response in raw form.
303mark('DIR identity, password on key (responder)')
304realm.run(['./responder', '-x', 'pkinit={"%s": 0}' % dir_file_enc_identity,
305           '-r', 'pkinit={"%s": "encrypted"}' % dir_file_enc_identity,
306           '-X', 'X509_user_identity=%s' % dir_enc_identity, realm.user_princ])
307# Supply the response through the convenience API.
308realm.run(['./responder', '-X', 'X509_user_identity=%s' % dir_enc_identity,
309           '-p', '%s=%s' % (dir_file_enc_identity, 'encrypted'),
310           realm.user_princ])
311realm.klist(realm.user_princ)
312realm.run([kvno, realm.host_princ])
313
314# PKINIT with PKCS12: identity, with no password on the bundle.
315mark('PKCS12 identity, no password')
316realm.kinit(realm.user_princ,
317            flags=['-X', 'X509_user_identity=%s' % p12_identity])
318realm.klist(realm.user_princ)
319realm.run([kvno, realm.host_princ])
320
321# PKINIT with PKCS12: identity, with a password on the bundle, supplied by the
322# prompter.
323# Expect failure if the responder does nothing, and we have no prompter.
324mark('PKCS12 identity, password on bundle (prompter)')
325realm.run(['./responder', '-x', 'pkinit={"%s": 0}' % p12_enc_identity,
326           '-X', 'X509_user_identity=%s' % p12_enc_identity, realm.user_princ],
327           expected_code=2)
328realm.kinit(realm.user_princ,
329            flags=['-X', 'X509_user_identity=%s' % p12_enc_identity],
330            password='encrypted')
331realm.klist(realm.user_princ)
332realm.run([kvno, realm.host_princ])
333
334# PKINIT with PKCS12: identity, with a password on the bundle, supplied by the
335# responder.
336# Supply the response in raw form.
337mark('PKCS12 identity, password on bundle (responder)')
338realm.run(['./responder', '-x', 'pkinit={"%s": 0}' % p12_enc_identity,
339           '-r', 'pkinit={"%s": "encrypted"}' % p12_enc_identity,
340           '-X', 'X509_user_identity=%s' % p12_enc_identity, realm.user_princ])
341# Supply the response through the convenience API.
342realm.run(['./responder', '-X', 'X509_user_identity=%s' % p12_enc_identity,
343           '-p', '%s=%s' % (p12_enc_identity, 'encrypted'),
344           realm.user_princ])
345realm.klist(realm.user_princ)
346realm.run([kvno, realm.host_princ])
347
348mark('pkinit_cert_match rules')
349
350# Match a single rule.
351rule = '<SAN>^user@KRBTEST.COM$'
352realm.run([kadminl, 'setstr', realm.user_princ, 'pkinit_cert_match', rule])
353realm.kinit(realm.user_princ,
354            flags=['-X', 'X509_user_identity=%s' % p12_identity])
355realm.klist(realm.user_princ)
356
357# Regression test for #8670: match a UPN SAN with a single rule.
358rule = '<SAN>^user@krbtest.com$'
359realm.run([kadminl, 'setstr', realm.user_princ, 'pkinit_cert_match', rule])
360realm.kinit(realm.user_princ,
361            flags=['-X', 'X509_user_identity=%s' % p12_upn_identity])
362realm.klist(realm.user_princ)
363
364# Match a combined rule (default prefix is &&).
365rule = '<SUBJECT>CN=user$<KU>digitalSignature,keyEncipherment'
366realm.run([kadminl, 'setstr', realm.user_princ, 'pkinit_cert_match', rule])
367realm.kinit(realm.user_princ,
368            flags=['-X', 'X509_user_identity=%s' % p12_identity])
369realm.klist(realm.user_princ)
370
371# Fail an && rule.
372rule = '&&<SUBJECT>O=OTHER.COM<SAN>^user@KRBTEST.COM$'
373realm.run([kadminl, 'setstr', realm.user_princ, 'pkinit_cert_match', rule])
374msg = 'kinit: Certificate mismatch while getting initial credentials'
375realm.kinit(realm.user_princ,
376            flags=['-X', 'X509_user_identity=%s' % p12_identity],
377            expected_code=1, expected_msg=msg)
378
379# Pass an || rule.
380rule = '||<SUBJECT>O=KRBTEST.COM<SAN>^otheruser@KRBTEST.COM$'
381realm.run([kadminl, 'setstr', realm.user_princ, 'pkinit_cert_match', rule])
382realm.kinit(realm.user_princ,
383            flags=['-X', 'X509_user_identity=%s' % p12_identity])
384realm.klist(realm.user_princ)
385
386# Fail an || rule.
387rule = '||<SUBJECT>O=OTHER.COM<SAN>^otheruser@KRBTEST.COM$'
388realm.run([kadminl, 'setstr', realm.user_princ, 'pkinit_cert_match', rule])
389msg = 'kinit: Certificate mismatch while getting initial credentials'
390realm.kinit(realm.user_princ,
391            flags=['-X', 'X509_user_identity=%s' % p12_identity],
392            expected_code=1, expected_msg=msg)
393
394# Authorize a client cert with no PKINIT extensions using subject and
395# issuer.  (Relies on EKU checking being turned off.)
396rule = '&&<SUBJECT>CN=user$<ISSUER>O=MIT,'
397realm.run([kadminl, 'setstr', realm.user_princ, 'pkinit_cert_match', rule])
398realm.kinit(realm.user_princ,
399            flags=['-X', 'X509_user_identity=%s' % p12_generic_identity])
400realm.klist(realm.user_princ)
401
402# Regression test for #8726: null deref when parsing a FILE residual
403# beginning with a comma.
404realm.kinit(realm.user_princ, flags=['-X', 'X509_user_identity=,'],
405            expected_code=1, expected_msg='Preauthentication failed while')
406
407softpkcs11rc = os.path.join(os.getcwd(), 'testdir', 'soft-pkcs11.rc')
408realm.env['SOFTPKCS11RC'] = softpkcs11rc
409
410# PKINIT with PKCS11: identity, with no need for a PIN.
411mark('PKCS11 identity, no PIN')
412conf = open(softpkcs11rc, 'w')
413conf.write("%s\t%s\t%s\t%s\n" % ('user', 'user token', user_pem, privkey_pem))
414conf.close()
415# Expect to succeed without having to supply any more information.
416realm.kinit(realm.user_princ,
417            flags=['-X', 'X509_user_identity=%s' % p11_identity])
418realm.klist(realm.user_princ)
419realm.run([kvno, realm.host_princ])
420
421# PKINIT with PKCS11: identity, with a PIN supplied by the prompter.
422mark('PKCS11 identity, with PIN (prompter)')
423os.remove(softpkcs11rc)
424conf = open(softpkcs11rc, 'w')
425conf.write("%s\t%s\t%s\t%s\n" % ('user', 'user token', user_pem,
426                                 privkey_enc_pem))
427conf.close()
428# Expect failure if the responder does nothing, and there's no prompter
429realm.run(['./responder', '-x', 'pkinit={"%s": 0}' % p11_token_identity,
430           '-X', 'X509_user_identity=%s' % p11_identity, realm.user_princ],
431          expected_code=2)
432realm.kinit(realm.user_princ,
433            flags=['-X', 'X509_user_identity=%s' % p11_identity],
434            password='encrypted')
435realm.klist(realm.user_princ)
436realm.run([kvno, realm.host_princ])
437
438# Supply the wrong PIN.
439mark('PKCS11 identity, wrong PIN')
440expected_trace = ('PKINIT client has no configured identity; giving up',)
441realm.kinit(realm.user_princ,
442            flags=['-X', 'X509_user_identity=%s' % p11_identity],
443            password='wrong', expected_code=1, expected_trace=expected_trace)
444
445# PKINIT with PKCS11: identity, with a PIN supplied by the responder.
446# Supply the response in raw form.
447mark('PKCS11 identity, with PIN (responder)')
448realm.run(['./responder', '-x', 'pkinit={"%s": 0}' % p11_token_identity,
449           '-r', 'pkinit={"%s": "encrypted"}' % p11_token_identity,
450           '-X', 'X509_user_identity=%s' % p11_identity, realm.user_princ])
451# Supply the response through the convenience API.
452realm.run(['./responder', '-X', 'X509_user_identity=%s' % p11_identity,
453           '-p', '%s=%s' % (p11_token_identity, 'encrypted'),
454           realm.user_princ])
455realm.klist(realm.user_princ)
456realm.run([kvno, realm.host_princ])
457
458success('PKINIT tests')
459