1 /*
2 * Authentication tests for the pam-krb5 module with ticket cache.
3 *
4 * This test case includes all tests that require Kerberos to be configured, a
5 * username and password available, and a ticket cache created, but with the
6 * PAM module running as the same user for which the ticket cache will be
7 * created (so without setuid and with chown doing nothing).
8 *
9 * Written by Russ Allbery <eagle@eyrie.org>
10 * Copyright 2017, 2020-2021 Russ Allbery <eagle@eyrie.org>
11 * Copyright 2011, 2012
12 * The Board of Trustees of the Leland Stanford Junior University
13 *
14 * SPDX-License-Identifier: BSD-3-clause or GPL-1+
15 */
16
17 #include <config.h>
18 #include <portable/krb5.h>
19 #include <portable/system.h>
20
21 #include <pwd.h>
22 #include <sys/stat.h>
23 #include <time.h>
24
25 #include <tests/fakepam/pam.h>
26 #include <tests/fakepam/script.h>
27 #include <tests/tap/basic.h>
28 #include <tests/tap/kerberos.h>
29 #include <tests/tap/process.h>
30 #include <tests/tap/string.h>
31
32 /* Additional data used by the cache check callback. */
33 struct extra {
34 char *realm;
35 char *cache_path;
36 };
37
38
39 /*
40 * PAM test callback to check whether we created a ticket cache and the ticket
41 * cache is for the correct user.
42 */
43 static void
check_cache(const char * file,const struct script_config * config,const struct extra * extra)44 check_cache(const char *file, const struct script_config *config,
45 const struct extra *extra)
46 {
47 struct stat st;
48 krb5_error_code code;
49 krb5_context ctx = NULL;
50 krb5_ccache ccache = NULL;
51 krb5_principal princ = NULL;
52 krb5_principal tgtprinc = NULL;
53 krb5_creds in, out;
54 char *principal = NULL;
55
56 /* Check ownership and permissions. */
57 is_int(0, stat(file, &st), "cache exists");
58 is_int(getuid(), st.st_uid, "...with correct UID");
59 is_int(getgid(), st.st_gid, "...with correct GID");
60 is_int(0600, (st.st_mode & 0777), "...with correct permissions");
61
62 /* Check the existence of the ticket cache and its principal. */
63 code = krb5_init_context(&ctx);
64 if (code != 0)
65 bail("cannot create Kerberos context");
66 code = krb5_cc_resolve(ctx, file, &ccache);
67 is_int(0, code, "able to resolve Kerberos ticket cache");
68 code = krb5_cc_get_principal(ctx, ccache, &princ);
69 is_int(0, code, "able to get principal");
70 code = krb5_unparse_name(ctx, princ, &principal);
71 is_int(0, code, "...and principal is valid");
72 is_string(config->extra[0], principal, "...and matches our principal");
73
74 /* Retrieve the krbtgt for the realm and check properties. */
75 code = krb5_build_principal_ext(
76 ctx, &tgtprinc, (unsigned int) strlen(extra->realm), extra->realm,
77 KRB5_TGS_NAME_SIZE, KRB5_TGS_NAME, strlen(extra->realm), extra->realm,
78 NULL);
79 if (code != 0)
80 bail("cannot create krbtgt principal name");
81 memset(&in, 0, sizeof(in));
82 memset(&out, 0, sizeof(out));
83 in.server = tgtprinc;
84 in.client = princ;
85 code = krb5_cc_retrieve_cred(ctx, ccache, KRB5_TC_MATCH_SRV_NAMEONLY, &in,
86 &out);
87 is_int(0, code, "able to get krbtgt credentials");
88 ok(out.times.endtime > time(NULL) + 30 * 60, "...good for 30 minutes");
89 krb5_free_cred_contents(ctx, &out);
90
91 /* Close things and release memory. */
92 krb5_free_principal(ctx, tgtprinc);
93 krb5_free_unparsed_name(ctx, principal);
94 krb5_free_principal(ctx, princ);
95 krb5_cc_close(ctx, ccache);
96 krb5_free_context(ctx);
97 }
98
99
100 /*
101 * Same as check_cache except unlink the ticket cache afterwards. Used to
102 * check the ticket cache in cases where the PAM module will not clean it up
103 * afterwards, such as calling pam_end with PAM_DATA_SILENT.
104 */
105 static void
check_cache_callback(pam_handle_t * pamh,const struct script_config * config,void * data)106 check_cache_callback(pam_handle_t *pamh, const struct script_config *config,
107 void *data)
108 {
109 struct extra *extra = data;
110 const char *cache, *file;
111 char *prefix;
112
113 cache = pam_getenv(pamh, "KRB5CCNAME");
114 ok(cache != NULL, "KRB5CCNAME is set in PAM environment");
115 if (cache == NULL)
116 return;
117 basprintf(&prefix, "FILE:/tmp/krb5cc_%lu_", (unsigned long) getuid());
118 diag("KRB5CCNAME = %s", cache);
119 ok(strncmp(prefix, cache, strlen(prefix)) == 0,
120 "cache file name prefix is correct");
121 free(prefix);
122 file = cache + strlen("FILE:");
123 extra->cache_path = bstrdup(file);
124 check_cache(file, config, extra);
125 }
126
127
128 int
main(void)129 main(void)
130 {
131 struct script_config config;
132 struct kerberos_config *krbconf;
133 char *k5login;
134 struct extra extra;
135 struct passwd pwd;
136 FILE *file;
137
138 /* Load the Kerberos principal and password from a file. */
139 krbconf = kerberos_setup(TAP_KRB_NEEDS_PASSWORD);
140 memset(&config, 0, sizeof(config));
141 config.user = krbconf->username;
142 extra.realm = krbconf->realm;
143 extra.cache_path = NULL;
144 config.authtok = krbconf->password;
145 config.extra[0] = krbconf->userprinc;
146
147 /* Generate a testing krb5.conf file. */
148 kerberos_generate_conf(krbconf->realm);
149
150 /* Create a fake passwd struct for our user. */
151 memset(&pwd, 0, sizeof(pwd));
152 pwd.pw_name = krbconf->username;
153 pwd.pw_uid = getuid();
154 pwd.pw_gid = getgid();
155 basprintf(&pwd.pw_dir, "%s/tmp", getenv("BUILD"));
156 pam_set_pwd(&pwd);
157
158 plan_lazy();
159
160 /* Basic test. */
161 run_script("data/scripts/cache/basic", &config);
162
163 /* Check the cache status before the session is closed. */
164 config.callback = check_cache_callback;
165 config.data = &extra;
166 run_script("data/scripts/cache/open-session", &config);
167 free(extra.cache_path);
168 extra.cache_path = NULL;
169
170 /*
171 * Try again but passing PAM_DATA_SILENT to pam_end. This should leave
172 * the ticket cache intact.
173 */
174 run_script("data/scripts/cache/end-data-silent", &config);
175 check_cache(extra.cache_path, &config, &extra);
176 if (unlink(extra.cache_path) < 0)
177 sysdiag("unable to unlink temporary cache %s", extra.cache_path);
178 free(extra.cache_path);
179 extra.cache_path = NULL;
180
181 /* Change the authenticating user and test search_k5login. */
182 pwd.pw_name = (char *) "testuser";
183 pam_set_pwd(&pwd);
184 config.user = "testuser";
185 basprintf(&k5login, "%s/.k5login", pwd.pw_dir);
186 file = fopen(k5login, "w");
187 if (file == NULL)
188 sysbail("cannot create %s", k5login);
189 if (fprintf(file, "%s\n", krbconf->userprinc) < 0)
190 sysbail("cannot write to %s", k5login);
191 if (fclose(file) < 0)
192 sysbail("cannot flush %s", k5login);
193 run_script("data/scripts/cache/search-k5login", &config);
194 free(extra.cache_path);
195 extra.cache_path = NULL;
196 config.callback = NULL;
197 run_script("data/scripts/cache/search-k5login-debug", &config);
198 unlink(k5login);
199 free(k5login);
200
201 /* Test search_k5login when no .k5login file exists. */
202 pwd.pw_name = krbconf->username;
203 pam_set_pwd(&pwd);
204 config.user = krbconf->username;
205 diag("testing search_k5login with no .k5login file");
206 run_script("data/scripts/cache/search-k5login", &config);
207
208 free(pwd.pw_dir);
209 return 0;
210 }
211