libtommath: Fix possible integer overflow CVE-2023-36328
[heimdal.git] / kdc / bx509d.c
blob793012baf4a1c5b0aaa9fda1966e8ed8378466b0
1 /*
2 * Copyright (c) 2019 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.
35 * This file implements a RESTful HTTPS API to an online CA, as well as an
36 * HTTP/Negotiate token issuer, as well as a way to get TGTs.
38 * Users are authenticated with Negotiate and/or Bearer.
40 * This is essentially a RESTful online CA sharing some code with the KDC's
41 * kx509 online CA, and also a proxy for PKINIT and GSS-API (Negotiate).
43 * See the manual page for HTTP API details.
45 * TBD:
46 * - rewrite to not use libmicrohttpd but an alternative more appropriate to
47 * Heimdal's license (though libmicrohttpd will do)
48 * - there should be an end-point for fetching an issuer's chain
50 * NOTES:
51 * - We use krb5_error_code values as much as possible. Where we need to use
52 * MHD_NO because we got that from an mhd function and cannot respond with
53 * an HTTP response, we use (krb5_error_code)-1, and later map that to
54 * MHD_NO.
56 * (MHD_NO is an ENOMEM-cannot-even-make-a-static-503-response level event.)
60 * Theory of operation:
62 * - We use libmicrohttpd (MHD) for the HTTP(S) implementation.
64 * - MHD has an online request processing model:
66 * - all requests are handled via the `dh' and `dh_cls' closure arguments
67 * of `MHD_start_daemon()'; ours is called `route()'
69 * - `dh' is called N+1 times:
70 * - once to allocate a request context
71 * - once for every N chunks of request body
72 * - once to process the request and produce a response
74 * - the response cannot begin to be produced before consuming the whole
75 * request body (for requests that have a body)
76 * (this seems like a bug in MHD)
78 * - the response body can be produced over multiple calls (i.e., in an
79 * online manner)
81 * - Our `route()' processes any POST request body form data / multipart by
82 * treating all the key/value pairs as if they had been additional URI query
83 * parameters.
85 * - Then `route()' calls a handler appropriate to the URI local-part with the
86 * request context, and the handler produces a response in one call.
88 * I.e., we turn the online MHD request processing into not-online. Our
89 * handlers are presented with complete requests and must produce complete
90 * responses in one call.
92 * - `route()' also does any authentication and CSRF protection so that the
93 * request handlers don't have to.
95 * This non-online request handling approach works for most everything we want
96 * to do. However, for /get-tgts with very large numbers of principals, we
97 * might have to revisit this, using MHD_create_response_from_callback() or
98 * MHD_create_response_from_pipe() (and a thread to do the actual work of
99 * producing the body) instead of MHD_create_response_from_buffer().
102 #define _XOPEN_SOURCE_EXTENDED 1
103 #define _DEFAULT_SOURCE 1
104 #define _BSD_SOURCE 1
105 #define _GNU_SOURCE 1
107 #include <sys/socket.h>
108 #include <sys/types.h>
109 #include <sys/stat.h>
110 #include <sys/time.h>
111 #include <ctype.h>
112 #include <dlfcn.h>
113 #include <errno.h>
114 #include <fcntl.h>
115 #include <pthread.h>
116 #include <signal.h>
117 #include <stdarg.h>
118 #include <stddef.h>
119 #include <stdint.h>
120 #include <stdio.h>
121 #include <stdlib.h>
122 #include <string.h>
123 #include <time.h>
124 #include <unistd.h>
125 #include <netdb.h>
126 #include <netinet/in.h>
127 #include <netinet/ip.h>
129 #include <microhttpd.h>
130 #include "kdc_locl.h"
131 #include "token_validator_plugin.h"
132 #include <getarg.h>
133 #include <roken.h>
134 #include <krb5.h>
135 #include <gssapi/gssapi.h>
136 #include <gssapi/gssapi_krb5.h>
137 #include <hx509.h>
138 #include "../lib/hx509/hx_locl.h"
139 #include <hx509-private.h>
141 #define heim_pcontext krb5_context
142 #define heim_pconfig krb5_context
143 #include <heimbase-svc.h>
145 #if MHD_VERSION < 0x00097002 || defined(MHD_YES)
146 /* libmicrohttpd changed these from int valued macros to an enum in 0.9.71 */
147 #ifdef MHD_YES
148 #undef MHD_YES
149 #undef MHD_NO
150 #endif
151 enum MHD_Result { MHD_NO = 0, MHD_YES = 1 };
152 #define MHD_YES 1
153 #define MHD_NO 0
154 typedef int heim_mhd_result;
155 #else
156 typedef enum MHD_Result heim_mhd_result;
157 #endif
159 enum k5_creds_kind { K5_CREDS_EPHEMERAL, K5_CREDS_CACHED };
162 * This is to keep track of memory we need to free, mainly because we had to
163 * duplicate data from the MHD POST form data processor.
165 struct free_tend_list {
166 void *freeme1;
167 void *freeme2;
168 struct free_tend_list *next;
171 /* Per-request context data structure */
172 typedef struct bx509_request_desc {
173 /* Common elements for Heimdal request/response services */
174 HEIM_SVC_REQUEST_DESC_COMMON_ELEMENTS;
176 struct MHD_Connection *connection;
177 struct MHD_PostProcessor *pp;
178 struct MHD_Response *response;
179 krb5_times token_times;
180 time_t req_life;
181 hx509_request req;
182 struct free_tend_list *free_list;
183 const char *for_cname;
184 const char *target;
185 const char *redir;
186 const char *method;
187 size_t post_data_size;
188 size_t san_idx; /* For /get-tgts */
189 enum k5_creds_kind cckind;
190 char *pkix_store;
191 char *tgts_filename;
192 FILE *tgts;
193 char *ccname;
194 char *freeme1;
195 char *csrf_token;
196 krb5_addresses tgt_addresses; /* For /get-tgt */
197 char frombuf[128];
198 } *bx509_request_desc;
200 static void
201 audit_trail(bx509_request_desc r, krb5_error_code ret)
203 const char *retname = NULL;
205 /* Get a symbolic name for some error codes */
206 #define CASE(x) case x : retname = #x; break
207 switch (ret) {
208 CASE(ENOMEM);
209 CASE(EACCES);
210 CASE(HDB_ERR_NOT_FOUND_HERE);
211 CASE(HDB_ERR_WRONG_REALM);
212 CASE(HDB_ERR_EXISTS);
213 CASE(HDB_ERR_KVNO_NOT_FOUND);
214 CASE(HDB_ERR_NOENTRY);
215 CASE(HDB_ERR_NO_MKEY);
216 CASE(KRB5KDC_ERR_BADOPTION);
217 CASE(KRB5KDC_ERR_CANNOT_POSTDATE);
218 CASE(KRB5KDC_ERR_CLIENT_NOTYET);
219 CASE(KRB5KDC_ERR_C_PRINCIPAL_UNKNOWN);
220 CASE(KRB5KDC_ERR_ETYPE_NOSUPP);
221 CASE(KRB5KDC_ERR_KEY_EXPIRED);
222 CASE(KRB5KDC_ERR_NAME_EXP);
223 CASE(KRB5KDC_ERR_NEVER_VALID);
224 CASE(KRB5KDC_ERR_NONE);
225 CASE(KRB5KDC_ERR_NULL_KEY);
226 CASE(KRB5KDC_ERR_PADATA_TYPE_NOSUPP);
227 CASE(KRB5KDC_ERR_POLICY);
228 CASE(KRB5KDC_ERR_PREAUTH_FAILED);
229 CASE(KRB5KDC_ERR_PREAUTH_REQUIRED);
230 CASE(KRB5KDC_ERR_SERVER_NOMATCH);
231 CASE(KRB5KDC_ERR_SERVICE_EXP);
232 CASE(KRB5KDC_ERR_SERVICE_NOTYET);
233 CASE(KRB5KDC_ERR_S_PRINCIPAL_UNKNOWN);
234 CASE(KRB5KDC_ERR_TRTYPE_NOSUPP);
235 CASE(KRB5KRB_ERR_RESPONSE_TOO_BIG);
236 /* XXX Add relevant error codes */
237 case 0:
238 retname = "SUCCESS";
239 break;
240 default:
241 retname = NULL;
242 break;
245 /* Let's save a few bytes */
246 if (retname && strncmp("KRB5KDC_", retname, sizeof("KRB5KDC_") - 1) == 0)
247 retname += sizeof("KRB5KDC_") - 1;
248 #undef PREFIX
249 heim_audit_trail((heim_svc_req_desc)r, ret, retname);
252 static krb5_log_facility *logfac;
253 static pthread_key_t k5ctx;
255 static krb5_error_code
256 get_krb5_context(krb5_context *contextp)
258 krb5_error_code ret;
260 if ((*contextp = pthread_getspecific(k5ctx)))
261 return 0;
262 if ((ret = krb5_init_context(contextp)))
263 return *contextp = NULL, ret;
264 if (logfac)
265 krb5_set_log_dest(*contextp, logfac);
266 (void) pthread_setspecific(k5ctx, *contextp);
267 return *contextp ? 0 : ENOMEM;
270 typedef enum {
271 CSRF_PROT_UNSPEC = 0,
272 CSRF_PROT_GET_WITH_HEADER = 1,
273 CSRF_PROT_GET_WITH_TOKEN = 2,
274 CSRF_PROT_POST_WITH_HEADER = 8,
275 CSRF_PROT_POST_WITH_TOKEN = 16,
276 } csrf_protection_type;
278 static csrf_protection_type csrf_prot_type = CSRF_PROT_UNSPEC;
279 static int port = -1;
280 static int allow_GET_flag = -1;
281 static int help_flag;
282 static int daemonize;
283 static int daemon_child_fd = -1;
284 static int verbose_counter;
285 static int version_flag;
286 static int reverse_proxied_flag;
287 static int thread_per_client_flag;
288 struct getarg_strings audiences;
289 static getarg_strings csrf_prot_type_strs;
290 static const char *csrf_header = "X-CSRF";
291 static const char *cert_file;
292 static const char *priv_key_file;
293 static const char *cache_dir;
294 static const char *csrf_key_file;
295 static char *impersonation_key_fn;
297 static char csrf_key[16];
299 static krb5_error_code resp(struct bx509_request_desc *, int,
300 enum MHD_ResponseMemoryMode, const char *,
301 const void *, size_t, const char *);
302 static krb5_error_code bad_req(struct bx509_request_desc *, krb5_error_code, int,
303 const char *, ...)
304 HEIMDAL_PRINTF_ATTRIBUTE((__printf__, 4, 5));
306 static krb5_error_code bad_enomem(struct bx509_request_desc *, krb5_error_code);
307 static krb5_error_code bad_400(struct bx509_request_desc *, krb5_error_code, char *);
308 static krb5_error_code bad_401(struct bx509_request_desc *, char *);
309 static krb5_error_code bad_403(struct bx509_request_desc *, krb5_error_code, char *);
310 static krb5_error_code bad_404(struct bx509_request_desc *, const char *);
311 static krb5_error_code bad_405(struct bx509_request_desc *, const char *);
312 static krb5_error_code bad_500(struct bx509_request_desc *, krb5_error_code, const char *);
313 static krb5_error_code bad_503(struct bx509_request_desc *, krb5_error_code, const char *);
314 static heim_mhd_result validate_csrf_token(struct bx509_request_desc *r);
316 static int
317 validate_token(struct bx509_request_desc *r)
319 krb5_error_code ret;
320 krb5_principal cprinc = NULL;
321 const char *token;
322 const char *host;
323 char token_type[64]; /* Plenty */
324 char *p;
325 krb5_data tok;
326 size_t host_len, brk, i;
328 memset(&r->token_times, 0, sizeof(r->token_times));
329 host = MHD_lookup_connection_value(r->connection, MHD_HEADER_KIND,
330 MHD_HTTP_HEADER_HOST);
331 if (host == NULL)
332 return bad_400(r, EINVAL, "Host header is missing");
334 /* Exclude port number here (IPv6-safe because of the below) */
335 host_len = ((p = strchr(host, ':'))) ? p - host : strlen(host);
337 token = MHD_lookup_connection_value(r->connection, MHD_HEADER_KIND,
338 MHD_HTTP_HEADER_AUTHORIZATION);
339 if (token == NULL)
340 return bad_401(r, "Authorization token is missing");
341 brk = strcspn(token, " \t");
342 if (token[brk] == '\0' || brk > sizeof(token_type) - 1)
343 return bad_401(r, "Authorization token is missing");
344 memcpy(token_type, token, brk);
345 token_type[brk] = '\0';
346 token += brk + 1;
347 tok.length = strlen(token);
348 tok.data = (void *)(uintptr_t)token;
350 for (i = 0; i < audiences.num_strings; i++)
351 if (strncasecmp(host, audiences.strings[i], host_len) == 0 &&
352 audiences.strings[i][host_len] == '\0')
353 break;
354 if (i == audiences.num_strings)
355 return bad_403(r, EINVAL, "Host: value is not accepted here");
357 r->sname = strdup(host); /* No need to check for ENOMEM here */
359 ret = kdc_validate_token(r->context, NULL /* realm */, token_type, &tok,
360 (const char **)&audiences.strings[i], 1,
361 &cprinc, &r->token_times);
362 if (ret)
363 return bad_403(r, ret, "Token validation failed");
364 if (cprinc == NULL)
365 return bad_403(r, ret, "Could not extract a principal name "
366 "from token");
367 ret = krb5_unparse_name(r->context, cprinc, &r->cname);
368 krb5_free_principal(r->context, cprinc);
369 if (ret)
370 return bad_503(r, ret, "Could not parse principal name");
371 return ret;
374 static void
375 generate_key(hx509_context context,
376 const char *key_name,
377 const char *gen_type,
378 unsigned long gen_bits,
379 char **fn)
381 struct hx509_generate_private_context *key_gen_ctx = NULL;
382 hx509_private_key key = NULL;
383 hx509_certs certs = NULL;
384 hx509_cert cert = NULL;
385 int ret;
387 if (strcmp(gen_type, "rsa") != 0)
388 errx(1, "Only RSA keys are supported at this time");
390 if (asprintf(fn, "PEM-FILE:%s/.%s_priv_key.pem",
391 cache_dir, key_name) == -1 ||
392 *fn == NULL)
393 err(1, "Could not set up private key for %s", key_name);
395 ret = _hx509_generate_private_key_init(context,
396 ASN1_OID_ID_PKCS1_RSAENCRYPTION,
397 &key_gen_ctx);
398 if (ret == 0)
399 ret = _hx509_generate_private_key_bits(context, key_gen_ctx, gen_bits);
400 if (ret == 0)
401 ret = _hx509_generate_private_key(context, key_gen_ctx, &key);
402 if (ret == 0)
403 cert = hx509_cert_init_private_key(context, key, NULL);
404 if (ret == 0)
405 ret = hx509_certs_init(context, *fn,
406 HX509_CERTS_CREATE | HX509_CERTS_UNPROTECT_ALL,
407 NULL, &certs);
408 if (ret == 0)
409 ret = hx509_certs_add(context, certs, cert);
410 if (ret == 0)
411 ret = hx509_certs_store(context, certs, 0, NULL);
412 if (ret)
413 hx509_err(context, 1, ret, "Could not generate and save private key "
414 "for %s", key_name);
416 _hx509_generate_private_key_free(&key_gen_ctx);
417 hx509_private_key_free(&key);
418 hx509_certs_free(&certs);
419 hx509_cert_free(cert);
422 static void
423 k5_free_context(void *ctx)
425 krb5_free_context(ctx);
428 #ifndef HAVE_UNLINKAT
429 static int
430 unlink1file(const char *dname, const char *name)
432 char p[PATH_MAX];
434 if (strlcpy(p, dname, sizeof(p)) < sizeof(p) &&
435 strlcat(p, "/", sizeof(p)) < sizeof(p) &&
436 strlcat(p, name, sizeof(p)) < sizeof(p))
437 return unlink(p);
438 return ERANGE;
440 #endif
442 static void
443 rm_cache_dir(void)
445 struct dirent *e;
446 DIR *d;
449 * This works, but not on Win32:
451 * (void) simple_execlp("rm", "rm", "-rf", cache_dir, NULL);
453 * We make no directories in `cache_dir', so we need not recurse.
455 if ((d = opendir(cache_dir)) == NULL)
456 return;
458 while ((e = readdir(d))) {
459 #ifdef HAVE_UNLINKAT
461 * Because unlinkat() takes a directory FD, implementing one for
462 * libroken is tricky at best. Instead we might want to implement an
463 * rm_dash_rf() function in lib/roken.
465 (void) unlinkat(dirfd(d), e->d_name, 0);
466 #else
467 (void) unlink1file(cache_dir, e->d_name);
468 #endif
470 (void) closedir(d);
471 (void) rmdir(cache_dir);
474 static krb5_error_code
475 mk_pkix_store(char **pkix_store)
477 char *s = NULL;
478 int ret = ENOMEM;
479 int fd;
481 if (*pkix_store) {
482 const char *fn = strchr(*pkix_store, ':');
484 fn = fn ? fn + 1 : *pkix_store;
485 (void) unlink(fn);
488 free(*pkix_store);
489 *pkix_store = NULL;
490 if (asprintf(&s, "PEM-FILE:%s/pkix-XXXXXX", cache_dir) == -1 ||
491 s == NULL) {
492 free(s);
493 return ret;
495 if ((fd = mkstemp(s + sizeof("PEM-FILE:") - 1)) == -1) {
496 free(s);
497 return errno;
499 (void) close(fd);
500 *pkix_store = s;
501 return 0;
504 static krb5_error_code
505 resp(struct bx509_request_desc *r,
506 int http_status_code,
507 enum MHD_ResponseMemoryMode rmmode,
508 const char *content_type,
509 const void *body,
510 size_t bodylen,
511 const char *token)
513 int mret = MHD_YES;
515 if (r->response)
516 return MHD_YES;
518 (void) gettimeofday(&r->tv_end, NULL);
519 if (http_status_code == MHD_HTTP_OK ||
520 http_status_code == MHD_HTTP_TEMPORARY_REDIRECT)
521 audit_trail(r, 0);
523 r->response = MHD_create_response_from_buffer(bodylen, rk_UNCONST(body),
524 rmmode);
525 if (r->response == NULL)
526 return -1;
527 if (r->csrf_token)
528 mret = MHD_add_response_header(r->response, "X-CSRF-Token", r->csrf_token);
529 if (mret == MHD_YES)
530 mret = MHD_add_response_header(r->response, MHD_HTTP_HEADER_CACHE_CONTROL,
531 "no-store, max-age=0");
532 if (mret == MHD_YES && http_status_code == MHD_HTTP_UNAUTHORIZED) {
533 mret = MHD_add_response_header(r->response,
534 MHD_HTTP_HEADER_WWW_AUTHENTICATE,
535 "Bearer");
536 if (mret == MHD_YES)
537 mret = MHD_add_response_header(r->response,
538 MHD_HTTP_HEADER_WWW_AUTHENTICATE,
539 "Negotiate");
540 } else if (mret == MHD_YES && http_status_code == MHD_HTTP_TEMPORARY_REDIRECT) {
541 const char *redir;
543 /* XXX Move this */
544 redir = MHD_lookup_connection_value(r->connection, MHD_GET_ARGUMENT_KIND,
545 "redirect");
546 mret = MHD_add_response_header(r->response, MHD_HTTP_HEADER_LOCATION,
547 redir);
548 if (mret != MHD_NO && token)
549 mret = MHD_add_response_header(r->response,
550 MHD_HTTP_HEADER_AUTHORIZATION,
551 token);
553 if (mret == MHD_YES && content_type) {
554 mret = MHD_add_response_header(r->response,
555 MHD_HTTP_HEADER_CONTENT_TYPE,
556 content_type);
558 if (mret == MHD_YES)
559 mret = MHD_queue_response(r->connection, http_status_code, r->response);
560 MHD_destroy_response(r->response);
561 return mret == MHD_NO ? -1 : 0;
564 static krb5_error_code
565 bad_reqv(struct bx509_request_desc *r,
566 krb5_error_code code,
567 int http_status_code,
568 const char *fmt,
569 va_list ap)
571 krb5_error_code ret;
572 const char *k5msg = NULL;
573 const char *emsg = NULL;
574 char *formatted = NULL;
575 char *msg = NULL;
577 heim_audit_setkv_number((heim_svc_req_desc)r, "http-status-code",
578 http_status_code);
579 (void) gettimeofday(&r->tv_end, NULL);
580 if (code == ENOMEM) {
581 if (r->context)
582 krb5_log_msg(r->context, logfac, 1, NULL, "Out of memory");
583 audit_trail(r, code);
584 return resp(r, http_status_code, MHD_RESPMEM_PERSISTENT,
585 NULL, fmt, strlen(fmt), NULL);
588 if (code) {
589 if (r->context)
590 emsg = k5msg = krb5_get_error_message(r->context, code);
591 else if (code > -1)
592 emsg = strerror(code);
593 else
594 emsg = "Unknown error";
597 ret = vasprintf(&formatted, fmt, ap);
598 if (code) {
599 if (ret > -1 && formatted)
600 ret = asprintf(&msg, "%s: %s (%d)", formatted, emsg, (int)code);
601 } else {
602 msg = formatted;
603 formatted = NULL;
605 heim_audit_addreason((heim_svc_req_desc)r, "%s", msg);
606 audit_trail(r, code);
607 if (r->context)
608 krb5_free_error_message(r->context, k5msg);
610 if (ret == -1 || msg == NULL) {
611 if (r->context)
612 krb5_log_msg(r->context, logfac, 1, NULL, "Out of memory");
613 return resp(r, MHD_HTTP_SERVICE_UNAVAILABLE, MHD_RESPMEM_PERSISTENT,
614 NULL, "Out of memory", sizeof("Out of memory") - 1, NULL);
617 ret = resp(r, http_status_code, MHD_RESPMEM_MUST_COPY,
618 NULL, msg, strlen(msg), NULL);
619 free(formatted);
620 free(msg);
621 return ret == -1 ? -1 : code;
624 static krb5_error_code
625 bad_req(struct bx509_request_desc *r,
626 krb5_error_code code,
627 int http_status_code,
628 const char *fmt,
629 ...)
631 krb5_error_code ret;
632 va_list ap;
634 va_start(ap, fmt);
635 ret = bad_reqv(r, code, http_status_code, fmt, ap);
636 va_end(ap);
637 return ret;
640 static krb5_error_code
641 bad_enomem(struct bx509_request_desc *r, krb5_error_code ret)
643 return bad_req(r, ret, MHD_HTTP_SERVICE_UNAVAILABLE,
644 "Out of memory");
647 static krb5_error_code
648 bad_400(struct bx509_request_desc *r, int ret, char *reason)
650 return bad_req(r, ret, MHD_HTTP_BAD_REQUEST, "%s", reason);
653 static krb5_error_code
654 bad_401(struct bx509_request_desc *r, char *reason)
656 return bad_req(r, EACCES, MHD_HTTP_UNAUTHORIZED, "%s", reason);
659 static krb5_error_code
660 bad_403(struct bx509_request_desc *r, krb5_error_code ret, char *reason)
662 return bad_req(r, ret, MHD_HTTP_FORBIDDEN, "%s", reason);
665 static krb5_error_code
666 bad_404(struct bx509_request_desc *r, const char *name)
668 return bad_req(r, ENOENT, MHD_HTTP_NOT_FOUND,
669 "Resource not found: %s", name);
672 static krb5_error_code
673 bad_405(struct bx509_request_desc *r, const char *method)
675 return bad_req(r, EPERM, MHD_HTTP_METHOD_NOT_ALLOWED,
676 "Method not supported: %s", method);
679 static krb5_error_code
680 bad_413(struct bx509_request_desc *r)
682 return bad_req(r, E2BIG, MHD_HTTP_METHOD_NOT_ALLOWED,
683 "POST request body too large");
686 static krb5_error_code
687 bad_500(struct bx509_request_desc *r,
688 krb5_error_code ret,
689 const char *reason)
691 return bad_req(r, ret, MHD_HTTP_INTERNAL_SERVER_ERROR,
692 "Internal error: %s", reason);
695 static krb5_error_code
696 bad_503(struct bx509_request_desc *r,
697 krb5_error_code ret,
698 const char *reason)
700 return bad_req(r, ret, MHD_HTTP_SERVICE_UNAVAILABLE,
701 "Service unavailable: %s", reason);
704 static krb5_error_code
705 good_bx509(struct bx509_request_desc *r)
707 krb5_error_code ret;
708 const char *fn;
709 size_t bodylen;
710 void *body;
713 * This `fn' thing is just to quiet linters that think "hey, strchr() can
714 * return NULL so...", but here we've build `r->pkix_store' and know it has
715 * a ':'.
717 if (r->pkix_store == NULL)
718 return bad_503(r, EINVAL, "Internal error"); /* Quiet warnings */
719 fn = strchr(r->pkix_store, ':');
720 fn = fn ? fn + 1 : r->pkix_store;
721 ret = rk_undumpdata(fn, &body, &bodylen);
722 if (ret)
723 return bad_503(r, ret, "Could not recover issued certificate "
724 "from PKIX store");
726 (void) gettimeofday(&r->tv_end, NULL);
727 ret = resp(r, MHD_HTTP_OK, MHD_RESPMEM_MUST_COPY, "application/x-pem-file",
728 body, bodylen, NULL);
729 free(body);
730 return ret;
733 static heim_mhd_result
734 bx509_param_cb(void *d,
735 enum MHD_ValueKind kind,
736 const char *key,
737 const char *val)
739 struct bx509_request_desc *r = d;
740 heim_oid oid = { 0, 0 };
742 if (strcmp(key, "eku") == 0 && val) {
743 heim_audit_addkv((heim_svc_req_desc)r, KDC_AUDIT_VIS, "requested_eku",
744 "%s", val);
745 r->error_code = der_parse_heim_oid(val, ".", &oid);
746 if (r->error_code == 0)
747 r->error_code = hx509_request_add_eku(r->context->hx509ctx, r->req, &oid);
748 der_free_oid(&oid);
749 } else if (strcmp(key, "dNSName") == 0 && val) {
750 heim_audit_addkv((heim_svc_req_desc)r, KDC_AUDIT_VIS,
751 "requested_dNSName", "%s", val);
752 r->error_code = hx509_request_add_dns_name(r->context->hx509ctx, r->req, val);
753 } else if (strcmp(key, "rfc822Name") == 0 && val) {
754 heim_audit_addkv((heim_svc_req_desc)r, KDC_AUDIT_VIS,
755 "requested_rfc822Name", "%s", val);
756 r->error_code = hx509_request_add_email(r->context->hx509ctx, r->req, val);
757 } else if (strcmp(key, "xMPPName") == 0 && val) {
758 heim_audit_addkv((heim_svc_req_desc)r, KDC_AUDIT_VIS,
759 "requested_xMPPName", "%s", val);
760 r->error_code = hx509_request_add_xmpp_name(r->context->hx509ctx, r->req,
761 val);
762 } else if (strcmp(key, "krb5PrincipalName") == 0 && val) {
763 heim_audit_addkv((heim_svc_req_desc)r, KDC_AUDIT_VIS,
764 "requested_krb5PrincipalName", "%s", val);
765 r->error_code = hx509_request_add_pkinit(r->context->hx509ctx, r->req,
766 val);
767 } else if (strcmp(key, "ms-upn") == 0 && val) {
768 heim_audit_addkv((heim_svc_req_desc)r, KDC_AUDIT_VIS,
769 "requested_ms_upn", "%s", val);
770 r->error_code = hx509_request_add_ms_upn_name(r->context->hx509ctx, r->req,
771 val);
772 } else if (strcmp(key, "registeredID") == 0 && val) {
773 heim_audit_addkv((heim_svc_req_desc)r, KDC_AUDIT_VIS,
774 "requested_registered_id", "%s", val);
775 r->error_code = der_parse_heim_oid(val, ".", &oid);
776 if (r->error_code == 0)
777 r->error_code = hx509_request_add_registered(r->context->hx509ctx, r->req,
778 &oid);
779 der_free_oid(&oid);
780 } else if (strcmp(key, "csr") == 0 && val) {
781 heim_audit_setkv_bool((heim_svc_req_desc)r, "requested_csr", TRUE);
782 r->error_code = 0; /* Handled upstairs */
783 } else if (strcmp(key, "lifetime") == 0 && val) {
784 r->req_life = parse_time(val, "day");
785 } else {
786 /* Produce error for unknown params */
787 heim_audit_setkv_bool((heim_svc_req_desc)r, "requested_unknown", TRUE);
788 krb5_set_error_message(r->context, r->error_code = ENOTSUP,
789 "Query parameter %s not supported", key);
791 return r->error_code == 0 ? MHD_YES : MHD_NO /* Stop iterating */;
794 static krb5_error_code
795 authorize_CSR(struct bx509_request_desc *r,
796 krb5_data *csr,
797 krb5_const_principal p)
799 krb5_error_code ret;
801 ret = hx509_request_parse_der(r->context->hx509ctx, csr, &r->req);
802 if (ret)
803 return bad_req(r, ret, MHD_HTTP_SERVICE_UNAVAILABLE,
804 "Could not parse CSR");
805 r->error_code = 0;
806 (void) MHD_get_connection_values(r->connection, MHD_GET_ARGUMENT_KIND,
807 bx509_param_cb, r);
808 ret = r->error_code;
809 if (ret)
810 return bad_req(r, ret, MHD_HTTP_SERVICE_UNAVAILABLE,
811 "Could not handle query parameters");
813 ret = kdc_authorize_csr(r->context, "bx509", r->req, p);
814 if (ret)
815 return bad_403(r, ret, "Not authorized to requested certificate");
816 return ret;
820 * hx509_certs_iter_f() callback to assign a private key to the first cert in a
821 * store.
823 static int HX509_LIB_CALL
824 set_priv_key(hx509_context context, void *d, hx509_cert c)
826 (void) _hx509_cert_assign_key(c, (hx509_private_key)d);
827 return -1; /* stop iteration */
830 static krb5_error_code
831 store_certs(hx509_context context,
832 const char *store,
833 hx509_certs store_these,
834 hx509_private_key key)
836 krb5_error_code ret;
837 hx509_certs certs = NULL;
839 ret = hx509_certs_init(context, store, HX509_CERTS_CREATE, NULL,
840 &certs);
841 if (ret == 0) {
842 if (key)
843 (void) hx509_certs_iter_f(context, store_these, set_priv_key, key);
844 hx509_certs_merge(context, certs, store_these);
846 if (ret == 0)
847 hx509_certs_store(context, certs, 0, NULL);
848 hx509_certs_free(&certs);
849 return ret;
852 /* Setup a CSR for bx509() */
853 static krb5_error_code
854 do_CA(struct bx509_request_desc *r, const char *csr)
856 krb5_error_code ret = 0;
857 krb5_principal p;
858 hx509_certs certs = NULL;
859 krb5_data d;
860 ssize_t bytes;
861 char *csr2, *q;
864 * Work around bug where microhttpd decodes %2b to + then + to space. That
865 * bug does not affect other base64 special characters that get URI
866 * %-encoded.
868 if ((csr2 = strdup(csr)) == NULL)
869 return bad_enomem(r, ENOMEM);
870 for (q = strchr(csr2, ' '); q; q = strchr(q + 1, ' '))
871 *q = '+';
873 ret = krb5_parse_name(r->context, r->cname, &p);
874 if (ret) {
875 free(csr2);
876 return bad_req(r, ret, MHD_HTTP_SERVICE_UNAVAILABLE,
877 "Could not parse principal name");
880 /* Set CSR */
881 if ((d.data = malloc(strlen(csr2))) == NULL) {
882 krb5_free_principal(r->context, p);
883 free(csr2);
884 return bad_enomem(r, ENOMEM);
887 bytes = rk_base64_decode(csr2, d.data);
888 free(csr2);
889 if (bytes < 0)
890 ret = errno ? errno : EINVAL;
891 else
892 d.length = bytes;
893 if (ret) {
894 krb5_free_principal(r->context, p);
895 free(d.data);
896 return bad_500(r, ret, "Invalid base64 encoding of CSR");
900 * Parses and validates the CSR, adds external extension requests from
901 * query parameters, then checks authorization.
903 ret = authorize_CSR(r, &d, p);
904 free(d.data);
905 d.data = 0;
906 d.length = 0;
907 if (ret) {
908 krb5_free_principal(r->context, p);
909 return ret; /* authorize_CSR() calls bad_req() */
912 /* Issue the certificate */
913 ret = kdc_issue_certificate(r->context, "bx509", logfac, r->req, p,
914 &r->token_times, r->req_life,
915 1 /* send_chain */, &certs);
916 krb5_free_principal(r->context, p);
917 if (ret) {
918 if (ret == KRB5KDC_ERR_POLICY || ret == EACCES)
919 return bad_403(r, ret,
920 "Certificate request denied for policy reasons");
921 return bad_500(r, ret, "Certificate issuance failed");
924 /* Setup PKIX store */
925 if ((ret = mk_pkix_store(&r->pkix_store)))
926 return bad_500(r, ret,
927 "Could not create PEM store for issued certificate");
929 ret = store_certs(r->context->hx509ctx, r->pkix_store, certs, NULL);
930 hx509_certs_free(&certs);
931 if (ret)
932 return bad_500(r, ret, "Failed to convert issued"
933 " certificate and chain to PEM");
934 return 0;
937 /* Copied from kdc/connect.c */
938 static void
939 addr_to_string(krb5_context context,
940 struct sockaddr *addr,
941 char *str,
942 size_t len)
944 krb5_error_code ret;
945 krb5_address a;
947 ret = krb5_sockaddr2address(context, addr, &a);
948 if (ret == 0) {
949 ret = krb5_print_address(&a, str, len, &len);
950 krb5_free_address(context, &a);
952 if (ret)
953 snprintf(str, len, "<family=%d>", addr->sa_family);
956 static void clean_req_desc(struct bx509_request_desc *);
958 static krb5_error_code
959 set_req_desc(struct MHD_Connection *connection,
960 const char *method,
961 const char *url,
962 struct bx509_request_desc **rp)
964 struct bx509_request_desc *r;
965 const union MHD_ConnectionInfo *ci;
966 const char *token;
967 krb5_error_code ret;
969 *rp = NULL;
970 if ((r = calloc(1, sizeof(*r))) == NULL)
971 return ENOMEM;
972 (void) gettimeofday(&r->tv_start, NULL);
974 ret = get_krb5_context(&r->context);
975 r->connection = connection;
976 r->response = NULL;
977 r->pp = NULL;
978 r->request.data = "<HTTP-REQUEST>";
979 r->request.length = sizeof("<HTTP-REQUEST>");
980 r->from = r->frombuf;
981 r->tgt_addresses.len = 0;
982 r->tgt_addresses.val = 0;
983 r->hcontext = r->context ? r->context->hcontext : NULL;
984 r->config = NULL;
985 r->logf = logfac;
986 r->csrf_token = NULL;
987 r->free_list = NULL;
988 r->method = method;
989 r->reqtype = url;
990 r->target = r->redir = NULL;
991 r->pkix_store = NULL;
992 r->for_cname = NULL;
993 r->freeme1 = NULL;
994 r->reason = NULL;
995 r->tgts_filename = NULL;
996 r->tgts = NULL;
997 r->ccname = NULL;
998 r->reply = NULL;
999 r->sname = NULL;
1000 r->cname = NULL;
1001 r->addr = NULL;
1002 r->req = NULL;
1003 r->req_life = 0;
1004 r->error_code = ret;
1005 r->kv = heim_dict_create(10);
1006 r->attributes = heim_dict_create(1);
1007 if (ret == 0 && (r->kv == NULL || r->attributes == NULL))
1008 r->error_code = ret = ENOMEM;
1009 ci = MHD_get_connection_info(connection,
1010 MHD_CONNECTION_INFO_CLIENT_ADDRESS);
1011 if (ci) {
1012 r->addr = ci->client_addr;
1013 addr_to_string(r->context, r->addr, r->frombuf, sizeof(r->frombuf));
1016 heim_audit_addkv((heim_svc_req_desc)r, 0, "method", "GET");
1017 heim_audit_addkv((heim_svc_req_desc)r, 0, "endpoint", "%s", r->reqtype);
1018 token = MHD_lookup_connection_value(r->connection, MHD_HEADER_KIND,
1019 MHD_HTTP_HEADER_AUTHORIZATION);
1020 if (token && r->kv) {
1021 const char *token_end;
1023 if ((token_end = strchr(token, ' ')) == NULL ||
1024 (token_end - token) > INT_MAX || (token_end - token) < 2)
1025 heim_audit_addkv((heim_svc_req_desc)r, 0, "auth", "<unknown>");
1026 else
1027 heim_audit_addkv((heim_svc_req_desc)r, 0, "auth", "%.*s",
1028 (int)(token_end - token), token);
1032 if (ret == 0)
1033 *rp = r;
1034 else
1035 clean_req_desc(r);
1036 return ret;
1039 static void
1040 clean_req_desc(struct bx509_request_desc *r)
1042 if (!r)
1043 return;
1044 while (r->free_list) {
1045 struct free_tend_list *ftl = r->free_list;
1046 r->free_list = r->free_list->next;
1047 free(ftl->freeme1);
1048 free(ftl->freeme2);
1049 free(ftl);
1051 if (r->pkix_store) {
1052 const char *fn = strchr(r->pkix_store, ':');
1055 * This `fn' thing is just to quiet linters that think "hey, strchr() can
1056 * return NULL so...", but here we've build `r->pkix_store' and know it has
1057 * a ':'.
1059 fn = fn ? fn + 1 : r->pkix_store;
1060 (void) unlink(fn);
1062 krb5_free_addresses(r->context, &r->tgt_addresses);
1063 hx509_request_free(&r->req);
1064 heim_release(r->attributes);
1065 heim_release(r->reason);
1066 heim_release(r->kv);
1067 if (r->ccname && r->cckind == K5_CREDS_EPHEMERAL) {
1068 const char *fn = r->ccname;
1070 if (strncmp(fn, "FILE:", sizeof("FILE:") - 1) == 0)
1071 fn += sizeof("FILE:") - 1;
1072 (void) unlink(fn);
1074 if (r->tgts)
1075 (void) fclose(r->tgts);
1076 if (r->tgts_filename) {
1077 (void) unlink(r->tgts_filename);
1078 free(r->tgts_filename);
1080 /* No need to destroy r->response */
1081 if (r->pp)
1082 MHD_destroy_post_processor(r->pp);
1083 free(r->csrf_token);
1084 free(r->pkix_store);
1085 free(r->freeme1);
1086 free(r->ccname);
1087 free(r->cname);
1088 free(r->sname);
1089 free(r);
1092 /* Implements GETs of /bx509 */
1093 static krb5_error_code
1094 bx509(struct bx509_request_desc *r)
1096 krb5_error_code ret;
1097 const char *csr;
1099 /* Get required inputs */
1100 csr = MHD_lookup_connection_value(r->connection, MHD_GET_ARGUMENT_KIND,
1101 "csr");
1102 if (csr == NULL)
1103 return bad_400(r, EINVAL, "CSR is missing");
1105 if (r->cname == NULL)
1106 return bad_403(r, EINVAL,
1107 "Could not extract principal name from token");
1109 /* Parse CSR, add extensions from parameters, authorize, issue cert */
1110 if ((ret = do_CA(r, csr)))
1111 return ret;
1113 /* Read and send the contents of the PKIX store */
1114 krb5_log_msg(r->context, logfac, 1, NULL, "Issued certificate to %s",
1115 r->cname);
1116 return good_bx509(r);
1120 * princ_fs_encode_sz() and princ_fs_encode() encode a principal name to be
1121 * safe for use as a file name. They function very much like URL encoders, but
1122 * '~' and '.' also get encoded, and '@' does not.
1124 * A corresponding decoder is not needed.
1126 * XXX Maybe use krb5_cc_default_for()!
1128 static size_t
1129 princ_fs_encode_sz(const char *in)
1131 size_t sz = strlen(in);
1133 while (*in) {
1134 unsigned char c = *(const unsigned char *)(in++);
1136 if (isalnum(c))
1137 continue;
1138 switch (c) {
1139 case '@':
1140 case '-':
1141 case '_':
1142 continue;
1143 default:
1144 sz += 2;
1147 return sz;
1150 static char *
1151 princ_fs_encode(const char *in)
1153 size_t len = strlen(in);
1154 size_t sz = princ_fs_encode_sz(in);
1155 size_t i, k;
1156 char *s;
1158 if ((s = malloc(sz + 1)) == NULL)
1159 return NULL;
1160 s[sz] = '\0';
1162 for (i = k = 0; i < len; i++) {
1163 char c = in[i];
1165 switch (c) {
1166 case '@':
1167 case '-':
1168 case '_':
1169 s[k++] = c;
1170 break;
1171 default:
1172 if (isalnum((unsigned char)c)) {
1173 s[k++] = c;
1174 } else {
1175 s[k++] = '%';
1176 s[k++] = "0123456789abcdef"[(c&0xff)>>4];
1177 s[k++] = "0123456789abcdef"[(c&0x0f)];
1181 return s;
1186 * Find an existing, live ccache for `princ' in `cache_dir' or acquire Kerberos
1187 * creds for `princ' with PKINIT and put them in a ccache in `cache_dir'.
1189 static krb5_error_code
1190 find_ccache(krb5_context context, const char *princ, char **ccname)
1192 krb5_error_code ret = ENOMEM;
1193 krb5_ccache cc = NULL;
1194 time_t life;
1195 char *s = NULL;
1197 *ccname = NULL;
1200 * Name the ccache after the principal. The principal may have special
1201 * characters in it, such as / or \ (path component separarot), or shell
1202 * special characters, so princ_fs_encode() it to make a ccache name.
1204 if ((s = princ_fs_encode(princ)) == NULL ||
1205 asprintf(ccname, "FILE:%s/%s.cc", cache_dir, s) == -1 ||
1206 *ccname == NULL) {
1207 free(s);
1208 return ENOMEM;
1210 free(s);
1212 if ((ret = krb5_cc_resolve(context, *ccname, &cc))) {
1213 /* krb5_cc_resolve() suceeds even if the file doesn't exist */
1214 free(*ccname);
1215 *ccname = NULL;
1216 cc = NULL;
1219 /* Check if we have a good enough credential */
1220 if (ret == 0 &&
1221 (ret = krb5_cc_get_lifetime(context, cc, &life)) == 0 && life > 60) {
1222 krb5_cc_close(context, cc);
1223 return 0;
1225 if (cc)
1226 krb5_cc_close(context, cc);
1227 return ret ? ret : ENOENT;
1230 static krb5_error_code
1231 get_ccache(struct bx509_request_desc *r, krb5_ccache *cc, int *won)
1233 krb5_error_code ret = 0;
1234 char *temp_ccname = NULL;
1235 const char *fn = NULL;
1236 time_t life;
1237 int fd = -1;
1240 * Open and lock a .new ccache file. Use .new to avoid garbage files on
1241 * crash.
1243 * We can race with other threads to do this, so we loop until we
1244 * definitively win or definitely lose the race. We win when we have a) an
1245 * open FD that is b) flock'ed, and c) we observe with lstat() that the
1246 * file we opened and locked is the same as on disk after locking.
1248 * We don't close the FD until we're done.
1250 * If we had a proper anon MEMORY ccache, we could instead use that for a
1251 * temporary ccache, and then the initialization of and move to the final
1252 * FILE ccache would take care to mkstemp() and rename() into place.
1253 * fcc_open() basically does a similar thing.
1255 *cc = NULL;
1256 *won = -1;
1257 if (asprintf(&temp_ccname, "%s.ccnew", r->ccname) == -1 ||
1258 temp_ccname == NULL)
1259 ret = ENOMEM;
1260 if (ret == 0)
1261 fn = temp_ccname + sizeof("FILE:") - 1;
1262 if (ret == 0) do {
1263 struct stat st1, st2;
1265 * Open and flock the temp ccache file.
1267 * XXX We should really a) use _krb5_xlock(), or move that into
1268 * lib/roken anyways, b) abstract this loop into a utility function in
1269 * lib/roken.
1271 if (fd != -1) {
1272 (void) close(fd);
1273 fd = -1;
1275 errno = 0;
1276 memset(&st1, 0, sizeof(st1));
1277 memset(&st2, 0xff, sizeof(st2));
1278 if (ret == 0 &&
1279 ((fd = open(fn, O_RDWR | O_CREAT, 0600)) == -1 ||
1280 flock(fd, LOCK_EX) == -1 ||
1281 (lstat(fn, &st1) == -1 && errno != ENOENT) ||
1282 fstat(fd, &st2) == -1))
1283 ret = errno;
1284 if (ret == 0 && errno == 0 &&
1285 st1.st_dev == st2.st_dev && st1.st_ino == st2.st_ino) {
1286 if (S_ISREG(st1.st_mode))
1287 break;
1288 if (unlink(fn) == -1)
1289 ret = errno;
1291 } while (ret == 0);
1293 /* Check if we lost any race to acquire Kerberos creds */
1294 if (ret == 0)
1295 ret = krb5_cc_resolve(r->context, temp_ccname, cc);
1296 if (ret == 0) {
1297 ret = krb5_cc_get_lifetime(r->context, *cc, &life);
1298 if (ret == 0 && life > 60)
1299 *won = 0; /* We lost the race, but we win: we get to do less work */
1300 *won = 1;
1301 ret = 0;
1303 free(temp_ccname);
1304 if (fd != -1)
1305 (void) close(fd); /* Drops the flock */
1306 return ret;
1310 * Acquire credentials for `princ' using PKINIT and the PKIX credentials in
1311 * `pkix_store', then place the result in the ccache named `ccname' (which will
1312 * be in our own private `cache_dir').
1314 * XXX This function could be rewritten using gss_acquire_cred_from() and
1315 * gss_store_cred_into() provided we add new generic cred store key/value pairs
1316 * for PKINIT.
1318 static krb5_error_code
1319 do_pkinit(struct bx509_request_desc *r, enum k5_creds_kind kind)
1321 krb5_get_init_creds_opt *opt = NULL;
1322 krb5_init_creds_context ctx = NULL;
1323 krb5_error_code ret = 0;
1324 krb5_ccache temp_cc = NULL;
1325 krb5_ccache cc = NULL;
1326 krb5_principal p = NULL;
1327 const char *crealm;
1328 const char *cname = r->for_cname ? r->for_cname : r->cname;
1330 if (kind == K5_CREDS_CACHED) {
1331 int won = -1;
1333 ret = get_ccache(r, &temp_cc, &won);
1334 if (ret || !won)
1335 goto out;
1337 * We won the race to do PKINIT. Setup to acquire Kerberos creds with
1338 * PKINIT.
1340 * We should really make sure that gss_acquire_cred_from() can do this
1341 * for us. We'd add generic cred store key/value pairs for PKIX cred
1342 * store, trust anchors, and so on, and acquire that way, then
1343 * gss_store_cred_into() to save it in a FILE ccache.
1345 } else {
1346 ret = krb5_cc_new_unique(r->context, "FILE", NULL, &temp_cc);
1349 if (ret == 0)
1350 ret = krb5_parse_name(r->context, cname, &p);
1351 if (ret == 0)
1352 crealm = krb5_principal_get_realm(r->context, p);
1353 if (ret == 0)
1354 ret = krb5_get_init_creds_opt_alloc(r->context, &opt);
1355 if (ret == 0)
1356 krb5_get_init_creds_opt_set_default_flags(r->context, "kinit", crealm,
1357 opt);
1358 if (ret == 0 && kind == K5_CREDS_EPHEMERAL &&
1359 !krb5_config_get_bool_default(r->context, NULL, TRUE,
1360 "get-tgt", "no_addresses", NULL)) {
1361 krb5_addresses addr;
1363 ret = _krb5_parse_address_no_lookup(r->context, r->frombuf, &addr);
1364 if (ret == 0)
1365 ret = krb5_append_addresses(r->context, &r->tgt_addresses,
1366 &addr);
1368 if (ret == 0) {
1369 if (r->tgt_addresses.len == 0)
1370 ret = krb5_get_init_creds_opt_set_addressless(r->context, opt, 1);
1371 else
1372 krb5_get_init_creds_opt_set_address_list(opt, &r->tgt_addresses);
1374 if (ret == 0)
1375 ret = krb5_get_init_creds_opt_set_pkinit(r->context, opt, p,
1376 r->pkix_store,
1377 NULL, /* pkinit_anchor */
1378 NULL, /* anchor_chain */
1379 NULL, /* pkinit_crl */
1380 0, /* flags */
1381 NULL, /* prompter */
1382 NULL, /* prompter data */
1383 NULL /* password */);
1384 if (ret == 0)
1385 ret = krb5_init_creds_init(r->context, p,
1386 NULL /* prompter */,
1387 NULL /* prompter data */,
1388 0 /* start_time */,
1389 opt, &ctx);
1392 * Finally, do the AS exchange w/ PKINIT, extract the new Kerberos creds
1393 * into temp_cc, and rename into place. Note that krb5_cc_move() closes
1394 * the source ccache, so we set temp_cc = NULL if it succeeds.
1396 if (ret == 0)
1397 ret = krb5_init_creds_get(r->context, ctx);
1398 if (ret == 0)
1399 ret = krb5_init_creds_store(r->context, ctx, temp_cc);
1400 if (kind == K5_CREDS_CACHED) {
1401 if (ret == 0)
1402 ret = krb5_cc_resolve(r->context, r->ccname, &cc);
1403 if (ret == 0)
1404 ret = krb5_cc_move(r->context, temp_cc, cc);
1405 if (ret == 0)
1406 temp_cc = NULL;
1407 } else if (ret == 0 && kind == K5_CREDS_EPHEMERAL) {
1408 ret = krb5_cc_get_full_name(r->context, temp_cc, &r->ccname);
1411 out:
1412 if (ctx)
1413 krb5_init_creds_free(r->context, ctx);
1414 krb5_get_init_creds_opt_free(r->context, opt);
1415 krb5_free_principal(r->context, p);
1416 krb5_cc_close(r->context, temp_cc);
1417 krb5_cc_close(r->context, cc);
1418 return ret;
1421 static krb5_error_code
1422 load_priv_key(krb5_context context, const char *fn, hx509_private_key *key)
1424 hx509_private_key *keys = NULL;
1425 krb5_error_code ret;
1426 hx509_certs certs = NULL;
1428 *key = NULL;
1429 ret = hx509_certs_init(context->hx509ctx, fn, 0, NULL, &certs);
1430 if (ret == ENOENT)
1431 return 0;
1432 if (ret == 0)
1433 ret = _hx509_certs_keys_get(context->hx509ctx, certs, &keys);
1434 if (ret == 0 && keys[0] == NULL)
1435 ret = ENOENT; /* XXX Better error please */
1436 if (ret == 0)
1437 *key = _hx509_private_key_ref(keys[0]);
1438 if (ret)
1439 krb5_set_error_message(context, ret, "Could not load private "
1440 "impersonation key from %s for PKINIT: %s", fn,
1441 hx509_get_error_string(context->hx509ctx, ret));
1442 _hx509_certs_keys_free(context->hx509ctx, keys);
1443 hx509_certs_free(&certs);
1444 return ret;
1447 static krb5_error_code
1448 k5_do_CA(struct bx509_request_desc *r)
1450 SubjectPublicKeyInfo spki;
1451 hx509_private_key key = NULL;
1452 krb5_error_code ret = 0;
1453 krb5_principal p = NULL;
1454 hx509_request req = NULL;
1455 hx509_certs certs = NULL;
1456 KeyUsage ku = int2KeyUsage(0);
1457 const char *cname = r->for_cname ? r->for_cname : r->cname;
1459 memset(&spki, 0, sizeof(spki));
1460 ku.digitalSignature = 1;
1462 /* Make a CSR (halfway -- we don't need to sign it here) */
1463 /* XXX Load impersonation key just once?? */
1464 ret = load_priv_key(r->context, impersonation_key_fn, &key);
1465 if (ret == 0)
1466 ret = hx509_request_init(r->context->hx509ctx, &req);
1467 if (ret == 0)
1468 ret = krb5_parse_name(r->context, cname, &p);
1469 if (ret == 0)
1470 ret = hx509_private_key2SPKI(r->context->hx509ctx, key, &spki);
1471 if (ret == 0)
1472 hx509_request_set_SubjectPublicKeyInfo(r->context->hx509ctx, req,
1473 &spki);
1474 free_SubjectPublicKeyInfo(&spki);
1475 if (ret == 0)
1476 ret = hx509_request_add_pkinit(r->context->hx509ctx, req, cname);
1477 if (ret == 0)
1478 ret = hx509_request_add_eku(r->context->hx509ctx, req,
1479 &asn1_oid_id_pkekuoid);
1481 /* Mark it authorized */
1482 if (ret == 0)
1483 ret = hx509_request_authorize_san(req, 0);
1484 if (ret == 0)
1485 ret = hx509_request_authorize_eku(req, 0);
1486 if (ret == 0)
1487 hx509_request_authorize_ku(req, ku);
1489 /* Issue the certificate */
1490 if (ret == 0)
1491 ret = kdc_issue_certificate(r->context, "get-tgt", logfac, req, p,
1492 &r->token_times, r->req_life,
1493 1 /* send_chain */, &certs);
1494 krb5_free_principal(r->context, p);
1495 hx509_request_free(&req);
1496 p = NULL;
1498 if (ret == KRB5KDC_ERR_POLICY || ret == EACCES) {
1499 hx509_private_key_free(&key);
1500 return bad_403(r, ret,
1501 "Certificate request denied for policy reasons");
1503 if (ret == ENOMEM) {
1504 hx509_private_key_free(&key);
1505 return bad_503(r, ret, "Certificate issuance failed");
1507 if (ret) {
1508 hx509_private_key_free(&key);
1509 return bad_500(r, ret, "Certificate issuance failed");
1512 /* Setup PKIX store and extract the certificate chain into it */
1513 ret = mk_pkix_store(&r->pkix_store);
1514 if (ret == 0)
1515 ret = store_certs(r->context->hx509ctx, r->pkix_store, certs, key);
1516 hx509_private_key_free(&key);
1517 hx509_certs_free(&certs);
1518 if (ret)
1519 return bad_500(r, ret,
1520 "Could not create PEM store for issued certificate");
1521 return 0;
1524 /* Get impersonated Kerberos credentials for `cprinc' */
1525 static krb5_error_code
1526 k5_get_creds(struct bx509_request_desc *r, enum k5_creds_kind kind)
1528 krb5_error_code ret;
1529 const char *cname = r->for_cname ? r->for_cname : r->cname;
1531 /* If we have a live ccache for `cprinc', we're done */
1532 r->cckind = kind;
1533 if (kind == K5_CREDS_CACHED &&
1534 (ret = find_ccache(r->context, cname, &r->ccname)) == 0)
1535 return ret; /* Success */
1538 * Else we have to acquire a credential for them using their bearer token
1539 * for authentication (and our keytab / initiator credentials perhaps).
1541 if ((ret = k5_do_CA(r)))
1542 return ret; /* k5_do_CA() calls bad_req() */
1544 if (ret == 0)
1545 ret = do_pkinit(r, kind);
1546 return ret;
1549 /* Accumulate strings */
1550 static void
1551 acc_str(char **acc, char *adds, size_t addslen)
1553 char *tmp = NULL;
1554 int l = addslen <= INT_MAX ? (int)addslen : INT_MAX;
1556 if (asprintf(&tmp, "%s%s%.*s",
1557 *acc ? *acc : "",
1558 *acc ? "; " : "", l, adds) > -1 &&
1559 tmp) {
1560 free(*acc);
1561 *acc = tmp;
1565 static char *
1566 fmt_gss_error(OM_uint32 code, gss_OID mech)
1568 gss_buffer_desc buf;
1569 OM_uint32 major, minor;
1570 OM_uint32 type = mech == GSS_C_NO_OID ? GSS_C_GSS_CODE: GSS_C_MECH_CODE;
1571 OM_uint32 more = 0;
1572 char *r = NULL;
1574 do {
1575 major = gss_display_status(&minor, code, type, mech, &more, &buf);
1576 if (!GSS_ERROR(major))
1577 acc_str(&r, (char *)buf.value, buf.length);
1578 gss_release_buffer(&minor, &buf);
1579 } while (!GSS_ERROR(major) && more);
1580 return r;
1583 static char *
1584 fmt_gss_errors(const char *r, OM_uint32 major, OM_uint32 minor, gss_OID mech)
1586 char *ma, *mi, *s;
1588 ma = fmt_gss_error(major, GSS_C_NO_OID);
1589 mi = mech == GSS_C_NO_OID ? NULL : fmt_gss_error(minor, mech);
1590 if (asprintf(&s, "%s: %s%s%s", r,
1591 ma ? ma : "Out of memory",
1592 mi ? ": " : "",
1593 mi ? mi : "") > -1 &&
1594 s) {
1595 free(ma);
1596 free(mi);
1597 return s;
1599 free(mi);
1600 return ma;
1603 /* GSS-API error */
1604 static krb5_error_code
1605 bad_req_gss(struct bx509_request_desc *r,
1606 OM_uint32 major,
1607 OM_uint32 minor,
1608 gss_OID mech,
1609 int http_status_code,
1610 const char *reason)
1612 krb5_error_code ret;
1613 char *msg = fmt_gss_errors(reason, major, minor, mech);
1615 if (major == GSS_S_BAD_NAME || major == GSS_S_BAD_NAMETYPE)
1616 http_status_code = MHD_HTTP_BAD_REQUEST;
1618 if (msg)
1619 ret = resp(r, http_status_code, MHD_RESPMEM_MUST_COPY, NULL,
1620 msg, strlen(msg), NULL);
1621 else
1622 ret = resp(r, http_status_code, MHD_RESPMEM_MUST_COPY, NULL,
1623 "Out of memory while formatting GSS error message",
1624 sizeof("Out of memory while formatting GSS error message") - 1, NULL);
1625 free(msg);
1626 return ret;
1629 /* Make an HTTP/Negotiate token */
1630 static krb5_error_code
1631 mk_nego_tok(struct bx509_request_desc *r,
1632 char **nego_tok,
1633 size_t *nego_toksz)
1635 gss_key_value_element_desc kv[1] = { { "ccache", r->ccname } };
1636 gss_key_value_set_desc store = { 1, kv };
1637 gss_buffer_desc token = GSS_C_EMPTY_BUFFER;
1638 gss_buffer_desc name = GSS_C_EMPTY_BUFFER;
1639 gss_cred_id_t cred = GSS_C_NO_CREDENTIAL;
1640 gss_ctx_id_t ctx = GSS_C_NO_CONTEXT;
1641 gss_name_t iname = GSS_C_NO_NAME;
1642 gss_name_t aname = GSS_C_NO_NAME;
1643 OM_uint32 major, minor, junk;
1644 krb5_error_code ret; /* More like a system error code here */
1645 const char *cname = r->for_cname ? r->for_cname : r->cname;
1646 char *token_b64 = NULL;
1648 *nego_tok = NULL;
1649 *nego_toksz = 0;
1651 /* Import initiator name */
1652 name.length = strlen(cname);
1653 name.value = rk_UNCONST(cname);
1654 major = gss_import_name(&minor, &name, GSS_KRB5_NT_PRINCIPAL_NAME, &iname);
1655 if (major != GSS_S_COMPLETE)
1656 return bad_req_gss(r, major, minor, GSS_C_NO_OID,
1657 MHD_HTTP_SERVICE_UNAVAILABLE,
1658 "Could not import cprinc parameter value as "
1659 "Kerberos principal name");
1661 /* Import target acceptor name */
1662 name.length = strlen(r->target);
1663 name.value = rk_UNCONST(r->target);
1664 major = gss_import_name(&minor, &name, GSS_C_NT_HOSTBASED_SERVICE, &aname);
1665 if (major != GSS_S_COMPLETE) {
1666 (void) gss_release_name(&junk, &iname);
1667 return bad_req_gss(r, major, minor, GSS_C_NO_OID,
1668 MHD_HTTP_SERVICE_UNAVAILABLE,
1669 "Could not import target parameter value as "
1670 "Kerberos principal name");
1673 /* Acquire a credential from the given ccache */
1674 major = gss_add_cred_from(&minor, cred, iname, GSS_KRB5_MECHANISM,
1675 GSS_C_INITIATE, GSS_C_INDEFINITE, 0, &store,
1676 &cred, NULL, NULL, NULL);
1677 (void) gss_release_name(&junk, &iname);
1678 if (major != GSS_S_COMPLETE) {
1679 (void) gss_release_name(&junk, &aname);
1680 return bad_req_gss(r, major, minor, GSS_KRB5_MECHANISM,
1681 MHD_HTTP_FORBIDDEN, "Could not acquire credentials "
1682 "for requested cprinc");
1685 major = gss_init_sec_context(&minor, cred, &ctx, aname,
1686 GSS_KRB5_MECHANISM, 0, GSS_C_INDEFINITE,
1687 NULL, GSS_C_NO_BUFFER, NULL, &token, NULL,
1688 NULL);
1689 (void) gss_delete_sec_context(&junk, &ctx, GSS_C_NO_BUFFER);
1690 (void) gss_release_name(&junk, &aname);
1691 (void) gss_release_cred(&junk, &cred);
1692 if (major != GSS_S_COMPLETE)
1693 return bad_req_gss(r, major, minor, GSS_KRB5_MECHANISM,
1694 MHD_HTTP_SERVICE_UNAVAILABLE, "Could not acquire "
1695 "Negotiate token for requested target");
1697 /* Encode token, output */
1698 ret = rk_base64_encode(token.value, token.length, &token_b64);
1699 (void) gss_release_buffer(&junk, &token);
1700 if (ret > 0)
1701 ret = asprintf(nego_tok, "Negotiate %s", token_b64);
1702 free(token_b64);
1703 if (ret < 0 || *nego_tok == NULL)
1704 return bad_req(r, errno, MHD_HTTP_SERVICE_UNAVAILABLE,
1705 "Could not allocate memory for encoding Negotiate "
1706 "token");
1707 *nego_toksz = ret;
1708 return 0;
1711 static krb5_error_code
1712 bnegotiate_get_target(struct bx509_request_desc *r)
1714 const char *target;
1715 const char *redir;
1716 const char *referer; /* misspelled on the wire, misspelled here, FYI */
1717 const char *authority;
1718 const char *local_part;
1719 char *s1 = NULL;
1720 char *s2 = NULL;
1722 target = MHD_lookup_connection_value(r->connection, MHD_GET_ARGUMENT_KIND,
1723 "target");
1724 redir = MHD_lookup_connection_value(r->connection, MHD_GET_ARGUMENT_KIND,
1725 "redirect");
1726 referer = MHD_lookup_connection_value(r->connection, MHD_HEADER_KIND,
1727 MHD_HTTP_HEADER_REFERER);
1728 if (target != NULL && redir == NULL) {
1729 r->target = target;
1730 return 0;
1732 if (target == NULL && redir == NULL)
1733 return bad_400(r, EINVAL,
1734 "Query missing 'target' or 'redirect' parameter value");
1735 if (target != NULL && redir != NULL)
1736 return bad_403(r, EACCES,
1737 "Only one of 'target' or 'redirect' parameter allowed");
1738 if (redir != NULL && referer == NULL)
1739 return bad_403(r, EACCES,
1740 "Redirect request without Referer header nor allowed");
1742 if (strncmp(referer, "https://", sizeof("https://") - 1) != 0 ||
1743 strncmp(redir, "https://", sizeof("https://") - 1) != 0)
1744 return bad_403(r, EACCES,
1745 "Redirect requests permitted only for https referrers");
1747 /* Parse out authority from each URI, redirect and referrer */
1748 authority = redir + sizeof("https://") - 1;
1749 if ((local_part = strchr(authority, '/')) == NULL)
1750 local_part = authority + strlen(authority);
1751 if ((s1 = strndup(authority, local_part - authority)) == NULL)
1752 return bad_enomem(r, ENOMEM);
1754 authority = referer + sizeof("https://") - 1;
1755 if ((local_part = strchr(authority, '/')) == NULL)
1756 local_part = authority + strlen(authority);
1757 if ((s2 = strndup(authority, local_part - authority)) == NULL) {
1758 free(s1);
1759 return bad_enomem(r, ENOMEM);
1762 /* Both must match */
1763 if (strcasecmp(s1, s2) != 0) {
1764 free(s2);
1765 free(s1);
1766 return bad_403(r, EACCES, "Redirect request does not match referer");
1768 free(s2);
1770 if (strchr(s1, '@')) {
1771 free(s1);
1772 return bad_403(r, EACCES,
1773 "Redirect request authority has login information");
1776 /* Extract hostname portion of authority and format GSS name */
1777 if (strchr(s1, ':'))
1778 *strchr(s1, ':') = '\0';
1779 if (asprintf(&r->freeme1, "HTTP@%s", s1) == -1 || r->freeme1 == NULL) {
1780 free(s1);
1781 return bad_enomem(r, ENOMEM);
1784 r->target = r->freeme1;
1785 r->redir = redir;
1786 free(s1);
1787 return 0;
1791 * Implements /bnegotiate end-point.
1793 * Query parameters (mutually exclusive):
1795 * - target=<name>
1796 * - redirect=<URL-encoded-URL>
1798 * If the redirect query parameter is set then the Referer: header must be as
1799 * well, and the authority of the redirect and Referer URIs must be the same.
1801 static krb5_error_code
1802 bnegotiate(struct bx509_request_desc *r)
1804 krb5_error_code ret;
1805 size_t nego_toksz = 0;
1806 char *nego_tok = NULL;
1808 ret = bnegotiate_get_target(r);
1809 if (ret)
1810 return ret; /* bnegotiate_get_target() calls bad_req() */
1811 heim_audit_addkv((heim_svc_req_desc)r, KDC_AUDIT_VIS, "target", "%s",
1812 r->target ? r->target : "<unknown>");
1813 heim_audit_setkv_bool((heim_svc_req_desc)r, "redir", !!r->redir);
1816 * Make sure we have Kerberos credentials for cprinc. If we have them
1817 * cached from earlier, this will be fast (all local), else it will involve
1818 * taking a file lock and talking to the KDC using kx509 and PKINIT.
1820 * Perhaps we could use S4U instead, which would speed up the slow path a
1821 * bit.
1823 ret = k5_get_creds(r, K5_CREDS_CACHED);
1824 if (ret)
1825 return bad_403(r, ret,
1826 "Could not acquire Kerberos credentials using PKINIT");
1828 /* Acquire the Negotiate token and output it */
1829 if (ret == 0 && r->ccname != NULL)
1830 ret = mk_nego_tok(r, &nego_tok, &nego_toksz);
1832 if (ret == 0) {
1833 /* Look ma', Negotiate as an OAuth-like token system! */
1834 if (r->redir)
1835 ret = resp(r, MHD_HTTP_TEMPORARY_REDIRECT, MHD_RESPMEM_PERSISTENT,
1836 NULL, "", 0, nego_tok);
1837 else
1838 ret = resp(r, MHD_HTTP_OK, MHD_RESPMEM_MUST_COPY,
1839 "application/x-negotiate-token", nego_tok, nego_toksz,
1840 NULL);
1843 free(nego_tok);
1844 return ret;
1847 static krb5_error_code
1848 authorize_TGT_REQ(struct bx509_request_desc *r)
1850 krb5_principal p = NULL;
1851 krb5_error_code ret;
1852 const char *for_cname = r->for_cname ? r->for_cname : r->cname;
1854 if (for_cname == r->cname || strcmp(r->cname, r->for_cname) == 0)
1855 return 0;
1857 ret = hx509_request_init(r->context->hx509ctx, &r->req);
1858 if (ret)
1859 return bad_500(r, ret, "Out of resources");
1860 heim_audit_addkv((heim_svc_req_desc)r, KDC_AUDIT_VIS,
1861 "requested_krb5PrincipalName", "%s", for_cname);
1862 ret = hx509_request_add_eku(r->context->hx509ctx, r->req,
1863 ASN1_OID_ID_PKEKUOID);
1864 if (ret == 0)
1865 ret = hx509_request_add_pkinit(r->context->hx509ctx, r->req,
1866 for_cname);
1867 if (ret == 0)
1868 ret = krb5_parse_name(r->context, r->cname, &p);
1869 if (ret == 0)
1870 ret = kdc_authorize_csr(r->context, "get-tgt", r->req, p);
1871 krb5_free_principal(r->context, p);
1872 hx509_request_free(&r->req);
1873 r->req = NULL;
1874 if (ret)
1875 return bad_403(r, ret, "Not authorized to requested TGT");
1876 return ret;
1879 static heim_mhd_result
1880 get_tgt_param_cb(void *d,
1881 enum MHD_ValueKind kind,
1882 const char *key,
1883 const char *val)
1885 struct bx509_request_desc *r = d;
1887 if (strcmp(key, "address") == 0 && val) {
1888 if (!krb5_config_get_bool_default(r->context, NULL,
1889 FALSE,
1890 "get-tgt", "allow_addresses", NULL)) {
1891 krb5_set_error_message(r->context, r->error_code = ENOTSUP,
1892 "Query parameter %s not allowed", key);
1893 } else {
1894 krb5_addresses addresses;
1896 r->error_code = _krb5_parse_address_no_lookup(r->context, val,
1897 &addresses);
1898 if (r->error_code == 0)
1899 r->error_code = krb5_append_addresses(r->context, &r->tgt_addresses,
1900 &addresses);
1901 krb5_free_addresses(r->context, &addresses);
1903 } else if (strcmp(key, "cname") == 0) {
1904 /* Handled upstairs */
1906 } else if (strcmp(key, "lifetime") == 0 && val) {
1907 r->req_life = parse_time(val, "day");
1908 } else {
1909 /* Produce error for unknown params */
1910 heim_audit_setkv_bool((heim_svc_req_desc)r, "requested_unknown", TRUE);
1911 krb5_set_error_message(r->context, r->error_code = ENOTSUP,
1912 "Query parameter %s not supported", key);
1914 return r->error_code == 0 ? MHD_YES : MHD_NO /* Stop iterating */;
1918 * Implements /get-tgt end-point.
1920 * Query parameters:
1922 * - cname=<name> (client principal name, if not the same as the authenticated
1923 * name, then this will be impersonated if allowed; may be
1924 * given only once)
1926 * - address=<IP> (IP address to add as a ticket address; may be given
1927 * multiple times)
1929 * - lifetime=<time> (requested lifetime for the ticket; may be given only
1930 * once)
1932 static krb5_error_code
1933 get_tgt(struct bx509_request_desc *r)
1935 krb5_error_code ret;
1936 size_t bodylen;
1937 const char *fn;
1938 void *body;
1940 r->for_cname = MHD_lookup_connection_value(r->connection,
1941 MHD_GET_ARGUMENT_KIND, "cname");
1942 if (r->for_cname && r->for_cname[0] == '\0')
1943 r->for_cname = NULL;
1944 ret = authorize_TGT_REQ(r);
1945 if (ret)
1946 return ret; /* authorize_TGT_REQ() calls bad_req() */
1948 r->error_code = 0;
1949 (void) MHD_get_connection_values(r->connection, MHD_GET_ARGUMENT_KIND,
1950 get_tgt_param_cb, r);
1951 ret = r->error_code;
1953 /* k5_get_creds() calls bad_req() */
1954 if (ret == 0)
1955 ret = k5_get_creds(r, K5_CREDS_EPHEMERAL);
1956 if (ret)
1957 return bad_403(r, ret,
1958 "Could not acquire Kerberos credentials using PKINIT");
1960 fn = strchr(r->ccname, ':');
1961 if (fn == NULL)
1962 return bad_500(r, ret, "Impossible error");
1963 fn++;
1964 if ((errno = rk_undumpdata(fn, &body, &bodylen)))
1965 return bad_503(r, ret, "Could not get TGT");
1967 ret = resp(r, MHD_HTTP_OK, MHD_RESPMEM_MUST_COPY,
1968 "application/x-krb5-ccache", body, bodylen, NULL);
1969 free(body);
1970 return ret;
1973 static int
1974 get_tgts_accumulate_ccache_write_json(struct bx509_request_desc *r,
1975 krb5_error_code code,
1976 const char *data,
1977 size_t datalen)
1979 heim_object_t k, v;
1980 heim_string_t text;
1981 heim_error_t e = NULL;
1982 heim_dict_t o;
1983 int ret;
1985 o = heim_dict_create(9);
1986 k = heim_string_create("name");
1987 v = heim_string_create(r->for_cname);
1988 if (o && k && v)
1989 ret = heim_dict_set_value(o, k, v);
1990 else
1991 ret = ENOMEM;
1993 if (ret == 0) {
1994 heim_release(v);
1995 heim_release(k);
1996 k = heim_string_create("error_code");
1997 v = heim_number_create(code);
1998 if (k && v)
1999 ret = heim_dict_set_value(o, k, v);
2001 if (ret == 0 && data != NULL) {
2002 heim_release(v);
2003 heim_release(k);
2004 k = heim_string_create("ccache");
2005 v = heim_data_create(data, datalen);
2006 if (k && v)
2007 ret = heim_dict_set_value(o, k, v);
2009 if (ret == 0 && code != 0) {
2010 const char *s = krb5_get_error_message(r->context, code);
2012 heim_release(v);
2013 heim_release(k);
2014 k = heim_string_create("error");
2015 v = heim_string_create(s ? s : "Out of memory");
2016 krb5_free_error_message(r->context, s);
2017 if (k && v)
2018 ret = heim_dict_set_value(o, k, v);
2020 heim_release(v);
2021 heim_release(k);
2022 if (ret) {
2023 heim_release(o);
2024 return bad_503(r, errno, "Out of memory");
2027 text = heim_json_copy_serialize(o,
2028 HEIM_JSON_F_NO_DATA_DICT |
2029 HEIM_JSON_F_ONE_LINE,
2030 &e);
2031 if (text) {
2032 const char *s = heim_string_get_utf8(text);
2034 (void) fwrite(s, strlen(s), 1, r->tgts);
2035 } else {
2036 const char *s = NULL;
2037 v = heim_error_copy_string(e);
2038 if (v)
2039 s = heim_string_get_utf8(v);
2040 if (s == NULL)
2041 s = "<unknown encoder error>";
2042 krb5_log_msg(r->context, logfac, 1, NULL, "Failed to encode JSON text with ccache or error for %s: %s",
2043 r->for_cname, s);
2044 heim_release(v);
2046 heim_release(text);
2047 heim_release(o);
2048 return MHD_YES;
2051 /* Writes one ccache to a response file, as JSON */
2052 static int
2053 get_tgts_accumulate_ccache(struct bx509_request_desc *r, krb5_error_code ret)
2055 const char *fn;
2056 size_t bodylen = 0;
2057 void *body = NULL;
2058 int res;
2060 if (r->tgts == NULL) {
2061 int fd = -1;
2063 if (asprintf(&r->tgts_filename,
2064 "%s/tgts-json-XXXXXX", cache_dir) == -1 ||
2065 r->tgts_filename == NULL) {
2066 free(r->tgts_filename);
2067 r->tgts_filename = NULL;
2069 return bad_enomem(r, r->error_code = ENOMEM);
2071 if ((fd = mkstemp(r->tgts_filename)) == -1)
2072 return bad_req(r, errno, MHD_HTTP_SERVICE_UNAVAILABLE,
2073 "%s", strerror(r->error_code = errno));
2074 if ((r->tgts = fdopen(fd, "w+")) == NULL) {
2075 (void) close(fd);
2076 return bad_req(r, errno, MHD_HTTP_SERVICE_UNAVAILABLE,
2077 "%s", strerror(r->error_code = errno));
2081 if (ret == 0) {
2082 fn = strchr(r->ccname, ':');
2083 if (fn == NULL)
2084 return bad_req(r, errno, MHD_HTTP_SERVICE_UNAVAILABLE,
2085 "Internal error (invalid credentials cache name)");
2086 fn++;
2087 if ((r->error_code = rk_undumpdata(fn, &body, &bodylen)))
2088 return bad_req(r, errno, MHD_HTTP_SERVICE_UNAVAILABLE,
2089 "%s", strerror(r->error_code));
2090 (void) unlink(fn);
2091 free(r->ccname);
2092 r->ccname = NULL;
2093 if (bodylen > INT_MAX >> 4) {
2094 free(body);
2095 return bad_req(r, errno, MHD_HTTP_SERVICE_UNAVAILABLE,
2096 "Credentials cache too large!");
2100 res = get_tgts_accumulate_ccache_write_json(r, ret, body, bodylen);
2101 free(body);
2102 return res;
2105 static heim_mhd_result
2106 get_tgts_param_authorize_cb(void *d,
2107 enum MHD_ValueKind kind,
2108 const char *key,
2109 const char *val)
2111 struct bx509_request_desc *r = d;
2112 krb5_error_code ret = 0;
2114 if (strcmp(key, "cname") != 0 || val == NULL)
2115 return MHD_YES;
2117 if (r->req == NULL) {
2118 ret = hx509_request_init(r->context->hx509ctx, &r->req);
2119 if (ret == 0)
2120 ret = hx509_request_add_eku(r->context->hx509ctx, r->req,
2121 ASN1_OID_ID_PKEKUOID);
2122 if (ret)
2123 return bad_500(r, ret, "Out of resources");
2125 heim_audit_addkv((heim_svc_req_desc)r, KDC_AUDIT_VIS,
2126 "requested_krb5PrincipalName", "%s", val);
2127 ret = hx509_request_add_pkinit(r->context->hx509ctx, r->req,
2128 val);
2129 if (ret)
2130 return bad_403(r, ret, "Not authorized to requested TGT");
2131 return MHD_YES;
2134 /* For each requested principal, produce a ccache */
2135 static heim_mhd_result
2136 get_tgts_param_execute_cb(void *d,
2137 enum MHD_ValueKind kind,
2138 const char *key,
2139 const char *val)
2141 struct bx509_request_desc *r = d;
2142 hx509_san_type san_type;
2143 krb5_error_code ret;
2144 size_t san_idx = r->san_idx++;
2145 const char *save_for_cname = r->for_cname;
2146 char *s = NULL;
2147 int res;
2149 /* We expect only cname=principal q-params here */
2150 if (strcmp(key, "cname") != 0 || val == NULL)
2151 return MHD_YES;
2154 * We expect the `san_idx'th SAN in the `r->req' request checked by
2155 * kdc_authorize_csr() to be the same as this cname. This happens
2156 * naturally because we add these SANs to `r->req' in the same order as we
2157 * visit them here (unless our HTTP library somehow went crazy).
2159 * Still, we check that it's the same SAN.
2161 ret = hx509_request_get_san(r->req, san_idx, &san_type, &s);
2162 if (ret == HX509_NO_ITEM ||
2163 san_type != HX509_SAN_TYPE_PKINIT ||
2164 strcmp(s, val) != 0) {
2166 * If the cname and SAN don't match, it's some weird internal error
2167 * (can't happen).
2169 krb5_set_error_message(r->context, r->error_code = EACCES,
2170 "PKINIT SAN not granted: %s (internal error)",
2171 val);
2172 ret = EACCES;
2176 * We're going to pretend to be this SAN for the purpose of acquring a TGT
2177 * for it. So we "push" `r->for_cname'.
2179 if (ret == 0)
2180 r->for_cname = val;
2183 * Our authorizer supports partial authorization where the whole request is
2184 * rejected but some features of it are permitted.
2186 * (In most end-points we don't want partial authorization, but in
2187 * /get-tgts we very much do.)
2189 if (ret == 0 && !hx509_request_san_authorized_p(r->req, san_idx)) {
2190 heim_audit_addkv((heim_svc_req_desc)r, KDC_AUDIT_VIS,
2191 "REJECT_krb5PrincipalName", "%s", val);
2192 krb5_set_error_message(r->context, r->error_code = EACCES,
2193 "PKINIT SAN denied: %s", val);
2194 ret = EACCES;
2196 if (ret == 0) {
2197 heim_audit_addkv((heim_svc_req_desc)r, KDC_AUDIT_VIS,
2198 "ACCEPT_krb5PrincipalName", "%s", val);
2199 ret = k5_get_creds(r, K5_CREDS_EPHEMERAL);
2200 if (ret == 0)
2201 heim_audit_addkv((heim_svc_req_desc)r, KDC_AUDIT_VIS,
2202 "ISSUE_krb5PrincipalName", "%s", val);
2206 * If ret == 0 this will gather the TGT we acquired, else it will acquire
2207 * the error we got.
2209 res = get_tgts_accumulate_ccache(r, ret);
2211 /* Now we "pop" `r->for_cname' */
2212 r->for_cname = save_for_cname;
2214 hx509_xfree(s);
2215 return res;
2219 * Implements /get-tgts end-point.
2221 * Query parameters:
2223 * - cname=<name> (client principal name, if not the same as the authenticated
2224 * name, then this will be impersonated if allowed; may be
2225 * given multiple times)
2227 static krb5_error_code
2228 get_tgts(struct bx509_request_desc *r)
2230 krb5_error_code ret;
2231 krb5_principal p = NULL;
2232 size_t bodylen;
2233 void *body;
2234 int res = MHD_YES;
2236 /* Prep to authorize */
2237 ret = krb5_parse_name(r->context, r->cname, &p);
2238 if (ret)
2239 return bad_403(r, ret, "Could not parse caller principal name");
2240 if (ret == 0) {
2241 /* Extract q-params other than `cname' */
2242 r->error_code = 0;
2243 res = MHD_get_connection_values(r->connection, MHD_GET_ARGUMENT_KIND,
2244 get_tgt_param_cb, r);
2245 if (r->response || res == MHD_NO) {
2246 krb5_free_principal(r->context, p);
2247 return res;
2250 ret = r->error_code;
2252 if (ret == 0) {
2254 * Check authorization of the authenticated client to the requested
2255 * client principal names (calls bad_req()).
2257 r->error_code = 0;
2258 res = MHD_get_connection_values(r->connection, MHD_GET_ARGUMENT_KIND,
2259 get_tgts_param_authorize_cb, r);
2260 if (r->response || res == MHD_NO) {
2261 krb5_free_principal(r->context, p);
2262 return res;
2265 ret = r->error_code;
2266 if (ret == 0) {
2267 /* Use the same configuration as /get-tgt (or should we?) */
2268 ret = kdc_authorize_csr(r->context, "get-tgt", r->req, p);
2271 * We tolerate EACCES because we support partial approval.
2273 * (KRB5_PLUGIN_NO_HANDLE means no plugin handled the authorization
2274 * check.)
2276 if (ret == EACCES || ret == KRB5_PLUGIN_NO_HANDLE)
2277 ret = 0;
2278 if (ret) {
2279 krb5_free_principal(r->context, p);
2280 return bad_403(r, ret, "Permission denied");
2284 if (ret == 0) {
2286 * Get the actual TGTs that were authorized.
2288 * get_tgts_param_execute_cb() calls bad_req()
2290 r->error_code = 0;
2291 res = MHD_get_connection_values(r->connection, MHD_GET_ARGUMENT_KIND,
2292 get_tgts_param_execute_cb, r);
2293 if (r->response || res == MHD_NO) {
2294 krb5_free_principal(r->context, p);
2295 return res;
2297 ret = r->error_code;
2299 krb5_free_principal(r->context, p);
2300 hx509_request_free(&r->req);
2301 r->req = NULL;
2304 * get_tgts_param_execute_cb() will write its JSON response to the file
2305 * named by r->ccname.
2307 if (fflush(r->tgts) != 0)
2308 return bad_503(r, ret, "Could not get TGT");
2309 if ((errno = rk_undumpdata(r->tgts_filename, &body, &bodylen)))
2310 return bad_503(r, ret, "Could not get TGT");
2312 ret = resp(r, MHD_HTTP_OK, MHD_RESPMEM_MUST_COPY,
2313 "application/x-krb5-ccaches-json", body, bodylen, NULL);
2314 free(body);
2315 return ret;
2318 static krb5_error_code
2319 health(const char *method, struct bx509_request_desc *r)
2321 if (strcmp(method, "HEAD") == 0)
2322 return resp(r, MHD_HTTP_OK, MHD_RESPMEM_PERSISTENT, NULL, "", 0, NULL);
2323 return resp(r, MHD_HTTP_OK, MHD_RESPMEM_PERSISTENT, NULL,
2324 "To determine the health of the service, use the /bx509 "
2325 "end-point.\n",
2326 sizeof("To determine the health of the service, use the "
2327 "/bx509 end-point.\n") - 1, NULL);
2331 static krb5_error_code
2332 mac_csrf_token(struct bx509_request_desc *r, krb5_storage *sp)
2334 krb5_error_code ret;
2335 krb5_data data;
2336 char mac[EVP_MAX_MD_SIZE];
2337 unsigned int maclen = sizeof(mac);
2338 HMAC_CTX *ctx = NULL;
2340 ret = krb5_storage_to_data(sp, &data);
2341 if (ret == 0 && (ctx = HMAC_CTX_new()) == NULL)
2342 ret = krb5_enomem(r->context);
2343 /* HMAC the token body and the client principal name */
2344 if (ret == 0) {
2345 if (HMAC_Init_ex(ctx, csrf_key, sizeof(csrf_key),
2346 EVP_sha256(),
2347 NULL) == 0) {
2348 HMAC_CTX_cleanup(ctx);
2349 ret = krb5_enomem(r->context);
2350 } else {
2351 HMAC_Update(ctx, data.data, data.length);
2352 if (r->cname)
2353 HMAC_Update(ctx, r->cname, strlen(r->cname));
2354 HMAC_Final(ctx, mac, &maclen);
2355 HMAC_CTX_cleanup(ctx);
2356 krb5_data_free(&data);
2357 data.length = maclen;
2358 data.data = mac;
2359 if (krb5_storage_write(sp, mac, maclen) != maclen)
2360 ret = krb5_enomem(r->context);
2363 if (ctx)
2364 HMAC_CTX_free(ctx);
2365 return ret;
2369 * Make a CSRF token. If one is also given, make one with the same body
2370 * content so we can check the HMAC.
2372 * Outputs the token and its age. Do not use either if the token does not
2373 * equal the given token.
2375 static krb5_error_code
2376 make_csrf_token(struct bx509_request_desc *r,
2377 const char *given,
2378 char **token,
2379 int64_t *age)
2381 krb5_error_code ret = 0;
2382 unsigned char given_decoded[128];
2383 krb5_storage *sp = NULL;
2384 krb5_data data;
2385 ssize_t dlen = -1;
2386 uint64_t nonce;
2387 int64_t t = 0;
2390 *age = 0;
2391 data.data = NULL;
2392 data.length = 0;
2393 if (given) {
2394 size_t len = strlen(given);
2396 /* Extract issue time and nonce from token */
2397 if (len >= sizeof(given_decoded))
2398 ret = ERANGE;
2399 if (ret == 0 && (dlen = rk_base64_decode(given, &given_decoded)) <= 0)
2400 ret = errno;
2401 if (ret == 0 &&
2402 (sp = krb5_storage_from_mem(given_decoded, dlen)) == NULL)
2403 ret = krb5_enomem(r->context);
2404 if (ret == 0)
2405 ret = krb5_ret_int64(sp, &t);
2406 if (ret == 0)
2407 ret = krb5_ret_uint64(sp, &nonce);
2408 krb5_storage_free(sp);
2409 sp = NULL;
2410 if (ret == 0)
2411 *age = time(NULL) - t;
2412 } else {
2413 t = time(NULL);
2414 krb5_generate_random_block((void *)&nonce, sizeof(nonce));
2417 if (ret == 0 && (sp = krb5_storage_emem()) == NULL)
2418 ret = krb5_enomem(r->context);
2419 if (ret == 0)
2420 ret = krb5_store_int64(sp, t);
2421 if (ret == 0)
2422 ret = krb5_store_uint64(sp, nonce);
2423 if (ret == 0)
2424 ret = mac_csrf_token(r, sp);
2425 if (ret == 0)
2426 ret = krb5_storage_to_data(sp, &data);
2427 if (ret == 0 && data.length > INT_MAX)
2428 ret = ERANGE;
2429 if (ret == 0 &&
2430 rk_base64_encode(data.data, data.length, token) < 0)
2431 ret = errno;
2432 krb5_storage_free(sp);
2433 krb5_data_free(&data);
2434 return ret;
2437 static heim_mhd_result
2438 validate_csrf_token(struct bx509_request_desc *r)
2440 const char *given;
2441 int64_t age;
2442 krb5_error_code ret;
2444 if ((((csrf_prot_type & CSRF_PROT_GET_WITH_HEADER) &&
2445 strcmp(r->method, "GET") == 0) ||
2446 ((csrf_prot_type & CSRF_PROT_POST_WITH_HEADER) &&
2447 strcmp(r->method, "POST") == 0)) &&
2448 MHD_lookup_connection_value(r->connection, MHD_HEADER_KIND,
2449 csrf_header) == NULL) {
2450 ret = bad_req(r, EACCES, MHD_HTTP_FORBIDDEN,
2451 "Request must have header \"%s\"", csrf_header);
2452 return ret == -1 ? MHD_NO : MHD_YES;
2455 if (strcmp(r->method, "GET") == 0 &&
2456 !(csrf_prot_type & CSRF_PROT_GET_WITH_TOKEN))
2457 return 0;
2458 if (strcmp(r->method, "POST") == 0 &&
2459 !(csrf_prot_type & CSRF_PROT_POST_WITH_TOKEN))
2460 return 0;
2462 given = MHD_lookup_connection_value(r->connection, MHD_HEADER_KIND,
2463 "X-CSRF-Token");
2464 ret = make_csrf_token(r, given, &r->csrf_token, &age);
2465 if (ret)
2466 return bad_503(r, ret, "Could not make or validate CSRF token");
2467 if (given == NULL)
2468 return bad_req(r, EACCES, MHD_HTTP_FORBIDDEN,
2469 "CSRF token needed; copy the X-CSRF-Token: response "
2470 "header to your next POST");
2471 if (strlen(given) != strlen(r->csrf_token) ||
2472 strcmp(given, r->csrf_token) != 0)
2473 return bad_403(r, EACCES, "Invalid CSRF token");
2474 if (age > 300)
2475 return bad_403(r, EACCES, "CSRF token expired");
2476 return 0;
2480 * MHD callback to free the request context when MHD is done sending the
2481 * response.
2483 static void
2484 cleanup_req(void *cls,
2485 struct MHD_Connection *connection,
2486 void **con_cls,
2487 enum MHD_RequestTerminationCode toe)
2489 struct bx509_request_desc *r = *con_cls;
2491 (void)cls;
2492 (void)connection;
2493 (void)toe;
2494 clean_req_desc(r);
2495 *con_cls = NULL;
2498 /* Callback for MHD POST form data processing */
2499 static heim_mhd_result
2500 ip(void *cls,
2501 enum MHD_ValueKind kind,
2502 const char *key,
2503 const char *content_name,
2504 const char *content_type,
2505 const char *transfer_encoding,
2506 const char *val,
2507 uint64_t off,
2508 size_t size)
2510 struct bx509_request_desc *r = cls;
2511 struct free_tend_list *ftl = calloc(1, sizeof(*ftl));
2512 char *keydup = strdup(key);
2513 char *valdup = strndup(val, size);
2515 (void)content_name; /* MIME attachment name */
2516 (void)content_type; /* Don't care -- MHD liked it */
2517 (void)transfer_encoding;
2518 (void)off; /* Offset in POST data */
2521 * We're going to MHD_set_connection_value(), but we need copies because
2522 * the MHD POST processor quite naturally keeps none of the chunks
2523 * received.
2525 if (ftl == NULL || keydup == NULL || valdup == NULL) {
2526 free(ftl);
2527 free(keydup);
2528 free(valdup);
2529 return MHD_NO;
2531 ftl->freeme1 = keydup;
2532 ftl->freeme2 = valdup;
2533 ftl->next = r->free_list;
2534 r->free_list = ftl;
2536 return MHD_set_connection_value(r->connection, MHD_GET_ARGUMENT_KIND,
2537 keydup, valdup);
2540 typedef krb5_error_code (*handler)(struct bx509_request_desc *);
2542 struct route {
2543 const char *local_part;
2544 handler h;
2545 unsigned int referer_ok:1;
2546 } routes[] = {
2547 { "/get-cert", bx509, 0 },
2548 { "/get-negotiate-token", bnegotiate, 1 },
2549 { "/get-tgt", get_tgt, 0 },
2550 { "/get-tgts", get_tgts, 0 },
2551 /* Lousy old names to be removed eventually */
2552 { "/bnegotiate", bnegotiate, 1 },
2553 { "/bx509", bx509, 0 },
2557 * We should commonalize all of:
2559 * - route() and related infrastructure
2560 * - including the CSRF functions
2561 * - and Negotiate/Bearer authentication
2563 * so that we end up with a simple framework that our daemons can invoke to
2564 * serve simple functions that take a fully-consumed request and send a
2565 * response.
2567 * Then:
2569 * - split out the CA and non-CA bits into separate daemons using that common
2570 * code,
2571 * - make httpkadmind use that common code,
2572 * - abstract out all the MHD stuff.
2575 /* Routes requests */
2576 static heim_mhd_result
2577 route(void *cls,
2578 struct MHD_Connection *connection,
2579 const char *url,
2580 const char *method,
2581 const char *version,
2582 const char *upload_data,
2583 size_t *upload_data_size,
2584 void **ctx)
2586 struct bx509_request_desc *r = *ctx;
2587 size_t i;
2588 int ret;
2590 if (r == NULL) {
2592 * This is the first call, right after headers were read.
2594 * We must return quickly so that any 100-Continue might be sent with
2595 * celerity. We want to make sure to send any 401s early, so we check
2596 * WWW-Authenticate now, not later.
2598 * We'll get called again to really do the processing. If we're
2599 * handling a POST then we'll also get called with upload_data != NULL,
2600 * possibly multiple times.
2602 if ((ret = set_req_desc(connection, method, url, &r)))
2603 return MHD_NO;
2604 *ctx = r;
2606 /* All requests other than /health require authentication */
2607 if (strcmp(url, "/health") == 0)
2608 return MHD_YES;
2611 * Authenticate and do CSRF protection.
2613 * If the Referer: header is set in the request, we don't want CSRF
2614 * protection as only /get-negotiate-token will accept a Referer:
2615 * header (see routes[] and below), so we'll call validate_csrf_token()
2616 * for the other routes or reject the request for having Referer: set.
2618 ret = validate_token(r);
2619 if (ret == 0 &&
2620 MHD_lookup_connection_value(r->connection, MHD_HEADER_KIND, "Referer") == NULL)
2621 ret = validate_csrf_token(r);
2624 * As this is the initial call to this handler, we must return now.
2626 * If authentication or CSRF protection failed then we'll already have
2627 * enqueued a 401, 403, or 5xx response and then we're done.
2629 * If both authentication and CSRF protection succeeded then no
2630 * response has been queued up and we'll get called again to finally
2631 * process the request, then this entire if block will not be executed.
2633 return ret == -1 ? MHD_NO : MHD_YES;
2636 /* Validate HTTP method */
2637 if (strcmp(method, "GET") != 0 &&
2638 strcmp(method, "POST") != 0 &&
2639 strcmp(method, "HEAD") != 0) {
2640 return bad_405(r, method) == -1 ? MHD_NO : MHD_YES;
2643 if ((strcmp(method, "HEAD") == 0 || strcmp(method, "GET") == 0) &&
2644 (strcmp(url, "/health") == 0 || strcmp(url, "/") == 0)) {
2645 /* /health end-point -- no authentication, no CSRF, no nothing */
2646 return health(method, r) == -1 ? MHD_NO : MHD_YES;
2649 if (r->cname == NULL)
2650 return bad_401(r, "Authorization token is missing");
2652 if (strcmp(method, "POST") == 0 && *upload_data_size != 0) {
2654 * Consume all the POST body and set form data as MHD_GET_ARGUMENT_KIND
2655 * (as if they had been URI query parameters).
2657 * We have to do this before we can MHD_queue_response() as MHD will
2658 * not consume the rest of the request body on its own, so it's an
2659 * error to MHD_queue_response() before we've done this, and if we do
2660 * then MHD just closes the connection.
2662 * 4KB should be more than enough buffer space for all the keys we
2663 * expect.
2665 if (r->pp == NULL)
2666 r->pp = MHD_create_post_processor(connection, 4096, ip, r);
2667 if (r->pp == NULL) {
2668 ret = bad_503(r, errno ? errno : ENOMEM,
2669 "Could not consume POST data");
2670 return ret == -1 ? MHD_NO : MHD_YES;
2672 if (r->post_data_size + *upload_data_size > 1UL<<17) {
2673 return bad_413(r) == -1 ? MHD_NO : MHD_YES;
2675 r->post_data_size += *upload_data_size;
2676 if (MHD_post_process(r->pp, upload_data,
2677 *upload_data_size) == MHD_NO) {
2678 ret = bad_503(r, errno ? errno : ENOMEM,
2679 "Could not consume POST data");
2680 return ret == -1 ? MHD_NO : MHD_YES;
2682 *upload_data_size = 0;
2683 return MHD_YES;
2687 * Either this is a HEAD, a GET, or a POST whose request body has now been
2688 * received completely and processed.
2691 /* Allow GET? */
2692 if (strcmp(method, "GET") == 0 && !allow_GET_flag) {
2693 /* No */
2694 return bad_405(r, method) == -1 ? MHD_NO : MHD_YES;
2697 for (i = 0; i < sizeof(routes)/sizeof(routes[0]); i++) {
2698 if (strcmp(url, routes[i].local_part) != 0)
2699 continue;
2700 if (!routes[i].referer_ok &&
2701 MHD_lookup_connection_value(r->connection,
2702 MHD_HEADER_KIND,
2703 "Referer") != NULL) {
2704 ret = bad_req(r, EACCES, MHD_HTTP_FORBIDDEN,
2705 "GET from browser not allowed");
2706 return ret == -1 ? MHD_NO : MHD_YES;
2708 if (strcmp(method, "HEAD") == 0)
2709 ret = resp(r, MHD_HTTP_OK, MHD_RESPMEM_PERSISTENT, NULL, "", 0,
2710 NULL);
2711 else
2712 ret = routes[i].h(r);
2713 return ret == -1 ? MHD_NO : MHD_YES;
2716 ret = bad_404(r, url);
2717 return ret == -1 ? MHD_NO : MHD_YES;
2720 static struct getargs args[] = {
2721 { "help", 'h', arg_flag, &help_flag, "Print usage message", NULL },
2722 { "version", '\0', arg_flag, &version_flag, "Print version", NULL },
2723 { NULL, 'H', arg_strings, &audiences,
2724 "expected token audience(s)", "HOSTNAME" },
2725 { "daemon", 'd', arg_flag, &daemonize, "daemonize", "daemonize" },
2726 { "daemon-child", 0, arg_flag, &daemon_child_fd, NULL, NULL }, /* priv */
2727 { "reverse-proxied", 0, arg_flag, &reverse_proxied_flag,
2728 "reverse proxied", "listen on 127.0.0.1 and do not use TLS" },
2729 { "port", 'p', arg_integer, &port, "port number (default: 443)", "PORT" },
2730 { "cache-dir", 0, arg_string, &cache_dir,
2731 "cache directory", "DIRECTORY" },
2732 { "allow-GET", 0, arg_negative_flag, &allow_GET_flag, NULL, NULL },
2733 { "csrf-header", 0, arg_flag,
2734 &csrf_header, "required request header", "HEADER-NAME" },
2735 { "csrf-protection-type", 0, arg_strings, &csrf_prot_type_strs,
2736 "Anti-CSRF protection type", "TYPE" },
2737 { "csrf-key-file", 0, arg_string, &csrf_key_file,
2738 "CSRF MAC key", "FILE" },
2739 { "cert", 0, arg_string, &cert_file,
2740 "certificate file path (PEM)", "HX509-STORE" },
2741 { "private-key", 0, arg_string, &priv_key_file,
2742 "private key file path (PEM)", "HX509-STORE" },
2743 { "thread-per-client", 't', arg_flag, &thread_per_client_flag,
2744 "thread per-client", "use thread per-client" },
2745 { "verbose", 'v', arg_counter, &verbose_counter, "verbose", "run verbosely" }
2748 static int
2749 usage(int e)
2751 arg_printusage(args, sizeof(args) / sizeof(args[0]), "bx509",
2752 "\nServes RESTful GETs of /get-cert, /get-tgt, /get-tgts, and\n"
2753 "/get-negotiate-toke, performing corresponding kx509 and, \n"
2754 "possibly, PKINIT requests to the KDCs of the requested \n"
2755 "realms (or just the given REALM).\n");
2756 exit(e);
2759 static int sigpipe[2] = { -1, -1 };
2761 static void
2762 sighandler(int sig)
2764 char c = sig;
2765 while (write(sigpipe[1], &c, sizeof(c)) == -1 && errno == EINTR)
2769 static void
2770 bx509_openlog(krb5_context context,
2771 const char *svc,
2772 krb5_log_facility **fac)
2774 char **s = NULL, **p;
2776 krb5_initlog(context, "bx509d", fac);
2777 s = krb5_config_get_strings(context, NULL, svc, "logging", NULL);
2778 if (s == NULL)
2779 s = krb5_config_get_strings(context, NULL, "logging", svc, NULL);
2780 if (s) {
2781 for(p = s; *p; p++)
2782 krb5_addlog_dest(context, *fac, *p);
2783 krb5_config_free_strings(s);
2784 } else {
2785 char *ss;
2786 if (asprintf(&ss, "0-1/FILE:%s/%s", hdb_db_dir(context),
2787 KDC_LOG_FILE) < 0)
2788 err(1, "out of memory");
2789 krb5_addlog_dest(context, *fac, ss);
2790 free(ss);
2792 krb5_set_warn_dest(context, *fac);
2795 static const char *sysplugin_dirs[] = {
2796 #ifdef _WIN32
2797 "$ORIGIN",
2798 #else
2799 "$ORIGIN/../lib/plugin/kdc",
2800 #endif
2801 #ifdef __APPLE__
2802 LIBDIR "/plugin/kdc",
2803 #endif
2804 NULL
2807 static void
2808 load_plugins(krb5_context context)
2810 const char * const *dirs = sysplugin_dirs;
2811 #ifndef _WIN32
2812 char **cfdirs;
2814 cfdirs = krb5_config_get_strings(context, NULL, "kdc", "plugin_dir", NULL);
2815 if (cfdirs)
2816 dirs = (const char * const *)cfdirs;
2817 #endif
2819 /* XXX kdc? */
2820 _krb5_load_plugins(context, "kdc", (const char **)dirs);
2822 #ifndef _WIN32
2823 krb5_config_free_strings(cfdirs);
2824 #endif
2827 static void
2828 get_csrf_prot_type(krb5_context context)
2830 char * const *strs = csrf_prot_type_strs.strings;
2831 size_t n = csrf_prot_type_strs.num_strings;
2832 size_t i;
2833 char **freeme = NULL;
2835 if (csrf_header == NULL)
2836 csrf_header = krb5_config_get_string(context, NULL, "bx509d",
2837 "csrf_protection_csrf_header",
2838 NULL);
2840 if (n == 0) {
2841 char * const *p;
2843 strs = freeme = krb5_config_get_strings(context, NULL, "bx509d",
2844 "csrf_protection_type", NULL);
2845 for (p = strs; p && p; p++)
2846 n++;
2849 for (i = 0; i < n; i++) {
2850 if (strcmp(strs[i], "GET-with-header") == 0)
2851 csrf_prot_type |= CSRF_PROT_GET_WITH_HEADER;
2852 else if (strcmp(strs[i], "GET-with-token") == 0)
2853 csrf_prot_type |= CSRF_PROT_GET_WITH_TOKEN;
2854 else if (strcmp(strs[i], "POST-with-header") == 0)
2855 csrf_prot_type |= CSRF_PROT_POST_WITH_HEADER;
2856 else if (strcmp(strs[i], "POST-with-token") == 0)
2857 csrf_prot_type |= CSRF_PROT_POST_WITH_TOKEN;
2859 free(freeme);
2862 * For GETs we default to no CSRF protection as our GETable resources are
2863 * safe and idempotent and we count on the browser not to make the
2864 * responses available to cross-site requests.
2866 * But, really, we don't want browsers even making these requests since, if
2867 * the browsers behave correctly, then there's no point, and if they don't
2868 * behave correctly then that could be catastrophic. Of course, there's no
2869 * guarantee that a browser won't have other catastrophic bugs, but still,
2870 * we should probably change this default in the future:
2872 * if (!(csrf_prot_type & CSRF_PROT_GET_WITH_HEADER) &&
2873 * !(csrf_prot_type & CSRF_PROT_GET_WITH_TOKEN))
2874 * csrf_prot_type |= <whatever-the-new-default-should-be>;
2878 * For POSTs we default to CSRF protection with anti-CSRF tokens even
2879 * though out POSTable resources are safe and idempotent when POSTed and we
2880 * could count on the browser not to make the responses available to
2881 * cross-site requests.
2883 if (!(csrf_prot_type & CSRF_PROT_POST_WITH_HEADER) &&
2884 !(csrf_prot_type & CSRF_PROT_POST_WITH_TOKEN))
2885 csrf_prot_type |= CSRF_PROT_POST_WITH_TOKEN;
2889 main(int argc, char **argv)
2891 unsigned int flags = MHD_USE_THREAD_PER_CONNECTION; /* XXX */
2892 struct sockaddr_in sin;
2893 struct MHD_Daemon *previous = NULL;
2894 struct MHD_Daemon *current = NULL;
2895 struct sigaction sa;
2896 krb5_context context = NULL;
2897 MHD_socket sock = MHD_INVALID_SOCKET;
2898 char *priv_key_pem = NULL;
2899 char *cert_pem = NULL;
2900 char sig;
2901 int optidx = 0;
2902 int ret;
2904 setprogname("bx509d");
2905 if (getarg(args, sizeof(args) / sizeof(args[0]), argc, argv, &optidx))
2906 usage(1);
2907 if (help_flag)
2908 usage(0);
2909 if (version_flag) {
2910 print_version(NULL);
2911 exit(0);
2913 if (argc > optidx) /* Add option to set a URI local part prefix? */
2914 usage(1);
2915 if (port < 0)
2916 errx(1, "Port number must be given");
2918 if ((errno = pthread_key_create(&k5ctx, k5_free_context)))
2919 err(1, "Could not create thread-specific storage");
2921 if ((errno = get_krb5_context(&context)))
2922 err(1, "Could not init krb5 context");
2924 bx509_openlog(context, "bx509d", &logfac);
2925 krb5_set_log_dest(context, logfac);
2926 load_plugins(context);
2928 if (allow_GET_flag == -1)
2929 warnx("It is safer to use --no-allow-GET");
2931 get_csrf_prot_type(context);
2933 krb5_generate_random_block((void *)&csrf_key, sizeof(csrf_key));
2934 if (csrf_key_file == NULL)
2935 csrf_key_file = krb5_config_get_string(context, NULL, "bx509d",
2936 "csrf_key_file", NULL);
2937 if (csrf_key_file) {
2938 ssize_t bytes;
2939 int fd;
2941 fd = open(csrf_key_file, O_RDONLY);
2942 if (fd == -1)
2943 err(1, "CSRF key file missing %s", csrf_key_file);
2944 bytes = read(fd, csrf_key, sizeof(csrf_key));
2945 if (bytes == -1)
2946 err(1, "Could not read CSRF key file %s", csrf_key_file);
2947 if (bytes != sizeof(csrf_key))
2948 errx(1, "CSRF key file too small (should be %lu) %s",
2949 (unsigned long)sizeof(csrf_key), csrf_key_file);
2952 if (audiences.num_strings == 0) {
2953 char localhost[MAXHOSTNAMELEN];
2955 ret = gethostname(localhost, sizeof(localhost));
2956 if (ret == -1)
2957 errx(1, "Could not determine local hostname; use --audience");
2959 if ((audiences.strings =
2960 calloc(1, sizeof(audiences.strings[0]))) == NULL ||
2961 (audiences.strings[0] = strdup(localhost)) == NULL)
2962 err(1, "Out of memory");
2963 audiences.num_strings = 1;
2966 if (daemonize && daemon_child_fd == -1)
2967 daemon_child_fd = roken_detach_prep(argc, argv, "--daemon-child");
2968 daemonize = 0;
2970 argc -= optidx;
2971 argv += optidx;
2972 if (argc != 0)
2973 usage(1);
2975 if (cache_dir == NULL) {
2976 char *s = NULL;
2978 if (asprintf(&s, "%s/bx509d-XXXXXX",
2979 getenv("TMPDIR") ? getenv("TMPDIR") : "/tmp") == -1 ||
2980 s == NULL ||
2981 (cache_dir = mkdtemp(s)) == NULL)
2982 err(1, "could not create temporary cache directory");
2983 if (verbose_counter)
2984 fprintf(stderr, "Note: using %s as cache directory\n", cache_dir);
2985 atexit(rm_cache_dir);
2986 setenv("TMPDIR", cache_dir, 1);
2989 generate_key(context->hx509ctx, "impersonation", "rsa", 2048, &impersonation_key_fn);
2991 again:
2992 if (cert_file && !priv_key_file)
2993 priv_key_file = cert_file;
2995 if (cert_file) {
2996 hx509_cursor cursor = NULL;
2997 hx509_certs certs = NULL;
2998 hx509_cert cert = NULL;
2999 time_t min_cert_life = 0;
3000 size_t len;
3001 void *s;
3003 ret = hx509_certs_init(context->hx509ctx, cert_file, 0, NULL, &certs);
3004 if (ret == 0)
3005 ret = hx509_certs_start_seq(context->hx509ctx, certs, &cursor);
3006 while (ret == 0 &&
3007 (ret = hx509_certs_next_cert(context->hx509ctx, certs,
3008 cursor, &cert)) == 0 && cert) {
3009 time_t notAfter = 0;
3011 if (!hx509_cert_have_private_key_only(cert) &&
3012 (notAfter = hx509_cert_get_notAfter(cert)) <= time(NULL) + 30)
3013 errx(1, "One or more certificates in %s are expired",
3014 cert_file);
3015 if (notAfter) {
3016 notAfter -= time(NULL);
3017 if (notAfter < 600)
3018 warnx("One or more certificates in %s expire soon",
3019 cert_file);
3020 /* Reload 5 minutes prior to expiration */
3021 if (notAfter < min_cert_life || min_cert_life < 1)
3022 min_cert_life = notAfter;
3024 hx509_cert_free(cert);
3026 if (certs)
3027 (void) hx509_certs_end_seq(context->hx509ctx, certs, cursor);
3028 if (min_cert_life > 4)
3029 alarm(min_cert_life >> 1);
3030 hx509_certs_free(&certs);
3031 if (ret)
3032 hx509_err(context->hx509ctx, 1, ret,
3033 "could not read certificate from %s", cert_file);
3035 if ((errno = rk_undumpdata(cert_file, &s, &len)) ||
3036 (cert_pem = strndup(s, len)) == NULL)
3037 err(1, "could not read certificate from %s", cert_file);
3038 if (strlen(cert_pem) != len)
3039 err(1, "NULs in certificate file contents: %s", cert_file);
3040 free(s);
3043 if (priv_key_file) {
3044 size_t len;
3045 void *s;
3047 if ((errno = rk_undumpdata(priv_key_file, &s, &len)) ||
3048 (priv_key_pem = strndup(s, len)) == NULL)
3049 err(1, "could not read private key from %s", priv_key_file);
3050 if (strlen(priv_key_pem) != len)
3051 err(1, "NULs in private key file contents: %s", priv_key_file);
3052 free(s);
3055 if (verbose_counter > 1)
3056 flags |= MHD_USE_DEBUG;
3057 if (thread_per_client_flag)
3058 flags |= MHD_USE_THREAD_PER_CONNECTION;
3061 if (pipe(sigpipe) == -1)
3062 err(1, "Could not set up key/cert reloading");
3063 memset(&sa, 0, sizeof(sa));
3064 sa.sa_handler = sighandler;
3065 if (reverse_proxied_flag) {
3067 * We won't use TLS in the reverse proxy case, so no need to reload
3068 * certs. But we'll still read them if given, and alarm() will get
3069 * called.
3071 (void) signal(SIGHUP, SIG_IGN);
3072 (void) signal(SIGUSR1, SIG_IGN);
3073 (void) signal(SIGALRM, SIG_IGN);
3074 } else {
3075 (void) sigaction(SIGHUP, &sa, NULL); /* Reload key & cert */
3076 (void) sigaction(SIGUSR1, &sa, NULL); /* Reload key & cert */
3077 (void) sigaction(SIGALRM, &sa, NULL); /* Reload key & cert */
3079 (void) sigaction(SIGINT, &sa, NULL); /* Graceful shutdown */
3080 (void) sigaction(SIGTERM, &sa, NULL); /* Graceful shutdown */
3081 (void) signal(SIGPIPE, SIG_IGN);
3083 if (previous)
3084 sock = MHD_quiesce_daemon(previous);
3086 if (reverse_proxied_flag) {
3088 * XXX IPv6 too. Create the sockets and tell MHD_start_daemon() about
3089 * them.
3091 sin.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
3092 sin.sin_family = AF_INET;
3093 sin.sin_port = htons(port);
3094 current = MHD_start_daemon(flags, port,
3096 * This is a connection access callback. We
3097 * don't use it.
3099 NULL, NULL,
3100 /* This is our request handler */
3101 route, (char *)NULL,
3102 MHD_OPTION_SOCK_ADDR, &sin,
3103 MHD_OPTION_CONNECTION_LIMIT, (unsigned int)200,
3104 MHD_OPTION_CONNECTION_TIMEOUT, (unsigned int)10,
3105 /* This is our request cleanup handler */
3106 MHD_OPTION_NOTIFY_COMPLETED, cleanup_req, NULL,
3107 MHD_OPTION_END);
3108 } else if (sock != MHD_INVALID_SOCKET) {
3110 * Restart following a possible certificate/key rollover, reusing the
3111 * listen socket returned by MHD_quiesce_daemon().
3113 current = MHD_start_daemon(flags | MHD_USE_SSL, port,
3114 NULL, NULL,
3115 route, (char *)NULL,
3116 MHD_OPTION_HTTPS_MEM_KEY, priv_key_pem,
3117 MHD_OPTION_HTTPS_MEM_CERT, cert_pem,
3118 MHD_OPTION_CONNECTION_LIMIT, (unsigned int)200,
3119 MHD_OPTION_CONNECTION_TIMEOUT, (unsigned int)10,
3120 MHD_OPTION_NOTIFY_COMPLETED, cleanup_req, NULL,
3121 MHD_OPTION_LISTEN_SOCKET, sock,
3122 MHD_OPTION_END);
3123 sock = MHD_INVALID_SOCKET;
3124 } else {
3126 * Initial MHD_start_daemon(), with TLS.
3128 * Subsequently we'll restart reusing the listen socket this creates.
3129 * See above.
3131 current = MHD_start_daemon(flags | MHD_USE_SSL, port,
3132 NULL, NULL,
3133 route, (char *)NULL,
3134 MHD_OPTION_HTTPS_MEM_KEY, priv_key_pem,
3135 MHD_OPTION_HTTPS_MEM_CERT, cert_pem,
3136 MHD_OPTION_CONNECTION_LIMIT, (unsigned int)200,
3137 MHD_OPTION_CONNECTION_TIMEOUT, (unsigned int)10,
3138 MHD_OPTION_NOTIFY_COMPLETED, cleanup_req, NULL,
3139 MHD_OPTION_END);
3141 if (current == NULL)
3142 err(1, "Could not start bx509 REST service");
3144 if (previous) {
3145 MHD_stop_daemon(previous);
3146 previous = NULL;
3149 if (verbose_counter)
3150 fprintf(stderr, "Ready!\n");
3151 if (daemon_child_fd != -1)
3152 roken_detach_finish(NULL, daemon_child_fd);
3154 /* Wait for signal, possibly SIGALRM, to reload certs and/or exit */
3155 while ((ret = read(sigpipe[0], &sig, sizeof(sig))) == -1 &&
3156 errno == EINTR)
3159 free(priv_key_pem);
3160 free(cert_pem);
3161 priv_key_pem = NULL;
3162 cert_pem = NULL;
3164 if (ret == 1 && (sig == SIGHUP || sig == SIGUSR1 || sig == SIGALRM)) {
3165 /* Reload certs and restart service gracefully */
3166 previous = current;
3167 current = NULL;
3168 goto again;
3171 MHD_stop_daemon(current);
3172 _krb5_unload_plugins(context, "kdc");
3173 pthread_key_delete(k5ctx);
3174 return 0;