
Excluded: libtomath and libedit files, most of which appear to be testing or example code not involved in production, and which are derived from an upstream that should perhaps have patches submitted upstream instead. fix https://github.com/heimdal/heimdal/issues/1111
3176 lines
104 KiB
C
3176 lines
104 KiB
C
/*
|
|
* Copyright (c) 2019 Kungliga Tekniska Högskolan
|
|
* (Royal Institute of Technology, Stockholm, Sweden).
|
|
* All rights reserved.
|
|
*
|
|
* Redistribution and use in source and binary forms, with or without
|
|
* modification, are permitted provided that the following conditions
|
|
* are met:
|
|
*
|
|
* 1. Redistributions of source code must retain the above copyright
|
|
* notice, this list of conditions and the following disclaimer.
|
|
*
|
|
* 2. Redistributions in binary form must reproduce the above copyright
|
|
* notice, this list of conditions and the following disclaimer in the
|
|
* documentation and/or other materials provided with the distribution.
|
|
*
|
|
* 3. Neither the name of the Institute nor the names of its contributors
|
|
* may be used to endorse or promote products derived from this software
|
|
* without specific prior written permission.
|
|
*
|
|
* THIS SOFTWARE IS PROVIDED BY THE INSTITUTE AND CONTRIBUTORS ``AS IS'' AND
|
|
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
|
* ARE DISCLAIMED. IN NO EVENT SHALL THE INSTITUTE OR CONTRIBUTORS BE LIABLE
|
|
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
|
|
* OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
|
|
* HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
|
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
|
|
* OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
|
|
* SUCH DAMAGE.
|
|
*/
|
|
|
|
/*
|
|
* This file implements a RESTful HTTPS API to an online CA, as well as an
|
|
* HTTP/Negotiate token issuer, as well as a way to get TGTs.
|
|
*
|
|
* Users are authenticated with Negotiate and/or Bearer.
|
|
*
|
|
* This is essentially a RESTful online CA sharing some code with the KDC's
|
|
* kx509 online CA, and also a proxy for PKINIT and GSS-API (Negotiate).
|
|
*
|
|
* See the manual page for HTTP API details.
|
|
*
|
|
* TBD:
|
|
* - rewrite to not use libmicrohttpd but an alternative more appropriate to
|
|
* Heimdal's license (though libmicrohttpd will do)
|
|
* - there should be an end-point for fetching an issuer's chain
|
|
*
|
|
* NOTES:
|
|
* - We use krb5_error_code values as much as possible. Where we need to use
|
|
* MHD_NO because we got that from an mhd function and cannot respond with
|
|
* an HTTP response, we use (krb5_error_code)-1, and later map that to
|
|
* MHD_NO.
|
|
*
|
|
* (MHD_NO is an ENOMEM-cannot-even-make-a-static-503-response level event.)
|
|
*/
|
|
|
|
/*
|
|
* Theory of operation:
|
|
*
|
|
* - We use libmicrohttpd (MHD) for the HTTP(S) implementation.
|
|
*
|
|
* - MHD has an online request processing model:
|
|
*
|
|
* - all requests are handled via the `dh' and `dh_cls' closure arguments
|
|
* of `MHD_start_daemon()'; ours is called `route()'
|
|
*
|
|
* - `dh' is called N+1 times:
|
|
* - once to allocate a request context
|
|
* - once for every N chunks of request body
|
|
* - once to process the request and produce a response
|
|
*
|
|
* - the response cannot begin to be produced before consuming the whole
|
|
* request body (for requests that have a body)
|
|
* (this seems like a bug in MHD)
|
|
*
|
|
* - the response body can be produced over multiple calls (i.e., in an
|
|
* online manner)
|
|
*
|
|
* - Our `route()' processes any POST request body form data / multipart by
|
|
* treating all the key/value pairs as if they had been additional URI query
|
|
* parameters.
|
|
*
|
|
* - Then `route()' calls a handler appropriate to the URI local-part with the
|
|
* request context, and the handler produces a response in one call.
|
|
*
|
|
* I.e., we turn the online MHD request processing into not-online. Our
|
|
* handlers are presented with complete requests and must produce complete
|
|
* responses in one call.
|
|
*
|
|
* - `route()' also does any authentication and CSRF protection so that the
|
|
* request handlers don't have to.
|
|
*
|
|
* This non-online request handling approach works for most everything we want
|
|
* to do. However, for /get-tgts with very large numbers of principals, we
|
|
* might have to revisit this, using MHD_create_response_from_callback() or
|
|
* MHD_create_response_from_pipe() (and a thread to do the actual work of
|
|
* producing the body) instead of MHD_create_response_from_buffer().
|
|
*/
|
|
|
|
#define _XOPEN_SOURCE_EXTENDED 1
|
|
#define _DEFAULT_SOURCE 1
|
|
#define _BSD_SOURCE 1
|
|
#define _GNU_SOURCE 1
|
|
|
|
#include <sys/socket.h>
|
|
#include <sys/types.h>
|
|
#include <sys/stat.h>
|
|
#include <sys/time.h>
|
|
#include <ctype.h>
|
|
#include <dlfcn.h>
|
|
#include <errno.h>
|
|
#include <fcntl.h>
|
|
#include <pthread.h>
|
|
#include <signal.h>
|
|
#include <stdarg.h>
|
|
#include <stddef.h>
|
|
#include <stdint.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <time.h>
|
|
#include <unistd.h>
|
|
#include <netdb.h>
|
|
#include <netinet/in.h>
|
|
#include <netinet/ip.h>
|
|
|
|
#include <microhttpd.h>
|
|
#include "kdc_locl.h"
|
|
#include "token_validator_plugin.h"
|
|
#include <getarg.h>
|
|
#include <roken.h>
|
|
#include <krb5.h>
|
|
#include <gssapi/gssapi.h>
|
|
#include <gssapi/gssapi_krb5.h>
|
|
#include <hx509.h>
|
|
#include "../lib/hx509/hx_locl.h"
|
|
#include <hx509-private.h>
|
|
|
|
#define heim_pcontext krb5_context
|
|
#define heim_pconfig krb5_context
|
|
#include <heimbase-svc.h>
|
|
|
|
#if MHD_VERSION < 0x00097002 || defined(MHD_YES)
|
|
/* libmicrohttpd changed these from int valued macros to an enum in 0.9.71 */
|
|
#ifdef MHD_YES
|
|
#undef MHD_YES
|
|
#undef MHD_NO
|
|
#endif
|
|
enum MHD_Result { MHD_NO = 0, MHD_YES = 1 };
|
|
#define MHD_YES 1
|
|
#define MHD_NO 0
|
|
typedef int heim_mhd_result;
|
|
#else
|
|
typedef enum MHD_Result heim_mhd_result;
|
|
#endif
|
|
|
|
enum k5_creds_kind { K5_CREDS_EPHEMERAL, K5_CREDS_CACHED };
|
|
|
|
/*
|
|
* This is to keep track of memory we need to free, mainly because we had to
|
|
* duplicate data from the MHD POST form data processor.
|
|
*/
|
|
struct free_tend_list {
|
|
void *freeme1;
|
|
void *freeme2;
|
|
struct free_tend_list *next;
|
|
};
|
|
|
|
/* Per-request context data structure */
|
|
typedef struct bx509_request_desc {
|
|
/* Common elements for Heimdal request/response services */
|
|
HEIM_SVC_REQUEST_DESC_COMMON_ELEMENTS;
|
|
|
|
struct MHD_Connection *connection;
|
|
struct MHD_PostProcessor *pp;
|
|
struct MHD_Response *response;
|
|
krb5_times token_times;
|
|
time_t req_life;
|
|
hx509_request req;
|
|
struct free_tend_list *free_list;
|
|
const char *for_cname;
|
|
const char *target;
|
|
const char *redir;
|
|
const char *method;
|
|
size_t post_data_size;
|
|
size_t san_idx; /* For /get-tgts */
|
|
enum k5_creds_kind cckind;
|
|
char *pkix_store;
|
|
char *tgts_filename;
|
|
FILE *tgts;
|
|
char *ccname;
|
|
char *freeme1;
|
|
char *csrf_token;
|
|
krb5_addresses tgt_addresses; /* For /get-tgt */
|
|
char frombuf[128];
|
|
} *bx509_request_desc;
|
|
|
|
static void
|
|
audit_trail(bx509_request_desc r, krb5_error_code ret)
|
|
{
|
|
const char *retname = NULL;
|
|
|
|
/* Get a symbolic name for some error codes */
|
|
#define CASE(x) case x : retname = #x; break
|
|
switch (ret) {
|
|
CASE(ENOMEM);
|
|
CASE(EACCES);
|
|
CASE(HDB_ERR_NOT_FOUND_HERE);
|
|
CASE(HDB_ERR_WRONG_REALM);
|
|
CASE(HDB_ERR_EXISTS);
|
|
CASE(HDB_ERR_KVNO_NOT_FOUND);
|
|
CASE(HDB_ERR_NOENTRY);
|
|
CASE(HDB_ERR_NO_MKEY);
|
|
CASE(KRB5KDC_ERR_BADOPTION);
|
|
CASE(KRB5KDC_ERR_CANNOT_POSTDATE);
|
|
CASE(KRB5KDC_ERR_CLIENT_NOTYET);
|
|
CASE(KRB5KDC_ERR_C_PRINCIPAL_UNKNOWN);
|
|
CASE(KRB5KDC_ERR_ETYPE_NOSUPP);
|
|
CASE(KRB5KDC_ERR_KEY_EXPIRED);
|
|
CASE(KRB5KDC_ERR_NAME_EXP);
|
|
CASE(KRB5KDC_ERR_NEVER_VALID);
|
|
CASE(KRB5KDC_ERR_NONE);
|
|
CASE(KRB5KDC_ERR_NULL_KEY);
|
|
CASE(KRB5KDC_ERR_PADATA_TYPE_NOSUPP);
|
|
CASE(KRB5KDC_ERR_POLICY);
|
|
CASE(KRB5KDC_ERR_PREAUTH_FAILED);
|
|
CASE(KRB5KDC_ERR_PREAUTH_REQUIRED);
|
|
CASE(KRB5KDC_ERR_SERVER_NOMATCH);
|
|
CASE(KRB5KDC_ERR_SERVICE_EXP);
|
|
CASE(KRB5KDC_ERR_SERVICE_NOTYET);
|
|
CASE(KRB5KDC_ERR_S_PRINCIPAL_UNKNOWN);
|
|
CASE(KRB5KDC_ERR_TRTYPE_NOSUPP);
|
|
CASE(KRB5KRB_ERR_RESPONSE_TOO_BIG);
|
|
/* XXX Add relevant error codes */
|
|
case 0:
|
|
retname = "SUCCESS";
|
|
break;
|
|
default:
|
|
retname = NULL;
|
|
break;
|
|
}
|
|
|
|
/* Let's save a few bytes */
|
|
if (retname && strncmp("KRB5KDC_", retname, sizeof("KRB5KDC_") - 1) == 0)
|
|
retname += sizeof("KRB5KDC_") - 1;
|
|
#undef PREFIX
|
|
heim_audit_trail((heim_svc_req_desc)r, ret, retname);
|
|
}
|
|
|
|
static krb5_log_facility *logfac;
|
|
static pthread_key_t k5ctx;
|
|
|
|
static krb5_error_code
|
|
get_krb5_context(krb5_context *contextp)
|
|
{
|
|
krb5_error_code ret;
|
|
|
|
if ((*contextp = pthread_getspecific(k5ctx)))
|
|
return 0;
|
|
if ((ret = krb5_init_context(contextp)))
|
|
return *contextp = NULL, ret;
|
|
if (logfac)
|
|
krb5_set_log_dest(*contextp, logfac);
|
|
(void) pthread_setspecific(k5ctx, *contextp);
|
|
return *contextp ? 0 : ENOMEM;
|
|
}
|
|
|
|
typedef enum {
|
|
CSRF_PROT_UNSPEC = 0,
|
|
CSRF_PROT_GET_WITH_HEADER = 1,
|
|
CSRF_PROT_GET_WITH_TOKEN = 2,
|
|
CSRF_PROT_POST_WITH_HEADER = 8,
|
|
CSRF_PROT_POST_WITH_TOKEN = 16,
|
|
} csrf_protection_type;
|
|
|
|
static csrf_protection_type csrf_prot_type = CSRF_PROT_UNSPEC;
|
|
static int port = -1;
|
|
static int allow_GET_flag = -1;
|
|
static int help_flag;
|
|
static int daemonize;
|
|
static int daemon_child_fd = -1;
|
|
static int verbose_counter;
|
|
static int version_flag;
|
|
static int reverse_proxied_flag;
|
|
static int thread_per_client_flag;
|
|
struct getarg_strings audiences;
|
|
static getarg_strings csrf_prot_type_strs;
|
|
static const char *csrf_header = "X-CSRF";
|
|
static const char *cert_file;
|
|
static const char *priv_key_file;
|
|
static const char *cache_dir;
|
|
static const char *csrf_key_file;
|
|
static char *impersonation_key_fn;
|
|
|
|
static char csrf_key[16];
|
|
|
|
static krb5_error_code resp(struct bx509_request_desc *, int,
|
|
enum MHD_ResponseMemoryMode, const char *,
|
|
const void *, size_t, const char *);
|
|
static krb5_error_code bad_req(struct bx509_request_desc *, krb5_error_code, int,
|
|
const char *, ...)
|
|
HEIMDAL_PRINTF_ATTRIBUTE((__printf__, 4, 5));
|
|
|
|
static krb5_error_code bad_enomem(struct bx509_request_desc *, krb5_error_code);
|
|
static krb5_error_code bad_400(struct bx509_request_desc *, krb5_error_code, char *);
|
|
static krb5_error_code bad_401(struct bx509_request_desc *, char *);
|
|
static krb5_error_code bad_403(struct bx509_request_desc *, krb5_error_code, char *);
|
|
static krb5_error_code bad_404(struct bx509_request_desc *, const char *);
|
|
static krb5_error_code bad_405(struct bx509_request_desc *, const char *);
|
|
static krb5_error_code bad_500(struct bx509_request_desc *, krb5_error_code, const char *);
|
|
static krb5_error_code bad_503(struct bx509_request_desc *, krb5_error_code, const char *);
|
|
static heim_mhd_result validate_csrf_token(struct bx509_request_desc *r);
|
|
|
|
static int
|
|
validate_token(struct bx509_request_desc *r)
|
|
{
|
|
krb5_error_code ret;
|
|
krb5_principal cprinc = NULL;
|
|
const char *token;
|
|
const char *host;
|
|
char token_type[64]; /* Plenty */
|
|
char *p;
|
|
krb5_data tok;
|
|
size_t host_len, brk, i;
|
|
|
|
memset(&r->token_times, 0, sizeof(r->token_times));
|
|
host = MHD_lookup_connection_value(r->connection, MHD_HEADER_KIND,
|
|
MHD_HTTP_HEADER_HOST);
|
|
if (host == NULL)
|
|
return bad_400(r, EINVAL, "Host header is missing");
|
|
|
|
/* Exclude port number here (IPv6-safe because of the below) */
|
|
host_len = ((p = strchr(host, ':'))) ? p - host : strlen(host);
|
|
|
|
token = MHD_lookup_connection_value(r->connection, MHD_HEADER_KIND,
|
|
MHD_HTTP_HEADER_AUTHORIZATION);
|
|
if (token == NULL)
|
|
return bad_401(r, "Authorization token is missing");
|
|
brk = strcspn(token, " \t");
|
|
if (token[brk] == '\0' || brk > sizeof(token_type) - 1)
|
|
return bad_401(r, "Authorization token is missing");
|
|
memcpy(token_type, token, brk);
|
|
token_type[brk] = '\0';
|
|
token += brk + 1;
|
|
tok.length = strlen(token);
|
|
tok.data = (void *)(uintptr_t)token;
|
|
|
|
for (i = 0; i < audiences.num_strings; i++)
|
|
if (strncasecmp(host, audiences.strings[i], host_len) == 0 &&
|
|
audiences.strings[i][host_len] == '\0')
|
|
break;
|
|
if (i == audiences.num_strings)
|
|
return bad_403(r, EINVAL, "Host: value is not accepted here");
|
|
|
|
r->sname = strdup(host); /* No need to check for ENOMEM here */
|
|
|
|
ret = kdc_validate_token(r->context, NULL /* realm */, token_type, &tok,
|
|
(const char **)&audiences.strings[i], 1,
|
|
&cprinc, &r->token_times);
|
|
if (ret)
|
|
return bad_403(r, ret, "Token validation failed");
|
|
if (cprinc == NULL)
|
|
return bad_403(r, ret, "Could not extract a principal name "
|
|
"from token");
|
|
ret = krb5_unparse_name(r->context, cprinc, &r->cname);
|
|
krb5_free_principal(r->context, cprinc);
|
|
if (ret)
|
|
return bad_503(r, ret, "Could not parse principal name");
|
|
return ret;
|
|
}
|
|
|
|
static void
|
|
generate_key(hx509_context context,
|
|
const char *key_name,
|
|
const char *gen_type,
|
|
unsigned long gen_bits,
|
|
char **fn)
|
|
{
|
|
struct hx509_generate_private_context *key_gen_ctx = NULL;
|
|
hx509_private_key key = NULL;
|
|
hx509_certs certs = NULL;
|
|
hx509_cert cert = NULL;
|
|
int ret;
|
|
|
|
if (strcmp(gen_type, "rsa") != 0)
|
|
errx(1, "Only RSA keys are supported at this time");
|
|
|
|
if (asprintf(fn, "PEM-FILE:%s/.%s_priv_key.pem",
|
|
cache_dir, key_name) == -1 ||
|
|
*fn == NULL)
|
|
err(1, "Could not set up private key for %s", key_name);
|
|
|
|
ret = _hx509_generate_private_key_init(context,
|
|
ASN1_OID_ID_PKCS1_RSAENCRYPTION,
|
|
&key_gen_ctx);
|
|
if (ret == 0)
|
|
ret = _hx509_generate_private_key_bits(context, key_gen_ctx, gen_bits);
|
|
if (ret == 0)
|
|
ret = _hx509_generate_private_key(context, key_gen_ctx, &key);
|
|
if (ret == 0)
|
|
cert = hx509_cert_init_private_key(context, key, NULL);
|
|
if (ret == 0)
|
|
ret = hx509_certs_init(context, *fn,
|
|
HX509_CERTS_CREATE | HX509_CERTS_UNPROTECT_ALL,
|
|
NULL, &certs);
|
|
if (ret == 0)
|
|
ret = hx509_certs_add(context, certs, cert);
|
|
if (ret == 0)
|
|
ret = hx509_certs_store(context, certs, 0, NULL);
|
|
if (ret)
|
|
hx509_err(context, 1, ret, "Could not generate and save private key "
|
|
"for %s", key_name);
|
|
|
|
_hx509_generate_private_key_free(&key_gen_ctx);
|
|
hx509_private_key_free(&key);
|
|
hx509_certs_free(&certs);
|
|
hx509_cert_free(cert);
|
|
}
|
|
|
|
static void
|
|
k5_free_context(void *ctx)
|
|
{
|
|
krb5_free_context(ctx);
|
|
}
|
|
|
|
#ifndef HAVE_UNLINKAT
|
|
static int
|
|
unlink1file(const char *dname, const char *name)
|
|
{
|
|
char p[PATH_MAX];
|
|
|
|
if (strlcpy(p, dname, sizeof(p)) < sizeof(p) &&
|
|
strlcat(p, "/", sizeof(p)) < sizeof(p) &&
|
|
strlcat(p, name, sizeof(p)) < sizeof(p))
|
|
return unlink(p);
|
|
return ERANGE;
|
|
}
|
|
#endif
|
|
|
|
static void
|
|
rm_cache_dir(void)
|
|
{
|
|
struct dirent *e;
|
|
DIR *d;
|
|
|
|
/*
|
|
* This works, but not on Win32:
|
|
*
|
|
* (void) simple_execlp("rm", "rm", "-rf", cache_dir, NULL);
|
|
*
|
|
* We make no directories in `cache_dir', so we need not recurse.
|
|
*/
|
|
if ((d = opendir(cache_dir)) == NULL)
|
|
return;
|
|
|
|
while ((e = readdir(d))) {
|
|
#ifdef HAVE_UNLINKAT
|
|
/*
|
|
* Because unlinkat() takes a directory FD, implementing one for
|
|
* libroken is tricky at best. Instead we might want to implement an
|
|
* rm_dash_rf() function in lib/roken.
|
|
*/
|
|
(void) unlinkat(dirfd(d), e->d_name, 0);
|
|
#else
|
|
(void) unlink1file(cache_dir, e->d_name);
|
|
#endif
|
|
}
|
|
(void) closedir(d);
|
|
(void) rmdir(cache_dir);
|
|
}
|
|
|
|
static krb5_error_code
|
|
mk_pkix_store(char **pkix_store)
|
|
{
|
|
char *s = NULL;
|
|
int ret = ENOMEM;
|
|
int fd;
|
|
|
|
if (*pkix_store) {
|
|
const char *fn = strchr(*pkix_store, ':');
|
|
|
|
fn = fn ? fn + 1 : *pkix_store;
|
|
(void) unlink(fn);
|
|
}
|
|
|
|
free(*pkix_store);
|
|
*pkix_store = NULL;
|
|
if (asprintf(&s, "PEM-FILE:%s/pkix-XXXXXX", cache_dir) == -1 ||
|
|
s == NULL) {
|
|
free(s);
|
|
return ret;
|
|
}
|
|
if ((fd = mkstemp(s + sizeof("PEM-FILE:") - 1)) == -1) {
|
|
free(s);
|
|
return errno;
|
|
}
|
|
(void) close(fd);
|
|
*pkix_store = s;
|
|
return 0;
|
|
}
|
|
|
|
static krb5_error_code
|
|
resp(struct bx509_request_desc *r,
|
|
int http_status_code,
|
|
enum MHD_ResponseMemoryMode rmmode,
|
|
const char *content_type,
|
|
const void *body,
|
|
size_t bodylen,
|
|
const char *token)
|
|
{
|
|
int mret = MHD_YES;
|
|
|
|
if (r->response)
|
|
return MHD_YES;
|
|
|
|
(void) gettimeofday(&r->tv_end, NULL);
|
|
if (http_status_code == MHD_HTTP_OK ||
|
|
http_status_code == MHD_HTTP_TEMPORARY_REDIRECT)
|
|
audit_trail(r, 0);
|
|
|
|
r->response = MHD_create_response_from_buffer(bodylen, rk_UNCONST(body),
|
|
rmmode);
|
|
if (r->response == NULL)
|
|
return -1;
|
|
if (r->csrf_token)
|
|
mret = MHD_add_response_header(r->response, "X-CSRF-Token", r->csrf_token);
|
|
if (mret == MHD_YES)
|
|
mret = MHD_add_response_header(r->response, MHD_HTTP_HEADER_CACHE_CONTROL,
|
|
"no-store, max-age=0");
|
|
if (mret == MHD_YES && http_status_code == MHD_HTTP_UNAUTHORIZED) {
|
|
mret = MHD_add_response_header(r->response,
|
|
MHD_HTTP_HEADER_WWW_AUTHENTICATE,
|
|
"Bearer");
|
|
if (mret == MHD_YES)
|
|
mret = MHD_add_response_header(r->response,
|
|
MHD_HTTP_HEADER_WWW_AUTHENTICATE,
|
|
"Negotiate");
|
|
} else if (mret == MHD_YES && http_status_code == MHD_HTTP_TEMPORARY_REDIRECT) {
|
|
const char *redir;
|
|
|
|
/* XXX Move this */
|
|
redir = MHD_lookup_connection_value(r->connection, MHD_GET_ARGUMENT_KIND,
|
|
"redirect");
|
|
mret = MHD_add_response_header(r->response, MHD_HTTP_HEADER_LOCATION,
|
|
redir);
|
|
if (mret != MHD_NO && token)
|
|
mret = MHD_add_response_header(r->response,
|
|
MHD_HTTP_HEADER_AUTHORIZATION,
|
|
token);
|
|
}
|
|
if (mret == MHD_YES && content_type) {
|
|
mret = MHD_add_response_header(r->response,
|
|
MHD_HTTP_HEADER_CONTENT_TYPE,
|
|
content_type);
|
|
}
|
|
if (mret == MHD_YES)
|
|
mret = MHD_queue_response(r->connection, http_status_code, r->response);
|
|
MHD_destroy_response(r->response);
|
|
return mret == MHD_NO ? -1 : 0;
|
|
}
|
|
|
|
static krb5_error_code
|
|
bad_reqv(struct bx509_request_desc *r,
|
|
krb5_error_code code,
|
|
int http_status_code,
|
|
const char *fmt,
|
|
va_list ap)
|
|
{
|
|
krb5_error_code ret;
|
|
const char *k5msg = NULL;
|
|
const char *emsg = NULL;
|
|
char *formatted = NULL;
|
|
char *msg = NULL;
|
|
|
|
heim_audit_setkv_number((heim_svc_req_desc)r, "http-status-code",
|
|
http_status_code);
|
|
(void) gettimeofday(&r->tv_end, NULL);
|
|
if (code == ENOMEM) {
|
|
if (r->context)
|
|
krb5_log_msg(r->context, logfac, 1, NULL, "Out of memory");
|
|
audit_trail(r, code);
|
|
return resp(r, http_status_code, MHD_RESPMEM_PERSISTENT,
|
|
NULL, fmt, strlen(fmt), NULL);
|
|
}
|
|
|
|
if (code) {
|
|
if (r->context)
|
|
emsg = k5msg = krb5_get_error_message(r->context, code);
|
|
else if (code > -1)
|
|
emsg = strerror(code);
|
|
else
|
|
emsg = "Unknown error";
|
|
}
|
|
|
|
ret = vasprintf(&formatted, fmt, ap);
|
|
if (code) {
|
|
if (ret > -1 && formatted)
|
|
ret = asprintf(&msg, "%s: %s (%d)", formatted, emsg, (int)code);
|
|
} else {
|
|
msg = formatted;
|
|
formatted = NULL;
|
|
}
|
|
heim_audit_addreason((heim_svc_req_desc)r, "%s", msg);
|
|
audit_trail(r, code);
|
|
if (r->context)
|
|
krb5_free_error_message(r->context, k5msg);
|
|
|
|
if (ret == -1 || msg == NULL) {
|
|
if (r->context)
|
|
krb5_log_msg(r->context, logfac, 1, NULL, "Out of memory");
|
|
return resp(r, MHD_HTTP_SERVICE_UNAVAILABLE, MHD_RESPMEM_PERSISTENT,
|
|
NULL, "Out of memory", sizeof("Out of memory") - 1, NULL);
|
|
}
|
|
|
|
ret = resp(r, http_status_code, MHD_RESPMEM_MUST_COPY,
|
|
NULL, msg, strlen(msg), NULL);
|
|
free(formatted);
|
|
free(msg);
|
|
return ret == -1 ? -1 : code;
|
|
}
|
|
|
|
static krb5_error_code
|
|
bad_req(struct bx509_request_desc *r,
|
|
krb5_error_code code,
|
|
int http_status_code,
|
|
const char *fmt,
|
|
...)
|
|
{
|
|
krb5_error_code ret;
|
|
va_list ap;
|
|
|
|
va_start(ap, fmt);
|
|
ret = bad_reqv(r, code, http_status_code, fmt, ap);
|
|
va_end(ap);
|
|
return ret;
|
|
}
|
|
|
|
static krb5_error_code
|
|
bad_enomem(struct bx509_request_desc *r, krb5_error_code ret)
|
|
{
|
|
return bad_req(r, ret, MHD_HTTP_SERVICE_UNAVAILABLE,
|
|
"Out of memory");
|
|
}
|
|
|
|
static krb5_error_code
|
|
bad_400(struct bx509_request_desc *r, int ret, char *reason)
|
|
{
|
|
return bad_req(r, ret, MHD_HTTP_BAD_REQUEST, "%s", reason);
|
|
}
|
|
|
|
static krb5_error_code
|
|
bad_401(struct bx509_request_desc *r, char *reason)
|
|
{
|
|
return bad_req(r, EACCES, MHD_HTTP_UNAUTHORIZED, "%s", reason);
|
|
}
|
|
|
|
static krb5_error_code
|
|
bad_403(struct bx509_request_desc *r, krb5_error_code ret, char *reason)
|
|
{
|
|
return bad_req(r, ret, MHD_HTTP_FORBIDDEN, "%s", reason);
|
|
}
|
|
|
|
static krb5_error_code
|
|
bad_404(struct bx509_request_desc *r, const char *name)
|
|
{
|
|
return bad_req(r, ENOENT, MHD_HTTP_NOT_FOUND,
|
|
"Resource not found: %s", name);
|
|
}
|
|
|
|
static krb5_error_code
|
|
bad_405(struct bx509_request_desc *r, const char *method)
|
|
{
|
|
return bad_req(r, EPERM, MHD_HTTP_METHOD_NOT_ALLOWED,
|
|
"Method not supported: %s", method);
|
|
}
|
|
|
|
static krb5_error_code
|
|
bad_413(struct bx509_request_desc *r)
|
|
{
|
|
return bad_req(r, E2BIG, MHD_HTTP_METHOD_NOT_ALLOWED,
|
|
"POST request body too large");
|
|
}
|
|
|
|
static krb5_error_code
|
|
bad_500(struct bx509_request_desc *r,
|
|
krb5_error_code ret,
|
|
const char *reason)
|
|
{
|
|
return bad_req(r, ret, MHD_HTTP_INTERNAL_SERVER_ERROR,
|
|
"Internal error: %s", reason);
|
|
}
|
|
|
|
static krb5_error_code
|
|
bad_503(struct bx509_request_desc *r,
|
|
krb5_error_code ret,
|
|
const char *reason)
|
|
{
|
|
return bad_req(r, ret, MHD_HTTP_SERVICE_UNAVAILABLE,
|
|
"Service unavailable: %s", reason);
|
|
}
|
|
|
|
static krb5_error_code
|
|
good_bx509(struct bx509_request_desc *r)
|
|
{
|
|
krb5_error_code ret;
|
|
const char *fn;
|
|
size_t bodylen;
|
|
void *body;
|
|
|
|
/*
|
|
* This `fn' thing is just to quiet linters that think "hey, strchr() can
|
|
* return NULL so...", but here we've build `r->pkix_store' and know it has
|
|
* a ':'.
|
|
*/
|
|
if (r->pkix_store == NULL)
|
|
return bad_503(r, EINVAL, "Internal error"); /* Quiet warnings */
|
|
fn = strchr(r->pkix_store, ':');
|
|
fn = fn ? fn + 1 : r->pkix_store;
|
|
ret = rk_undumpdata(fn, &body, &bodylen);
|
|
if (ret)
|
|
return bad_503(r, ret, "Could not recover issued certificate "
|
|
"from PKIX store");
|
|
|
|
(void) gettimeofday(&r->tv_end, NULL);
|
|
ret = resp(r, MHD_HTTP_OK, MHD_RESPMEM_MUST_COPY, "application/x-pem-file",
|
|
body, bodylen, NULL);
|
|
free(body);
|
|
return ret;
|
|
}
|
|
|
|
static heim_mhd_result
|
|
bx509_param_cb(void *d,
|
|
enum MHD_ValueKind kind,
|
|
const char *key,
|
|
const char *val)
|
|
{
|
|
struct bx509_request_desc *r = d;
|
|
heim_oid oid = { 0, 0 };
|
|
|
|
if (strcmp(key, "eku") == 0 && val) {
|
|
heim_audit_addkv((heim_svc_req_desc)r, KDC_AUDIT_VIS, "requested_eku",
|
|
"%s", val);
|
|
r->error_code = der_parse_heim_oid(val, ".", &oid);
|
|
if (r->error_code == 0)
|
|
r->error_code = hx509_request_add_eku(r->context->hx509ctx, r->req, &oid);
|
|
der_free_oid(&oid);
|
|
} else if (strcmp(key, "dNSName") == 0 && val) {
|
|
heim_audit_addkv((heim_svc_req_desc)r, KDC_AUDIT_VIS,
|
|
"requested_dNSName", "%s", val);
|
|
r->error_code = hx509_request_add_dns_name(r->context->hx509ctx, r->req, val);
|
|
} else if (strcmp(key, "rfc822Name") == 0 && val) {
|
|
heim_audit_addkv((heim_svc_req_desc)r, KDC_AUDIT_VIS,
|
|
"requested_rfc822Name", "%s", val);
|
|
r->error_code = hx509_request_add_email(r->context->hx509ctx, r->req, val);
|
|
} else if (strcmp(key, "xMPPName") == 0 && val) {
|
|
heim_audit_addkv((heim_svc_req_desc)r, KDC_AUDIT_VIS,
|
|
"requested_xMPPName", "%s", val);
|
|
r->error_code = hx509_request_add_xmpp_name(r->context->hx509ctx, r->req,
|
|
val);
|
|
} else if (strcmp(key, "krb5PrincipalName") == 0 && val) {
|
|
heim_audit_addkv((heim_svc_req_desc)r, KDC_AUDIT_VIS,
|
|
"requested_krb5PrincipalName", "%s", val);
|
|
r->error_code = hx509_request_add_pkinit(r->context->hx509ctx, r->req,
|
|
val);
|
|
} else if (strcmp(key, "ms-upn") == 0 && val) {
|
|
heim_audit_addkv((heim_svc_req_desc)r, KDC_AUDIT_VIS,
|
|
"requested_ms_upn", "%s", val);
|
|
r->error_code = hx509_request_add_ms_upn_name(r->context->hx509ctx, r->req,
|
|
val);
|
|
} else if (strcmp(key, "registeredID") == 0 && val) {
|
|
heim_audit_addkv((heim_svc_req_desc)r, KDC_AUDIT_VIS,
|
|
"requested_registered_id", "%s", val);
|
|
r->error_code = der_parse_heim_oid(val, ".", &oid);
|
|
if (r->error_code == 0)
|
|
r->error_code = hx509_request_add_registered(r->context->hx509ctx, r->req,
|
|
&oid);
|
|
der_free_oid(&oid);
|
|
} else if (strcmp(key, "csr") == 0 && val) {
|
|
heim_audit_setkv_bool((heim_svc_req_desc)r, "requested_csr", TRUE);
|
|
r->error_code = 0; /* Handled upstairs */
|
|
} else if (strcmp(key, "lifetime") == 0 && val) {
|
|
r->req_life = parse_time(val, "day");
|
|
} else {
|
|
/* Produce error for unknown params */
|
|
heim_audit_setkv_bool((heim_svc_req_desc)r, "requested_unknown", TRUE);
|
|
krb5_set_error_message(r->context, r->error_code = ENOTSUP,
|
|
"Query parameter %s not supported", key);
|
|
}
|
|
return r->error_code == 0 ? MHD_YES : MHD_NO /* Stop iterating */;
|
|
}
|
|
|
|
static krb5_error_code
|
|
authorize_CSR(struct bx509_request_desc *r,
|
|
krb5_data *csr,
|
|
krb5_const_principal p)
|
|
{
|
|
krb5_error_code ret;
|
|
|
|
ret = hx509_request_parse_der(r->context->hx509ctx, csr, &r->req);
|
|
if (ret)
|
|
return bad_req(r, ret, MHD_HTTP_SERVICE_UNAVAILABLE,
|
|
"Could not parse CSR");
|
|
r->error_code = 0;
|
|
(void) MHD_get_connection_values(r->connection, MHD_GET_ARGUMENT_KIND,
|
|
bx509_param_cb, r);
|
|
ret = r->error_code;
|
|
if (ret)
|
|
return bad_req(r, ret, MHD_HTTP_SERVICE_UNAVAILABLE,
|
|
"Could not handle query parameters");
|
|
|
|
ret = kdc_authorize_csr(r->context, "bx509", r->req, p);
|
|
if (ret)
|
|
return bad_403(r, ret, "Not authorized to requested certificate");
|
|
return ret;
|
|
}
|
|
|
|
/*
|
|
* hx509_certs_iter_f() callback to assign a private key to the first cert in a
|
|
* store.
|
|
*/
|
|
static int HX509_LIB_CALL
|
|
set_priv_key(hx509_context context, void *d, hx509_cert c)
|
|
{
|
|
(void) _hx509_cert_assign_key(c, (hx509_private_key)d);
|
|
return -1; /* stop iteration */
|
|
}
|
|
|
|
static krb5_error_code
|
|
store_certs(hx509_context context,
|
|
const char *store,
|
|
hx509_certs store_these,
|
|
hx509_private_key key)
|
|
{
|
|
krb5_error_code ret;
|
|
hx509_certs certs = NULL;
|
|
|
|
ret = hx509_certs_init(context, store, HX509_CERTS_CREATE, NULL,
|
|
&certs);
|
|
if (ret == 0) {
|
|
if (key)
|
|
(void) hx509_certs_iter_f(context, store_these, set_priv_key, key);
|
|
hx509_certs_merge(context, certs, store_these);
|
|
}
|
|
if (ret == 0)
|
|
hx509_certs_store(context, certs, 0, NULL);
|
|
hx509_certs_free(&certs);
|
|
return ret;
|
|
}
|
|
|
|
/* Setup a CSR for bx509() */
|
|
static krb5_error_code
|
|
do_CA(struct bx509_request_desc *r, const char *csr)
|
|
{
|
|
krb5_error_code ret = 0;
|
|
krb5_principal p;
|
|
hx509_certs certs = NULL;
|
|
krb5_data d;
|
|
ssize_t bytes;
|
|
char *csr2, *q;
|
|
|
|
/*
|
|
* Work around bug where microhttpd decodes %2b to + then + to space. That
|
|
* bug does not affect other base64 special characters that get URI
|
|
* %-encoded.
|
|
*/
|
|
if ((csr2 = strdup(csr)) == NULL)
|
|
return bad_enomem(r, ENOMEM);
|
|
for (q = strchr(csr2, ' '); q; q = strchr(q + 1, ' '))
|
|
*q = '+';
|
|
|
|
ret = krb5_parse_name(r->context, r->cname, &p);
|
|
if (ret) {
|
|
free(csr2);
|
|
return bad_req(r, ret, MHD_HTTP_SERVICE_UNAVAILABLE,
|
|
"Could not parse principal name");
|
|
}
|
|
|
|
/* Set CSR */
|
|
if ((d.data = malloc(strlen(csr2))) == NULL) {
|
|
krb5_free_principal(r->context, p);
|
|
free(csr2);
|
|
return bad_enomem(r, ENOMEM);
|
|
}
|
|
|
|
bytes = rk_base64_decode(csr2, d.data);
|
|
free(csr2);
|
|
if (bytes < 0)
|
|
ret = errno ? errno : EINVAL;
|
|
else
|
|
d.length = bytes;
|
|
if (ret) {
|
|
krb5_free_principal(r->context, p);
|
|
free(d.data);
|
|
return bad_500(r, ret, "Invalid base64 encoding of CSR");
|
|
}
|
|
|
|
/*
|
|
* Parses and validates the CSR, adds external extension requests from
|
|
* query parameters, then checks authorization.
|
|
*/
|
|
ret = authorize_CSR(r, &d, p);
|
|
free(d.data);
|
|
d.data = 0;
|
|
d.length = 0;
|
|
if (ret) {
|
|
krb5_free_principal(r->context, p);
|
|
return ret; /* authorize_CSR() calls bad_req() */
|
|
}
|
|
|
|
/* Issue the certificate */
|
|
ret = kdc_issue_certificate(r->context, "bx509", logfac, r->req, p,
|
|
&r->token_times, r->req_life,
|
|
1 /* send_chain */, &certs);
|
|
krb5_free_principal(r->context, p);
|
|
if (ret) {
|
|
if (ret == KRB5KDC_ERR_POLICY || ret == EACCES)
|
|
return bad_403(r, ret,
|
|
"Certificate request denied for policy reasons");
|
|
return bad_500(r, ret, "Certificate issuance failed");
|
|
}
|
|
|
|
/* Setup PKIX store */
|
|
if ((ret = mk_pkix_store(&r->pkix_store)))
|
|
return bad_500(r, ret,
|
|
"Could not create PEM store for issued certificate");
|
|
|
|
ret = store_certs(r->context->hx509ctx, r->pkix_store, certs, NULL);
|
|
hx509_certs_free(&certs);
|
|
if (ret)
|
|
return bad_500(r, ret, "Failed to convert issued"
|
|
" certificate and chain to PEM");
|
|
return 0;
|
|
}
|
|
|
|
/* Copied from kdc/connect.c */
|
|
static void
|
|
addr_to_string(krb5_context context,
|
|
struct sockaddr *addr,
|
|
char *str,
|
|
size_t len)
|
|
{
|
|
krb5_error_code ret;
|
|
krb5_address a;
|
|
|
|
ret = krb5_sockaddr2address(context, addr, &a);
|
|
if (ret == 0) {
|
|
ret = krb5_print_address(&a, str, len, &len);
|
|
krb5_free_address(context, &a);
|
|
}
|
|
if (ret)
|
|
snprintf(str, len, "<family=%d>", addr->sa_family);
|
|
}
|
|
|
|
static void clean_req_desc(struct bx509_request_desc *);
|
|
|
|
static krb5_error_code
|
|
set_req_desc(struct MHD_Connection *connection,
|
|
const char *method,
|
|
const char *url,
|
|
struct bx509_request_desc **rp)
|
|
{
|
|
struct bx509_request_desc *r;
|
|
const union MHD_ConnectionInfo *ci;
|
|
const char *token;
|
|
krb5_error_code ret;
|
|
|
|
*rp = NULL;
|
|
if ((r = calloc(1, sizeof(*r))) == NULL)
|
|
return ENOMEM;
|
|
(void) gettimeofday(&r->tv_start, NULL);
|
|
|
|
ret = get_krb5_context(&r->context);
|
|
r->connection = connection;
|
|
r->response = NULL;
|
|
r->pp = NULL;
|
|
r->request.data = "<HTTP-REQUEST>";
|
|
r->request.length = sizeof("<HTTP-REQUEST>");
|
|
r->from = r->frombuf;
|
|
r->tgt_addresses.len = 0;
|
|
r->tgt_addresses.val = 0;
|
|
r->hcontext = r->context ? r->context->hcontext : NULL;
|
|
r->config = NULL;
|
|
r->logf = logfac;
|
|
r->csrf_token = NULL;
|
|
r->free_list = NULL;
|
|
r->method = method;
|
|
r->reqtype = url;
|
|
r->target = r->redir = NULL;
|
|
r->pkix_store = NULL;
|
|
r->for_cname = NULL;
|
|
r->freeme1 = NULL;
|
|
r->reason = NULL;
|
|
r->tgts_filename = NULL;
|
|
r->tgts = NULL;
|
|
r->ccname = NULL;
|
|
r->reply = NULL;
|
|
r->sname = NULL;
|
|
r->cname = NULL;
|
|
r->addr = NULL;
|
|
r->req = NULL;
|
|
r->req_life = 0;
|
|
r->error_code = ret;
|
|
r->kv = heim_dict_create(10);
|
|
r->attributes = heim_dict_create(1);
|
|
if (ret == 0 && (r->kv == NULL || r->attributes == NULL))
|
|
r->error_code = ret = ENOMEM;
|
|
ci = MHD_get_connection_info(connection,
|
|
MHD_CONNECTION_INFO_CLIENT_ADDRESS);
|
|
if (ci) {
|
|
r->addr = ci->client_addr;
|
|
addr_to_string(r->context, r->addr, r->frombuf, sizeof(r->frombuf));
|
|
}
|
|
|
|
heim_audit_addkv((heim_svc_req_desc)r, 0, "method", "GET");
|
|
heim_audit_addkv((heim_svc_req_desc)r, 0, "endpoint", "%s", r->reqtype);
|
|
token = MHD_lookup_connection_value(r->connection, MHD_HEADER_KIND,
|
|
MHD_HTTP_HEADER_AUTHORIZATION);
|
|
if (token && r->kv) {
|
|
const char *token_end;
|
|
|
|
if ((token_end = strchr(token, ' ')) == NULL ||
|
|
(token_end - token) > INT_MAX || (token_end - token) < 2)
|
|
heim_audit_addkv((heim_svc_req_desc)r, 0, "auth", "<unknown>");
|
|
else
|
|
heim_audit_addkv((heim_svc_req_desc)r, 0, "auth", "%.*s",
|
|
(int)(token_end - token), token);
|
|
|
|
}
|
|
|
|
if (ret == 0)
|
|
*rp = r;
|
|
else
|
|
clean_req_desc(r);
|
|
return ret;
|
|
}
|
|
|
|
static void
|
|
clean_req_desc(struct bx509_request_desc *r)
|
|
{
|
|
if (!r)
|
|
return;
|
|
while (r->free_list) {
|
|
struct free_tend_list *ftl = r->free_list;
|
|
r->free_list = r->free_list->next;
|
|
free(ftl->freeme1);
|
|
free(ftl->freeme2);
|
|
free(ftl);
|
|
}
|
|
if (r->pkix_store) {
|
|
const char *fn = strchr(r->pkix_store, ':');
|
|
|
|
/*
|
|
* This `fn' thing is just to quiet linters that think "hey, strchr() can
|
|
* return NULL so...", but here we've build `r->pkix_store' and know it has
|
|
* a ':'.
|
|
*/
|
|
fn = fn ? fn + 1 : r->pkix_store;
|
|
(void) unlink(fn);
|
|
}
|
|
krb5_free_addresses(r->context, &r->tgt_addresses);
|
|
hx509_request_free(&r->req);
|
|
heim_release(r->attributes);
|
|
heim_release(r->reason);
|
|
heim_release(r->kv);
|
|
if (r->ccname && r->cckind == K5_CREDS_EPHEMERAL) {
|
|
const char *fn = r->ccname;
|
|
|
|
if (strncmp(fn, "FILE:", sizeof("FILE:") - 1) == 0)
|
|
fn += sizeof("FILE:") - 1;
|
|
(void) unlink(fn);
|
|
}
|
|
if (r->tgts)
|
|
(void) fclose(r->tgts);
|
|
if (r->tgts_filename) {
|
|
(void) unlink(r->tgts_filename);
|
|
free(r->tgts_filename);
|
|
}
|
|
/* No need to destroy r->response */
|
|
if (r->pp)
|
|
MHD_destroy_post_processor(r->pp);
|
|
free(r->csrf_token);
|
|
free(r->pkix_store);
|
|
free(r->freeme1);
|
|
free(r->ccname);
|
|
free(r->cname);
|
|
free(r->sname);
|
|
free(r);
|
|
}
|
|
|
|
/* Implements GETs of /bx509 */
|
|
static krb5_error_code
|
|
bx509(struct bx509_request_desc *r)
|
|
{
|
|
krb5_error_code ret;
|
|
const char *csr;
|
|
|
|
/* Get required inputs */
|
|
csr = MHD_lookup_connection_value(r->connection, MHD_GET_ARGUMENT_KIND,
|
|
"csr");
|
|
if (csr == NULL)
|
|
return bad_400(r, EINVAL, "CSR is missing");
|
|
|
|
if (r->cname == NULL)
|
|
return bad_403(r, EINVAL,
|
|
"Could not extract principal name from token");
|
|
|
|
/* Parse CSR, add extensions from parameters, authorize, issue cert */
|
|
if ((ret = do_CA(r, csr)))
|
|
return ret;
|
|
|
|
/* Read and send the contents of the PKIX store */
|
|
krb5_log_msg(r->context, logfac, 1, NULL, "Issued certificate to %s",
|
|
r->cname);
|
|
return good_bx509(r);
|
|
}
|
|
|
|
/*
|
|
* princ_fs_encode_sz() and princ_fs_encode() encode a principal name to be
|
|
* safe for use as a file name. They function very much like URL encoders, but
|
|
* '~' and '.' also get encoded, and '@' does not.
|
|
*
|
|
* A corresponding decoder is not needed.
|
|
*
|
|
* XXX Maybe use krb5_cc_default_for()!
|
|
*/
|
|
static size_t
|
|
princ_fs_encode_sz(const char *in)
|
|
{
|
|
size_t sz = strlen(in);
|
|
|
|
while (*in) {
|
|
unsigned char c = *(const unsigned char *)(in++);
|
|
|
|
if (isalnum(c))
|
|
continue;
|
|
switch (c) {
|
|
case '@':
|
|
case '-':
|
|
case '_':
|
|
continue;
|
|
default:
|
|
sz += 2;
|
|
}
|
|
}
|
|
return sz;
|
|
}
|
|
|
|
static char *
|
|
princ_fs_encode(const char *in)
|
|
{
|
|
size_t len = strlen(in);
|
|
size_t sz = princ_fs_encode_sz(in);
|
|
size_t i, k;
|
|
char *s;
|
|
|
|
if ((s = malloc(sz + 1)) == NULL)
|
|
return NULL;
|
|
s[sz] = '\0';
|
|
|
|
for (i = k = 0; i < len; i++) {
|
|
char c = in[i];
|
|
|
|
switch (c) {
|
|
case '@':
|
|
case '-':
|
|
case '_':
|
|
s[k++] = c;
|
|
break;
|
|
default:
|
|
if (isalnum((unsigned char)c)) {
|
|
s[k++] = c;
|
|
} else {
|
|
s[k++] = '%';
|
|
s[k++] = "0123456789abcdef"[(c&0xff)>>4];
|
|
s[k++] = "0123456789abcdef"[(c&0x0f)];
|
|
}
|
|
}
|
|
}
|
|
return s;
|
|
}
|
|
|
|
|
|
/*
|
|
* Find an existing, live ccache for `princ' in `cache_dir' or acquire Kerberos
|
|
* creds for `princ' with PKINIT and put them in a ccache in `cache_dir'.
|
|
*/
|
|
static krb5_error_code
|
|
find_ccache(krb5_context context, const char *princ, char **ccname)
|
|
{
|
|
krb5_error_code ret = ENOMEM;
|
|
krb5_ccache cc = NULL;
|
|
time_t life;
|
|
char *s = NULL;
|
|
|
|
*ccname = NULL;
|
|
|
|
/*
|
|
* Name the ccache after the principal. The principal may have special
|
|
* characters in it, such as / or \ (path component separarot), or shell
|
|
* special characters, so princ_fs_encode() it to make a ccache name.
|
|
*/
|
|
if ((s = princ_fs_encode(princ)) == NULL ||
|
|
asprintf(ccname, "FILE:%s/%s.cc", cache_dir, s) == -1 ||
|
|
*ccname == NULL) {
|
|
free(s);
|
|
return ENOMEM;
|
|
}
|
|
free(s);
|
|
|
|
if ((ret = krb5_cc_resolve(context, *ccname, &cc))) {
|
|
/* krb5_cc_resolve() suceeds even if the file doesn't exist */
|
|
free(*ccname);
|
|
*ccname = NULL;
|
|
cc = NULL;
|
|
}
|
|
|
|
/* Check if we have a good enough credential */
|
|
if (ret == 0 &&
|
|
(ret = krb5_cc_get_lifetime(context, cc, &life)) == 0 && life > 60) {
|
|
krb5_cc_close(context, cc);
|
|
return 0;
|
|
}
|
|
if (cc)
|
|
krb5_cc_close(context, cc);
|
|
return ret ? ret : ENOENT;
|
|
}
|
|
|
|
static krb5_error_code
|
|
get_ccache(struct bx509_request_desc *r, krb5_ccache *cc, int *won)
|
|
{
|
|
krb5_error_code ret = 0;
|
|
char *temp_ccname = NULL;
|
|
const char *fn = NULL;
|
|
time_t life;
|
|
int fd = -1;
|
|
|
|
/*
|
|
* Open and lock a .new ccache file. Use .new to avoid garbage files on
|
|
* crash.
|
|
*
|
|
* We can race with other threads to do this, so we loop until we
|
|
* definitively win or definitely lose the race. We win when we have a) an
|
|
* open FD that is b) flock'ed, and c) we observe with lstat() that the
|
|
* file we opened and locked is the same as on disk after locking.
|
|
*
|
|
* We don't close the FD until we're done.
|
|
*
|
|
* If we had a proper anon MEMORY ccache, we could instead use that for a
|
|
* temporary ccache, and then the initialization of and move to the final
|
|
* FILE ccache would take care to mkstemp() and rename() into place.
|
|
* fcc_open() basically does a similar thing.
|
|
*/
|
|
*cc = NULL;
|
|
*won = -1;
|
|
if (asprintf(&temp_ccname, "%s.ccnew", r->ccname) == -1 ||
|
|
temp_ccname == NULL)
|
|
ret = ENOMEM;
|
|
if (ret == 0)
|
|
fn = temp_ccname + sizeof("FILE:") - 1;
|
|
if (ret == 0) do {
|
|
struct stat st1, st2;
|
|
/*
|
|
* Open and flock the temp ccache file.
|
|
*
|
|
* XXX We should really a) use _krb5_xlock(), or move that into
|
|
* lib/roken anyways, b) abstract this loop into a utility function in
|
|
* lib/roken.
|
|
*/
|
|
if (fd != -1) {
|
|
(void) close(fd);
|
|
fd = -1;
|
|
}
|
|
errno = 0;
|
|
memset(&st1, 0, sizeof(st1));
|
|
memset(&st2, 0xff, sizeof(st2));
|
|
if (ret == 0 &&
|
|
((fd = open(fn, O_RDWR | O_CREAT, 0600)) == -1 ||
|
|
flock(fd, LOCK_EX) == -1 ||
|
|
(lstat(fn, &st1) == -1 && errno != ENOENT) ||
|
|
fstat(fd, &st2) == -1))
|
|
ret = errno;
|
|
if (ret == 0 && errno == 0 &&
|
|
st1.st_dev == st2.st_dev && st1.st_ino == st2.st_ino) {
|
|
if (S_ISREG(st1.st_mode))
|
|
break;
|
|
if (unlink(fn) == -1)
|
|
ret = errno;
|
|
}
|
|
} while (ret == 0);
|
|
|
|
/* Check if we lost any race to acquire Kerberos creds */
|
|
if (ret == 0)
|
|
ret = krb5_cc_resolve(r->context, temp_ccname, cc);
|
|
if (ret == 0) {
|
|
ret = krb5_cc_get_lifetime(r->context, *cc, &life);
|
|
if (ret == 0 && life > 60)
|
|
*won = 0; /* We lost the race, but we win: we get to do less work */
|
|
*won = 1;
|
|
ret = 0;
|
|
}
|
|
free(temp_ccname);
|
|
if (fd != -1)
|
|
(void) close(fd); /* Drops the flock */
|
|
return ret;
|
|
}
|
|
|
|
/*
|
|
* Acquire credentials for `princ' using PKINIT and the PKIX credentials in
|
|
* `pkix_store', then place the result in the ccache named `ccname' (which will
|
|
* be in our own private `cache_dir').
|
|
*
|
|
* XXX This function could be rewritten using gss_acquire_cred_from() and
|
|
* gss_store_cred_into() provided we add new generic cred store key/value pairs
|
|
* for PKINIT.
|
|
*/
|
|
static krb5_error_code
|
|
do_pkinit(struct bx509_request_desc *r, enum k5_creds_kind kind)
|
|
{
|
|
krb5_get_init_creds_opt *opt = NULL;
|
|
krb5_init_creds_context ctx = NULL;
|
|
krb5_error_code ret = 0;
|
|
krb5_ccache temp_cc = NULL;
|
|
krb5_ccache cc = NULL;
|
|
krb5_principal p = NULL;
|
|
const char *crealm;
|
|
const char *cname = r->for_cname ? r->for_cname : r->cname;
|
|
|
|
if (kind == K5_CREDS_CACHED) {
|
|
int won = -1;
|
|
|
|
ret = get_ccache(r, &temp_cc, &won);
|
|
if (ret || !won)
|
|
goto out;
|
|
/*
|
|
* We won the race to do PKINIT. Setup to acquire Kerberos creds with
|
|
* PKINIT.
|
|
*
|
|
* We should really make sure that gss_acquire_cred_from() can do this
|
|
* for us. We'd add generic cred store key/value pairs for PKIX cred
|
|
* store, trust anchors, and so on, and acquire that way, then
|
|
* gss_store_cred_into() to save it in a FILE ccache.
|
|
*/
|
|
} else {
|
|
ret = krb5_cc_new_unique(r->context, "FILE", NULL, &temp_cc);
|
|
}
|
|
|
|
if (ret == 0)
|
|
ret = krb5_parse_name(r->context, cname, &p);
|
|
if (ret == 0)
|
|
crealm = krb5_principal_get_realm(r->context, p);
|
|
if (ret == 0)
|
|
ret = krb5_get_init_creds_opt_alloc(r->context, &opt);
|
|
if (ret == 0)
|
|
krb5_get_init_creds_opt_set_default_flags(r->context, "kinit", crealm,
|
|
opt);
|
|
if (ret == 0 && kind == K5_CREDS_EPHEMERAL &&
|
|
!krb5_config_get_bool_default(r->context, NULL, TRUE,
|
|
"get-tgt", "no_addresses", NULL)) {
|
|
krb5_addresses addr;
|
|
|
|
ret = _krb5_parse_address_no_lookup(r->context, r->frombuf, &addr);
|
|
if (ret == 0)
|
|
ret = krb5_append_addresses(r->context, &r->tgt_addresses,
|
|
&addr);
|
|
}
|
|
if (ret == 0) {
|
|
if (r->tgt_addresses.len == 0)
|
|
ret = krb5_get_init_creds_opt_set_addressless(r->context, opt, 1);
|
|
else
|
|
krb5_get_init_creds_opt_set_address_list(opt, &r->tgt_addresses);
|
|
}
|
|
if (ret == 0)
|
|
ret = krb5_get_init_creds_opt_set_pkinit(r->context, opt, p,
|
|
r->pkix_store,
|
|
NULL, /* pkinit_anchor */
|
|
NULL, /* anchor_chain */
|
|
NULL, /* pkinit_crl */
|
|
0, /* flags */
|
|
NULL, /* prompter */
|
|
NULL, /* prompter data */
|
|
NULL /* password */);
|
|
if (ret == 0)
|
|
ret = krb5_init_creds_init(r->context, p,
|
|
NULL /* prompter */,
|
|
NULL /* prompter data */,
|
|
0 /* start_time */,
|
|
opt, &ctx);
|
|
|
|
/*
|
|
* Finally, do the AS exchange w/ PKINIT, extract the new Kerberos creds
|
|
* into temp_cc, and rename into place. Note that krb5_cc_move() closes
|
|
* the source ccache, so we set temp_cc = NULL if it succeeds.
|
|
*/
|
|
if (ret == 0)
|
|
ret = krb5_init_creds_get(r->context, ctx);
|
|
if (ret == 0)
|
|
ret = krb5_init_creds_store(r->context, ctx, temp_cc);
|
|
if (kind == K5_CREDS_CACHED) {
|
|
if (ret == 0)
|
|
ret = krb5_cc_resolve(r->context, r->ccname, &cc);
|
|
if (ret == 0)
|
|
ret = krb5_cc_move(r->context, temp_cc, cc);
|
|
if (ret == 0)
|
|
temp_cc = NULL;
|
|
} else if (ret == 0 && kind == K5_CREDS_EPHEMERAL) {
|
|
ret = krb5_cc_get_full_name(r->context, temp_cc, &r->ccname);
|
|
}
|
|
|
|
out:
|
|
if (ctx)
|
|
krb5_init_creds_free(r->context, ctx);
|
|
krb5_get_init_creds_opt_free(r->context, opt);
|
|
krb5_free_principal(r->context, p);
|
|
krb5_cc_close(r->context, temp_cc);
|
|
krb5_cc_close(r->context, cc);
|
|
return ret;
|
|
}
|
|
|
|
static krb5_error_code
|
|
load_priv_key(krb5_context context, const char *fn, hx509_private_key *key)
|
|
{
|
|
hx509_private_key *keys = NULL;
|
|
krb5_error_code ret;
|
|
hx509_certs certs = NULL;
|
|
|
|
*key = NULL;
|
|
ret = hx509_certs_init(context->hx509ctx, fn, 0, NULL, &certs);
|
|
if (ret == ENOENT)
|
|
return 0;
|
|
if (ret == 0)
|
|
ret = _hx509_certs_keys_get(context->hx509ctx, certs, &keys);
|
|
if (ret == 0 && keys[0] == NULL)
|
|
ret = ENOENT; /* XXX Better error please */
|
|
if (ret == 0)
|
|
*key = _hx509_private_key_ref(keys[0]);
|
|
if (ret)
|
|
krb5_set_error_message(context, ret, "Could not load private "
|
|
"impersonation key from %s for PKINIT: %s", fn,
|
|
hx509_get_error_string(context->hx509ctx, ret));
|
|
_hx509_certs_keys_free(context->hx509ctx, keys);
|
|
hx509_certs_free(&certs);
|
|
return ret;
|
|
}
|
|
|
|
static krb5_error_code
|
|
k5_do_CA(struct bx509_request_desc *r)
|
|
{
|
|
SubjectPublicKeyInfo spki;
|
|
hx509_private_key key = NULL;
|
|
krb5_error_code ret = 0;
|
|
krb5_principal p = NULL;
|
|
hx509_request req = NULL;
|
|
hx509_certs certs = NULL;
|
|
KeyUsage ku = int2KeyUsage(0);
|
|
const char *cname = r->for_cname ? r->for_cname : r->cname;
|
|
|
|
memset(&spki, 0, sizeof(spki));
|
|
ku.digitalSignature = 1;
|
|
|
|
/* Make a CSR (halfway -- we don't need to sign it here) */
|
|
/* XXX Load impersonation key just once?? */
|
|
ret = load_priv_key(r->context, impersonation_key_fn, &key);
|
|
if (ret == 0)
|
|
ret = hx509_request_init(r->context->hx509ctx, &req);
|
|
if (ret == 0)
|
|
ret = krb5_parse_name(r->context, cname, &p);
|
|
if (ret == 0)
|
|
ret = hx509_private_key2SPKI(r->context->hx509ctx, key, &spki);
|
|
if (ret == 0)
|
|
hx509_request_set_SubjectPublicKeyInfo(r->context->hx509ctx, req,
|
|
&spki);
|
|
free_SubjectPublicKeyInfo(&spki);
|
|
if (ret == 0)
|
|
ret = hx509_request_add_pkinit(r->context->hx509ctx, req, cname);
|
|
if (ret == 0)
|
|
ret = hx509_request_add_eku(r->context->hx509ctx, req,
|
|
&asn1_oid_id_pkekuoid);
|
|
|
|
/* Mark it authorized */
|
|
if (ret == 0)
|
|
ret = hx509_request_authorize_san(req, 0);
|
|
if (ret == 0)
|
|
ret = hx509_request_authorize_eku(req, 0);
|
|
if (ret == 0)
|
|
hx509_request_authorize_ku(req, ku);
|
|
|
|
/* Issue the certificate */
|
|
if (ret == 0)
|
|
ret = kdc_issue_certificate(r->context, "get-tgt", logfac, req, p,
|
|
&r->token_times, r->req_life,
|
|
1 /* send_chain */, &certs);
|
|
krb5_free_principal(r->context, p);
|
|
hx509_request_free(&req);
|
|
p = NULL;
|
|
|
|
if (ret == KRB5KDC_ERR_POLICY || ret == EACCES) {
|
|
hx509_private_key_free(&key);
|
|
return bad_403(r, ret,
|
|
"Certificate request denied for policy reasons");
|
|
}
|
|
if (ret == ENOMEM) {
|
|
hx509_private_key_free(&key);
|
|
return bad_503(r, ret, "Certificate issuance failed");
|
|
}
|
|
if (ret) {
|
|
hx509_private_key_free(&key);
|
|
return bad_500(r, ret, "Certificate issuance failed");
|
|
}
|
|
|
|
/* Setup PKIX store and extract the certificate chain into it */
|
|
ret = mk_pkix_store(&r->pkix_store);
|
|
if (ret == 0)
|
|
ret = store_certs(r->context->hx509ctx, r->pkix_store, certs, key);
|
|
hx509_private_key_free(&key);
|
|
hx509_certs_free(&certs);
|
|
if (ret)
|
|
return bad_500(r, ret,
|
|
"Could not create PEM store for issued certificate");
|
|
return 0;
|
|
}
|
|
|
|
/* Get impersonated Kerberos credentials for `cprinc' */
|
|
static krb5_error_code
|
|
k5_get_creds(struct bx509_request_desc *r, enum k5_creds_kind kind)
|
|
{
|
|
krb5_error_code ret;
|
|
const char *cname = r->for_cname ? r->for_cname : r->cname;
|
|
|
|
/* If we have a live ccache for `cprinc', we're done */
|
|
r->cckind = kind;
|
|
if (kind == K5_CREDS_CACHED &&
|
|
(ret = find_ccache(r->context, cname, &r->ccname)) == 0)
|
|
return ret; /* Success */
|
|
|
|
/*
|
|
* Else we have to acquire a credential for them using their bearer token
|
|
* for authentication (and our keytab / initiator credentials perhaps).
|
|
*/
|
|
if ((ret = k5_do_CA(r)))
|
|
return ret; /* k5_do_CA() calls bad_req() */
|
|
|
|
if (ret == 0)
|
|
ret = do_pkinit(r, kind);
|
|
return ret;
|
|
}
|
|
|
|
/* Accumulate strings */
|
|
static void
|
|
acc_str(char **acc, char *adds, size_t addslen)
|
|
{
|
|
char *tmp = NULL;
|
|
int l = addslen <= INT_MAX ? (int)addslen : INT_MAX;
|
|
|
|
if (asprintf(&tmp, "%s%s%.*s",
|
|
*acc ? *acc : "",
|
|
*acc ? "; " : "", l, adds) > -1 &&
|
|
tmp) {
|
|
free(*acc);
|
|
*acc = tmp;
|
|
}
|
|
}
|
|
|
|
static char *
|
|
fmt_gss_error(OM_uint32 code, gss_OID mech)
|
|
{
|
|
gss_buffer_desc buf;
|
|
OM_uint32 major, minor;
|
|
OM_uint32 type = mech == GSS_C_NO_OID ? GSS_C_GSS_CODE: GSS_C_MECH_CODE;
|
|
OM_uint32 more = 0;
|
|
char *r = NULL;
|
|
|
|
do {
|
|
major = gss_display_status(&minor, code, type, mech, &more, &buf);
|
|
if (!GSS_ERROR(major))
|
|
acc_str(&r, (char *)buf.value, buf.length);
|
|
gss_release_buffer(&minor, &buf);
|
|
} while (!GSS_ERROR(major) && more);
|
|
return r;
|
|
}
|
|
|
|
static char *
|
|
fmt_gss_errors(const char *r, OM_uint32 major, OM_uint32 minor, gss_OID mech)
|
|
{
|
|
char *ma, *mi, *s;
|
|
|
|
ma = fmt_gss_error(major, GSS_C_NO_OID);
|
|
mi = mech == GSS_C_NO_OID ? NULL : fmt_gss_error(minor, mech);
|
|
if (asprintf(&s, "%s: %s%s%s", r,
|
|
ma ? ma : "Out of memory",
|
|
mi ? ": " : "",
|
|
mi ? mi : "") > -1 &&
|
|
s) {
|
|
free(ma);
|
|
free(mi);
|
|
return s;
|
|
}
|
|
free(mi);
|
|
return ma;
|
|
}
|
|
|
|
/* GSS-API error */
|
|
static krb5_error_code
|
|
bad_req_gss(struct bx509_request_desc *r,
|
|
OM_uint32 major,
|
|
OM_uint32 minor,
|
|
gss_OID mech,
|
|
int http_status_code,
|
|
const char *reason)
|
|
{
|
|
krb5_error_code ret;
|
|
char *msg = fmt_gss_errors(reason, major, minor, mech);
|
|
|
|
if (major == GSS_S_BAD_NAME || major == GSS_S_BAD_NAMETYPE)
|
|
http_status_code = MHD_HTTP_BAD_REQUEST;
|
|
|
|
if (msg)
|
|
ret = resp(r, http_status_code, MHD_RESPMEM_MUST_COPY, NULL,
|
|
msg, strlen(msg), NULL);
|
|
else
|
|
ret = resp(r, http_status_code, MHD_RESPMEM_MUST_COPY, NULL,
|
|
"Out of memory while formatting GSS error message",
|
|
sizeof("Out of memory while formatting GSS error message") - 1, NULL);
|
|
free(msg);
|
|
return ret;
|
|
}
|
|
|
|
/* Make an HTTP/Negotiate token */
|
|
static krb5_error_code
|
|
mk_nego_tok(struct bx509_request_desc *r,
|
|
char **nego_tok,
|
|
size_t *nego_toksz)
|
|
{
|
|
gss_key_value_element_desc kv[1] = { { "ccache", r->ccname } };
|
|
gss_key_value_set_desc store = { 1, kv };
|
|
gss_buffer_desc token = GSS_C_EMPTY_BUFFER;
|
|
gss_buffer_desc name = GSS_C_EMPTY_BUFFER;
|
|
gss_cred_id_t cred = GSS_C_NO_CREDENTIAL;
|
|
gss_ctx_id_t ctx = GSS_C_NO_CONTEXT;
|
|
gss_name_t iname = GSS_C_NO_NAME;
|
|
gss_name_t aname = GSS_C_NO_NAME;
|
|
OM_uint32 major, minor, junk;
|
|
krb5_error_code ret; /* More like a system error code here */
|
|
const char *cname = r->for_cname ? r->for_cname : r->cname;
|
|
char *token_b64 = NULL;
|
|
|
|
*nego_tok = NULL;
|
|
*nego_toksz = 0;
|
|
|
|
/* Import initiator name */
|
|
name.length = strlen(cname);
|
|
name.value = rk_UNCONST(cname);
|
|
major = gss_import_name(&minor, &name, GSS_KRB5_NT_PRINCIPAL_NAME, &iname);
|
|
if (major != GSS_S_COMPLETE)
|
|
return bad_req_gss(r, major, minor, GSS_C_NO_OID,
|
|
MHD_HTTP_SERVICE_UNAVAILABLE,
|
|
"Could not import cprinc parameter value as "
|
|
"Kerberos principal name");
|
|
|
|
/* Import target acceptor name */
|
|
name.length = strlen(r->target);
|
|
name.value = rk_UNCONST(r->target);
|
|
major = gss_import_name(&minor, &name, GSS_C_NT_HOSTBASED_SERVICE, &aname);
|
|
if (major != GSS_S_COMPLETE) {
|
|
(void) gss_release_name(&junk, &iname);
|
|
return bad_req_gss(r, major, minor, GSS_C_NO_OID,
|
|
MHD_HTTP_SERVICE_UNAVAILABLE,
|
|
"Could not import target parameter value as "
|
|
"Kerberos principal name");
|
|
}
|
|
|
|
/* Acquire a credential from the given ccache */
|
|
major = gss_add_cred_from(&minor, cred, iname, GSS_KRB5_MECHANISM,
|
|
GSS_C_INITIATE, GSS_C_INDEFINITE, 0, &store,
|
|
&cred, NULL, NULL, NULL);
|
|
(void) gss_release_name(&junk, &iname);
|
|
if (major != GSS_S_COMPLETE) {
|
|
(void) gss_release_name(&junk, &aname);
|
|
return bad_req_gss(r, major, minor, GSS_KRB5_MECHANISM,
|
|
MHD_HTTP_FORBIDDEN, "Could not acquire credentials "
|
|
"for requested cprinc");
|
|
}
|
|
|
|
major = gss_init_sec_context(&minor, cred, &ctx, aname,
|
|
GSS_KRB5_MECHANISM, 0, GSS_C_INDEFINITE,
|
|
NULL, GSS_C_NO_BUFFER, NULL, &token, NULL,
|
|
NULL);
|
|
(void) gss_delete_sec_context(&junk, &ctx, GSS_C_NO_BUFFER);
|
|
(void) gss_release_name(&junk, &aname);
|
|
(void) gss_release_cred(&junk, &cred);
|
|
if (major != GSS_S_COMPLETE)
|
|
return bad_req_gss(r, major, minor, GSS_KRB5_MECHANISM,
|
|
MHD_HTTP_SERVICE_UNAVAILABLE, "Could not acquire "
|
|
"Negotiate token for requested target");
|
|
|
|
/* Encode token, output */
|
|
ret = rk_base64_encode(token.value, token.length, &token_b64);
|
|
(void) gss_release_buffer(&junk, &token);
|
|
if (ret > 0)
|
|
ret = asprintf(nego_tok, "Negotiate %s", token_b64);
|
|
free(token_b64);
|
|
if (ret < 0 || *nego_tok == NULL)
|
|
return bad_req(r, errno, MHD_HTTP_SERVICE_UNAVAILABLE,
|
|
"Could not allocate memory for encoding Negotiate "
|
|
"token");
|
|
*nego_toksz = ret;
|
|
return 0;
|
|
}
|
|
|
|
static krb5_error_code
|
|
bnegotiate_get_target(struct bx509_request_desc *r)
|
|
{
|
|
const char *target;
|
|
const char *redir;
|
|
const char *referer; /* misspelled on the wire, misspelled here, FYI */
|
|
const char *authority;
|
|
const char *local_part;
|
|
char *s1 = NULL;
|
|
char *s2 = NULL;
|
|
|
|
target = MHD_lookup_connection_value(r->connection, MHD_GET_ARGUMENT_KIND,
|
|
"target");
|
|
redir = MHD_lookup_connection_value(r->connection, MHD_GET_ARGUMENT_KIND,
|
|
"redirect");
|
|
referer = MHD_lookup_connection_value(r->connection, MHD_HEADER_KIND,
|
|
MHD_HTTP_HEADER_REFERER);
|
|
if (target != NULL && redir == NULL) {
|
|
r->target = target;
|
|
return 0;
|
|
}
|
|
if (target == NULL && redir == NULL)
|
|
return bad_400(r, EINVAL,
|
|
"Query missing 'target' or 'redirect' parameter value");
|
|
if (target != NULL && redir != NULL)
|
|
return bad_403(r, EACCES,
|
|
"Only one of 'target' or 'redirect' parameter allowed");
|
|
if (redir != NULL && referer == NULL)
|
|
return bad_403(r, EACCES,
|
|
"Redirect request without Referer header nor allowed");
|
|
|
|
if (strncmp(referer, "https://", sizeof("https://") - 1) != 0 ||
|
|
strncmp(redir, "https://", sizeof("https://") - 1) != 0)
|
|
return bad_403(r, EACCES,
|
|
"Redirect requests permitted only for https referrers");
|
|
|
|
/* Parse out authority from each URI, redirect and referrer */
|
|
authority = redir + sizeof("https://") - 1;
|
|
if ((local_part = strchr(authority, '/')) == NULL)
|
|
local_part = authority + strlen(authority);
|
|
if ((s1 = strndup(authority, local_part - authority)) == NULL)
|
|
return bad_enomem(r, ENOMEM);
|
|
|
|
authority = referer + sizeof("https://") - 1;
|
|
if ((local_part = strchr(authority, '/')) == NULL)
|
|
local_part = authority + strlen(authority);
|
|
if ((s2 = strndup(authority, local_part - authority)) == NULL) {
|
|
free(s1);
|
|
return bad_enomem(r, ENOMEM);
|
|
}
|
|
|
|
/* Both must match */
|
|
if (strcasecmp(s1, s2) != 0) {
|
|
free(s2);
|
|
free(s1);
|
|
return bad_403(r, EACCES, "Redirect request does not match referer");
|
|
}
|
|
free(s2);
|
|
|
|
if (strchr(s1, '@')) {
|
|
free(s1);
|
|
return bad_403(r, EACCES,
|
|
"Redirect request authority has login information");
|
|
}
|
|
|
|
/* Extract hostname portion of authority and format GSS name */
|
|
if (strchr(s1, ':'))
|
|
*strchr(s1, ':') = '\0';
|
|
if (asprintf(&r->freeme1, "HTTP@%s", s1) == -1 || r->freeme1 == NULL) {
|
|
free(s1);
|
|
return bad_enomem(r, ENOMEM);
|
|
}
|
|
|
|
r->target = r->freeme1;
|
|
r->redir = redir;
|
|
free(s1);
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* Implements /bnegotiate end-point.
|
|
*
|
|
* Query parameters (mutually exclusive):
|
|
*
|
|
* - target=<name>
|
|
* - redirect=<URL-encoded-URL>
|
|
*
|
|
* If the redirect query parameter is set then the Referer: header must be as
|
|
* well, and the authority of the redirect and Referer URIs must be the same.
|
|
*/
|
|
static krb5_error_code
|
|
bnegotiate(struct bx509_request_desc *r)
|
|
{
|
|
krb5_error_code ret;
|
|
size_t nego_toksz = 0;
|
|
char *nego_tok = NULL;
|
|
|
|
ret = bnegotiate_get_target(r);
|
|
if (ret)
|
|
return ret; /* bnegotiate_get_target() calls bad_req() */
|
|
heim_audit_addkv((heim_svc_req_desc)r, KDC_AUDIT_VIS, "target", "%s",
|
|
r->target ? r->target : "<unknown>");
|
|
heim_audit_setkv_bool((heim_svc_req_desc)r, "redir", !!r->redir);
|
|
|
|
/*
|
|
* Make sure we have Kerberos credentials for cprinc. If we have them
|
|
* cached from earlier, this will be fast (all local), else it will involve
|
|
* taking a file lock and talking to the KDC using kx509 and PKINIT.
|
|
*
|
|
* Perhaps we could use S4U instead, which would speed up the slow path a
|
|
* bit.
|
|
*/
|
|
ret = k5_get_creds(r, K5_CREDS_CACHED);
|
|
if (ret)
|
|
return bad_403(r, ret,
|
|
"Could not acquire Kerberos credentials using PKINIT");
|
|
|
|
/* Acquire the Negotiate token and output it */
|
|
if (ret == 0 && r->ccname != NULL)
|
|
ret = mk_nego_tok(r, &nego_tok, &nego_toksz);
|
|
|
|
if (ret == 0) {
|
|
/* Look ma', Negotiate as an OAuth-like token system! */
|
|
if (r->redir)
|
|
ret = resp(r, MHD_HTTP_TEMPORARY_REDIRECT, MHD_RESPMEM_PERSISTENT,
|
|
NULL, "", 0, nego_tok);
|
|
else
|
|
ret = resp(r, MHD_HTTP_OK, MHD_RESPMEM_MUST_COPY,
|
|
"application/x-negotiate-token", nego_tok, nego_toksz,
|
|
NULL);
|
|
}
|
|
|
|
free(nego_tok);
|
|
return ret;
|
|
}
|
|
|
|
static krb5_error_code
|
|
authorize_TGT_REQ(struct bx509_request_desc *r)
|
|
{
|
|
krb5_principal p = NULL;
|
|
krb5_error_code ret;
|
|
const char *for_cname = r->for_cname ? r->for_cname : r->cname;
|
|
|
|
if (for_cname == r->cname || strcmp(r->cname, r->for_cname) == 0)
|
|
return 0;
|
|
|
|
ret = hx509_request_init(r->context->hx509ctx, &r->req);
|
|
if (ret)
|
|
return bad_500(r, ret, "Out of resources");
|
|
heim_audit_addkv((heim_svc_req_desc)r, KDC_AUDIT_VIS,
|
|
"requested_krb5PrincipalName", "%s", for_cname);
|
|
ret = hx509_request_add_eku(r->context->hx509ctx, r->req,
|
|
ASN1_OID_ID_PKEKUOID);
|
|
if (ret == 0)
|
|
ret = hx509_request_add_pkinit(r->context->hx509ctx, r->req,
|
|
for_cname);
|
|
if (ret == 0)
|
|
ret = krb5_parse_name(r->context, r->cname, &p);
|
|
if (ret == 0)
|
|
ret = kdc_authorize_csr(r->context, "get-tgt", r->req, p);
|
|
krb5_free_principal(r->context, p);
|
|
hx509_request_free(&r->req);
|
|
r->req = NULL;
|
|
if (ret)
|
|
return bad_403(r, ret, "Not authorized to requested TGT");
|
|
return ret;
|
|
}
|
|
|
|
static heim_mhd_result
|
|
get_tgt_param_cb(void *d,
|
|
enum MHD_ValueKind kind,
|
|
const char *key,
|
|
const char *val)
|
|
{
|
|
struct bx509_request_desc *r = d;
|
|
|
|
if (strcmp(key, "address") == 0 && val) {
|
|
if (!krb5_config_get_bool_default(r->context, NULL,
|
|
FALSE,
|
|
"get-tgt", "allow_addresses", NULL)) {
|
|
krb5_set_error_message(r->context, r->error_code = ENOTSUP,
|
|
"Query parameter %s not allowed", key);
|
|
} else {
|
|
krb5_addresses addresses;
|
|
|
|
r->error_code = _krb5_parse_address_no_lookup(r->context, val,
|
|
&addresses);
|
|
if (r->error_code == 0)
|
|
r->error_code = krb5_append_addresses(r->context, &r->tgt_addresses,
|
|
&addresses);
|
|
krb5_free_addresses(r->context, &addresses);
|
|
}
|
|
} else if (strcmp(key, "cname") == 0) {
|
|
/* Handled upstairs */
|
|
;
|
|
} else if (strcmp(key, "lifetime") == 0 && val) {
|
|
r->req_life = parse_time(val, "day");
|
|
} else {
|
|
/* Produce error for unknown params */
|
|
heim_audit_setkv_bool((heim_svc_req_desc)r, "requested_unknown", TRUE);
|
|
krb5_set_error_message(r->context, r->error_code = ENOTSUP,
|
|
"Query parameter %s not supported", key);
|
|
}
|
|
return r->error_code == 0 ? MHD_YES : MHD_NO /* Stop iterating */;
|
|
}
|
|
|
|
/*
|
|
* Implements /get-tgt end-point.
|
|
*
|
|
* Query parameters:
|
|
*
|
|
* - cname=<name> (client principal name, if not the same as the authenticated
|
|
* name, then this will be impersonated if allowed; may be
|
|
* given only once)
|
|
*
|
|
* - address=<IP> (IP address to add as a ticket address; may be given
|
|
* multiple times)
|
|
*
|
|
* - lifetime=<time> (requested lifetime for the ticket; may be given only
|
|
* once)
|
|
*/
|
|
static krb5_error_code
|
|
get_tgt(struct bx509_request_desc *r)
|
|
{
|
|
krb5_error_code ret;
|
|
size_t bodylen;
|
|
const char *fn;
|
|
void *body;
|
|
|
|
r->for_cname = MHD_lookup_connection_value(r->connection,
|
|
MHD_GET_ARGUMENT_KIND, "cname");
|
|
if (r->for_cname && r->for_cname[0] == '\0')
|
|
r->for_cname = NULL;
|
|
ret = authorize_TGT_REQ(r);
|
|
if (ret)
|
|
return ret; /* authorize_TGT_REQ() calls bad_req() */
|
|
|
|
r->error_code = 0;
|
|
(void) MHD_get_connection_values(r->connection, MHD_GET_ARGUMENT_KIND,
|
|
get_tgt_param_cb, r);
|
|
ret = r->error_code;
|
|
|
|
/* k5_get_creds() calls bad_req() */
|
|
if (ret == 0)
|
|
ret = k5_get_creds(r, K5_CREDS_EPHEMERAL);
|
|
if (ret)
|
|
return bad_403(r, ret,
|
|
"Could not acquire Kerberos credentials using PKINIT");
|
|
|
|
fn = strchr(r->ccname, ':');
|
|
if (fn == NULL)
|
|
return bad_500(r, ret, "Impossible error");
|
|
fn++;
|
|
if ((errno = rk_undumpdata(fn, &body, &bodylen)))
|
|
return bad_503(r, ret, "Could not get TGT");
|
|
|
|
ret = resp(r, MHD_HTTP_OK, MHD_RESPMEM_MUST_COPY,
|
|
"application/x-krb5-ccache", body, bodylen, NULL);
|
|
free(body);
|
|
return ret;
|
|
}
|
|
|
|
static int
|
|
get_tgts_accumulate_ccache_write_json(struct bx509_request_desc *r,
|
|
krb5_error_code code,
|
|
const char *data,
|
|
size_t datalen)
|
|
{
|
|
heim_object_t k, v;
|
|
heim_string_t text;
|
|
heim_error_t e = NULL;
|
|
heim_dict_t o;
|
|
int ret;
|
|
|
|
o = heim_dict_create(9);
|
|
k = heim_string_create("name");
|
|
v = heim_string_create(r->for_cname);
|
|
if (o && k && v)
|
|
ret = heim_dict_set_value(o, k, v);
|
|
else
|
|
ret = ENOMEM;
|
|
|
|
if (ret == 0) {
|
|
heim_release(v);
|
|
heim_release(k);
|
|
k = heim_string_create("error_code");
|
|
v = heim_number_create(code);
|
|
if (k && v)
|
|
ret = heim_dict_set_value(o, k, v);
|
|
}
|
|
if (ret == 0 && data != NULL) {
|
|
heim_release(v);
|
|
heim_release(k);
|
|
k = heim_string_create("ccache");
|
|
v = heim_data_create(data, datalen);
|
|
if (k && v)
|
|
ret = heim_dict_set_value(o, k, v);
|
|
}
|
|
if (ret == 0 && code != 0) {
|
|
const char *s = krb5_get_error_message(r->context, code);
|
|
|
|
heim_release(v);
|
|
heim_release(k);
|
|
k = heim_string_create("error");
|
|
v = heim_string_create(s ? s : "Out of memory");
|
|
krb5_free_error_message(r->context, s);
|
|
if (k && v)
|
|
ret = heim_dict_set_value(o, k, v);
|
|
}
|
|
heim_release(v);
|
|
heim_release(k);
|
|
if (ret) {
|
|
heim_release(o);
|
|
return bad_503(r, errno, "Out of memory");
|
|
}
|
|
|
|
text = heim_json_copy_serialize(o,
|
|
HEIM_JSON_F_NO_DATA_DICT |
|
|
HEIM_JSON_F_ONE_LINE,
|
|
&e);
|
|
if (text) {
|
|
const char *s = heim_string_get_utf8(text);
|
|
|
|
(void) fwrite(s, strlen(s), 1, r->tgts);
|
|
} else {
|
|
const char *s = NULL;
|
|
v = heim_error_copy_string(e);
|
|
if (v)
|
|
s = heim_string_get_utf8(v);
|
|
if (s == NULL)
|
|
s = "<unknown encoder error>";
|
|
krb5_log_msg(r->context, logfac, 1, NULL, "Failed to encode JSON text with ccache or error for %s: %s",
|
|
r->for_cname, s);
|
|
heim_release(v);
|
|
}
|
|
heim_release(text);
|
|
heim_release(o);
|
|
return MHD_YES;
|
|
}
|
|
|
|
/* Writes one ccache to a response file, as JSON */
|
|
static int
|
|
get_tgts_accumulate_ccache(struct bx509_request_desc *r, krb5_error_code ret)
|
|
{
|
|
const char *fn;
|
|
size_t bodylen = 0;
|
|
void *body = NULL;
|
|
int res;
|
|
|
|
if (r->tgts == NULL) {
|
|
int fd = -1;
|
|
|
|
if (asprintf(&r->tgts_filename,
|
|
"%s/tgts-json-XXXXXX", cache_dir) == -1 ||
|
|
r->tgts_filename == NULL) {
|
|
free(r->tgts_filename);
|
|
r->tgts_filename = NULL;
|
|
|
|
return bad_enomem(r, r->error_code = ENOMEM);
|
|
}
|
|
if ((fd = mkstemp(r->tgts_filename)) == -1)
|
|
return bad_req(r, errno, MHD_HTTP_SERVICE_UNAVAILABLE,
|
|
"%s", strerror(r->error_code = errno));
|
|
if ((r->tgts = fdopen(fd, "w+")) == NULL) {
|
|
(void) close(fd);
|
|
return bad_req(r, errno, MHD_HTTP_SERVICE_UNAVAILABLE,
|
|
"%s", strerror(r->error_code = errno));
|
|
}
|
|
}
|
|
|
|
if (ret == 0) {
|
|
fn = strchr(r->ccname, ':');
|
|
if (fn == NULL)
|
|
return bad_req(r, errno, MHD_HTTP_SERVICE_UNAVAILABLE,
|
|
"Internal error (invalid credentials cache name)");
|
|
fn++;
|
|
if ((r->error_code = rk_undumpdata(fn, &body, &bodylen)))
|
|
return bad_req(r, errno, MHD_HTTP_SERVICE_UNAVAILABLE,
|
|
"%s", strerror(r->error_code));
|
|
(void) unlink(fn);
|
|
free(r->ccname);
|
|
r->ccname = NULL;
|
|
if (bodylen > INT_MAX >> 4) {
|
|
free(body);
|
|
return bad_req(r, errno, MHD_HTTP_SERVICE_UNAVAILABLE,
|
|
"Credentials cache too large!");
|
|
}
|
|
}
|
|
|
|
res = get_tgts_accumulate_ccache_write_json(r, ret, body, bodylen);
|
|
free(body);
|
|
return res;
|
|
}
|
|
|
|
static heim_mhd_result
|
|
get_tgts_param_authorize_cb(void *d,
|
|
enum MHD_ValueKind kind,
|
|
const char *key,
|
|
const char *val)
|
|
{
|
|
struct bx509_request_desc *r = d;
|
|
krb5_error_code ret = 0;
|
|
|
|
if (strcmp(key, "cname") != 0 || val == NULL)
|
|
return MHD_YES;
|
|
|
|
if (r->req == NULL) {
|
|
ret = hx509_request_init(r->context->hx509ctx, &r->req);
|
|
if (ret == 0)
|
|
ret = hx509_request_add_eku(r->context->hx509ctx, r->req,
|
|
ASN1_OID_ID_PKEKUOID);
|
|
if (ret)
|
|
return bad_500(r, ret, "Out of resources");
|
|
}
|
|
heim_audit_addkv((heim_svc_req_desc)r, KDC_AUDIT_VIS,
|
|
"requested_krb5PrincipalName", "%s", val);
|
|
ret = hx509_request_add_pkinit(r->context->hx509ctx, r->req,
|
|
val);
|
|
if (ret)
|
|
return bad_403(r, ret, "Not authorized to requested TGT");
|
|
return MHD_YES;
|
|
}
|
|
|
|
/* For each requested principal, produce a ccache */
|
|
static heim_mhd_result
|
|
get_tgts_param_execute_cb(void *d,
|
|
enum MHD_ValueKind kind,
|
|
const char *key,
|
|
const char *val)
|
|
{
|
|
struct bx509_request_desc *r = d;
|
|
hx509_san_type san_type;
|
|
krb5_error_code ret;
|
|
size_t san_idx = r->san_idx++;
|
|
const char *save_for_cname = r->for_cname;
|
|
char *s = NULL;
|
|
int res;
|
|
|
|
/* We expect only cname=principal q-params here */
|
|
if (strcmp(key, "cname") != 0 || val == NULL)
|
|
return MHD_YES;
|
|
|
|
/*
|
|
* We expect the `san_idx'th SAN in the `r->req' request checked by
|
|
* kdc_authorize_csr() to be the same as this cname. This happens
|
|
* naturally because we add these SANs to `r->req' in the same order as we
|
|
* visit them here (unless our HTTP library somehow went crazy).
|
|
*
|
|
* Still, we check that it's the same SAN.
|
|
*/
|
|
ret = hx509_request_get_san(r->req, san_idx, &san_type, &s);
|
|
if (ret == HX509_NO_ITEM ||
|
|
san_type != HX509_SAN_TYPE_PKINIT ||
|
|
strcmp(s, val) != 0) {
|
|
/*
|
|
* If the cname and SAN don't match, it's some weird internal error
|
|
* (can't happen).
|
|
*/
|
|
krb5_set_error_message(r->context, r->error_code = EACCES,
|
|
"PKINIT SAN not granted: %s (internal error)",
|
|
val);
|
|
ret = EACCES;
|
|
}
|
|
|
|
/*
|
|
* We're going to pretend to be this SAN for the purpose of acquring a TGT
|
|
* for it. So we "push" `r->for_cname'.
|
|
*/
|
|
if (ret == 0)
|
|
r->for_cname = val;
|
|
|
|
/*
|
|
* Our authorizer supports partial authorization where the whole request is
|
|
* rejected but some features of it are permitted.
|
|
*
|
|
* (In most end-points we don't want partial authorization, but in
|
|
* /get-tgts we very much do.)
|
|
*/
|
|
if (ret == 0 && !hx509_request_san_authorized_p(r->req, san_idx)) {
|
|
heim_audit_addkv((heim_svc_req_desc)r, KDC_AUDIT_VIS,
|
|
"REJECT_krb5PrincipalName", "%s", val);
|
|
krb5_set_error_message(r->context, r->error_code = EACCES,
|
|
"PKINIT SAN denied: %s", val);
|
|
ret = EACCES;
|
|
}
|
|
if (ret == 0) {
|
|
heim_audit_addkv((heim_svc_req_desc)r, KDC_AUDIT_VIS,
|
|
"ACCEPT_krb5PrincipalName", "%s", val);
|
|
ret = k5_get_creds(r, K5_CREDS_EPHEMERAL);
|
|
if (ret == 0)
|
|
heim_audit_addkv((heim_svc_req_desc)r, KDC_AUDIT_VIS,
|
|
"ISSUE_krb5PrincipalName", "%s", val);
|
|
}
|
|
|
|
/*
|
|
* If ret == 0 this will gather the TGT we acquired, else it will acquire
|
|
* the error we got.
|
|
*/
|
|
res = get_tgts_accumulate_ccache(r, ret);
|
|
|
|
/* Now we "pop" `r->for_cname' */
|
|
r->for_cname = save_for_cname;
|
|
|
|
hx509_xfree(s);
|
|
return res;
|
|
}
|
|
|
|
/*
|
|
* Implements /get-tgts end-point.
|
|
*
|
|
* Query parameters:
|
|
*
|
|
* - cname=<name> (client principal name, if not the same as the authenticated
|
|
* name, then this will be impersonated if allowed; may be
|
|
* given multiple times)
|
|
*/
|
|
static krb5_error_code
|
|
get_tgts(struct bx509_request_desc *r)
|
|
{
|
|
krb5_error_code ret;
|
|
krb5_principal p = NULL;
|
|
size_t bodylen;
|
|
void *body;
|
|
int res = MHD_YES;
|
|
|
|
/* Prep to authorize */
|
|
ret = krb5_parse_name(r->context, r->cname, &p);
|
|
if (ret)
|
|
return bad_403(r, ret, "Could not parse caller principal name");
|
|
if (ret == 0) {
|
|
/* Extract q-params other than `cname' */
|
|
r->error_code = 0;
|
|
res = MHD_get_connection_values(r->connection, MHD_GET_ARGUMENT_KIND,
|
|
get_tgt_param_cb, r);
|
|
if (r->response || res == MHD_NO) {
|
|
krb5_free_principal(r->context, p);
|
|
return res;
|
|
}
|
|
|
|
ret = r->error_code;
|
|
}
|
|
if (ret == 0) {
|
|
/*
|
|
* Check authorization of the authenticated client to the requested
|
|
* client principal names (calls bad_req()).
|
|
*/
|
|
r->error_code = 0;
|
|
res = MHD_get_connection_values(r->connection, MHD_GET_ARGUMENT_KIND,
|
|
get_tgts_param_authorize_cb, r);
|
|
if (r->response || res == MHD_NO) {
|
|
krb5_free_principal(r->context, p);
|
|
return res;
|
|
}
|
|
|
|
ret = r->error_code;
|
|
if (ret == 0) {
|
|
/* Use the same configuration as /get-tgt (or should we?) */
|
|
ret = kdc_authorize_csr(r->context, "get-tgt", r->req, p);
|
|
|
|
/*
|
|
* We tolerate EACCES because we support partial approval.
|
|
*
|
|
* (KRB5_PLUGIN_NO_HANDLE means no plugin handled the authorization
|
|
* check.)
|
|
*/
|
|
if (ret == EACCES || ret == KRB5_PLUGIN_NO_HANDLE)
|
|
ret = 0;
|
|
if (ret) {
|
|
krb5_free_principal(r->context, p);
|
|
return bad_403(r, ret, "Permission denied");
|
|
}
|
|
}
|
|
}
|
|
if (ret == 0) {
|
|
/*
|
|
* Get the actual TGTs that were authorized.
|
|
*
|
|
* get_tgts_param_execute_cb() calls bad_req()
|
|
*/
|
|
r->error_code = 0;
|
|
res = MHD_get_connection_values(r->connection, MHD_GET_ARGUMENT_KIND,
|
|
get_tgts_param_execute_cb, r);
|
|
if (r->response || res == MHD_NO) {
|
|
krb5_free_principal(r->context, p);
|
|
return res;
|
|
}
|
|
ret = r->error_code;
|
|
}
|
|
krb5_free_principal(r->context, p);
|
|
hx509_request_free(&r->req);
|
|
r->req = NULL;
|
|
|
|
/*
|
|
* get_tgts_param_execute_cb() will write its JSON response to the file
|
|
* named by r->ccname.
|
|
*/
|
|
if (fflush(r->tgts) != 0)
|
|
return bad_503(r, ret, "Could not get TGT");
|
|
if ((errno = rk_undumpdata(r->tgts_filename, &body, &bodylen)))
|
|
return bad_503(r, ret, "Could not get TGT");
|
|
|
|
ret = resp(r, MHD_HTTP_OK, MHD_RESPMEM_MUST_COPY,
|
|
"application/x-krb5-ccaches-json", body, bodylen, NULL);
|
|
free(body);
|
|
return ret;
|
|
}
|
|
|
|
static krb5_error_code
|
|
health(const char *method, struct bx509_request_desc *r)
|
|
{
|
|
if (strcmp(method, "HEAD") == 0)
|
|
return resp(r, MHD_HTTP_OK, MHD_RESPMEM_PERSISTENT, NULL, "", 0, NULL);
|
|
return resp(r, MHD_HTTP_OK, MHD_RESPMEM_PERSISTENT, NULL,
|
|
"To determine the health of the service, use the /bx509 "
|
|
"end-point.\n",
|
|
sizeof("To determine the health of the service, use the "
|
|
"/bx509 end-point.\n") - 1, NULL);
|
|
|
|
}
|
|
|
|
static krb5_error_code
|
|
mac_csrf_token(struct bx509_request_desc *r, krb5_storage *sp)
|
|
{
|
|
krb5_error_code ret;
|
|
krb5_data data;
|
|
char mac[EVP_MAX_MD_SIZE];
|
|
unsigned int maclen = sizeof(mac);
|
|
HMAC_CTX *ctx = NULL;
|
|
|
|
ret = krb5_storage_to_data(sp, &data);
|
|
if (ret == 0 && (ctx = HMAC_CTX_new()) == NULL)
|
|
ret = krb5_enomem(r->context);
|
|
/* HMAC the token body and the client principal name */
|
|
if (ret == 0) {
|
|
if (HMAC_Init_ex(ctx, csrf_key, sizeof(csrf_key),
|
|
EVP_sha256(),
|
|
NULL) == 0) {
|
|
HMAC_CTX_cleanup(ctx);
|
|
ret = krb5_enomem(r->context);
|
|
} else {
|
|
HMAC_Update(ctx, data.data, data.length);
|
|
if (r->cname)
|
|
HMAC_Update(ctx, r->cname, strlen(r->cname));
|
|
HMAC_Final(ctx, mac, &maclen);
|
|
HMAC_CTX_cleanup(ctx);
|
|
krb5_data_free(&data);
|
|
data.length = maclen;
|
|
data.data = mac;
|
|
if (krb5_storage_write(sp, mac, maclen) != maclen)
|
|
ret = krb5_enomem(r->context);
|
|
}
|
|
}
|
|
if (ctx)
|
|
HMAC_CTX_free(ctx);
|
|
return ret;
|
|
}
|
|
|
|
/*
|
|
* Make a CSRF token. If one is also given, make one with the same body
|
|
* content so we can check the HMAC.
|
|
*
|
|
* Outputs the token and its age. Do not use either if the token does not
|
|
* equal the given token.
|
|
*/
|
|
static krb5_error_code
|
|
make_csrf_token(struct bx509_request_desc *r,
|
|
const char *given,
|
|
char **token,
|
|
int64_t *age)
|
|
{
|
|
krb5_error_code ret = 0;
|
|
unsigned char given_decoded[128];
|
|
krb5_storage *sp = NULL;
|
|
krb5_data data;
|
|
ssize_t dlen = -1;
|
|
uint64_t nonce;
|
|
int64_t t = 0;
|
|
|
|
|
|
*age = 0;
|
|
data.data = NULL;
|
|
data.length = 0;
|
|
if (given) {
|
|
size_t len = strlen(given);
|
|
|
|
/* Extract issue time and nonce from token */
|
|
if (len >= sizeof(given_decoded))
|
|
ret = ERANGE;
|
|
if (ret == 0 && (dlen = rk_base64_decode(given, &given_decoded)) <= 0)
|
|
ret = errno;
|
|
if (ret == 0 &&
|
|
(sp = krb5_storage_from_mem(given_decoded, dlen)) == NULL)
|
|
ret = krb5_enomem(r->context);
|
|
if (ret == 0)
|
|
ret = krb5_ret_int64(sp, &t);
|
|
if (ret == 0)
|
|
ret = krb5_ret_uint64(sp, &nonce);
|
|
krb5_storage_free(sp);
|
|
sp = NULL;
|
|
if (ret == 0)
|
|
*age = time(NULL) - t;
|
|
} else {
|
|
t = time(NULL);
|
|
krb5_generate_random_block((void *)&nonce, sizeof(nonce));
|
|
}
|
|
|
|
if (ret == 0 && (sp = krb5_storage_emem()) == NULL)
|
|
ret = krb5_enomem(r->context);
|
|
if (ret == 0)
|
|
ret = krb5_store_int64(sp, t);
|
|
if (ret == 0)
|
|
ret = krb5_store_uint64(sp, nonce);
|
|
if (ret == 0)
|
|
ret = mac_csrf_token(r, sp);
|
|
if (ret == 0)
|
|
ret = krb5_storage_to_data(sp, &data);
|
|
if (ret == 0 && data.length > INT_MAX)
|
|
ret = ERANGE;
|
|
if (ret == 0 &&
|
|
rk_base64_encode(data.data, data.length, token) < 0)
|
|
ret = errno;
|
|
krb5_storage_free(sp);
|
|
krb5_data_free(&data);
|
|
return ret;
|
|
}
|
|
|
|
static heim_mhd_result
|
|
validate_csrf_token(struct bx509_request_desc *r)
|
|
{
|
|
const char *given;
|
|
int64_t age;
|
|
krb5_error_code ret;
|
|
|
|
if ((((csrf_prot_type & CSRF_PROT_GET_WITH_HEADER) &&
|
|
strcmp(r->method, "GET") == 0) ||
|
|
((csrf_prot_type & CSRF_PROT_POST_WITH_HEADER) &&
|
|
strcmp(r->method, "POST") == 0)) &&
|
|
MHD_lookup_connection_value(r->connection, MHD_HEADER_KIND,
|
|
csrf_header) == NULL) {
|
|
ret = bad_req(r, EACCES, MHD_HTTP_FORBIDDEN,
|
|
"Request must have header \"%s\"", csrf_header);
|
|
return ret == -1 ? MHD_NO : MHD_YES;
|
|
}
|
|
|
|
if (strcmp(r->method, "GET") == 0 &&
|
|
!(csrf_prot_type & CSRF_PROT_GET_WITH_TOKEN))
|
|
return 0;
|
|
if (strcmp(r->method, "POST") == 0 &&
|
|
!(csrf_prot_type & CSRF_PROT_POST_WITH_TOKEN))
|
|
return 0;
|
|
|
|
given = MHD_lookup_connection_value(r->connection, MHD_HEADER_KIND,
|
|
"X-CSRF-Token");
|
|
ret = make_csrf_token(r, given, &r->csrf_token, &age);
|
|
if (ret)
|
|
return bad_503(r, ret, "Could not make or validate CSRF token");
|
|
if (given == NULL)
|
|
return bad_req(r, EACCES, MHD_HTTP_FORBIDDEN,
|
|
"CSRF token needed; copy the X-CSRF-Token: response "
|
|
"header to your next POST");
|
|
if (strlen(given) != strlen(r->csrf_token) ||
|
|
strcmp(given, r->csrf_token) != 0)
|
|
return bad_403(r, EACCES, "Invalid CSRF token");
|
|
if (age > 300)
|
|
return bad_403(r, EACCES, "CSRF token expired");
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* MHD callback to free the request context when MHD is done sending the
|
|
* response.
|
|
*/
|
|
static void
|
|
cleanup_req(void *cls,
|
|
struct MHD_Connection *connection,
|
|
void **con_cls,
|
|
enum MHD_RequestTerminationCode toe)
|
|
{
|
|
struct bx509_request_desc *r = *con_cls;
|
|
|
|
(void)cls;
|
|
(void)connection;
|
|
(void)toe;
|
|
clean_req_desc(r);
|
|
*con_cls = NULL;
|
|
}
|
|
|
|
/* Callback for MHD POST form data processing */
|
|
static heim_mhd_result
|
|
ip(void *cls,
|
|
enum MHD_ValueKind kind,
|
|
const char *key,
|
|
const char *content_name,
|
|
const char *content_type,
|
|
const char *transfer_encoding,
|
|
const char *val,
|
|
uint64_t off,
|
|
size_t size)
|
|
{
|
|
struct bx509_request_desc *r = cls;
|
|
struct free_tend_list *ftl = calloc(1, sizeof(*ftl));
|
|
char *keydup = strdup(key);
|
|
char *valdup = strndup(val, size);
|
|
|
|
(void)content_name; /* MIME attachment name */
|
|
(void)content_type; /* Don't care -- MHD liked it */
|
|
(void)transfer_encoding;
|
|
(void)off; /* Offset in POST data */
|
|
|
|
/*
|
|
* We're going to MHD_set_connection_value(), but we need copies because
|
|
* the MHD POST processor quite naturally keeps none of the chunks
|
|
* received.
|
|
*/
|
|
if (ftl == NULL || keydup == NULL || valdup == NULL) {
|
|
free(ftl);
|
|
free(keydup);
|
|
free(valdup);
|
|
return MHD_NO;
|
|
}
|
|
ftl->freeme1 = keydup;
|
|
ftl->freeme2 = valdup;
|
|
ftl->next = r->free_list;
|
|
r->free_list = ftl;
|
|
|
|
return MHD_set_connection_value(r->connection, MHD_GET_ARGUMENT_KIND,
|
|
keydup, valdup);
|
|
}
|
|
|
|
typedef krb5_error_code (*handler)(struct bx509_request_desc *);
|
|
|
|
struct route {
|
|
const char *local_part;
|
|
handler h;
|
|
unsigned int referer_ok:1;
|
|
} routes[] = {
|
|
{ "/get-cert", bx509, 0 },
|
|
{ "/get-negotiate-token", bnegotiate, 1 },
|
|
{ "/get-tgt", get_tgt, 0 },
|
|
{ "/get-tgts", get_tgts, 0 },
|
|
/* Lousy old names to be removed eventually */
|
|
{ "/bnegotiate", bnegotiate, 1 },
|
|
{ "/bx509", bx509, 0 },
|
|
};
|
|
|
|
/*
|
|
* We should commonalize all of:
|
|
*
|
|
* - route() and related infrastructure
|
|
* - including the CSRF functions
|
|
* - and Negotiate/Bearer authentication
|
|
*
|
|
* so that we end up with a simple framework that our daemons can invoke to
|
|
* serve simple functions that take a fully-consumed request and send a
|
|
* response.
|
|
*
|
|
* Then:
|
|
*
|
|
* - split out the CA and non-CA bits into separate daemons using that common
|
|
* code,
|
|
* - make httpkadmind use that common code,
|
|
* - abstract out all the MHD stuff.
|
|
*/
|
|
|
|
/* Routes requests */
|
|
static heim_mhd_result
|
|
route(void *cls,
|
|
struct MHD_Connection *connection,
|
|
const char *url,
|
|
const char *method,
|
|
const char *version,
|
|
const char *upload_data,
|
|
size_t *upload_data_size,
|
|
void **ctx)
|
|
{
|
|
struct bx509_request_desc *r = *ctx;
|
|
size_t i;
|
|
int ret;
|
|
|
|
if (r == NULL) {
|
|
/*
|
|
* This is the first call, right after headers were read.
|
|
*
|
|
* We must return quickly so that any 100-Continue might be sent with
|
|
* celerity. We want to make sure to send any 401s early, so we check
|
|
* WWW-Authenticate now, not later.
|
|
*
|
|
* We'll get called again to really do the processing. If we're
|
|
* handling a POST then we'll also get called with upload_data != NULL,
|
|
* possibly multiple times.
|
|
*/
|
|
if ((ret = set_req_desc(connection, method, url, &r)))
|
|
return MHD_NO;
|
|
*ctx = r;
|
|
|
|
/* All requests other than /health require authentication */
|
|
if (strcmp(url, "/health") == 0)
|
|
return MHD_YES;
|
|
|
|
/*
|
|
* Authenticate and do CSRF protection.
|
|
*
|
|
* If the Referer: header is set in the request, we don't want CSRF
|
|
* protection as only /get-negotiate-token will accept a Referer:
|
|
* header (see routes[] and below), so we'll call validate_csrf_token()
|
|
* for the other routes or reject the request for having Referer: set.
|
|
*/
|
|
ret = validate_token(r);
|
|
if (ret == 0 &&
|
|
MHD_lookup_connection_value(r->connection, MHD_HEADER_KIND, "Referer") == NULL)
|
|
ret = validate_csrf_token(r);
|
|
|
|
/*
|
|
* As this is the initial call to this handler, we must return now.
|
|
*
|
|
* If authentication or CSRF protection failed then we'll already have
|
|
* enqueued a 401, 403, or 5xx response and then we're done.
|
|
*
|
|
* If both authentication and CSRF protection succeeded then no
|
|
* response has been queued up and we'll get called again to finally
|
|
* process the request, then this entire if block will not be executed.
|
|
*/
|
|
return ret == -1 ? MHD_NO : MHD_YES;
|
|
}
|
|
|
|
/* Validate HTTP method */
|
|
if (strcmp(method, "GET") != 0 &&
|
|
strcmp(method, "POST") != 0 &&
|
|
strcmp(method, "HEAD") != 0) {
|
|
return bad_405(r, method) == -1 ? MHD_NO : MHD_YES;
|
|
}
|
|
|
|
if ((strcmp(method, "HEAD") == 0 || strcmp(method, "GET") == 0) &&
|
|
(strcmp(url, "/health") == 0 || strcmp(url, "/") == 0)) {
|
|
/* /health end-point -- no authentication, no CSRF, no nothing */
|
|
return health(method, r) == -1 ? MHD_NO : MHD_YES;
|
|
}
|
|
|
|
if (r->cname == NULL)
|
|
return bad_401(r, "Authorization token is missing");
|
|
|
|
if (strcmp(method, "POST") == 0 && *upload_data_size != 0) {
|
|
/*
|
|
* Consume all the POST body and set form data as MHD_GET_ARGUMENT_KIND
|
|
* (as if they had been URI query parameters).
|
|
*
|
|
* We have to do this before we can MHD_queue_response() as MHD will
|
|
* not consume the rest of the request body on its own, so it's an
|
|
* error to MHD_queue_response() before we've done this, and if we do
|
|
* then MHD just closes the connection.
|
|
*
|
|
* 4KB should be more than enough buffer space for all the keys we
|
|
* expect.
|
|
*/
|
|
if (r->pp == NULL)
|
|
r->pp = MHD_create_post_processor(connection, 4096, ip, r);
|
|
if (r->pp == NULL) {
|
|
ret = bad_503(r, errno ? errno : ENOMEM,
|
|
"Could not consume POST data");
|
|
return ret == -1 ? MHD_NO : MHD_YES;
|
|
}
|
|
if (r->post_data_size + *upload_data_size > 1UL<<17) {
|
|
return bad_413(r) == -1 ? MHD_NO : MHD_YES;
|
|
}
|
|
r->post_data_size += *upload_data_size;
|
|
if (MHD_post_process(r->pp, upload_data,
|
|
*upload_data_size) == MHD_NO) {
|
|
ret = bad_503(r, errno ? errno : ENOMEM,
|
|
"Could not consume POST data");
|
|
return ret == -1 ? MHD_NO : MHD_YES;
|
|
}
|
|
*upload_data_size = 0;
|
|
return MHD_YES;
|
|
}
|
|
|
|
/*
|
|
* Either this is a HEAD, a GET, or a POST whose request body has now been
|
|
* received completely and processed.
|
|
*/
|
|
|
|
/* Allow GET? */
|
|
if (strcmp(method, "GET") == 0 && !allow_GET_flag) {
|
|
/* No */
|
|
return bad_405(r, method) == -1 ? MHD_NO : MHD_YES;
|
|
}
|
|
|
|
for (i = 0; i < sizeof(routes)/sizeof(routes[0]); i++) {
|
|
if (strcmp(url, routes[i].local_part) != 0)
|
|
continue;
|
|
if (!routes[i].referer_ok &&
|
|
MHD_lookup_connection_value(r->connection,
|
|
MHD_HEADER_KIND,
|
|
"Referer") != NULL) {
|
|
ret = bad_req(r, EACCES, MHD_HTTP_FORBIDDEN,
|
|
"GET from browser not allowed");
|
|
return ret == -1 ? MHD_NO : MHD_YES;
|
|
}
|
|
if (strcmp(method, "HEAD") == 0)
|
|
ret = resp(r, MHD_HTTP_OK, MHD_RESPMEM_PERSISTENT, NULL, "", 0,
|
|
NULL);
|
|
else
|
|
ret = routes[i].h(r);
|
|
return ret == -1 ? MHD_NO : MHD_YES;
|
|
}
|
|
|
|
ret = bad_404(r, url);
|
|
return ret == -1 ? MHD_NO : MHD_YES;
|
|
}
|
|
|
|
static struct getargs args[] = {
|
|
{ "help", 'h', arg_flag, &help_flag, "Print usage message", NULL },
|
|
{ "version", '\0', arg_flag, &version_flag, "Print version", NULL },
|
|
{ NULL, 'H', arg_strings, &audiences,
|
|
"expected token audience(s)", "HOSTNAME" },
|
|
{ "daemon", 'd', arg_flag, &daemonize, "daemonize", "daemonize" },
|
|
{ "daemon-child", 0, arg_flag, &daemon_child_fd, NULL, NULL }, /* priv */
|
|
{ "reverse-proxied", 0, arg_flag, &reverse_proxied_flag,
|
|
"reverse proxied", "listen on 127.0.0.1 and do not use TLS" },
|
|
{ "port", 'p', arg_integer, &port, "port number (default: 443)", "PORT" },
|
|
{ "cache-dir", 0, arg_string, &cache_dir,
|
|
"cache directory", "DIRECTORY" },
|
|
{ "allow-GET", 0, arg_negative_flag, &allow_GET_flag, NULL, NULL },
|
|
{ "csrf-header", 0, arg_flag,
|
|
&csrf_header, "required request header", "HEADER-NAME" },
|
|
{ "csrf-protection-type", 0, arg_strings, &csrf_prot_type_strs,
|
|
"Anti-CSRF protection type", "TYPE" },
|
|
{ "csrf-key-file", 0, arg_string, &csrf_key_file,
|
|
"CSRF MAC key", "FILE" },
|
|
{ "cert", 0, arg_string, &cert_file,
|
|
"certificate file path (PEM)", "HX509-STORE" },
|
|
{ "private-key", 0, arg_string, &priv_key_file,
|
|
"private key file path (PEM)", "HX509-STORE" },
|
|
{ "thread-per-client", 't', arg_flag, &thread_per_client_flag,
|
|
"thread per-client", "use thread per-client" },
|
|
{ "verbose", 'v', arg_counter, &verbose_counter, "verbose", "run verbosely" }
|
|
};
|
|
|
|
static int
|
|
usage(int e)
|
|
{
|
|
arg_printusage(args, sizeof(args) / sizeof(args[0]), "bx509",
|
|
"\nServes RESTful GETs of /get-cert, /get-tgt, /get-tgts, and\n"
|
|
"/get-negotiate-toke, performing corresponding kx509 and, \n"
|
|
"possibly, PKINIT requests to the KDCs of the requested \n"
|
|
"realms (or just the given REALM).\n");
|
|
exit(e);
|
|
}
|
|
|
|
static int sigpipe[2] = { -1, -1 };
|
|
|
|
static void
|
|
sighandler(int sig)
|
|
{
|
|
char c = sig;
|
|
while (write(sigpipe[1], &c, sizeof(c)) == -1 && errno == EINTR)
|
|
;
|
|
}
|
|
|
|
static void
|
|
bx509_openlog(krb5_context context,
|
|
const char *svc,
|
|
krb5_log_facility **fac)
|
|
{
|
|
char **s = NULL, **p;
|
|
|
|
krb5_initlog(context, "bx509d", fac);
|
|
s = krb5_config_get_strings(context, NULL, svc, "logging", NULL);
|
|
if (s == NULL)
|
|
s = krb5_config_get_strings(context, NULL, "logging", svc, NULL);
|
|
if (s) {
|
|
for(p = s; *p; p++)
|
|
krb5_addlog_dest(context, *fac, *p);
|
|
krb5_config_free_strings(s);
|
|
} else {
|
|
char *ss;
|
|
if (asprintf(&ss, "0-1/FILE:%s/%s", hdb_db_dir(context),
|
|
KDC_LOG_FILE) < 0)
|
|
err(1, "out of memory");
|
|
krb5_addlog_dest(context, *fac, ss);
|
|
free(ss);
|
|
}
|
|
krb5_set_warn_dest(context, *fac);
|
|
}
|
|
|
|
static const char *sysplugin_dirs[] = {
|
|
#ifdef _WIN32
|
|
"$ORIGIN",
|
|
#else
|
|
"$ORIGIN/../lib/plugin/kdc",
|
|
#endif
|
|
#ifdef __APPLE__
|
|
LIBDIR "/plugin/kdc",
|
|
#endif
|
|
NULL
|
|
};
|
|
|
|
static void
|
|
load_plugins(krb5_context context)
|
|
{
|
|
const char * const *dirs = sysplugin_dirs;
|
|
#ifndef _WIN32
|
|
char **cfdirs;
|
|
|
|
cfdirs = krb5_config_get_strings(context, NULL, "kdc", "plugin_dir", NULL);
|
|
if (cfdirs)
|
|
dirs = (const char * const *)cfdirs;
|
|
#endif
|
|
|
|
/* XXX kdc? */
|
|
_krb5_load_plugins(context, "kdc", (const char **)dirs);
|
|
|
|
#ifndef _WIN32
|
|
krb5_config_free_strings(cfdirs);
|
|
#endif
|
|
}
|
|
|
|
static void
|
|
get_csrf_prot_type(krb5_context context)
|
|
{
|
|
char * const *strs = csrf_prot_type_strs.strings;
|
|
size_t n = csrf_prot_type_strs.num_strings;
|
|
size_t i;
|
|
char **freeme = NULL;
|
|
|
|
if (csrf_header == NULL)
|
|
csrf_header = krb5_config_get_string(context, NULL, "bx509d",
|
|
"csrf_protection_csrf_header",
|
|
NULL);
|
|
|
|
if (n == 0) {
|
|
char * const *p;
|
|
|
|
strs = freeme = krb5_config_get_strings(context, NULL, "bx509d",
|
|
"csrf_protection_type", NULL);
|
|
for (p = strs; p && p; p++)
|
|
n++;
|
|
}
|
|
|
|
for (i = 0; i < n; i++) {
|
|
if (strcmp(strs[i], "GET-with-header") == 0)
|
|
csrf_prot_type |= CSRF_PROT_GET_WITH_HEADER;
|
|
else if (strcmp(strs[i], "GET-with-token") == 0)
|
|
csrf_prot_type |= CSRF_PROT_GET_WITH_TOKEN;
|
|
else if (strcmp(strs[i], "POST-with-header") == 0)
|
|
csrf_prot_type |= CSRF_PROT_POST_WITH_HEADER;
|
|
else if (strcmp(strs[i], "POST-with-token") == 0)
|
|
csrf_prot_type |= CSRF_PROT_POST_WITH_TOKEN;
|
|
}
|
|
free(freeme);
|
|
|
|
/*
|
|
* For GETs we default to no CSRF protection as our GETable resources are
|
|
* safe and idempotent and we count on the browser not to make the
|
|
* responses available to cross-site requests.
|
|
*
|
|
* But, really, we don't want browsers even making these requests since, if
|
|
* the browsers behave correctly, then there's no point, and if they don't
|
|
* behave correctly then that could be catastrophic. Of course, there's no
|
|
* guarantee that a browser won't have other catastrophic bugs, but still,
|
|
* we should probably change this default in the future:
|
|
*
|
|
* if (!(csrf_prot_type & CSRF_PROT_GET_WITH_HEADER) &&
|
|
* !(csrf_prot_type & CSRF_PROT_GET_WITH_TOKEN))
|
|
* csrf_prot_type |= <whatever-the-new-default-should-be>;
|
|
*/
|
|
|
|
/*
|
|
* For POSTs we default to CSRF protection with anti-CSRF tokens even
|
|
* though out POSTable resources are safe and idempotent when POSTed and we
|
|
* could count on the browser not to make the responses available to
|
|
* cross-site requests.
|
|
*/
|
|
if (!(csrf_prot_type & CSRF_PROT_POST_WITH_HEADER) &&
|
|
!(csrf_prot_type & CSRF_PROT_POST_WITH_TOKEN))
|
|
csrf_prot_type |= CSRF_PROT_POST_WITH_TOKEN;
|
|
}
|
|
|
|
int
|
|
main(int argc, char **argv)
|
|
{
|
|
unsigned int flags = MHD_USE_THREAD_PER_CONNECTION; /* XXX */
|
|
struct sockaddr_in sin;
|
|
struct MHD_Daemon *previous = NULL;
|
|
struct MHD_Daemon *current = NULL;
|
|
struct sigaction sa;
|
|
krb5_context context = NULL;
|
|
MHD_socket sock = MHD_INVALID_SOCKET;
|
|
char *priv_key_pem = NULL;
|
|
char *cert_pem = NULL;
|
|
char sig;
|
|
int optidx = 0;
|
|
int ret;
|
|
|
|
setprogname("bx509d");
|
|
if (getarg(args, sizeof(args) / sizeof(args[0]), argc, argv, &optidx))
|
|
usage(1);
|
|
if (help_flag)
|
|
usage(0);
|
|
if (version_flag) {
|
|
print_version(NULL);
|
|
exit(0);
|
|
}
|
|
if (argc > optidx) /* Add option to set a URI local part prefix? */
|
|
usage(1);
|
|
if (port < 0)
|
|
errx(1, "Port number must be given");
|
|
|
|
if ((errno = pthread_key_create(&k5ctx, k5_free_context)))
|
|
err(1, "Could not create thread-specific storage");
|
|
|
|
if ((errno = get_krb5_context(&context)))
|
|
err(1, "Could not init krb5 context");
|
|
|
|
bx509_openlog(context, "bx509d", &logfac);
|
|
krb5_set_log_dest(context, logfac);
|
|
load_plugins(context);
|
|
|
|
if (allow_GET_flag == -1)
|
|
warnx("It is safer to use --no-allow-GET");
|
|
|
|
get_csrf_prot_type(context);
|
|
|
|
krb5_generate_random_block((void *)&csrf_key, sizeof(csrf_key));
|
|
if (csrf_key_file == NULL)
|
|
csrf_key_file = krb5_config_get_string(context, NULL, "bx509d",
|
|
"csrf_key_file", NULL);
|
|
if (csrf_key_file) {
|
|
ssize_t bytes;
|
|
int fd;
|
|
|
|
fd = open(csrf_key_file, O_RDONLY);
|
|
if (fd == -1)
|
|
err(1, "CSRF key file missing %s", csrf_key_file);
|
|
bytes = read(fd, csrf_key, sizeof(csrf_key));
|
|
if (bytes == -1)
|
|
err(1, "Could not read CSRF key file %s", csrf_key_file);
|
|
if (bytes != sizeof(csrf_key))
|
|
errx(1, "CSRF key file too small (should be %lu) %s",
|
|
(unsigned long)sizeof(csrf_key), csrf_key_file);
|
|
}
|
|
|
|
if (audiences.num_strings == 0) {
|
|
char localhost[MAXHOSTNAMELEN];
|
|
|
|
ret = gethostname(localhost, sizeof(localhost));
|
|
if (ret == -1)
|
|
errx(1, "Could not determine local hostname; use --audience");
|
|
|
|
if ((audiences.strings =
|
|
calloc(1, sizeof(audiences.strings[0]))) == NULL ||
|
|
(audiences.strings[0] = strdup(localhost)) == NULL)
|
|
err(1, "Out of memory");
|
|
audiences.num_strings = 1;
|
|
}
|
|
|
|
if (daemonize && daemon_child_fd == -1)
|
|
daemon_child_fd = roken_detach_prep(argc, argv, "--daemon-child");
|
|
daemonize = 0;
|
|
|
|
argc -= optidx;
|
|
argv += optidx;
|
|
if (argc != 0)
|
|
usage(1);
|
|
|
|
if (cache_dir == NULL) {
|
|
char *s = NULL;
|
|
|
|
if (asprintf(&s, "%s/bx509d-XXXXXX",
|
|
getenv("TMPDIR") ? getenv("TMPDIR") : "/tmp") == -1 ||
|
|
s == NULL ||
|
|
(cache_dir = mkdtemp(s)) == NULL)
|
|
err(1, "could not create temporary cache directory");
|
|
if (verbose_counter)
|
|
fprintf(stderr, "Note: using %s as cache directory\n", cache_dir);
|
|
atexit(rm_cache_dir);
|
|
setenv("TMPDIR", cache_dir, 1);
|
|
}
|
|
|
|
generate_key(context->hx509ctx, "impersonation", "rsa", 2048, &impersonation_key_fn);
|
|
|
|
again:
|
|
if (cert_file && !priv_key_file)
|
|
priv_key_file = cert_file;
|
|
|
|
if (cert_file) {
|
|
hx509_cursor cursor = NULL;
|
|
hx509_certs certs = NULL;
|
|
hx509_cert cert = NULL;
|
|
time_t min_cert_life = 0;
|
|
size_t len;
|
|
void *s;
|
|
|
|
ret = hx509_certs_init(context->hx509ctx, cert_file, 0, NULL, &certs);
|
|
if (ret == 0)
|
|
ret = hx509_certs_start_seq(context->hx509ctx, certs, &cursor);
|
|
while (ret == 0 &&
|
|
(ret = hx509_certs_next_cert(context->hx509ctx, certs,
|
|
cursor, &cert)) == 0 && cert) {
|
|
time_t notAfter = 0;
|
|
|
|
if (!hx509_cert_have_private_key_only(cert) &&
|
|
(notAfter = hx509_cert_get_notAfter(cert)) <= time(NULL) + 30)
|
|
errx(1, "One or more certificates in %s are expired",
|
|
cert_file);
|
|
if (notAfter) {
|
|
notAfter -= time(NULL);
|
|
if (notAfter < 600)
|
|
warnx("One or more certificates in %s expire soon",
|
|
cert_file);
|
|
/* Reload 5 minutes prior to expiration */
|
|
if (notAfter < min_cert_life || min_cert_life < 1)
|
|
min_cert_life = notAfter;
|
|
}
|
|
hx509_cert_free(cert);
|
|
}
|
|
if (certs)
|
|
(void) hx509_certs_end_seq(context->hx509ctx, certs, cursor);
|
|
if (min_cert_life > 4)
|
|
alarm(min_cert_life >> 1);
|
|
hx509_certs_free(&certs);
|
|
if (ret)
|
|
hx509_err(context->hx509ctx, 1, ret,
|
|
"could not read certificate from %s", cert_file);
|
|
|
|
if ((errno = rk_undumpdata(cert_file, &s, &len)) ||
|
|
(cert_pem = strndup(s, len)) == NULL)
|
|
err(1, "could not read certificate from %s", cert_file);
|
|
if (strlen(cert_pem) != len)
|
|
err(1, "NULs in certificate file contents: %s", cert_file);
|
|
free(s);
|
|
}
|
|
|
|
if (priv_key_file) {
|
|
size_t len;
|
|
void *s;
|
|
|
|
if ((errno = rk_undumpdata(priv_key_file, &s, &len)) ||
|
|
(priv_key_pem = strndup(s, len)) == NULL)
|
|
err(1, "could not read private key from %s", priv_key_file);
|
|
if (strlen(priv_key_pem) != len)
|
|
err(1, "NULs in private key file contents: %s", priv_key_file);
|
|
free(s);
|
|
}
|
|
|
|
if (verbose_counter > 1)
|
|
flags |= MHD_USE_DEBUG;
|
|
if (thread_per_client_flag)
|
|
flags |= MHD_USE_THREAD_PER_CONNECTION;
|
|
|
|
|
|
if (pipe(sigpipe) == -1)
|
|
err(1, "Could not set up key/cert reloading");
|
|
memset(&sa, 0, sizeof(sa));
|
|
sa.sa_handler = sighandler;
|
|
if (reverse_proxied_flag) {
|
|
/*
|
|
* We won't use TLS in the reverse proxy case, so no need to reload
|
|
* certs. But we'll still read them if given, and alarm() will get
|
|
* called.
|
|
*/
|
|
(void) signal(SIGHUP, SIG_IGN);
|
|
(void) signal(SIGUSR1, SIG_IGN);
|
|
(void) signal(SIGALRM, SIG_IGN);
|
|
} else {
|
|
(void) sigaction(SIGHUP, &sa, NULL); /* Reload key & cert */
|
|
(void) sigaction(SIGUSR1, &sa, NULL); /* Reload key & cert */
|
|
(void) sigaction(SIGALRM, &sa, NULL); /* Reload key & cert */
|
|
}
|
|
(void) sigaction(SIGINT, &sa, NULL); /* Graceful shutdown */
|
|
(void) sigaction(SIGTERM, &sa, NULL); /* Graceful shutdown */
|
|
(void) signal(SIGPIPE, SIG_IGN);
|
|
|
|
if (previous)
|
|
sock = MHD_quiesce_daemon(previous);
|
|
|
|
if (reverse_proxied_flag) {
|
|
/*
|
|
* XXX IPv6 too. Create the sockets and tell MHD_start_daemon() about
|
|
* them.
|
|
*/
|
|
sin.sin_addr.s_addr = htonl(INADDR_LOOPBACK);
|
|
sin.sin_family = AF_INET;
|
|
sin.sin_port = htons(port);
|
|
current = MHD_start_daemon(flags, port,
|
|
/*
|
|
* This is a connection access callback. We
|
|
* don't use it.
|
|
*/
|
|
NULL, NULL,
|
|
/* This is our request handler */
|
|
route, (char *)NULL,
|
|
MHD_OPTION_SOCK_ADDR, &sin,
|
|
MHD_OPTION_CONNECTION_LIMIT, (unsigned int)200,
|
|
MHD_OPTION_CONNECTION_TIMEOUT, (unsigned int)10,
|
|
/* This is our request cleanup handler */
|
|
MHD_OPTION_NOTIFY_COMPLETED, cleanup_req, NULL,
|
|
MHD_OPTION_END);
|
|
} else if (sock != MHD_INVALID_SOCKET) {
|
|
/*
|
|
* Restart following a possible certificate/key rollover, reusing the
|
|
* listen socket returned by MHD_quiesce_daemon().
|
|
*/
|
|
current = MHD_start_daemon(flags | MHD_USE_SSL, port,
|
|
NULL, NULL,
|
|
route, (char *)NULL,
|
|
MHD_OPTION_HTTPS_MEM_KEY, priv_key_pem,
|
|
MHD_OPTION_HTTPS_MEM_CERT, cert_pem,
|
|
MHD_OPTION_CONNECTION_LIMIT, (unsigned int)200,
|
|
MHD_OPTION_CONNECTION_TIMEOUT, (unsigned int)10,
|
|
MHD_OPTION_NOTIFY_COMPLETED, cleanup_req, NULL,
|
|
MHD_OPTION_LISTEN_SOCKET, sock,
|
|
MHD_OPTION_END);
|
|
sock = MHD_INVALID_SOCKET;
|
|
} else {
|
|
/*
|
|
* Initial MHD_start_daemon(), with TLS.
|
|
*
|
|
* Subsequently we'll restart reusing the listen socket this creates.
|
|
* See above.
|
|
*/
|
|
current = MHD_start_daemon(flags | MHD_USE_SSL, port,
|
|
NULL, NULL,
|
|
route, (char *)NULL,
|
|
MHD_OPTION_HTTPS_MEM_KEY, priv_key_pem,
|
|
MHD_OPTION_HTTPS_MEM_CERT, cert_pem,
|
|
MHD_OPTION_CONNECTION_LIMIT, (unsigned int)200,
|
|
MHD_OPTION_CONNECTION_TIMEOUT, (unsigned int)10,
|
|
MHD_OPTION_NOTIFY_COMPLETED, cleanup_req, NULL,
|
|
MHD_OPTION_END);
|
|
}
|
|
if (current == NULL)
|
|
err(1, "Could not start bx509 REST service");
|
|
|
|
if (previous) {
|
|
MHD_stop_daemon(previous);
|
|
previous = NULL;
|
|
}
|
|
|
|
if (verbose_counter)
|
|
fprintf(stderr, "Ready!\n");
|
|
if (daemon_child_fd != -1)
|
|
roken_detach_finish(NULL, daemon_child_fd);
|
|
|
|
/* Wait for signal, possibly SIGALRM, to reload certs and/or exit */
|
|
while ((ret = read(sigpipe[0], &sig, sizeof(sig))) == -1 &&
|
|
errno == EINTR)
|
|
;
|
|
|
|
free(priv_key_pem);
|
|
free(cert_pem);
|
|
priv_key_pem = NULL;
|
|
cert_pem = NULL;
|
|
|
|
if (ret == 1 && (sig == SIGHUP || sig == SIGUSR1 || sig == SIGALRM)) {
|
|
/* Reload certs and restart service gracefully */
|
|
previous = current;
|
|
current = NULL;
|
|
goto again;
|
|
}
|
|
|
|
MHD_stop_daemon(current);
|
|
_krb5_unload_plugins(context, "kdc");
|
|
pthread_key_delete(k5ctx);
|
|
return 0;
|
|
}
|