2 * Copyright (c) 2022 Kungliga Tekniska Högskolan
3 * (Royal Institute of Technology, Stockholm, Sweden).
6 * Redistribution and use in source and binary forms, with or without
7 * modification, are permitted provided that the following conditions
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
37 * This program implements two things:
39 * - a utility for testing the `kdc_authorize_csr()' function and the plugins
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.
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.
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
,
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]);
108 arg_printusage(args
, num_args
, NULL
, "PATH-TO-DER-CSR PRINCIPAL");
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"
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"
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",
132 static const char *sysplugin_dirs
[] = {
136 "$ORIGIN/../lib/plugin/kdc",
139 LIBDIR
"/plugin/kdc",
145 load_plugins(krb5_context context
)
147 const char * const *dirs
= sysplugin_dirs
;
151 cfdirs
= krb5_config_get_strings(context
, NULL
, "kdc", "plugin_dir", NULL
);
153 dirs
= (const char * const *)cfdirs
;
156 _krb5_load_plugins(context
, "kdc", (const char **)dirs
);
159 krb5_config_free_strings(cfdirs
);
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
)
172 s
= string_encode(subject
);
176 ret
= stat_authz(s
, thing
);
178 ret
= stat_authz(s
, "all");
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
;
201 int none_granted
= 1;
206 * A krb5_context and log facility for logging would be nice, but this is
207 * all just for testing.
212 cmd
= strndup(req
->data
, req
->length
);
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
);
224 s
= cmd
+ sizeof("check ") - 1;
225 subject
= strtok_r(s
, " ", &next
);
228 while ((tok
= strtok_r(s
, " ", &next
))) {
231 ret2
= authorize(subject
, tok
);
232 result
= rk_strpoolprintf(result
, "%s%s:%s",
235 ret2
== 0 ? "granted" : "denied");
241 if (ret2
!= 0 && ret
== 0)
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
);
257 if (none_granted
&& ignore_flag
) {
258 rk_strpoolfree(result
);
261 rep
.length
= sizeof("ignore") - 1;
262 (*complete_cb
)(complete_cb_data
, KRB5_PLUGIN_NO_HANDLE
, &rep
);
266 s
= rk_strpoolcollect(result
); /* frees `result' */
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
);
274 if (asprintf(&res
, "denied %s", s
) == -1)
275 errx(1, "Out of memory");
277 errx(1, "Out of memory");
280 rep
.length
= strlen(res
);
282 (*complete_cb
)(complete_cb_data
, ret
, &rep
);
288 make_feature_argument(const char *kind
,
289 hx509_san_type san_type
,
292 const char *san_type_str
= NULL
;
295 if (strcmp(kind
, "san") != 0) {
296 if (asprintf(&s
, "%s=%s", kind
, value
) == -1 || s
== NULL
)
297 errx(1, "Out of memory");
302 case HX509_SAN_TYPE_EMAIL
:
303 san_type_str
= "email";
305 case HX509_SAN_TYPE_DNSNAME
:
306 san_type_str
= "dnsname";
308 case HX509_SAN_TYPE_DN
:
311 case HX509_SAN_TYPE_REGISTERED_ID
:
312 san_type_str
= "registered_id";
314 case HX509_SAN_TYPE_XMPP
:
315 san_type_str
= "xmpp";
317 case HX509_SAN_TYPE_PKINIT
:
318 case HX509_SAN_TYPE_MS_UPN
:
319 san_type_str
= "pkinit";
321 case HX509_SAN_TYPE_DNSSRV
:
322 san_type_str
= "dnssrv";
325 warnx("SAN type not supported");
329 if (asprintf(&s
, "san_%s=%s", san_type_str
, value
) == -1 || s
== NULL
)
330 errx(1, "Out of memory");
335 main(int argc
, char **argv
)
337 krb5_log_facility
*logf
;
339 krb5_context context
;
341 krb5_principal princ
= NULL
;
342 const char *argv0
= argv
[0];
345 setprogname(argv
[0]);
346 if (getarg(args
, num_args
, argc
, argv
, &optidx
))
351 print_version(argv
[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");
369 setenv("HEIM_IPC_DIR", socket_dir
, 1);
377 svc
= krb5_config_get_string(context
, NULL
,
378 app_string
? app_string
: "kdc",
379 "ipc_csr_authorizer", "service", 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
);
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 */
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
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
);
416 unsigned n
= hx509_request_count_unauthorized(csr
);
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
;
433 ret2
= hx509_request_get_san(csr
, i
, &san_type
, &san
);
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)
444 /* The SAN is on our command line */
445 if (granted
&& good
== -1)
455 /* Check partial approval of EKUs */
456 for (i
= 0; ret2
== 0; i
++) {
457 char *feature
= NULL
;
461 ret2
= hx509_request_get_eku(csr
, i
, &eku
);
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)
472 /* The SAN is on our command line */
473 if (granted
&& 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
);
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.
508 string_encode_sz(const char *in
)
510 size_t sz
= strlen(in
);
526 if (!isalnum((unsigned char)c
))
535 string_encode(const char *in
)
537 size_t len
= strlen(in
);
538 size_t sz
= string_encode_sz(in
);
543 if ((s
= malloc(sz
+ 1)) == NULL
)
547 for (i
= k
= 0; i
< len
; i
++, first
= 0) {
548 unsigned char c
= ((const unsigned char *)in
)[i
];
559 s
[k
++] = "0123456789abcdef"[(c
&0xff)>>4];
560 s
[k
++] = "0123456789abcdef"[(c
&0x0f)];
570 s
[k
++] = "0123456789abcdef"[(c
&0xff)>>4];
571 s
[k
++] = "0123456789abcdef"[(c
&0x0f)];
579 stat_authz(const char *subject
,
586 if (authz_dir
== NULL
)
587 return KRB5_PLUGIN_NO_HANDLE
;
589 ret
= asprintf(&p
, "%s/%s/%s", authz_dir
, subject
, thing
);
591 ret
= asprintf(&p
, "%s/%s", authz_dir
, subject
);
592 if (ret
== -1 || p
== NULL
)
596 return ret
== 0 ? 0 : errno
;