2 * Copyright (c) 2019 Kungliga Tekniska Högskolan
3 * (Royal Institute of Technology, Stockholm, Sweden).
6 * Redistribution and use in source and binary forms, with or without
7 * modification, are permitted provided that the following conditions
10 * 1. Redistributions of source code must retain the above copyright
11 * notice, this list of conditions and the following disclaimer.
13 * 2. Redistributions in binary form must reproduce the above copyright
14 * notice, this list of conditions and the following disclaimer in the
15 * documentation and/or other materials provided with the distribution.
17 * 3. Neither the name of the Institute nor the names of its contributors
18 * may be used to endorse or promote products derived from this software
19 * without specific prior written permission.
21 * THIS SOFTWARE IS PROVIDED BY THE INSTITUTE AND CONTRIBUTORS ``AS IS'' AND
22 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
23 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
24 * ARE DISCLAIMED. IN NO EVENT SHALL THE INSTITUTE OR CONTRIBUTORS BE LIABLE
25 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
26 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
27 * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
28 * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
29 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
30 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
35 * This file implements a RESTful HTTPS API to an online CA, as well as an
36 * HTTP/Negotiate token issuer.
38 * Users are authenticated with bearer tokens.
40 * This is essentially a RESTful online CA sharing code with the KDC's kx509
41 * online CA, and also a proxy for PKINIT and GSS-API (Negotiate).
43 * To get a key certified:
45 * GET /bx509?csr=<base64-encoded-PKCS#10-CSR>
47 * To get an HTTP/Negotiate token:
49 * GET /bnegotiate?target=<acceptor-principal>
51 * which, if authorized, produces a Negotiate token (base64-encoded, as
52 * expected, with the "Negotiate " prefix, ready to be put in an Authorization:
56 * - rewrite to not use libmicrohttpd but an alternative more appropriate to
57 * Heimdal's license (though libmicrohttpd will do)
58 * - /bx509 should include the certificate chain
59 * - /bx509 should support HTTP/Negotiate
60 * - there should be an end-point for fetching an issuer's chain
61 * - maybe add /bkrb5 which returns a KRB-CRED with the user's TGT
64 * - We use krb5_error_code values as much as possible. Where we need to use
65 * MHD_NO because we got that from an mhd function and cannot respond with
66 * an HTTP response, we use (krb5_error_code)-1, and later map that to
69 * (MHD_NO is an ENOMEM-cannot-even-make-a-static-503-response level event.)
72 #define _XOPEN_SOURCE_EXTENDED 1
73 #define _DEFAULT_SOURCE 1
77 #include <sys/socket.h>
78 #include <sys/types.h>
96 #include <netinet/in.h>
97 #include <netinet/ip.h>
99 #include <microhttpd.h>
100 #include "kdc_locl.h"
101 #include "token_validator_plugin.h"
105 #include <gssapi/gssapi.h>
106 #include <gssapi/gssapi_krb5.h>
108 #include "../lib/hx509/hx_locl.h"
109 #include <hx509-private.h>
111 #define heim_pcontext krb5_context
112 #define heim_pconfig krb5_context
113 #include <heimbase-svc.h>
115 typedef struct bx509_request_desc {
116 HEIM_SVC_REQUEST_DESC_COMMON_ELEMENTS;
118 struct MHD_Connection *connection;
119 krb5_times token_times;
122 const char *for_cname;
128 krb5_addresses tgt_addresses; /* For /get-tgt */
130 } *bx509_request_desc;
133 audit_trail(bx509_request_desc r, krb5_error_code ret)
135 const char *retname = NULL;
137 /* Get a symbolic name for some error codes */
138 #define CASE(x) case x : retname = #x; break
142 CASE(HDB_ERR_NOT_FOUND_HERE);
143 CASE(HDB_ERR_WRONG_REALM);
144 CASE(HDB_ERR_EXISTS);
145 CASE(HDB_ERR_KVNO_NOT_FOUND);
146 CASE(HDB_ERR_NOENTRY);
147 CASE(HDB_ERR_NO_MKEY);
148 CASE(KRB5KDC_ERR_BADOPTION);
149 CASE(KRB5KDC_ERR_CANNOT_POSTDATE);
150 CASE(KRB5KDC_ERR_CLIENT_NOTYET);
151 CASE(KRB5KDC_ERR_C_PRINCIPAL_UNKNOWN);
152 CASE(KRB5KDC_ERR_ETYPE_NOSUPP);
153 CASE(KRB5KDC_ERR_KEY_EXPIRED);
154 CASE(KRB5KDC_ERR_NAME_EXP);
155 CASE(KRB5KDC_ERR_NEVER_VALID);
156 CASE(KRB5KDC_ERR_NONE);
157 CASE(KRB5KDC_ERR_NULL_KEY);
158 CASE(KRB5KDC_ERR_PADATA_TYPE_NOSUPP);
159 CASE(KRB5KDC_ERR_POLICY);
160 CASE(KRB5KDC_ERR_PREAUTH_FAILED);
161 CASE(KRB5KDC_ERR_PREAUTH_REQUIRED);
162 CASE(KRB5KDC_ERR_SERVER_NOMATCH);
163 CASE(KRB5KDC_ERR_SERVICE_EXP);
164 CASE(KRB5KDC_ERR_SERVICE_NOTYET);
165 CASE(KRB5KDC_ERR_S_PRINCIPAL_UNKNOWN);
166 CASE(KRB5KDC_ERR_TRTYPE_NOSUPP);
167 CASE(KRB5KRB_ERR_RESPONSE_TOO_BIG);
168 /* XXX Add relevant error codes */
177 /* Let's save a few bytes */
178 if (retname && strncmp("KRB5KDC_", retname, sizeof("KRB5KDC_") - 1) == 0)
179 retname += sizeof("KRB5KDC_") - 1;
181 heim_audit_trail((heim_svc_req_desc)r, ret, retname);
184 static krb5_log_facility *logfac;
185 static pthread_key_t k5ctx;
187 static krb5_error_code
188 get_krb5_context(krb5_context *contextp)
192 if ((*contextp = pthread_getspecific(k5ctx)))
194 if ((ret = krb5_init_context(contextp)))
195 return *contextp = NULL, ret;
196 (void) pthread_setspecific(k5ctx, *contextp);
197 return *contextp ? 0 : ENOMEM;
200 static int port = -1;
201 static int help_flag;
202 static int daemonize;
203 static int daemon_child_fd = -1;
204 static int verbose_counter;
205 static int version_flag;
206 static int reverse_proxied_flag;
207 static int thread_per_client_flag;
208 struct getarg_strings audiences;
209 static const char *cert_file;
210 static const char *priv_key_file;
211 static const char *cache_dir;
212 static char *impersonation_key_fn;
214 static krb5_error_code resp(struct bx509_request_desc *, int,
215 enum MHD_ResponseMemoryMode, const char *,
216 const void *, size_t, const char *);
217 static krb5_error_code bad_req(struct bx509_request_desc *, krb5_error_code, int,
219 HEIMDAL_PRINTF_ATTRIBUTE((__printf__, 4, 5));
221 static krb5_error_code bad_enomem(struct bx509_request_desc *, krb5_error_code);
222 static krb5_error_code bad_400(struct bx509_request_desc *, krb5_error_code, char *);
223 static krb5_error_code bad_401(struct bx509_request_desc *, char *);
224 static krb5_error_code bad_403(struct bx509_request_desc *, krb5_error_code, char *);
225 static krb5_error_code bad_404(struct bx509_request_desc *, const char *);
226 static krb5_error_code bad_405(struct bx509_request_desc *, const char *);
227 static krb5_error_code bad_500(struct bx509_request_desc *, krb5_error_code, const char *);
228 static krb5_error_code bad_503(struct bx509_request_desc *, krb5_error_code, const char *);
231 validate_token(struct bx509_request_desc *r)
234 krb5_principal cprinc = NULL;
237 char token_type[64]; /* Plenty */
240 size_t host_len, brk, i;
242 memset(&r->token_times, 0, sizeof(r->token_times));
243 host = MHD_lookup_connection_value(r->connection, MHD_HEADER_KIND,
244 MHD_HTTP_HEADER_HOST);
246 return bad_400(r, EINVAL, "Host header is missing");
248 /* Exclude port number here (IPv6-safe because of the below) */
249 host_len = ((p = strchr(host, ':'))) ? p - host : strlen(host);
251 token = MHD_lookup_connection_value(r->connection, MHD_HEADER_KIND,
252 MHD_HTTP_HEADER_AUTHORIZATION);
254 return bad_401(r, "Authorization token is missing");
255 brk = strcspn(token, " \t");
256 if (token[brk] == '\0' || brk > sizeof(token_type) - 1)
257 return bad_401(r, "Authorization token is missing");
258 memcpy(token_type, token, brk);
259 token_type[brk] = '\0';
261 tok.length = strlen(token);
262 tok.data = (void *)(uintptr_t)token;
264 for (i = 0; i < audiences.num_strings; i++)
265 if (strncasecmp(host, audiences.strings[i], host_len) == 0 &&
266 audiences.strings[i][host_len] == '\0')
268 if (i == audiences.num_strings)
269 return bad_403(r, EINVAL, "Host: value is not accepted here");
271 r->sname = strdup(host); /* No need to check for ENOMEM here */
273 ret = kdc_validate_token(r->context, NULL /* realm */, token_type, &tok,
274 (const char **)&audiences.strings[i], 1,
275 &cprinc, &r->token_times);
277 return bad_403(r, ret, "Token validation failed");
279 return bad_403(r, ret, "Could not extract a principal name "
281 ret = krb5_unparse_name(r->context, cprinc, &r->cname);
282 krb5_free_principal(r->context, cprinc);
284 return bad_503(r, ret, "Could not parse principal name");
289 generate_key(hx509_context context,
290 const char *key_name,
291 const char *gen_type,
292 unsigned long gen_bits,
295 struct hx509_generate_private_context *key_gen_ctx = NULL;
296 hx509_private_key key = NULL;
297 hx509_certs certs = NULL;
298 hx509_cert cert = NULL;
301 if (strcmp(gen_type, "rsa") != 0)
302 errx(1, "Only RSA keys are supported at this time");
304 if (asprintf(fn, "PEM-FILE:%s/.%s_priv_key.pem",
305 cache_dir, key_name) == -1 ||
307 err(1, "Could not set up private key for %s", key_name);
309 ret = _hx509_generate_private_key_init(context,
310 ASN1_OID_ID_PKCS1_RSAENCRYPTION,
313 ret = _hx509_generate_private_key_bits(context, key_gen_ctx, gen_bits);
315 ret = _hx509_generate_private_key(context, key_gen_ctx, &key);
317 cert = hx509_cert_init_private_key(context, key, NULL);
319 ret = hx509_certs_init(context, *fn,
320 HX509_CERTS_CREATE | HX509_CERTS_UNPROTECT_ALL,
323 ret = hx509_certs_add(context, certs, cert);
325 ret = hx509_certs_store(context, certs, 0, NULL);
327 hx509_err(context, 1, ret, "Could not generate and save private key "
330 _hx509_generate_private_key_free(&key_gen_ctx);
331 hx509_private_key_free(&key);
332 hx509_certs_free(&certs);
333 hx509_cert_free(cert);
337 k5_free_context(void *ctx)
339 krb5_free_context(ctx);
342 #ifndef HAVE_UNLINKAT
344 unlink1file(const char *dname, const char *name)
348 if (strlcpy(p, dname, sizeof(p)) < sizeof(p) &&
349 strlcat(p, "/", sizeof(p)) < sizeof(p) &&
350 strlcat(p, name, sizeof(p)) < sizeof(p))
363 * This works, but not on Win32:
365 * (void) simple_execlp("rm", "rm", "-rf", cache_dir, NULL);
367 * We make no directories in `cache_dir', so we need not recurse.
369 if ((d = opendir(cache_dir)) == NULL)
372 while ((e = readdir(d))) {
375 * Because unlinkat() takes a directory FD, implementing one for
376 * libroken is tricky at best. Instead we might want to implement an
377 * rm_dash_rf() function in lib/roken.
379 (void) unlinkat(dirfd(d), e->d_name, 0);
381 (void) unlink1file(cache_dir, e->d_name);
385 (void) rmdir(cache_dir);
388 static krb5_error_code
389 mk_pkix_store(char **pkix_store)
396 if (asprintf(&s, "PEM-FILE:%s/pkix-XXXXXX", cache_dir) == -1 ||
402 * This way of using mkstemp() isn't safer than mktemp(), but we want to
403 * quiet the warning that we'd get if we used mktemp().
405 if ((fd = mkstemp(s + sizeof("PEM-FILE:") - 1)) == -1) {
415 * XXX Shouldn't be a body, but a status message. The body should be
416 * configurable to be from a file. MHD doesn't give us a way to set the
417 * response status message though, just the body.
419 static krb5_error_code
420 resp(struct bx509_request_desc *r,
421 int http_status_code,
422 enum MHD_ResponseMemoryMode rmmode,
423 const char *content_type,
428 struct MHD_Response *response;
431 (void) gettimeofday(&r->tv_end, NULL);
432 if (http_status_code == MHD_HTTP_OK ||
433 http_status_code == MHD_HTTP_TEMPORARY_REDIRECT)
436 response = MHD_create_response_from_buffer(bodylen, rk_UNCONST(body),
438 if (response == NULL)
440 mret = MHD_add_response_header(response, MHD_HTTP_HEADER_CACHE_CONTROL,
441 "no-store, max-age=0");
442 if (mret == MHD_YES && http_status_code == MHD_HTTP_UNAUTHORIZED) {
443 mret = MHD_add_response_header(response,
444 MHD_HTTP_HEADER_WWW_AUTHENTICATE,
447 mret = MHD_add_response_header(response,
448 MHD_HTTP_HEADER_WWW_AUTHENTICATE,
450 } else if (mret == MHD_YES && http_status_code == MHD_HTTP_TEMPORARY_REDIRECT) {
454 redir = MHD_lookup_connection_value(r->connection, MHD_GET_ARGUMENT_KIND,
456 mret = MHD_add_response_header(response, MHD_HTTP_HEADER_LOCATION,
458 if (mret != MHD_NO && token)
459 mret = MHD_add_response_header(response,
460 MHD_HTTP_HEADER_AUTHORIZATION,
463 if (mret == MHD_YES && content_type) {
464 mret = MHD_add_response_header(response,
465 MHD_HTTP_HEADER_CONTENT_TYPE,
469 mret = MHD_queue_response(r->connection, http_status_code, response);
470 MHD_destroy_response(response);
471 return mret == MHD_NO ? -1 : 0;
474 static krb5_error_code
475 bad_reqv(struct bx509_request_desc *r,
476 krb5_error_code code,
477 int http_status_code,
482 krb5_context context = NULL;
483 const char *k5msg = NULL;
484 const char *emsg = NULL;
485 char *formatted = NULL;
488 heim_audit_addkv((heim_svc_req_desc)r, 0, "http-status-code", "%d",
490 (void) gettimeofday(&r->tv_end, NULL);
491 if (code == ENOMEM) {
493 krb5_log_msg(r->context, logfac, 1, NULL, "Out of memory");
494 audit_trail(r, code);
495 return resp(r, http_status_code, MHD_RESPMEM_PERSISTENT,
496 NULL, fmt, strlen(fmt), NULL);
501 emsg = k5msg = krb5_get_error_message(r->context, code);
503 emsg = strerror(code);
506 ret = vasprintf(&formatted, fmt, ap) == -1;
508 if (ret > -1 && formatted)
509 ret = asprintf(&msg, "%s: %s (%d)", formatted, emsg, (int)code);
514 heim_audit_addreason((heim_svc_req_desc)r, "%s", formatted);
515 audit_trail(r, code);
516 krb5_free_error_message(context, k5msg);
518 if (ret == -1 || msg == NULL) {
520 krb5_log_msg(r->context, logfac, 1, NULL, "Out of memory");
521 return resp(r, MHD_HTTP_SERVICE_UNAVAILABLE, MHD_RESPMEM_PERSISTENT,
522 NULL, "Out of memory", sizeof("Out of memory") - 1, NULL);
525 ret = resp(r, http_status_code, MHD_RESPMEM_MUST_COPY,
526 NULL, msg, strlen(msg), NULL);
529 return ret == -1 ? -1 : code;
532 static krb5_error_code
533 bad_req(struct bx509_request_desc *r,
534 krb5_error_code code,
535 int http_status_code,
543 ret = bad_reqv(r, code, http_status_code, fmt, ap);
548 static krb5_error_code
549 bad_enomem(struct bx509_request_desc *r, krb5_error_code ret)
551 return bad_req(r, ret, MHD_HTTP_SERVICE_UNAVAILABLE,
555 static krb5_error_code
556 bad_400(struct bx509_request_desc *r, int ret, char *reason)
558 return bad_req(r, ret, MHD_HTTP_BAD_REQUEST, "%s", reason);
561 static krb5_error_code
562 bad_401(struct bx509_request_desc *r, char *reason)
564 return bad_req(r, EACCES, MHD_HTTP_UNAUTHORIZED, "%s", reason);
567 static krb5_error_code
568 bad_403(struct bx509_request_desc *r, krb5_error_code ret, char *reason)
570 return bad_req(r, ret, MHD_HTTP_FORBIDDEN, "%s", reason);
573 static krb5_error_code
574 bad_404(struct bx509_request_desc *r, const char *name)
576 return bad_req(r, ENOENT, MHD_HTTP_NOT_FOUND,
577 "Resource not found: %s", name);
580 static krb5_error_code
581 bad_405(struct bx509_request_desc *r, const char *method)
583 return bad_req(r, EPERM, MHD_HTTP_METHOD_NOT_ALLOWED,
584 "Method not supported: %s", method);
587 static krb5_error_code
588 bad_500(struct bx509_request_desc *r,
592 return bad_req(r, ret, MHD_HTTP_INTERNAL_SERVER_ERROR,
593 "Internal error: %s", reason);
596 static krb5_error_code
597 bad_503(struct bx509_request_desc *r,
601 return bad_req(r, ret, MHD_HTTP_SERVICE_UNAVAILABLE,
602 "Service unavailable: %s", reason);
605 static krb5_error_code
606 good_bx509(struct bx509_request_desc *r)
612 ret = rk_undumpdata(strchr(r->pkix_store, ':') + 1, &body, &bodylen);
614 return bad_503(r, ret, "Could not recover issued certificate "
617 (void) gettimeofday(&r->tv_end, NULL);
618 ret = resp(r, MHD_HTTP_OK, MHD_RESPMEM_MUST_COPY, "application/x-pem-file",
619 body, bodylen, NULL);
625 bx509_param_cb(void *d,
626 enum MHD_ValueKind kind,
630 struct bx509_request_desc *r = d;
631 heim_oid oid = { 0, 0 };
633 if (strcmp(key, "eku") == 0 && val) {
634 heim_audit_addkv((heim_svc_req_desc)r, KDC_AUDIT_VIS, "requested_eku",
636 r->ret = der_parse_heim_oid(val, ".", &oid);
638 r->ret = hx509_request_add_eku(r->context->hx509ctx, r->req, &oid);
640 } else if (strcmp(key, "dNSName") == 0 && val) {
641 heim_audit_addkv((heim_svc_req_desc)r, KDC_AUDIT_VIS,
642 "requested_dNSName", "%s", val);
643 r->ret = hx509_request_add_dns_name(r->context->hx509ctx, r->req, val);
644 } else if (strcmp(key, "rfc822Name") == 0 && val) {
645 heim_audit_addkv((heim_svc_req_desc)r, KDC_AUDIT_VIS,
646 "requested_rfc822Name", "%s", val);
647 r->ret = hx509_request_add_email(r->context->hx509ctx, r->req, val);
648 } else if (strcmp(key, "xMPPName") == 0 && val) {
649 heim_audit_addkv((heim_svc_req_desc)r, KDC_AUDIT_VIS,
650 "requested_xMPPName", "%s", val);
651 r->ret = hx509_request_add_xmpp_name(r->context->hx509ctx, r->req,
653 } else if (strcmp(key, "krb5PrincipalName") == 0 && val) {
654 heim_audit_addkv((heim_svc_req_desc)r, KDC_AUDIT_VIS,
655 "requested_krb5PrincipalName", "%s", val);
656 r->ret = hx509_request_add_pkinit(r->context->hx509ctx, r->req,
658 } else if (strcmp(key, "ms-upn") == 0 && val) {
659 heim_audit_addkv((heim_svc_req_desc)r, KDC_AUDIT_VIS,
660 "requested_ms_upn", "%s", val);
661 r->ret = hx509_request_add_ms_upn_name(r->context->hx509ctx, r->req,
663 } else if (strcmp(key, "registeredID") == 0 && val) {
664 heim_audit_addkv((heim_svc_req_desc)r, KDC_AUDIT_VIS,
665 "requested_registered_id", "%s", val);
666 r->ret = der_parse_heim_oid(val, ".", &oid);
668 r->ret = hx509_request_add_registered(r->context->hx509ctx, r->req,
671 } else if (strcmp(key, "csr") == 0 && val) {
672 heim_audit_addkv((heim_svc_req_desc)r, 0, "requested_csr", "true");
673 r->ret = 0; /* Handled upstairs */
674 } else if (strcmp(key, "lifetime") == 0 && val) {
675 r->req_life = parse_time(val, "day");
677 /* Produce error for unknown params */
678 heim_audit_addkv((heim_svc_req_desc)r, 0, "requested_unknown", "true");
679 krb5_set_error_message(r->context, r->ret = ENOTSUP,
680 "Query parameter %s not supported", key);
682 return r->ret == 0 ? MHD_YES : MHD_NO /* Stop iterating */;
685 static krb5_error_code
686 authorize_CSR(struct bx509_request_desc *r,
688 krb5_const_principal p)
692 ret = hx509_request_parse_der(r->context->hx509ctx, csr, &r->req);
694 return bad_req(r, ret, MHD_HTTP_SERVICE_UNAVAILABLE,
695 "Could not parse CSR");
697 (void) MHD_get_connection_values(r->connection, MHD_GET_ARGUMENT_KIND,
701 return bad_req(r, ret, MHD_HTTP_SERVICE_UNAVAILABLE,
702 "Could not handle query parameters");
704 ret = kdc_authorize_csr(r->context, "bx509", r->req, p);
706 return bad_403(r, ret, "Not authorized to requested certificate");
711 * hx509_certs_iter_f() callback to assign a private key to the first cert in a
714 static int HX509_LIB_CALL
715 set_priv_key(hx509_context context, void *d, hx509_cert c)
717 (void) _hx509_cert_assign_key(c, (hx509_private_key)d);
718 return -1; /* stop iteration */
721 static krb5_error_code
722 store_certs(hx509_context context,
724 hx509_certs store_these,
725 hx509_private_key key)
728 hx509_certs certs = NULL;
730 ret = hx509_certs_init(context, store, HX509_CERTS_CREATE, NULL,
734 (void) hx509_certs_iter_f(context, store_these, set_priv_key, key);
735 hx509_certs_merge(context, certs, store_these);
738 hx509_certs_store(context, certs, 0, NULL);
739 hx509_certs_free(&certs);
743 /* Setup a CSR for bx509() */
744 static krb5_error_code
745 do_CA(struct bx509_request_desc *r, const char *csr)
747 krb5_error_code ret = 0;
749 hx509_certs certs = NULL;
755 * Work around bug where microhttpd decodes %2b to + then + to space. That
756 * bug does not affect other base64 special characters that get URI
759 if ((csr2 = strdup(csr)) == NULL)
760 return bad_enomem(r, ENOMEM);
761 for (q = strchr(csr2, ' '); q; q = strchr(q + 1, ' '))
764 ret = krb5_parse_name(r->context, r->cname, &p);
767 return bad_req(r, ret, MHD_HTTP_SERVICE_UNAVAILABLE,
768 "Could not parse principal name");
772 if ((d.data = malloc(strlen(csr2))) == NULL) {
773 krb5_free_principal(r->context, p);
774 return bad_enomem(r, ENOMEM);
777 bytes = rk_base64_decode(csr2, d.data);
784 krb5_free_principal(r->context, p);
786 return bad_500(r, ret, "Invalid base64 encoding of CSR");
790 * Parses and validates the CSR, adds external extension requests from
791 * query parameters, then checks authorization.
793 ret = authorize_CSR(r, &d, p);
798 krb5_free_principal(r->context, p);
799 return ret; /* authorize_CSR() calls bad_req() */
802 /* Issue the certificate */
803 ret = kdc_issue_certificate(r->context, "bx509", logfac, r->req, p,
804 &r->token_times, r->req_life,
805 1 /* send_chain */, &certs);
806 krb5_free_principal(r->context, p);
808 if (ret == KRB5KDC_ERR_POLICY || ret == EACCES)
809 return bad_403(r, ret,
810 "Certificate request denied for policy reasons");
811 return bad_500(r, ret, "Certificate issuance failed");
814 /* Setup PKIX store */
815 if ((ret = mk_pkix_store(&r->pkix_store)))
816 return bad_500(r, ret,
817 "Could not create PEM store for issued certificate");
819 ret = store_certs(r->context->hx509ctx, r->pkix_store, certs, NULL);
820 hx509_certs_free(&certs);
822 (void) unlink(strchr(r->pkix_store, ':') + 1);
823 return bad_500(r, ret,
824 "Failed convert issued certificate and chain to PEM");
829 /* Copied from kdc/connect.c */
831 addr_to_string(krb5_context context,
832 struct sockaddr *addr,
839 ret = krb5_sockaddr2address(context, addr, &a);
841 ret = krb5_print_address(&a, str, len, &len);
842 krb5_free_address(context, &a);
845 snprintf(str, len, "<family=%d>", addr->sa_family);
848 static krb5_error_code
849 set_req_desc(struct MHD_Connection *connection,
851 struct bx509_request_desc *r)
853 const union MHD_ConnectionInfo *ci;
857 memset(r, 0, sizeof(*r));
858 (void) gettimeofday(&r->tv_start, NULL);
860 ret = get_krb5_context(&r->context);
861 r->connection = connection;
862 r->request.data = "<HTTP-REQUEST>";
863 r->request.length = sizeof("<HTTP-REQUEST>");
864 r->from = r->frombuf;
865 r->tgt_addresses.len = 0;
866 r->tgt_addresses.val = 0;
867 r->hcontext = r->context->hcontext;
871 r->target = r->redir = NULL;
872 r->pkix_store = NULL;
884 r->kv = heim_array_create();
885 ci = MHD_get_connection_info(connection,
886 MHD_CONNECTION_INFO_CLIENT_ADDRESS);
888 r->addr = ci->client_addr;
889 addr_to_string(r->context, r->addr, r->frombuf, sizeof(r->frombuf));
892 heim_audit_addkv((heim_svc_req_desc)r, 0, "method", "GET");
893 heim_audit_addkv((heim_svc_req_desc)r, 0, "endpoint", "%s", r->reqtype);
894 token = MHD_lookup_connection_value(r->connection, MHD_HEADER_KIND,
895 MHD_HTTP_HEADER_AUTHORIZATION);
896 if (token && r->kv) {
897 const char *token_end;
899 if ((token_end = strchr(token, ' ')) == NULL ||
900 (token_end - token) > INT_MAX || (token_end - token) < 2)
901 heim_audit_addkv((heim_svc_req_desc)r, 0, "auth", "<unknown>");
903 heim_audit_addkv((heim_svc_req_desc)r, 0, "auth", "%.*s",
904 (int)(token_end - token), token);
908 if (ret == 0 && r->kv == NULL) {
909 krb5_log_msg(r->context, logfac, 1, NULL, "Out of memory");
916 clean_req_desc(struct bx509_request_desc *r)
921 (void) unlink(strchr(r->pkix_store, ':') + 1);
922 krb5_free_addresses(r->context, &r->tgt_addresses);
923 hx509_request_free(&r->req);
924 heim_release(r->reason);
933 /* Implements GETs of /bx509 */
934 static krb5_error_code
935 bx509(struct bx509_request_desc *r)
940 /* Get required inputs */
941 csr = MHD_lookup_connection_value(r->connection, MHD_GET_ARGUMENT_KIND,
944 return bad_400(r, EINVAL, "CSR is missing");
946 if ((ret = validate_token(r)))
947 return ret; /* validate_token() calls bad_req() */
949 if (r->cname == NULL)
950 return bad_403(r, EINVAL,
951 "Could not extract principal name from token");
953 /* Parse CSR, add extensions from parameters, authorize, issue cert */
954 if ((ret = do_CA(r, csr)))
957 /* Read and send the contents of the PKIX store */
958 krb5_log_msg(r->context, logfac, 1, NULL, "Issued certificate to %s",
960 return good_bx509(r);
964 * princ_fs_encode_sz() and princ_fs_encode() encode a principal name to be
965 * safe for use as a file name. They function very much like URL encoders, but
966 * '~' and '.' also get encoded, and '@' does not.
968 * A corresponding decoder is not needed.
971 princ_fs_encode_sz(const char *in)
973 size_t sz = strlen(in);
976 unsigned char c = *(const unsigned char *)(in++);
993 princ_fs_encode(const char *in)
995 size_t len = strlen(in);
996 size_t sz = princ_fs_encode_sz(in);
1000 if ((s = malloc(sz + 1)) == NULL)
1004 for (i = k = 0; i < len; i++) {
1018 s[k++] = "0123456789abcdef"[(c&0xff)>>4];
1019 s[k++] = "0123456789abcdef"[(c&0x0f)];
1028 * Find an existing, live ccache for `princ' in `cache_dir' or acquire Kerberos
1029 * creds for `princ' with PKINIT and put them in a ccache in `cache_dir'.
1031 static krb5_error_code
1032 find_ccache(krb5_context context, const char *princ, char **ccname)
1034 krb5_error_code ret = ENOMEM;
1035 krb5_ccache cc = NULL;
1042 * Name the ccache after the principal. The principal may have special
1043 * characters in it, such as / or \ (path component separarot), or shell
1044 * special characters, so princ_fs_encode() it to make a ccache name.
1046 if ((s = princ_fs_encode(princ)) == NULL ||
1047 asprintf(ccname, "FILE:%s/%s.cc", cache_dir, s) == -1 ||
1052 if ((ret = krb5_cc_resolve(context, *ccname, &cc))) {
1053 /* krb5_cc_resolve() suceeds even if the file doesn't exist */
1059 /* Check if we have a good enough credential */
1061 (ret = krb5_cc_get_lifetime(context, cc, &life)) == 0 && life > 60) {
1062 krb5_cc_close(context, cc);
1066 krb5_cc_close(context, cc);
1067 return ret ? ret : ENOENT;
1070 enum k5_creds_kind { K5_CREDS_EPHEMERAL, K5_CREDS_CACHED };
1072 static krb5_error_code
1073 get_ccache(struct bx509_request_desc *r, krb5_ccache *cc, int *won)
1075 krb5_error_code ret = 0;
1076 struct stat st1, st2;
1077 char *temp_ccname = NULL;
1078 const char *fn = NULL;
1083 * Open and lock a .new ccache file. Use .new to avoid garbage files on
1086 * We can race with other threads to do this, so we loop until we
1087 * definitively win or definitely lose the race. We win when we have a) an
1088 * open FD that is b) flock'ed, and c) we observe with lstat() that the
1089 * file we opened and locked is the same as on disk after locking.
1091 * We don't close the FD until we're done.
1093 * If we had a proper anon MEMORY ccache, we could instead use that for a
1094 * temporary ccache, and then the initialization of and move to the final
1095 * FILE ccache would take care to mkstemp() and rename() into place.
1096 * fcc_open() basically does a similar thing.
1100 if (asprintf(&temp_ccname, "%s.ccnew", r->ccname) == -1 ||
1101 temp_ccname == NULL)
1104 fn = temp_ccname + sizeof("FILE:") - 1;
1107 * Open and flock the temp ccache file.
1109 * XXX We should really a) use _krb5_xlock(), or move that into
1110 * lib/roken anyways, b) abstract this loop into a utility function in
1119 ((fd = open(fn, O_RDWR | O_CREAT, 0600)) == -1 ||
1120 flock(fd, LOCK_EX) == -1 ||
1121 (lstat(fn, &st1) == -1 && errno != ENOENT) ||
1122 fstat(fd, &st2) == -1))
1124 if (ret == 0 && errno == 0 &&
1125 st1.st_dev == st2.st_dev && st1.st_ino == st2.st_ino) {
1126 if (S_ISREG(st1.st_mode))
1128 if (unlink(fn) == -1)
1133 /* Check if we lost any race to acquire Kerberos creds */
1135 ret = krb5_cc_resolve(r->context, temp_ccname, cc);
1137 ret = krb5_cc_get_lifetime(r->context, *cc, &life);
1138 if (ret == 0 && life > 60)
1139 *won = 0; /* We lost the race, but we win: we get to do less work */
1145 (void) close(fd); /* Drops the flock */
1150 * Acquire credentials for `princ' using PKINIT and the PKIX credentials in
1151 * `pkix_store', then place the result in the ccache named `ccname' (which will
1152 * be in our own private `cache_dir').
1154 * XXX This function could be rewritten using gss_acquire_cred_from() and
1155 * gss_store_cred_into() provided we add new generic cred store key/value pairs
1158 static krb5_error_code
1159 do_pkinit(struct bx509_request_desc *r, enum k5_creds_kind kind)
1161 krb5_get_init_creds_opt *opt = NULL;
1162 krb5_init_creds_context ctx = NULL;
1163 krb5_error_code ret = 0;
1164 krb5_ccache temp_cc = NULL;
1165 krb5_ccache cc = NULL;
1166 krb5_principal p = NULL;
1168 const char *cname = r->for_cname ? r->for_cname : r->cname;
1170 if (kind == K5_CREDS_CACHED) {
1173 ret = get_ccache(r, &temp_cc, &won);
1177 * We won the race to do PKINIT. Setup to acquire Kerberos creds with
1180 * We should really make sure that gss_acquire_cred_from() can do this
1181 * for us. We'd add generic cred store key/value pairs for PKIX cred
1182 * store, trust anchors, and so on, and acquire that way, then
1183 * gss_store_cred_into() to save it in a FILE ccache.
1186 ret = krb5_cc_new_unique(r->context, "FILE", NULL, &temp_cc);
1189 ret = krb5_parse_name(r->context, cname, &p);
1191 crealm = krb5_principal_get_realm(r->context, p);
1193 ret = krb5_get_init_creds_opt_alloc(r->context, &opt);
1195 krb5_get_init_creds_opt_set_default_flags(r->context, "kinit", crealm,
1197 if (ret == 0 && kind == K5_CREDS_EPHEMERAL &&
1198 !krb5_config_get_bool_default(r->context, NULL, TRUE,
1199 "get-tgt", "no_addresses", NULL)) {
1200 krb5_addresses addr;
1202 ret = _krb5_parse_address_no_lookup(r->context, r->frombuf, &addr);
1204 ret = krb5_append_addresses(r->context, &r->tgt_addresses,
1207 if (ret == 0 && r->tgt_addresses.len == 0)
1208 ret = krb5_get_init_creds_opt_set_addressless(r->context, opt, 1);
1210 krb5_get_init_creds_opt_set_address_list(opt, &r->tgt_addresses);
1212 ret = krb5_get_init_creds_opt_set_pkinit(r->context, opt, p,
1214 NULL, /* pkinit_anchor */
1215 NULL, /* anchor_chain */
1216 NULL, /* pkinit_crl */
1218 NULL, /* prompter */
1219 NULL, /* prompter data */
1220 NULL /* password */);
1222 ret = krb5_init_creds_init(r->context, p,
1223 NULL /* prompter */,
1224 NULL /* prompter data */,
1229 * Finally, do the AS exchange w/ PKINIT, extract the new Kerberos creds
1230 * into temp_cc, and rename into place. Note that krb5_cc_move() closes
1231 * the source ccache, so we set temp_cc = NULL if it succeeds.
1234 ret = krb5_init_creds_get(r->context, ctx);
1236 ret = krb5_init_creds_store(r->context, ctx, temp_cc);
1237 if (kind == K5_CREDS_CACHED) {
1239 ret = krb5_cc_resolve(r->context, r->ccname, &cc);
1241 ret = krb5_cc_move(r->context, temp_cc, cc);
1244 } else if (ret == 0 && kind == K5_CREDS_EPHEMERAL) {
1245 ret = krb5_cc_get_full_name(r->context, temp_cc, &r->ccname);
1250 krb5_init_creds_free(r->context, ctx);
1251 krb5_get_init_creds_opt_free(r->context, opt);
1252 krb5_free_principal(r->context, p);
1253 krb5_cc_close(r->context, temp_cc);
1254 krb5_cc_close(r->context, cc);
1258 static krb5_error_code
1259 load_priv_key(krb5_context context, const char *fn, hx509_private_key *key)
1261 hx509_private_key *keys = NULL;
1262 krb5_error_code ret;
1263 hx509_certs certs = NULL;
1266 ret = hx509_certs_init(context->hx509ctx, fn, 0, NULL, &certs);
1270 ret = _hx509_certs_keys_get(context->hx509ctx, certs, &keys);
1271 if (ret == 0 && keys[0] == NULL)
1272 ret = ENOENT; /* XXX Better error please */
1274 *key = _hx509_private_key_ref(keys[0]);
1276 krb5_set_error_message(context, ret, "Could not load private "
1277 "impersonation key from %s for PKINIT: %s", fn,
1278 hx509_get_error_string(context->hx509ctx, ret));
1279 _hx509_certs_keys_free(context->hx509ctx, keys);
1280 hx509_certs_free(&certs);
1284 static krb5_error_code
1285 k5_do_CA(struct bx509_request_desc *r)
1287 SubjectPublicKeyInfo spki;
1288 hx509_private_key key = NULL;
1289 krb5_error_code ret = 0;
1290 krb5_principal p = NULL;
1291 hx509_request req = NULL;
1292 hx509_certs certs = NULL;
1293 KeyUsage ku = int2KeyUsage(0);
1294 const char *cname = r->for_cname ? r->for_cname : r->cname;
1296 memset(&spki, 0, sizeof(spki));
1297 ku.digitalSignature = 1;
1299 /* Make a CSR (halfway -- we don't need to sign it here) */
1300 /* XXX Load impersonation key just once?? */
1301 ret = load_priv_key(r->context, impersonation_key_fn, &key);
1303 ret = hx509_request_init(r->context->hx509ctx, &req);
1305 ret = krb5_parse_name(r->context, cname, &p);
1307 hx509_private_key2SPKI(r->context->hx509ctx, key, &spki);
1309 hx509_request_set_SubjectPublicKeyInfo(r->context->hx509ctx, req,
1311 free_SubjectPublicKeyInfo(&spki);
1313 ret = hx509_request_add_pkinit(r->context->hx509ctx, req, cname);
1315 ret = hx509_request_add_eku(r->context->hx509ctx, req,
1316 &asn1_oid_id_pkekuoid);
1318 /* Mark it authorized */
1320 ret = hx509_request_authorize_san(req, 0);
1322 ret = hx509_request_authorize_eku(req, 0);
1324 hx509_request_authorize_ku(req, ku);
1326 /* Issue the certificate */
1328 ret = kdc_issue_certificate(r->context, "get-tgt", logfac, req, p,
1329 &r->token_times, r->req_life,
1330 1 /* send_chain */, &certs);
1331 krb5_free_principal(r->context, p);
1332 hx509_request_free(&req);
1335 if (ret == KRB5KDC_ERR_POLICY || ret == EACCES) {
1336 hx509_private_key_free(&key);
1337 return bad_403(r, ret,
1338 "Certificate request denied for policy reasons");
1340 if (ret == ENOMEM) {
1341 hx509_private_key_free(&key);
1342 return bad_503(r, ret, "Certificate issuance failed");
1345 hx509_private_key_free(&key);
1346 return bad_500(r, ret, "Certificate issuance failed");
1349 /* Setup PKIX store and extract the certificate chain into it */
1350 ret = mk_pkix_store(&r->pkix_store);
1352 ret = store_certs(r->context->hx509ctx, r->pkix_store, certs, key);
1353 hx509_private_key_free(&key);
1354 hx509_certs_free(&certs);
1356 return bad_500(r, ret,
1357 "Could not create PEM store for issued certificate");
1361 /* Get impersonated Kerberos credentials for `cprinc' */
1362 static krb5_error_code
1363 k5_get_creds(struct bx509_request_desc *r, enum k5_creds_kind kind)
1365 krb5_error_code ret;
1366 const char *cname = r->for_cname ? r->for_cname : r->cname;
1368 /* If we have a live ccache for `cprinc', we're done */
1369 if (kind == K5_CREDS_CACHED &&
1370 (ret = find_ccache(r->context, cname, &r->ccname)) == 0)
1371 return ret; /* Success */
1374 * Else we have to acquire a credential for them using their bearer token
1375 * for authentication (and our keytab / initiator credentials perhaps).
1377 if ((ret = k5_do_CA(r)))
1378 return ret; /* k5_do_CA() calls bad_req() */
1380 if (ret == 0 && (ret = do_pkinit(r, kind)))
1381 ret = bad_403(r, ret,
1382 "Could not acquire Kerberos credentials using PKINIT");
1386 /* Accumulate strings */
1388 acc_str(char **acc, char *adds, size_t addslen)
1391 int l = addslen <= INT_MAX ? (int)addslen : INT_MAX;
1393 if (asprintf(&tmp, "%s%s%.*s",
1395 *acc ? "; " : "", l, adds) > -1 &&
1403 fmt_gss_error(OM_uint32 code, gss_OID mech)
1405 gss_buffer_desc buf;
1406 OM_uint32 major, minor;
1407 OM_uint32 type = mech == GSS_C_NO_OID ? GSS_C_GSS_CODE: GSS_C_MECH_CODE;
1412 major = gss_display_status(&minor, code, type, mech, &more, &buf);
1413 if (!GSS_ERROR(major))
1414 acc_str(&r, (char *)buf.value, buf.length);
1415 gss_release_buffer(&minor, &buf);
1416 } while (!GSS_ERROR(major) && more);
1417 return r ? r : "Out of memory while formatting GSS-API error";
1421 fmt_gss_errors(const char *r, OM_uint32 major, OM_uint32 minor, gss_OID mech)
1425 ma = fmt_gss_error(major, GSS_C_NO_OID);
1426 mi = mech == GSS_C_NO_OID ? NULL : fmt_gss_error(minor, mech);
1427 if (asprintf(&s, "%s: %s%s%s", r, ma, mi ? ": " : "", mi ? mi : "") > -1 &&
1438 static krb5_error_code
1439 bad_req_gss(struct bx509_request_desc *r,
1443 int http_status_code,
1446 krb5_error_code ret;
1447 char *msg = fmt_gss_errors(reason, major, minor, mech);
1449 if (major == GSS_S_BAD_NAME || major == GSS_S_BAD_NAMETYPE)
1450 http_status_code = MHD_HTTP_BAD_REQUEST;
1452 ret = resp(r, http_status_code, MHD_RESPMEM_MUST_COPY, NULL,
1453 msg, strlen(msg), NULL);
1458 /* Make an HTTP/Negotiate token */
1459 static krb5_error_code
1460 mk_nego_tok(struct bx509_request_desc *r,
1464 gss_key_value_element_desc kv[1] = { { "ccache", r->ccname } };
1465 gss_key_value_set_desc store = { 1, kv };
1466 gss_buffer_desc token = GSS_C_EMPTY_BUFFER;
1467 gss_buffer_desc name = GSS_C_EMPTY_BUFFER;
1468 gss_cred_id_t cred = GSS_C_NO_CREDENTIAL;
1469 gss_ctx_id_t ctx = GSS_C_NO_CONTEXT;
1470 gss_name_t iname = GSS_C_NO_NAME;
1471 gss_name_t aname = GSS_C_NO_NAME;
1472 OM_uint32 major, minor, junk;
1473 krb5_error_code ret; /* More like a system error code here */
1474 const char *cname = r->for_cname ? r->for_cname : r->cname;
1475 char *token_b64 = NULL;
1480 /* Import initiator name */
1481 name.length = strlen(cname);
1482 name.value = rk_UNCONST(cname);
1483 major = gss_import_name(&minor, &name, GSS_KRB5_NT_PRINCIPAL_NAME, &iname);
1484 if (major != GSS_S_COMPLETE)
1485 return bad_req_gss(r, major, minor, GSS_C_NO_OID,
1486 MHD_HTTP_SERVICE_UNAVAILABLE,
1487 "Could not import cprinc parameter value as "
1488 "Kerberos principal name");
1490 /* Import target acceptor name */
1491 name.length = strlen(r->target);
1492 name.value = rk_UNCONST(r->target);
1493 major = gss_import_name(&minor, &name, GSS_C_NT_HOSTBASED_SERVICE, &aname);
1494 if (major != GSS_S_COMPLETE) {
1495 (void) gss_release_name(&junk, &iname);
1496 return bad_req_gss(r, major, minor, GSS_C_NO_OID,
1497 MHD_HTTP_SERVICE_UNAVAILABLE,
1498 "Could not import target parameter value as "
1499 "Kerberos principal name");
1502 /* Acquire a credential from the given ccache */
1503 major = gss_add_cred_from(&minor, cred, iname, GSS_KRB5_MECHANISM,
1504 GSS_C_INITIATE, GSS_C_INDEFINITE, 0, &store,
1505 &cred, NULL, NULL, NULL);
1506 (void) gss_release_name(&junk, &iname);
1507 if (major != GSS_S_COMPLETE) {
1508 (void) gss_release_name(&junk, &aname);
1509 return bad_req_gss(r, major, minor, GSS_KRB5_MECHANISM,
1510 MHD_HTTP_FORBIDDEN, "Could not acquire credentials "
1511 "for requested cprinc");
1514 major = gss_init_sec_context(&minor, cred, &ctx, aname,
1515 GSS_KRB5_MECHANISM, 0, GSS_C_INDEFINITE,
1516 NULL, GSS_C_NO_BUFFER, NULL, &token, NULL,
1518 (void) gss_delete_sec_context(&junk, &ctx, GSS_C_NO_BUFFER);
1519 (void) gss_release_name(&junk, &aname);
1520 (void) gss_release_cred(&junk, &cred);
1521 if (major != GSS_S_COMPLETE)
1522 return bad_req_gss(r, major, minor, GSS_KRB5_MECHANISM,
1523 MHD_HTTP_SERVICE_UNAVAILABLE, "Could not acquire "
1524 "Negotiate token for requested target");
1526 /* Encode token, output */
1527 ret = rk_base64_encode(token.value, token.length, &token_b64);
1528 (void) gss_release_buffer(&junk, &token);
1530 ret = asprintf(nego_tok, "Negotiate %s", token_b64);
1532 if (ret < 0 || *nego_tok == NULL)
1533 return bad_req(r, errno, MHD_HTTP_SERVICE_UNAVAILABLE,
1534 "Could not allocate memory for encoding Negotiate "
1540 static krb5_error_code
1541 bnegotiate_get_target(struct bx509_request_desc *r)
1545 const char *referer; /* misspelled on the wire, misspelled here, FYI */
1546 const char *authority;
1547 const char *local_part;
1551 target = MHD_lookup_connection_value(r->connection, MHD_GET_ARGUMENT_KIND,
1553 redir = MHD_lookup_connection_value(r->connection, MHD_GET_ARGUMENT_KIND,
1555 referer = MHD_lookup_connection_value(r->connection, MHD_HEADER_KIND,
1556 MHD_HTTP_HEADER_REFERER);
1557 if (target != NULL && redir == NULL) {
1561 if (target == NULL && redir == NULL)
1562 return bad_400(r, EINVAL,
1563 "Query missing 'target' or 'redirect' parameter value");
1564 if (target != NULL && redir != NULL)
1565 return bad_403(r, EACCES,
1566 "Only one of 'target' or 'redirect' parameter allowed");
1567 if (redir != NULL && referer == NULL)
1568 return bad_403(r, EACCES,
1569 "Redirect request without Referer header nor allowed");
1571 if (strncmp(referer, "https://", sizeof("https://") - 1) != 0 ||
1572 strncmp(redir, "https://", sizeof("https://") - 1) != 0)
1573 return bad_403(r, EACCES,
1574 "Redirect requests permitted only for https referrers");
1576 /* Parse out authority from each URI, redirect and referrer */
1577 authority = redir + sizeof("https://") - 1;
1578 if ((local_part = strchr(authority, '/')) == NULL)
1579 local_part = authority + strlen(authority);
1580 if ((s1 = strndup(authority, local_part - authority)) == NULL)
1581 return bad_enomem(r, ENOMEM);
1583 authority = referer + sizeof("https://") - 1;
1584 if ((local_part = strchr(authority, '/')) == NULL)
1585 local_part = authority + strlen(authority);
1586 if ((s2 = strndup(authority, local_part - authority)) == NULL) {
1588 return bad_enomem(r, ENOMEM);
1591 /* Both must match */
1592 if (strcasecmp(s1, s2) != 0) {
1595 return bad_403(r, EACCES, "Redirect request does not match referer");
1599 if (strchr(s1, '@')) {
1601 return bad_403(r, EACCES,
1602 "Redirect request authority has login information");
1605 /* Extract hostname portion of authority and format GSS name */
1606 if (strchr(s1, ':'))
1607 *strchr(s1, ':') = '\0';
1608 if (asprintf(&r->freeme1, "HTTP@%s", s1) == -1 || r->freeme1 == NULL) {
1610 return bad_enomem(r, ENOMEM);
1613 r->target = r->freeme1;
1620 * Implements /bnegotiate end-point.
1622 * Query parameters (mutually exclusive):
1625 * - redirect=<URL-encoded-URL>
1627 * If the redirect query parameter is set then the Referer: header must be as
1628 * well, and the authority of the redirect and Referer URIs must be the same.
1630 static krb5_error_code
1631 bnegotiate(struct bx509_request_desc *r)
1633 krb5_error_code ret;
1634 size_t nego_toksz = 0;
1635 char *nego_tok = NULL;
1637 ret = bnegotiate_get_target(r);
1639 heim_audit_addkv((heim_svc_req_desc)r, KDC_AUDIT_VIS, "target", "%s",
1640 r->target ? r->target : "<unknown>");
1641 heim_audit_addkv((heim_svc_req_desc)r, 0, "redir", "%s",
1642 r->redir ? "yes" : "no");
1643 ret = validate_token(r);
1645 /* bnegotiate_get_target() and validate_token() call bad_req() */
1650 * Make sure we have Kerberos credentials for cprinc. If we have them
1651 * cached from earlier, this will be fast (all local), else it will involve
1652 * taking a file lock and talking to the KDC using kx509 and PKINIT.
1654 * Perhaps we could use S4U instead, which would speed up the slow path a
1657 ret = k5_get_creds(r, K5_CREDS_CACHED);
1661 /* Acquire the Negotiate token and output it */
1662 if (ret == 0 && r->ccname != NULL)
1663 ret = mk_nego_tok(r, &nego_tok, &nego_toksz);
1666 /* Look ma', Negotiate as an OAuth-like token system! */
1668 ret = resp(r, MHD_HTTP_TEMPORARY_REDIRECT, MHD_RESPMEM_PERSISTENT,
1669 NULL, "", 0, nego_tok);
1671 ret = resp(r, MHD_HTTP_OK, MHD_RESPMEM_MUST_COPY,
1672 "application/x-negotiate-token", nego_tok, nego_toksz,
1680 static krb5_error_code
1681 authorize_TGT_REQ(struct bx509_request_desc *r)
1683 krb5_principal p = NULL;
1684 krb5_error_code ret;
1685 const char *for_cname = r->for_cname ? r->for_cname : r->cname;
1687 if (for_cname == r->cname || strcmp(r->cname, r->for_cname) == 0)
1690 ret = krb5_parse_name(r->context, r->cname, &p);
1691 ret = hx509_request_init(r->context->hx509ctx, &r->req);
1693 return bad_500(r, ret, "Out of resources");
1694 heim_audit_addkv((heim_svc_req_desc)r, KDC_AUDIT_VIS,
1695 "requested_krb5PrincipalName", "%s", for_cname);
1696 ret = hx509_request_add_eku(r->context->hx509ctx, r->req,
1697 ASN1_OID_ID_PKEKUOID);
1699 ret = hx509_request_add_pkinit(r->context->hx509ctx, r->req,
1702 ret = kdc_authorize_csr(r->context, "get-tgt", r->req, p);
1703 krb5_free_principal(r->context, p);
1704 hx509_request_free(&r->req);
1706 return bad_403(r, ret, "Not authorized to requested TGT");
1711 get_tgt_param_cb(void *d,
1712 enum MHD_ValueKind kind,
1716 struct bx509_request_desc *r = d;
1718 if (strcmp(key, "address") == 0 && val) {
1719 if (!krb5_config_get_bool_default(r->context, NULL,
1721 "get-tgt", "allow_addresses", NULL)) {
1722 krb5_set_error_message(r->context, r->ret = ENOTSUP,
1723 "Query parameter %s not allowed", key);
1725 krb5_addresses addresses;
1727 r->ret = _krb5_parse_address_no_lookup(r->context, val,
1730 r->ret = krb5_append_addresses(r->context, &r->tgt_addresses,
1732 krb5_free_addresses(r->context, &addresses);
1734 } else if (strcmp(key, "cname") == 0) {
1735 /* Handled upstairs */
1737 } else if (strcmp(key, "lifetime") == 0 && val) {
1738 r->req_life = parse_time(val, "day");
1740 /* Produce error for unknown params */
1741 heim_audit_addkv((heim_svc_req_desc)r, 0, "requested_unknown", "true");
1742 krb5_set_error_message(r->context, r->ret = ENOTSUP,
1743 "Query parameter %s not supported", key);
1745 return r->ret == 0 ? MHD_YES : MHD_NO /* Stop iterating */;
1749 * Implements /get-tgt end-point.
1751 * Query parameters (mutually exclusive):
1753 * - cname=<name> (client principal name, if not the same as the authenticated
1754 * name, then this will be impersonated if allowed)
1756 static krb5_error_code
1757 get_tgt(struct bx509_request_desc *r)
1759 krb5_error_code ret;
1764 r->for_cname = MHD_lookup_connection_value(r->connection,
1765 MHD_GET_ARGUMENT_KIND, "cname");
1766 if (r->for_cname && r->for_cname[0] == '\0')
1767 r->for_cname = NULL;
1768 ret = validate_token(r);
1770 ret = authorize_TGT_REQ(r);
1771 /* validate_token() and authorize_TGT_REQ() call bad_req() */
1776 (void) MHD_get_connection_values(r->connection, MHD_GET_ARGUMENT_KIND,
1777 get_tgt_param_cb, r);
1780 /* k5_get_creds() calls bad_req() */
1781 ret = k5_get_creds(r, K5_CREDS_EPHEMERAL);
1785 fn = strchr(r->ccname, ':');
1787 return bad_500(r, ret, "Impossible error");
1789 if ((errno = rk_undumpdata(fn, &body, &bodylen))) {
1791 return bad_503(r, ret, "Could not get TGT");
1794 ret = resp(r, MHD_HTTP_OK, MHD_RESPMEM_MUST_COPY,
1795 "application/x-krb5-ccache", body, bodylen, NULL);
1800 static krb5_error_code
1801 health(const char *method, struct bx509_request_desc *r)
1803 if (strcmp(method, "HEAD") == 0)
1804 return resp(r, MHD_HTTP_OK, MHD_RESPMEM_PERSISTENT, NULL, "", 0, NULL);
1805 return resp(r, MHD_HTTP_OK, MHD_RESPMEM_PERSISTENT, NULL,
1806 "To determine the health of the service, use the /bx509 "
1808 sizeof("To determine the health of the service, use the "
1809 "/bx509 end-point.\n") - 1, NULL);
1813 /* Implements the entirety of this REST service */
1816 struct MHD_Connection *connection,
1819 const char *version,
1820 const char *upload_data,
1821 size_t *upload_data_size,
1824 static int aptr = 0;
1825 struct bx509_request_desc r;
1830 * This is the first call, right after headers were read.
1832 * We must return quickly so that any 100-Continue might be sent with
1835 * We'll get called again to really do the processing. If we handled
1836 * POSTs then we'd also get called with upload_data != NULL between the
1837 * first and last calls. We need to keep no state between the first
1838 * and last calls, but we do need to distinguish first and last call,
1839 * so we use the ctx argument for this.
1845 if ((ret = set_req_desc(connection, url, &r)))
1846 return bad_503(&r, ret, "Could not initialize request state");
1847 if ((strcmp(method, "HEAD") == 0 || strcmp(method, "GET") == 0) &&
1848 (strcmp(url, "/health") == 0 || strcmp(url, "/") == 0))
1849 ret = health(method, &r);
1850 else if (strcmp(method, "GET") != 0)
1851 ret = bad_405(&r, method);
1852 else if (strcmp(url, "/get-cert") == 0 ||
1853 strcmp(url, "/bx509") == 0) /* old name */
1855 else if (strcmp(url, "/get-negotiate-token") == 0 ||
1856 strcmp(url, "/bnegotiate") == 0) /* old name */
1857 ret = bnegotiate(&r);
1858 else if (strcmp(url, "/get-tgt") == 0)
1861 ret = bad_404(&r, url);
1864 return ret == -1 ? MHD_NO : MHD_YES;
1867 static struct getargs args[] = {
1868 { "help", 'h', arg_flag, &help_flag, "Print usage message", NULL },
1869 { "version", '\0', arg_flag, &version_flag, "Print version", NULL },
1870 { NULL, 'H', arg_strings, &audiences,
1871 "expected token audience(s) of bx509 service", "HOSTNAME" },
1872 { "daemon", 'd', arg_flag, &daemonize, "daemonize", "daemonize" },
1873 { "daemon-child", 0, arg_flag, &daemon_child_fd, NULL, NULL }, /* priv */
1874 { "reverse-proxied", 0, arg_flag, &reverse_proxied_flag,
1875 "reverse proxied", "listen on 127.0.0.1 and do not use TLS" },
1876 { NULL, 'p', arg_integer, &port, "PORT", "port number (default: 443)" },
1877 { "cache-dir", 0, arg_string, &cache_dir,
1878 "cache directory", "DIRECTORY" },
1879 { "cert", 0, arg_string, &cert_file,
1880 "certificate file path (PEM)", "HX509-STORE" },
1881 { "private-key", 0, arg_string, &priv_key_file,
1882 "private key file path (PEM)", "HX509-STORE" },
1883 { "thread-per-client", 't', arg_flag, &thread_per_client_flag,
1884 "thread per-client", "use thread per-client" },
1885 { "verbose", 'v', arg_counter, &verbose_counter, "verbose", "run verbosely" }
1891 arg_printusage(args, sizeof(args) / sizeof(args[0]), "bx509",
1892 "\nServes RESTful GETs of /bx509 and /bnegotiate,\n"
1893 "performing corresponding kx509 and, possibly, PKINIT requests\n"
1894 "to the KDCs of the requested realms (or just the given REALM).\n");
1898 static int sigpipe[2] = { -1, -1 };
1904 while (write(sigpipe[1], &c, sizeof(c)) == -1 && errno == EINTR)
1909 bx509_openlog(krb5_context context,
1911 krb5_log_facility **fac)
1913 char **s = NULL, **p;
1915 krb5_initlog(context, "bx509d", fac);
1916 s = krb5_config_get_strings(context, NULL, svc, "logging", NULL);
1918 s = krb5_config_get_strings(context, NULL, "logging", svc, NULL);
1921 krb5_addlog_dest(context, *fac, *p);
1922 krb5_config_free_strings(s);
1925 if (asprintf(&ss, "0-1/FILE:%s/%s", hdb_db_dir(context),
1927 err(1, "out of memory");
1928 krb5_addlog_dest(context, *fac, ss);
1931 krb5_set_warn_dest(context, *fac);
1934 static const char *sysplugin_dirs[] = {
1938 "$ORIGIN/../lib/plugin/kdc",
1941 LIBDIR "/plugin/kdc",
1947 load_plugins(krb5_context context)
1949 const char * const *dirs = sysplugin_dirs;
1953 cfdirs = krb5_config_get_strings(context, NULL, "kdc", "plugin_dir", NULL);
1955 dirs = (const char * const *)cfdirs;
1959 _krb5_load_plugins(context, "kdc", (const char **)dirs);
1962 krb5_config_free_strings(cfdirs);
1967 main(int argc, char **argv)
1969 unsigned int flags = MHD_USE_THREAD_PER_CONNECTION; /* XXX */
1970 struct sockaddr_in sin;
1971 struct MHD_Daemon *previous = NULL;
1972 struct MHD_Daemon *current = NULL;
1973 struct sigaction sa;
1974 krb5_context context = NULL;
1975 MHD_socket sock = MHD_INVALID_SOCKET;
1976 char *priv_key_pem = NULL;
1977 char *cert_pem = NULL;
1982 setprogname("bx509d");
1983 if (getarg(args, sizeof(args) / sizeof(args[0]), argc, argv, &optidx))
1988 print_version(NULL);
1991 if (argc > optidx) /* Add option to set a URI local part prefix? */
1994 errx(1, "Port number must be given");
1996 if (audiences.num_strings == 0) {
1997 char localhost[MAXHOSTNAMELEN];
1999 ret = gethostname(localhost, sizeof(localhost));
2001 errx(1, "Could not determine local hostname; use --audience");
2003 if ((audiences.strings =
2004 calloc(1, sizeof(audiences.strings[0]))) == NULL ||
2005 (audiences.strings[0] = strdup(localhost)) == NULL)
2006 err(1, "Out of memory");
2007 audiences.num_strings = 1;
2010 if (daemonize && daemon_child_fd == -1)
2011 daemon_child_fd = roken_detach_prep(argc, argv, "--daemon-child");
2017 if ((errno = pthread_key_create(&k5ctx, k5_free_context)))
2018 err(1, "Could not create thread-specific storage");
2020 if ((errno = get_krb5_context(&context)))
2021 err(1, "Could not init krb5 context");
2023 bx509_openlog(context, "bx509d", &logfac);
2024 load_plugins(context);
2026 if (cache_dir == NULL) {
2029 if (asprintf(&s, "%s/bx509d-XXXXXX",
2030 getenv("TMPDIR") ? getenv("TMPDIR") : "/tmp") == -1 ||
2032 (cache_dir = mkdtemp(s)) == NULL)
2033 err(1, "could not create temporary cache directory");
2034 if (verbose_counter)
2035 fprintf(stderr, "Note: using %s as cache directory\n", cache_dir);
2036 atexit(rm_cache_dir);
2037 setenv("TMPDIR", cache_dir, 1);
2040 generate_key(context->hx509ctx, "impersonation", "rsa", 2048, &impersonation_key_fn);
2043 if (cert_file && !priv_key_file)
2044 priv_key_file = cert_file;
2047 hx509_cursor cursor = NULL;
2048 hx509_certs certs = NULL;
2049 hx509_cert cert = NULL;
2050 time_t min_cert_life = 0;
2054 ret = hx509_certs_init(context->hx509ctx, cert_file, 0, NULL, &certs);
2056 ret = hx509_certs_start_seq(context->hx509ctx, certs, &cursor);
2058 (ret = hx509_certs_next_cert(context->hx509ctx, certs,
2059 cursor, &cert)) == 0 && cert) {
2060 time_t notAfter = 0;
2062 if (!hx509_cert_have_private_key_only(cert) &&
2063 (notAfter = hx509_cert_get_notAfter(cert)) <= time(NULL) + 30)
2064 errx(1, "One or more certificates in %s are expired",
2067 notAfter -= time(NULL);
2069 warnx("One or more certificates in %s expire soon",
2071 /* Reload 5 minutes prior to expiration */
2072 if (notAfter < min_cert_life || min_cert_life < 1)
2073 min_cert_life = notAfter;
2075 hx509_cert_free(cert);
2078 (void) hx509_certs_end_seq(context->hx509ctx, certs, cursor);
2079 if (min_cert_life > 4)
2080 alarm(min_cert_life >> 1);
2081 hx509_certs_free(&certs);
2083 hx509_err(context->hx509ctx, 1, ret,
2084 "could not read certificate from %s", cert_file);
2086 if ((errno = rk_undumpdata(cert_file, &s, &len)) ||
2087 (cert_pem = strndup(s, len)) == NULL)
2088 err(1, "could not read certificate from %s", cert_file);
2089 if (strlen(cert_pem) != len)
2090 err(1, "NULs in certificate file contents: %s", cert_file);
2094 if (priv_key_file) {
2098 if ((errno = rk_undumpdata(priv_key_file, &s, &len)) ||
2099 (priv_key_pem = strndup(s, len)) == NULL)
2100 err(1, "could not read private key from %s", priv_key_file);
2101 if (strlen(priv_key_pem) != len)
2102 err(1, "NULs in private key file contents: %s", priv_key_file);
2106 if (verbose_counter > 1)
2107 flags |= MHD_USE_DEBUG;
2108 if (thread_per_client_flag)
2109 flags |= MHD_USE_THREAD_PER_CONNECTION;
2112 if (pipe(sigpipe) == -1)
2113 err(1, "Could not set up key/cert reloading");
2114 memset(&sa, 0, sizeof(sa));
2115 sa.sa_handler = sighandler;
2116 if (reverse_proxied_flag) {
2118 * We won't use TLS in the reverse proxy case, so no need to reload
2119 * certs. But we'll still read them if given, and alarm() will get
2122 (void) signal(SIGHUP, SIG_IGN);
2123 (void) signal(SIGUSR1, SIG_IGN);
2124 (void) signal(SIGALRM, SIG_IGN);
2126 (void) sigaction(SIGHUP, &sa, NULL); /* Reload key & cert */
2127 (void) sigaction(SIGUSR1, &sa, NULL); /* Reload key & cert */
2128 (void) sigaction(SIGALRM, &sa, NULL); /* Reload key & cert */
2130 (void) sigaction(SIGINT, &sa, NULL); /* Graceful shutdown */
2131 (void) sigaction(SIGTERM, &sa, NULL); /* Graceful shutdown */
2132 (void) signal(SIGPIPE, SIG_IGN);
2135 sock = MHD_quiesce_daemon(previous);
2137 if (reverse_proxied_flag) {
2139 * XXX IPv6 too. Create the sockets and tell MHD_start_daemon() about
2142 sin.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
2143 sin.sin_family = AF_INET;
2144 sin.sin_port = htons(port);
2145 current = MHD_start_daemon(flags, port,
2147 route, (char *)NULL,
2148 MHD_OPTION_SOCK_ADDR, &sin,
2149 MHD_OPTION_CONNECTION_LIMIT, (unsigned int)200,
2150 MHD_OPTION_CONNECTION_TIMEOUT, (unsigned int)10,
2152 } else if (sock != MHD_INVALID_SOCKET) {
2154 * Certificate/key rollover: reuse the listen socket returned by
2155 * MHD_quiesce_daemon().
2157 current = MHD_start_daemon(flags | MHD_USE_SSL, port,
2159 route, (char *)NULL,
2160 MHD_OPTION_HTTPS_MEM_KEY, priv_key_pem,
2161 MHD_OPTION_HTTPS_MEM_CERT, cert_pem,
2162 MHD_OPTION_CONNECTION_LIMIT, (unsigned int)200,
2163 MHD_OPTION_CONNECTION_TIMEOUT, (unsigned int)10,
2164 MHD_OPTION_LISTEN_SOCKET, sock,
2166 sock = MHD_INVALID_SOCKET;
2168 current = MHD_start_daemon(flags | MHD_USE_SSL, port,
2170 route, (char *)NULL,
2171 MHD_OPTION_HTTPS_MEM_KEY, priv_key_pem,
2172 MHD_OPTION_HTTPS_MEM_CERT, cert_pem,
2173 MHD_OPTION_CONNECTION_LIMIT, (unsigned int)200,
2174 MHD_OPTION_CONNECTION_TIMEOUT, (unsigned int)10,
2177 if (current == NULL)
2178 err(1, "Could not start bx509 REST service");
2181 MHD_stop_daemon(previous);
2185 if (verbose_counter)
2186 fprintf(stderr, "Ready!\n");
2187 if (daemon_child_fd != -1)
2188 roken_detach_finish(NULL, daemon_child_fd);
2190 /* Wait for signal, possibly SIGALRM, to reload certs and/or exit */
2191 while ((ret = read(sigpipe[0], &sig, sizeof(sig))) == -1 &&
2197 priv_key_pem = NULL;
2200 if (ret == 1 && (sig == SIGHUP || sig == SIGUSR1 || sig == SIGALRM)) {
2201 /* Reload certs and restart service gracefully */
2207 MHD_stop_daemon(current);
2208 _krb5_unload_plugins(context, "kdc");
2209 pthread_key_delete(k5ctx);