base: Fix code spelling
[heimdal.git] / kdc / test_csr_authorizer.c
blob200af16c16daa03979a23ae8e1c69f6b7bbcd695
1 /*
2 * Copyright (c) 2022 Kungliga Tekniska Högskolan
3 * (Royal Institute of Technology, Stockholm, Sweden).
4 * All rights reserved.
6 * Redistribution and use in source and binary forms, with or without
7 * modification, are permitted provided that the following conditions
8 * are met:
10 * 1. Redistributions of source code must retain the above copyright
11 * notice, this list of conditions and the following disclaimer.
13 * 2. Redistributions in binary form must reproduce the above copyright
14 * notice, this list of conditions and the following disclaimer in the
15 * documentation and/or other materials provided with the distribution.
17 * 3. Neither the name of the Institute nor the names of its contributors
18 * may be used to endorse or promote products derived from this software
19 * without specific prior written permission.
21 * THIS SOFTWARE IS PROVIDED BY THE INSTITUTE AND CONTRIBUTORS ``AS IS'' AND
22 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
23 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
24 * ARE DISCLAIMED. IN NO EVENT SHALL THE INSTITUTE OR CONTRIBUTORS BE LIABLE
25 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
26 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
27 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
28 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
29 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
30 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
31 * SUCH DAMAGE.
33 #include "kdc_locl.h"
34 #include <heim-ipc.h>
37 * This program implements two things:
39 * - a utility for testing the `kdc_authorize_csr()' function and the plugins
40 * that uses,
42 * and
44 * - a server for the IPC authorizer.
46 * For the latter, requested certificate SANs and EKUs are authorized by
47 * checking for existence of files of the form:
49 * /<path>/<princ>/<ext>-<value>
51 * where <path> is given as an option.
53 * <princ> is a requesting client principal name with all characters other than
54 * alphanumeric, '-', '_', and non-leading '.' URL-encoded.
56 * <ext> is one of:
58 * - pkinit (SAN)
59 * - xmpp (SAN)
60 * - email (SAN)
61 * - ms-upn (SAN)
62 * - dnsname (SAN)
63 * - eku (EKU OID)
65 * and <value> is a display form of the SAN or EKU OID, with SANs URL-encoded
66 * just like principal names (see above).
68 * OIDs are of the form "1.2.3.4.5".
70 * Only digitalSignature and nonRepudiation key usage values are permitted.
73 static int help_flag;
74 static int version_flag;
75 static int daemon_flag;
76 static int daemon_child_flag = -1;
77 static int ignore_flag = 0;
78 static int server_flag = 0;
79 static const char *app_string = "kdc";
80 static const char *socket_dir;
81 static const char *authz_dir;
83 struct getargs args[] = {
84 { "help", 'h', arg_flag, &help_flag,
85 "Print usage message", NULL },
86 { "version", 'v', arg_flag, &version_flag,
87 "Print version", NULL },
88 { "app", 'a', arg_string, &app_string,
89 "App to test (kdc or bx509); default: kdc", "APPNAME" },
90 { "socket-dir", 'S', arg_string, &socket_dir,
91 "IPC socket directory", "DIR" },
92 { "authorization-dir", 'A', arg_string, &authz_dir,
93 "authorization directory", "DIR" },
94 { "server", '\0', arg_flag, &server_flag,
95 "Server mode", NULL },
96 { "ignore", 'I', arg_flag, &ignore_flag,
97 "ignore requests", NULL },
98 { "daemon", 'd', arg_flag, &daemon_flag,
99 "daemonize", NULL },
100 { "daemon-child", '\0', arg_flag, &daemon_child_flag,
101 "internal-use-only option", NULL },
103 size_t num_args = sizeof(args) / sizeof(args[0]);
105 static int
106 usage(int e)
108 arg_printusage(args, num_args, NULL, "PATH-TO-DER-CSR PRINCIPAL");
109 fprintf(stderr,
110 "\tExercise CSR authorization plugins for a given CSR for a\n"
111 "\tgiven principal.\n\n"
112 "\tServer-mode (--server) looks for files in the \n"
113 "\t--authorization-dir DIR directory named:\n"
114 "\n"
115 "\t\teku=OID\n"
116 "\t\tsan_pkinit=PRINCIPAL\n"
117 "\t\tsan_ms_upn=PRINCIPAL\n"
118 "\t\tsan_dnsname=DOMAINNAME\n"
119 "\t\tsan_xmpp=JABBER-ID\n"
120 "\t\tsan_email=EMAIL\n"
121 "\n"
122 "\tClient-mode positional arguments are:\n\n"
123 "\t\tPATH-TO-DER-CSR PRETEND-CLIENT-PRINCIPAL [...]\n\n"
124 "\twhere {...} are requested features that must be granted\n"
125 "\tif the request is only partially authorized.\n\n"
126 "\tClient example:\n\t\t%s PKCS10:/tmp/csr.der foo@TEST.H5L.SE\n",
127 getprogname());
128 exit(e);
129 return e;
132 static const char *sysplugin_dirs[] = {
133 #ifdef _WIN32
134 "$ORIGIN",
135 #else
136 "$ORIGIN/../lib/plugin/kdc",
137 #endif
138 #ifdef __APPLE__
139 LIBDIR "/plugin/kdc",
140 #endif
141 NULL
144 static void
145 load_plugins(krb5_context context)
147 const char * const *dirs = sysplugin_dirs;
148 #ifndef _WIN32
149 char **cfdirs;
151 cfdirs = krb5_config_get_strings(context, NULL, "kdc", "plugin_dir", NULL);
152 if (cfdirs)
153 dirs = (const char * const *)cfdirs;
154 #endif
156 _krb5_load_plugins(context, "kdc", (const char **)dirs);
158 #ifndef _WIN32
159 krb5_config_free_strings(cfdirs);
160 #endif
163 static char *string_encode(const char *);
164 static int stat_authz(const char *, const char *);
166 static krb5_error_code
167 authorize(const char *subject, const char *thing)
169 krb5_error_code ret;
170 char *s = NULL;
172 s = string_encode(subject);
173 if (s == NULL)
174 return ENOMEM;
176 ret = stat_authz(s, thing);
177 if (ret == ENOENT)
178 ret = stat_authz(s, "all");
179 if (ret == ENOENT)
180 ret = EACCES;
181 free(s);
182 return ret;
185 static void
186 service(void *ctx,
187 const heim_octet_string *req,
188 const heim_icred cred,
189 heim_ipc_complete complete_cb,
190 heim_sipc_call complete_cb_data)
192 krb5_error_code ret = 0;
193 struct rk_strpool *result = NULL;
194 krb5_data rep;
195 const char *subject;
196 char *cmd;
197 char *next = NULL;
198 char *res = NULL;
199 char *tok;
200 char *s;
201 int none_granted = 1;
202 int all_granted = 1;
203 int first = 1;
206 * A krb5_context and log facility for logging would be nice, but this is
207 * all just for testing.
210 (void)ctx;
212 cmd = strndup(req->data, req->length);
213 if (cmd == NULL)
214 errx(1, "Out of memory");
216 if (strncmp(cmd, "check ", sizeof("check ") - 1) != 0) {
217 rep.data = "Invalid request command (must be \"check ...\")";
218 rep.length = sizeof("Invalid request command (must be \"check ...\")") - 1;
219 (*complete_cb)(complete_cb_data, EINVAL, &rep);
220 free(cmd);
221 return;
224 s = cmd + sizeof("check ") - 1;
225 subject = strtok_r(s, " ", &next);
226 s = NULL;
228 while ((tok = strtok_r(s, " ", &next))) {
229 int ret2;
231 ret2 = authorize(subject, tok);
232 result = rk_strpoolprintf(result, "%s%s:%s",
233 first ? "" : ",",
234 tok,
235 ret2 == 0 ? "granted" : "denied");
236 if (ret2 == 0)
237 none_granted = 0;
238 else
239 all_granted = 0;
241 if (ret2 != 0 && ret == 0)
242 ret = ret2;
244 first = 0;
246 free(cmd);
248 if (ret == 0 && all_granted) {
249 rk_strpoolfree(result);
251 rep.data = "granted";
252 rep.length = sizeof("granted") - 1;
253 (*complete_cb)(complete_cb_data, 0, &rep);
254 return;
257 if (none_granted && ignore_flag) {
258 rk_strpoolfree(result);
260 rep.data = "ignore";
261 rep.length = sizeof("ignore") - 1;
262 (*complete_cb)(complete_cb_data, KRB5_PLUGIN_NO_HANDLE, &rep);
263 return;
266 s = rk_strpoolcollect(result); /* frees `result' */
267 if (s == NULL) {
268 rep.data = "denied out-of-memory";
269 rep.length = sizeof("denied out-of-memory") - 1;
270 (*complete_cb)(complete_cb_data, KRB5_PLUGIN_NO_HANDLE, &rep);
271 return;
274 if (asprintf(&res, "denied %s", s) == -1)
275 errx(1, "Out of memory");
276 if (res == NULL)
277 errx(1, "Out of memory");
279 rep.data = res;
280 rep.length = strlen(res);
282 (*complete_cb)(complete_cb_data, ret, &rep);
283 free(res);
284 free(s);
287 static char *
288 make_feature_argument(const char *kind,
289 hx509_san_type san_type,
290 const char *value)
292 const char *san_type_str = NULL;
293 char *s = NULL;
295 if (strcmp(kind, "san") != 0) {
296 if (asprintf(&s, "%s=%s", kind, value) == -1 || s == NULL)
297 errx(1, "Out of memory");
298 return s;
301 switch (san_type) {
302 case HX509_SAN_TYPE_EMAIL:
303 san_type_str = "email";
304 break;
305 case HX509_SAN_TYPE_DNSNAME:
306 san_type_str = "dnsname";
307 break;
308 case HX509_SAN_TYPE_DN:
309 san_type_str = "dn";
310 break;
311 case HX509_SAN_TYPE_REGISTERED_ID:
312 san_type_str = "registered_id";
313 break;
314 case HX509_SAN_TYPE_XMPP:
315 san_type_str = "xmpp";
316 break;
317 case HX509_SAN_TYPE_PKINIT:
318 case HX509_SAN_TYPE_MS_UPN:
319 san_type_str = "pkinit";
320 break;
321 case HX509_SAN_TYPE_DNSSRV:
322 san_type_str = "dnssrv";
323 break;
324 default:
325 warnx("SAN type not supported");
326 return "";
329 if (asprintf(&s, "san_%s=%s", san_type_str, value) == -1 || s == NULL)
330 errx(1, "Out of memory");
331 return s;
335 main(int argc, char **argv)
337 krb5_log_facility *logf;
338 krb5_error_code ret;
339 krb5_context context;
340 hx509_request csr;
341 krb5_principal princ = NULL;
342 const char *argv0 = argv[0];
343 int optidx = 0;
345 setprogname(argv[0]);
346 if (getarg(args, num_args, argc, argv, &optidx))
347 return usage(1);
348 if (help_flag)
349 return usage(0);
350 if (version_flag) {
351 print_version(argv[0]);
352 return 0;
355 if ((errno = krb5_init_context(&context)))
356 err(1, "Could not initialize krb5_context");
357 if ((ret = krb5_initlog(context, argv0, &logf)) ||
358 (ret = krb5_addlog_dest(context, logf, "0-5/STDERR")))
359 krb5_err(context, 1, ret, "Could not set up logging to stderr");
360 load_plugins(context);
362 if (server_flag && daemon_flag)
363 daemon_child_flag = roken_detach_prep(argc, argv, "--daemon-child");
365 argc -= optidx;
366 argv += optidx;
368 if (socket_dir)
369 setenv("HEIM_IPC_DIR", socket_dir, 1);
371 if (server_flag) {
372 const char *svc;
373 heim_sipc un;
375 rk_pidfile(NULL);
377 svc = krb5_config_get_string(context, NULL,
378 app_string ? app_string : "kdc",
379 "ipc_csr_authorizer", "service", NULL);
380 if (svc == NULL)
381 svc = "org.h5l.csr_authorizer";
383 /* `service' is our request handler; `argv' is its callback data */
384 ret = heim_sipc_service_unix(svc, service, NULL, &un);
385 if (ret)
386 krb5_err(context, 1, ret,
387 "Could not setup service on Unix domain socket "
388 "%s/.heim_%s-socket", socket_dir, svc);
390 roken_detach_finish(NULL, daemon_child_flag);
392 /* Enter the IPC event loop */
393 heim_ipc_main();
394 return 0;
397 /* Client mode */
398 if (argc < 2)
399 usage(1);
401 /* Parse the given CSR */
402 if ((ret = hx509_request_parse(context->hx509ctx, argv[0], &csr)))
403 krb5_err(context, 1, ret, "Could not parse PKCS#10 CSR from %s", argv[0]);
406 * Parse the client principal that we'll pretend is an authenticated client
407 * principal.
409 if ((ret = krb5_parse_name(context, argv[1], &princ)))
410 krb5_err(context, 1, ret, "Could not parse principal %s", argv[1]);
412 /* Call the authorizer */
413 ret = kdc_authorize_csr(context, app_string, csr, princ);
415 if (ret) {
416 unsigned n = hx509_request_count_unauthorized(csr);
417 size_t i, k;
418 int ret2 = 0;
419 int good = -1;
422 * Check partial approval of SANs.
424 * Iterate over the SANs in the request, and for each check if a) it
425 * was granted, b) it's on the remainder of our argv[].
427 for (i = 0; ret2 == 0; i++) {
428 hx509_san_type san_type;
429 char *feature = NULL;
430 char *san = NULL;
431 int granted;
433 ret2 = hx509_request_get_san(csr, i, &san_type, &san);
434 if (ret2)
435 break;
437 feature = make_feature_argument("san", san_type, san);
439 granted = hx509_request_san_authorized_p(csr, i);
440 for (k = 2; k < argc; k++) {
441 if (strcmp(feature, argv[k]) != 0)
442 continue;
444 /* The SAN is on our command line */
445 if (granted && good == -1)
446 good = 1;
447 else if (!granted)
448 good = 0;
449 break;
452 hx509_xfree(san);
455 /* Check partial approval of EKUs */
456 for (i = 0; ret2 == 0; i++) {
457 char *feature = NULL;
458 char *eku = NULL;
459 int granted;
461 ret2 = hx509_request_get_eku(csr, i, &eku);
462 if (ret2)
463 break;
465 feature = make_feature_argument("eku", 0, eku);
467 granted = hx509_request_eku_authorized_p(csr, i);
468 for (k = 2; k < argc; k++) {
469 if (strcmp(feature, argv[k]) != 0)
470 continue;
472 /* The SAN is on our command line */
473 if (granted && good == -1)
474 good = 1;
475 else if (!granted)
476 good = 0;
477 break;
480 hx509_xfree(eku);
483 if (good != 1) {
484 krb5_free_principal(context, princ);
485 _krb5_unload_plugins(context, "kdc");
486 hx509_request_free(&csr);
487 krb5_err(context, 1, ret,
488 "Authorization failed with %u rejected features", n);
492 printf("Authorized!\n");
493 krb5_free_principal(context, princ);
494 _krb5_unload_plugins(context, "kdc");
495 krb5_free_context(context);
496 hx509_request_free(&csr);
497 return 0;
501 * string_encode_sz() and string_encode() encode a string to be safe for use as
502 * a file name. They function very much like URL encoders, but '~' also gets
503 * encoded, and '@', '-', '_', and non-leading '.' do not.
505 * A corresponding decoder is not needed.
507 static size_t
508 string_encode_sz(const char *in)
510 size_t sz = strlen(in);
511 int first = 1;
513 while (*in) {
514 char c = *(in++);
516 switch (c) {
517 case '@':
518 case '-':
519 case '_':
520 break;
521 case '.':
522 if (first)
523 sz += 2;
524 break;
525 default:
526 if (!isalnum((unsigned char)c))
527 sz += 2;
529 first = 0;
531 return sz;
534 static char *
535 string_encode(const char *in)
537 size_t len = strlen(in);
538 size_t sz = string_encode_sz(in);
539 size_t i, k;
540 char *s;
541 int first = 1;
543 if ((s = malloc(sz + 1)) == NULL)
544 return NULL;
545 s[sz] = '\0';
547 for (i = k = 0; i < len; i++, first = 0) {
548 unsigned char c = ((const unsigned char *)in)[i];
550 switch (c) {
551 case '@':
552 case '-':
553 case '_':
554 s[k++] = c;
555 break;
556 case '.':
557 if (first) {
558 s[k++] = '%';
559 s[k++] = "0123456789abcdef"[(c&0xff)>>4];
560 s[k++] = "0123456789abcdef"[(c&0x0f)];
561 } else {
562 s[k++] = c;
564 break;
565 default:
566 if (isalnum(c)) {
567 s[k++] = c;
568 } else {
569 s[k++] = '%';
570 s[k++] = "0123456789abcdef"[(c&0xff)>>4];
571 s[k++] = "0123456789abcdef"[(c&0x0f)];
575 return s;
578 static int
579 stat_authz(const char *subject,
580 const char *thing)
582 struct stat st;
583 char *p = NULL;
584 int ret;
586 if (authz_dir == NULL)
587 return KRB5_PLUGIN_NO_HANDLE;
588 if (thing)
589 ret = asprintf(&p, "%s/%s/%s", authz_dir, subject, thing);
590 else
591 ret = asprintf(&p, "%s/%s", authz_dir, subject);
592 if (ret == -1 || p == NULL)
593 return ENOMEM;
594 ret = stat(p, &st);
595 free(p);
596 return ret == 0 ? 0 : errno;