1 /*
2  * Test suite for libwebauth Kerberos credential import.
3  *
4  * Tests importing pre-generated credentials that are part of the package test
5  * data, which means that it can run without a Kerberos configuration.
6  *
7  * Written by Russ Allbery <eagle@eyrie.org>
8  * Copyright 2012, 2013
9  *     The Board of Trustees of the Leland Stanford Junior University
10  *
11  * See LICENSE for licensing terms.
12  */
13 
14 #include <config.h>
15 #include <portable/krb5.h>
16 #include <portable/system.h>
17 
18 #include <tests/tap/basic.h>
19 #include <tests/tap/string.h>
20 #include <webauth/basic.h>
21 #include <webauth/krb5.h>
22 
23 /* MIT doesn't provide this typedef. */
24 #ifdef HAVE_KRB5_MIT
25 typedef krb5_address **krb5_addresses;
26 #endif
27 
28 /*
29  * How to reference addresses, whether addresses are present, and how to free
30  * them.  This has to be done differently in MIT and Heimdal since the data
31  * structures they use for addresses are much different.
32  */
33 #ifdef HAVE_KRB5_MIT
34 # define ADDRESSES(data)        (data->addresses)
35 # define HAS_ADDRESSES(data)    (data->addresses != NULL)
36 # define FREE_ADDRESSES(data)                                   \
37     do {                                                        \
38         if (data->addresses != NULL)                            \
39             krb5_free_addresses(data->ctx, data->addresses);    \
40     } while (0)
41 #else
42 # define ADDRESSES(data)        (&data->addresses)
43 # define HAS_ADDRESSES(data)    (data->addresses.len > 0)
44 # define FREE_ADDRESSES(data)                                   \
45     do {                                                        \
46         krb5_free_addresses(data->ctx, &data->addresses);       \
47     } while (0)
48 #endif
49 
50 /*
51  * Three addresses that we use for testing.  The first IPv4 address is the one
52  * in tests/data/creds/service, the second is in tests/data/creds/addresses,
53  * and the third is the IPv6 address in tests/data/creds/addresses.
54  */
55 static const unsigned char test_addr1_data[4] = { 171, 67, 24, 175 };
56 static const unsigned char test_addr2_data[4] = { 171, 67, 225, 134 };
57 static const unsigned char test_addr3_data[16] = {
58     0x26, 0x07, 0xf6, 0xd0, 0x00, 0x00, 0xa2, 0x00,
59     0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x65
60 };
61 
62 /* Build krb5_address structs, which are different across implementations. */
63 #ifdef HAVE_KRB5_MIT
64 static const krb5_address test_addr1 = {
65     KV5M_ADDRESS, ADDRTYPE_INET, 4, (unsigned char *) test_addr1_data
66 };
67 static const krb5_address test_addr2 = {
68     KV5M_ADDRESS, ADDRTYPE_INET, 4, (unsigned char *) test_addr2_data
69 };
70 static const krb5_address test_addr3 = {
71     KV5M_ADDRESS, ADDRTYPE_INET6, 16, (unsigned char *) test_addr3_data
72 };
73 #else
74 static const krb5_address test_addr1 = {
75     KRB5_ADDRESS_INET, { 4, (void *) test_addr1_data }
76 };
77 static const krb5_address test_addr2 = {
78     KRB5_ADDRESS_INET, { 4, (void *) test_addr2_data }
79 };
80 static const krb5_address test_addr3 = {
81     KRB5_ADDRESS_INET6, { 16, (void *) test_addr3_data }
82 };
83 #endif
84 
85 /*
86  * Holds Kerberos credential data that we can use in the test.  We
87  * intentionally don't extract difficult things like the actual ticket.  This
88  * is a separate struct so that we can write two different functions to
89  * extract the data from either MIT or Heimdal into a consistent structure.
90  */
91 struct cred_data {
92     krb5_context ctx;
93     char *client;
94     char *server;
95     time_t endtime;
96     time_t renew_till;
97     bool forwardable;
98     krb5_enctype enctype;
99     krb5_addresses addresses;
100 };
101 
102 #define CHECK(ctx, s, m) check_status((ctx), (s), (m), __FILE__, __LINE__)
103 #define CHECK_BAIL(ctx, s) bail_status((ctx), (s), __FILE__, __LINE__)
104 
105 
106 /*
107  * Check the status of a WebAuth call.
108  */
109 static void
check_status(struct webauth_context * ctx,int s,const char * message,const char * file,unsigned long line)110 check_status(struct webauth_context *ctx, int s, const char *message,
111              const char *file, unsigned long line)
112 {
113     if (s != WA_ERR_NONE)
114         diag("webauth call failed at %s line %lu: %s (%d)\n", file, line,
115              webauth_error_message(ctx, s), s);
116     is_int(s, WA_ERR_NONE, "%s", message);
117 }
118 
119 
120 /*
121  * The same, but call bail if the WebAuth call fails.
122  */
123 static void
bail_status(struct webauth_context * ctx,int s,const char * file,unsigned long line)124 bail_status(struct webauth_context *ctx, int s, const char *file,
125             unsigned long line)
126 {
127     if (s != WA_ERR_NONE)
128         bail("webauth call failed at %s line %lu: %s (%d)\n", file, line,
129              webauth_error_message(ctx, s), s);
130 }
131 
132 
133 /*
134  * Extract implementation-specific data from a credential structure.  There
135  * are some parts of the credential structure that are named differently based
136  * on whether the implementation is MIT or Heimdal.
137  */
138 #if defined(HAVE_KRB5_MIT)
139 
140 static void
extract_cred_data_impl(krb5_context ctx,krb5_creds * cred,struct cred_data * data)141 extract_cred_data_impl(krb5_context ctx, krb5_creds *cred,
142                        struct cred_data *data)
143 {
144     data->forwardable = (cred->ticket_flags & TKT_FLG_FORWARDABLE) != 0;
145     data->enctype = cred->keyblock.enctype;
146     if (cred->addresses != NULL && cred->addresses[0] != NULL)
147         krb5_copy_addresses(ctx, cred->addresses, &data->addresses);
148 }
149 
150 #else
151 
152 static void
extract_cred_data_impl(krb5_context ctx,krb5_creds * cred,struct cred_data * data)153 extract_cred_data_impl(krb5_context ctx, krb5_creds *cred,
154                        struct cred_data *data)
155 {
156     data->forwardable = (cred->flags.i & KDC_OPT_FORWARDABLE) != 0;
157     data->enctype = cred->session.keytype;
158     if (cred->addresses.len > 0)
159         krb5_copy_addresses(ctx, &cred->addresses, &data->addresses);
160 }
161 
162 #endif
163 
164 
165 /*
166  * Given a krb5_creds structure, extract data from it into a newly-allocated
167  * cred_data structure and return the new structure.
168  */
169 static struct cred_data *
extract_cred_data(krb5_context ctx,krb5_creds * cred)170 extract_cred_data(krb5_context ctx, krb5_creds *cred)
171 {
172     struct cred_data *data;
173     char *principal;
174 
175     data = bcalloc(1, sizeof(struct cred_data));
176     data->ctx = ctx;
177     if (krb5_unparse_name(ctx, cred->client, &principal) == 0) {
178         data->client = bstrdup(principal);
179         krb5_free_unparsed_name(ctx, principal);
180     }
181     if (krb5_unparse_name(ctx, cred->server, &principal) == 0) {
182         data->server = bstrdup(principal);
183         krb5_free_unparsed_name(ctx, principal);
184     }
185     data->endtime = cred->times.endtime;
186     data->renew_till = cred->times.renew_till;
187     extract_cred_data_impl(ctx, cred, data);
188     return data;
189 }
190 
191 
192 /*
193  * Free a cred_data structure.
194  */
195 static void
free_cred_data(struct cred_data * data)196 free_cred_data(struct cred_data *data)
197 {
198     free(data->server);
199     free(data->client);
200     FREE_ADDRESSES(data);
201     krb5_free_context(data->ctx);
202     free(data);
203 }
204 
205 
206 /*
207  * Given the path to a test credential and the credential included in it,
208  * create a new WebAuth Kerberos context and initialize it from that test
209  * credential.  Then, find the credential in the ticket cache and read the
210  * Kerberos credential from it.  Returns the new WebAuth Kerberos context.
211  */
212 static struct cred_data *
import_cred(struct webauth_context * ctx,const char * file)213 import_cred(struct webauth_context *ctx, const char *file)
214 {
215     char *path, *tmpdir, *cache, *message;
216     FILE *input;
217     char buffer[BUFSIZ];
218     size_t size;
219     int s;
220     struct webauth_krb5 *kc;
221     krb5_context krb5_ctx;
222     krb5_ccache cc;
223     krb5_cc_cursor cursor;
224     krb5_creds cred;
225     krb5_error_code code;
226     struct cred_data *data;
227 
228     /* Read the encoded token. */
229     path = test_file_path(file);
230     if (path == NULL)
231         sysbail("cannot find %s", file);
232     input = fopen(path, "r");
233     if (input == NULL)
234         sysbail("cannot open %s", path);
235     size = fread(buffer, 1, sizeof(buffer), input);
236     if (ferror(input))
237         sysbail("cannot read %s", path);
238     fclose(input);
239 
240     /* Import the credential and create a ticket cache. */
241     tmpdir = test_tmpdir();
242     basprintf(&cache, "%s/krb5cc_import", tmpdir);
243     s = webauth_krb5_new(ctx, &kc);
244     CHECK_BAIL(ctx, s);
245     basprintf(&message, "import %s cred", file);
246     s = webauth_krb5_import_cred(ctx, kc, buffer, size, cache);
247     CHECK(ctx, s, message);
248     free(message);
249 
250     /* Create a Kerberos context and pull the credential from the cache. */
251     memset(&cred, 0, sizeof(cred));
252     code = krb5_init_context(&krb5_ctx);
253     if (code != 0)
254         bail("cannot create Kerberos context");
255     code = krb5_cc_resolve(krb5_ctx, cache, &cc);
256     is_int(0, code, "... open Kerberos ticket cache");
257     code = krb5_cc_start_seq_get(krb5_ctx, cc, &cursor);
258     if (code != 0)
259         bail("cannot create cursor on ticket cache");
260     code = krb5_cc_next_cred(krb5_ctx, cc, &cursor, &cred);
261     is_int(0, code, "... read first ticket");
262     krb5_cc_end_seq_get(krb5_ctx, cc, &cursor);
263     krb5_cc_close(krb5_ctx, cc);
264 
265     /* Extract credential data into our own struct. */
266     data = extract_cred_data(krb5_ctx, &cred);
267     krb5_free_cred_contents(krb5_ctx, &cred);
268 
269     /* Clean up and return. */
270     test_file_path_free(path);
271     free(cache);
272     test_tmpdir_free(tmpdir);
273     return data;
274 }
275 
276 
277 int
main(void)278 main(void)
279 {
280     struct webauth_context *ctx;
281     struct cred_data *data;
282 
283     if (webauth_context_init(&ctx, NULL) != WA_ERR_NONE)
284         bail("cannot initialize WebAuth context");
285 
286     plan(63);
287 
288     /* Basic credential with nothing special. */
289     data = import_cred(ctx, "data/creds/basic");
290     is_string("thoron@heimdal.stanford.edu", data->client,
291               "... client principal");
292     is_string("krbtgt/heimdal.stanford.edu@heimdal.stanford.edu",
293               data->server, "... server principal");
294     is_int(1355447711, data->endtime, "... end time");
295     is_int(0, data->renew_till, "... not renewable");
296     ok(!data->forwardable, "... not forwardable");
297     is_int(ENCTYPE_AES256_CTS_HMAC_SHA1_96, data->enctype,
298            "... session enctype");
299     ok(!HAS_ADDRESSES(data), "... no addresses");
300     free_cred_data(data);
301 
302     /* Forwardable and renewable credential. */
303     data = import_cred(ctx, "data/creds/renewable");
304     is_string("thoron@heimdal.stanford.edu", data->client,
305               "... client principal");
306     is_string("krbtgt/heimdal.stanford.edu@heimdal.stanford.edu",
307               data->server, "... server principal");
308     is_int(1355529261, data->endtime, "... end time");
309     is_int(1356047658, data->renew_till, "... renew until time");
310     ok(data->forwardable, "... forwardable");
311     is_int(ENCTYPE_AES256_CTS_HMAC_SHA1_96, data->enctype,
312            "... session enctype");
313     ok(!HAS_ADDRESSES(data), "... no addresses");
314     free_cred_data(data);
315 
316     /* Service ticket with restricted enctypes and address. */
317     data = import_cred(ctx, "data/creds/service");
318     is_string("thoron@heimdal.stanford.edu", data->client,
319               "... client principal");
320     is_string("host/example.stanford.edu@heimdal.stanford.edu",
321               data->server, "... server principal");
322     is_int(1355529505, data->endtime, "... end time");
323     is_int(1356047903, data->renew_till, "... renew until time");
324     ok(data->forwardable, "... forwardable");
325     is_int(ENCTYPE_DES3_CBC_SHA1, data->enctype, "... session enctype");
326     ok(HAS_ADDRESSES(data), "... addresses are present");
327     ok(krb5_address_search(data->ctx, &test_addr1, ADDRESSES(data)),
328        "... found expected IPv4 address");
329     free_cred_data(data);
330 
331     /* Ticket with multiple addresses. */
332     data = import_cred(ctx, "data/creds/addresses");
333     is_string("thoron@heimdal.stanford.edu", data->client,
334               "... client principal");
335     is_string("krbtgt/heimdal.stanford.edu@heimdal.stanford.edu",
336               data->server, "... server principal");
337     is_int(1355532627, data->endtime, "... end time");
338     is_int(1356051022, data->renew_till, "... renew until time");
339     ok(data->forwardable, "... forwardable");
340     is_int(ENCTYPE_AES256_CTS_HMAC_SHA1_96, data->enctype,
341            "... session enctype");
342     ok(HAS_ADDRESSES(data), "... addresses are present");
343     ok(krb5_address_search(data->ctx, &test_addr2, ADDRESSES(data)),
344        "... found expected IPv4 address");
345     ok(krb5_address_search(data->ctx, &test_addr3, ADDRESSES(data)),
346        "... found expected IPv6 address");
347     free_cred_data(data);
348 
349     /* Active Directory ticket encoded using Heimdal libraries. */
350     data = import_cred(ctx, "data/creds/ad-heimdal");
351     is_string("thoron@NT.STANFORD.EDU", data->client,
352               "... client principal");
353     is_string("krbtgt/NT.STANFORD.EDU@NT.STANFORD.EDU",
354               data->server, "... server principal");
355     is_int(1355559675, data->endtime, "... end time");
356     is_int(1356074475, data->renew_till, "... renew until time");
357     ok(data->forwardable, "... forwardable");
358     is_int(ENCTYPE_AES256_CTS_HMAC_SHA1_96, data->enctype,
359            "... session enctype");
360     ok(!HAS_ADDRESSES(data), "... no addresses");
361     free_cred_data(data);
362 
363     /* Heimdal ticket with the old encoding (reversed flag bits). */
364     data = import_cred(ctx, "data/creds/old-heimdal");
365     is_string("thoron@heimdal.stanford.edu", data->client,
366               "... client principal");
367     is_string("krbtgt/heimdal.stanford.edu@heimdal.stanford.edu",
368               data->server, "... server principal");
369     is_int(1355556533, data->endtime, "... end time");
370     is_int(1356074930, data->renew_till, "... renew until time");
371     ok(data->forwardable, "... forwardable");
372     is_int(ENCTYPE_AES256_CTS_HMAC_SHA1_96, data->enctype,
373            "... session enctype");
374     ok(!HAS_ADDRESSES(data), "... no addresses");
375     free_cred_data(data);
376 
377     /* Clean up. */
378     webauth_context_free(ctx);
379     return 0;
380 }
381