httpkadmind: Make more like bx509d internally

- Correct handling of POST (before POSTs with non-zero-length bodies
   would cause the server to close the connection).

 - Add CSRF features from bx509d.
This commit is contained in:
Nicolas Williams
2022-08-22 22:38:03 -05:00
parent ae527bf97c
commit 56c6120522
3 changed files with 503 additions and 97 deletions

View File

@@ -43,6 +43,10 @@
.Op Fl Fl daemon-child .Op Fl Fl daemon-child
.Op Fl Fl reverse-proxied .Op Fl Fl reverse-proxied
.Op Fl p Ar port number (default: 443) .Op Fl p Ar port number (default: 443)
.Op Fl Fl allow-GET
.Op Fl Fl no-allow-GET
.Op Fl Fl GET-with-csrf-token
.Op Fl Fl csrf-header= Ns Ar HEADER-NAME
.Op Fl Fl temp-dir= Ns Ar DIRECTORY .Op Fl Fl temp-dir= Ns Ar DIRECTORY
.Op Fl Fl cert=HX509-STORE .Op Fl Fl cert=HX509-STORE
.Op Fl Fl private-key=HX509-STORE .Op Fl Fl private-key=HX509-STORE
@@ -64,7 +68,7 @@
.Xc .Xc
.Oc .Oc
.Sh DESCRIPTION .Sh DESCRIPTION
Serves the following resources: Serves the following resources over HTTP:
.Ar /get-keys and .Ar /get-keys and
.Ar /get-config . .Ar /get-config .
.Pp .Pp
@@ -75,6 +79,12 @@ end-point allows callers to get a principal's keys in
format for named principals, possibly performing write operations format for named principals, possibly performing write operations
such as creating a non-existent principal, or rotating its keys, such as creating a non-existent principal, or rotating its keys,
if requested. if requested.
Note that this end-point can cause KDC HDB principal entries to
be modified or created incidental to fetching the principal's
keys.
The use of the HTTP POST method is required when this end-point
writes to the KDC's HDB.
See below.
.Pp .Pp
The The
.Ar /get-config .Ar /get-config
@@ -98,7 +108,87 @@ end-point accepts a single query parameter:
.Bl -tag -width Ds -offset indent .Bl -tag -width Ds -offset indent
.It Ar princ=PRINCIPAL . .It Ar princ=PRINCIPAL .
.El .El
.Sh HTTP APIS
All HTTP APIs served by this program accept POSTs, with all
request parameters given as either URI query parameters, and/or
as form data in the POST request body, in either
.Ar application/x-www-form-urlencoded
or
.Ar multipart/formdata .
If GETs are enabled, then request parameters must be supplied as
URI query parameters.
.Pp .Pp
Note that requests that cause changes to the HDB must always be
done via POST, never GET.
.Pp
URI query parameters must be of the form
.Ar param0=value&param1=value...
Some parameters can be given multiple values -- see the
descriptions of the end-points.
.Sh CROSS-SITE REQUEST FORGERY PROTECTION
.Em None
of the resources service by this service are intended to be
executed by web pages.
.Pp
Most of the resources provided by this service are
.Dq safe
in the sense that they do not change server-side state besides
logging, and in that they are idempotent, but they are
only safe to execute
.Em if and only if
the requesting party is trusted to see the response.
Since none of these resources are intended to be used from web
pages, it is important that web pages not be able to execute them
.Em and
observe the responses.
.Pp
Some of the resources provided by this service do change
server-side state, specifically principal entries in the KDC's
HDB.
Those always require the use of POST, not GET.
.Pp
In a web browser context, pages from other origins will be able
to attempt requests to this service, but should never be able to
see the responses because browsers normally wouldn't allow that.
Nonetheless, anti cross site request forgery (CSRF) protection
may be desirable.
.Pp
This service provides the following CSRF protection features:
.Bl -tag -width Ds -offset indent
.It requests are rejected if they have a
.Dq Referer
(except the experimental /get-negotiate-token end-point)
.It the service can be configured to require a header that would make the
request not Dq simple
.It GETs can be disabled (see options), thus requiring POSTs
.It GETs can be required to have a CSRF token (see below)
.It POSTs can be required to have a CSRF token
.El
.Pp
The experimental
.Ar /get-negotiate-token
end-point, however, always accepts
.Dq Referer
requests.
.Pp
To obtain a CSRF token, first execute the request without the
CSRF token, and the resulting error
response will include a
.Ar X-CSRF-Token
response header.
.Pp
To execute a request with a CSRF token, first obtain a CSRF token
as described above, then copy the token to the request as the
value of the request's
.Ar X-CSRF-Token
header.
.Pp
The key for keying the CSRF token HMAC is that of the first
current key for the
.Sq WELLKNOWN/CSRFTOKEN
principal for the realm being used.
Every realm served by this service must have this principal.
.Sh GETTING KEYTABS
The The
.Ar /get-keys .Ar /get-keys
end-point accepts various parameters: end-point accepts various parameters:
@@ -243,6 +333,51 @@ Serves HTTP instead of HTTPS, accepting only looped-back connections.
.Xc .Xc
PORT PORT
.It Xo .It Xo
.Fl Fl allow-GET
.Xc
If given, then HTTP GET will be allowed for the various end-points
other than
.Ar /health .
Otherwise only HEAD and POST will be allowed.
By default GETs are allowed, but this will change soon.
.It Xo
.Fl Fl no-allow-GET
.Xc
If given then HTTP GETs will be rejected for the various
end-points other than
.Ar /health .
.It Xo
.Fl Fl csrf-protection-type= Ns Ar CSRF-PROTECTION-TYPE
.Xc
Possible values of
.Ar CSRF-PROTECTION-TYPE
are
.Bl -bullet -compact -offset indent
.It
.Li GET-with-header
.It
.Li GET-with-token
.It
.Li POST-with-header
.It
.Li POST-with-token
.El
This may be given multiple times.
The default is to require CSRF tokens for POST requests, and to
require neither a non-simple header nor a CSRF token for GET
requests.
.Pp
See
.Sx CROSS-SITE REQUEST FORGERY PROTECTION .
.It Xo
.Fl Fl csrf-header= Ns Ar HEADER-NAME
.Xc
If given, then all requests other than to the
.Ar /health
service must have the given request
.Ar HEADER-NAME
set (the value is irrelevant).
.It Xo
.Fl Fl temp-dir= Ns Ar DIRECTORY .Fl Fl temp-dir= Ns Ar DIRECTORY
.Xc .Xc
Directory for temp files. Directory for temp files.

View File

@@ -128,6 +128,12 @@ typedef enum MHD_Result heim_mhd_result;
* here. * here.
*/ */
struct free_tend_list {
void *freeme1;
void *freeme2;
struct free_tend_list *next;
};
/* Our request description structure */ /* Our request description structure */
typedef struct kadmin_request_desc { typedef struct kadmin_request_desc {
HEIM_SVC_REQUEST_DESC_COMMON_ELEMENTS; HEIM_SVC_REQUEST_DESC_COMMON_ELEMENTS;
@@ -165,6 +171,8 @@ typedef struct kadmin_request_desc {
* be damned. * be damned.
*/ */
hx509_request req; /* For authz only */ hx509_request req; /* For authz only */
struct free_tend_list *free_list;
struct MHD_PostProcessor *pp;
heim_array_t service_names; heim_array_t service_names;
heim_array_t hostnames; heim_array_t hostnames;
heim_array_t spns; heim_array_t spns;
@@ -176,8 +184,11 @@ typedef struct kadmin_request_desc {
char *keytab_name; char *keytab_name;
char *freeme1; char *freeme1;
char *enctypes; char *enctypes;
char *cache_control;
char *csrf_token;
const char *method; const char *method;
krb5_timestamp pw_end; krb5_timestamp pw_end;
size_t post_data_size;
unsigned int response_set:1; unsigned int response_set:1;
unsigned int materialize:1; unsigned int materialize:1;
unsigned int rotate_now:1; unsigned int rotate_now:1;
@@ -306,8 +317,18 @@ get_krb5_context(krb5_context *contextp)
return ret; return ret;
} }
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 port = -1;
static int help_flag; static int help_flag;
static int allow_GET_flag = -1;
static int daemonize; static int daemonize;
static int daemon_child_fd = -1; static int daemon_child_fd = -1;
static int local_hdb; static int local_hdb;
@@ -318,6 +339,8 @@ static int version_flag;
static int reverse_proxied_flag; static int reverse_proxied_flag;
static int thread_per_client_flag; static int thread_per_client_flag;
struct getarg_strings audiences; 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 *cert_file;
static const char *priv_key_file; static const char *priv_key_file;
static const char *cache_dir; static const char *cache_dir;
@@ -426,7 +449,7 @@ out:
static krb5_error_code resp(kadmin_request_desc, int, krb5_error_code, static krb5_error_code resp(kadmin_request_desc, int, krb5_error_code,
enum MHD_ResponseMemoryMode, const char *, enum MHD_ResponseMemoryMode, const char *,
const void *, size_t, const char *, const char *); const void *, size_t, const char *);
static krb5_error_code bad_req(kadmin_request_desc, krb5_error_code, int, static krb5_error_code bad_req(kadmin_request_desc, krb5_error_code, int,
const char *, ...) const char *, ...)
HEIMDAL_PRINTF_ATTRIBUTE((__printf__, 4, 5)); HEIMDAL_PRINTF_ATTRIBUTE((__printf__, 4, 5));
@@ -435,7 +458,7 @@ static krb5_error_code bad_enomem(kadmin_request_desc, krb5_error_code);
static krb5_error_code bad_400(kadmin_request_desc, krb5_error_code, const char *); static krb5_error_code bad_400(kadmin_request_desc, krb5_error_code, const char *);
static krb5_error_code bad_401(kadmin_request_desc, const char *); static krb5_error_code bad_401(kadmin_request_desc, const char *);
static krb5_error_code bad_403(kadmin_request_desc, krb5_error_code, const char *); static krb5_error_code bad_403(kadmin_request_desc, krb5_error_code, const char *);
static krb5_error_code bad_404(kadmin_request_desc, const char *); static krb5_error_code bad_404(kadmin_request_desc, krb5_error_code, const char *);
static krb5_error_code bad_405(kadmin_request_desc, const char *); static krb5_error_code bad_405(kadmin_request_desc, const char *);
/*static krb5_error_code bad_500(kadmin_request_desc, krb5_error_code, const char *);*/ /*static krb5_error_code bad_500(kadmin_request_desc, krb5_error_code, const char *);*/
static krb5_error_code bad_503(kadmin_request_desc, krb5_error_code, const char *); static krb5_error_code bad_503(kadmin_request_desc, krb5_error_code, const char *);
@@ -636,8 +659,7 @@ resp(kadmin_request_desc r,
const char *content_type, const char *content_type,
const void *body, const void *body,
size_t bodylen, size_t bodylen,
const char *token, const char *token)
const char *csrf)
{ {
struct MHD_Response *response; struct MHD_Response *response;
int mret = MHD_YES; int mret = MHD_YES;
@@ -660,16 +682,15 @@ resp(kadmin_request_desc r,
return -1; return -1;
mret = MHD_add_response_header(response, MHD_HTTP_HEADER_AGE, "0"); mret = MHD_add_response_header(response, MHD_HTTP_HEADER_AGE, "0");
if (mret == MHD_YES && http_status_code == MHD_HTTP_OK) { if (mret == MHD_YES && http_status_code == MHD_HTTP_OK) {
static HEIMDAL_THREAD_LOCAL char *cache_control = NULL;
krb5_timestamp now; krb5_timestamp now;
free(cache_control); free(r->cache_control);
cache_control = NULL; r->cache_control = NULL;
krb5_timeofday(r->context, &now); krb5_timeofday(r->context, &now);
if (r->pw_end && r->pw_end > now) { if (r->pw_end && r->pw_end > now) {
if (asprintf(&cache_control, "no-store, max-age=%lld", if (asprintf(&r->cache_control, "no-store, max-age=%lld",
(long long)r->pw_end - now) == -1 || (long long)r->pw_end - now) == -1 ||
cache_control == NULL) r->cache_control == NULL)
/* Soft handling of ENOMEM here */ /* Soft handling of ENOMEM here */
mret = MHD_add_response_header(response, mret = MHD_add_response_header(response,
MHD_HTTP_HEADER_CACHE_CONTROL, MHD_HTTP_HEADER_CACHE_CONTROL,
@@ -677,7 +698,7 @@ resp(kadmin_request_desc r,
else else
mret = MHD_add_response_header(response, mret = MHD_add_response_header(response,
MHD_HTTP_HEADER_CACHE_CONTROL, MHD_HTTP_HEADER_CACHE_CONTROL,
cache_control); r->cache_control);
} else } else
mret = MHD_add_response_header(response, mret = MHD_add_response_header(response,
@@ -709,10 +730,10 @@ resp(kadmin_request_desc r,
http_status_code = MHD_HTTP_SERVICE_UNAVAILABLE; http_status_code = MHD_HTTP_SERVICE_UNAVAILABLE;
} }
if (mret == MHD_YES && csrf) if (mret == MHD_YES && r->csrf_token)
mret = MHD_add_response_header(response, mret = MHD_add_response_header(response,
"X-CSRF-Token", "X-CSRF-Token",
csrf); r->csrf_token);
if (mret == MHD_YES && content_type) { if (mret == MHD_YES && content_type) {
mret = MHD_add_response_header(response, mret = MHD_add_response_header(response,
@@ -751,7 +772,7 @@ bad_reqv(kadmin_request_desc r,
if (context) if (context)
krb5_log_msg(context, logfac, 1, NULL, "Out of memory"); krb5_log_msg(context, logfac, 1, NULL, "Out of memory");
return resp(r, http_status_code, code, MHD_RESPMEM_PERSISTENT, return resp(r, http_status_code, code, MHD_RESPMEM_PERSISTENT,
NULL, fmt, BODYLEN_IS_STRLEN, NULL, NULL); NULL, fmt, BODYLEN_IS_STRLEN, NULL);
} }
if (code) { if (code) {
@@ -778,11 +799,11 @@ bad_reqv(kadmin_request_desc r,
krb5_log_msg(context, logfac, 1, NULL, "Out of memory"); krb5_log_msg(context, logfac, 1, NULL, "Out of memory");
return resp(r, MHD_HTTP_SERVICE_UNAVAILABLE, ENOMEM, return resp(r, MHD_HTTP_SERVICE_UNAVAILABLE, ENOMEM,
MHD_RESPMEM_PERSISTENT, NULL, MHD_RESPMEM_PERSISTENT, NULL,
"Out of memory", BODYLEN_IS_STRLEN, NULL, NULL); "Out of memory", BODYLEN_IS_STRLEN, NULL);
} }
ret = resp(r, http_status_code, code, MHD_RESPMEM_MUST_COPY, ret = resp(r, http_status_code, code, MHD_RESPMEM_MUST_COPY,
NULL, msg, BODYLEN_IS_STRLEN, NULL, NULL); NULL, msg, BODYLEN_IS_STRLEN, NULL);
free(formatted); free(formatted);
free(msg); free(msg);
return ret == -1 ? -1 : code; return ret == -1 ? -1 : code;
@@ -830,9 +851,9 @@ bad_403(kadmin_request_desc r, krb5_error_code ret, const char *reason)
} }
static krb5_error_code static krb5_error_code
bad_404(kadmin_request_desc r, const char *name) bad_404(kadmin_request_desc r, krb5_error_code ret, const char *name)
{ {
return bad_req(r, ENOENT, MHD_HTTP_NOT_FOUND, return bad_req(r, ret, MHD_HTTP_NOT_FOUND,
"Resource not found: %s", name); "Resource not found: %s", name);
} }
@@ -843,6 +864,13 @@ bad_405(kadmin_request_desc r, const char *method)
"Method not supported: %s", method); "Method not supported: %s", method);
} }
static krb5_error_code
bad_413(kadmin_request_desc r)
{
return bad_req(r, E2BIG, MHD_HTTP_METHOD_NOT_ALLOWED,
"POST request body too large");
}
static krb5_error_code static krb5_error_code
bad_method_want_POST(kadmin_request_desc r) bad_method_want_POST(kadmin_request_desc r)
{ {
@@ -888,7 +916,7 @@ good_ext_keytab(kadmin_request_desc r)
return bad_503(r, ret, "Could not recover keytab from temp file"); return bad_503(r, ret, "Could not recover keytab from temp file");
ret = resp(r, MHD_HTTP_OK, 0, MHD_RESPMEM_MUST_COPY, ret = resp(r, MHD_HTTP_OK, 0, MHD_RESPMEM_MUST_COPY,
"application/octet-stream", body, bodylen, NULL, NULL); "application/octet-stream", body, bodylen, NULL);
free(body); free(body);
return ret; return ret;
} }
@@ -1543,7 +1571,7 @@ static krb5_error_code check_csrf(kadmin_request_desc);
* When this returns a response will have been set. * When this returns a response will have been set.
*/ */
static krb5_error_code static krb5_error_code
get_keysN(kadmin_request_desc r, const char *method) get_keysN(kadmin_request_desc r)
{ {
krb5_error_code ret; krb5_error_code ret;
size_t nhosts; size_t nhosts;
@@ -1556,11 +1584,17 @@ get_keysN(kadmin_request_desc r, const char *method)
if (ret) if (ret)
return ret; /* authorize_req() calls bad_req() on error */ return ret; /* authorize_req() calls bad_req() on error */
/*
* If we have a r->kadm_handle already it's because we validated a CSRF
* token. It may not be a handle to a realm we wanted though.
*/
if (r->kadm_handle)
kadm5_destroy(r->kadm_handle);
r->kadm_handle = NULL;
ret = get_kadm_handle(r->context, r->realm ? r->realm : realm, ret = get_kadm_handle(r->context, r->realm ? r->realm : realm,
0 /* want_write */, &r->kadm_handle); 0 /* want_write */, &r->kadm_handle);
if (ret)
if (strcmp(method, "POST") == 0 && (ret = check_csrf(r))) return bad_404(r, ret, "Could not connect to realm");
return ret; /* check_csrf() calls bad_req() on error */
nhosts = heim_array_get_length(r->hostnames); nhosts = heim_array_get_length(r->hostnames);
nsvcs = heim_array_get_length(r->service_names); nsvcs = heim_array_get_length(r->service_names);
@@ -1631,7 +1665,7 @@ get_keysN(kadmin_request_desc r, const char *method)
krb5_log_msg(r->context, logfac, 1, NULL, krb5_log_msg(r->context, logfac, 1, NULL,
"Redirect %s to primary server", r->cname); "Redirect %s to primary server", r->cname);
return resp(r, MHD_HTTP_TEMPORARY_REDIRECT, KADM5_READ_ONLY, return resp(r, MHD_HTTP_TEMPORARY_REDIRECT, KADM5_READ_ONLY,
MHD_RESPMEM_PERSISTENT, NULL, "", 0, NULL, NULL); MHD_RESPMEM_PERSISTENT, NULL, "", 0, NULL);
} else { } else {
krb5_log_msg(r->context, logfac, 1, NULL, "HDB is read-only"); krb5_log_msg(r->context, logfac, 1, NULL, "HDB is read-only");
return bad_403(r, ret, "HDB is read-only"); return bad_403(r, ret, "HDB is read-only");
@@ -1664,25 +1698,33 @@ addr_to_string(krb5_context context,
snprintf(str, len, "<family=%d>", addr->sa_family); snprintf(str, len, "<family=%d>", addr->sa_family);
} }
static void clean_req_desc(kadmin_request_desc);
static krb5_error_code static krb5_error_code
set_req_desc(struct MHD_Connection *connection, set_req_desc(struct MHD_Connection *connection,
const char *method, const char *method,
const char *url, const char *url,
kadmin_request_desc r) kadmin_request_desc *rp)
{ {
const union MHD_ConnectionInfo *ci; const union MHD_ConnectionInfo *ci;
kadmin_request_desc r;
const char *token; const char *token;
krb5_error_code ret; krb5_error_code ret;
memset(r, 0, sizeof(*r)); *rp = NULL;
(void) gettimeofday(&r->tv_start, NULL); if ((r = calloc(1, sizeof(*r))) == NULL)
return ENOMEM;
if ((ret = get_krb5_context(&r->context))) (void) gettimeofday(&r->tv_start, NULL);
if ((ret = get_krb5_context(&r->context))) {
free(r);
return ret; return ret;
}
/* HEIM_SVC_REQUEST_DESC_COMMON_ELEMENTS fields */ /* HEIM_SVC_REQUEST_DESC_COMMON_ELEMENTS fields */
r->request.data = "<HTTP-REQUEST>"; r->request.data = "<HTTP-REQUEST>";
r->request.length = sizeof("<HTTP-REQUEST>"); r->request.length = sizeof("<HTTP-REQUEST>");
r->from = r->frombuf; r->from = r->frombuf;
r->free_list = NULL;
r->config = NULL; r->config = NULL;
r->logf = logfac; r->logf = logfac;
r->reqtype = url; r->reqtype = url;
@@ -1692,6 +1734,7 @@ set_req_desc(struct MHD_Connection *connection,
r->cname = NULL; r->cname = NULL;
r->addr = NULL; r->addr = NULL;
r->kv = heim_dict_create(10); r->kv = heim_dict_create(10);
r->pp = NULL;
r->attributes = heim_dict_create(1); r->attributes = heim_dict_create(1);
/* Our fields */ /* Our fields */
r->connection = connection; r->connection = connection;
@@ -1702,6 +1745,7 @@ set_req_desc(struct MHD_Connection *connection,
r->spns = heim_array_create(); r->spns = heim_array_create();
r->keytab_name = NULL; r->keytab_name = NULL;
r->enctypes = NULL; r->enctypes = NULL;
r->cache_control = NULL;
r->freeme1 = NULL; r->freeme1 = NULL;
r->method = method; r->method = method;
r->cprinc = NULL; r->cprinc = NULL;
@@ -1736,6 +1780,10 @@ set_req_desc(struct MHD_Connection *connection,
krb5_log_msg(r->context, logfac, 1, NULL, "Out of memory"); krb5_log_msg(r->context, logfac, 1, NULL, "Out of memory");
ret = r->error_code = ENOMEM; ret = r->error_code = ENOMEM;
} }
if (ret == 0)
*rp = r;
else
clean_req_desc(r);
return ret; return ret;
} }
@@ -1751,32 +1799,49 @@ clean_req_desc(kadmin_request_desc r)
(void) unlink(strchr(r->keytab_name, ':') + 1); (void) unlink(strchr(r->keytab_name, ':') + 1);
if (r->kadm_handle) if (r->kadm_handle)
kadm5_destroy(r->kadm_handle); kadm5_destroy(r->kadm_handle);
if (r->pp)
MHD_destroy_post_processor(r->pp);
hx509_request_free(&r->req); hx509_request_free(&r->req);
heim_release(r->service_names); heim_release(r->service_names);
heim_release(r->attributes);
heim_release(r->hostnames); heim_release(r->hostnames);
heim_release(r->reason); heim_release(r->reason);
heim_release(r->spns); heim_release(r->spns);
heim_release(r->kv); heim_release(r->kv);
krb5_free_principal(r->context, r->cprinc); krb5_free_principal(r->context, r->cprinc);
free(r->cache_control);
free(r->keytab_name); free(r->keytab_name);
free(r->csrf_token);
free(r->enctypes); free(r->enctypes);
free(r->freeme1); free(r->freeme1);
free(r->cname); free(r->cname);
free(r->sname); free(r->sname);
free(r->realm);
free(r);
}
static void
cleanup_req(void *cls,
struct MHD_Connection *connection,
void **con_cls,
enum MHD_RequestTerminationCode toe)
{
kadmin_request_desc r = *con_cls;
(void)cls;
(void)connection;
(void)toe;
clean_req_desc(r);
*con_cls = NULL;
} }
/* Implements GETs of /get-keys */ /* Implements GETs of /get-keys */
static krb5_error_code static krb5_error_code
get_keys(kadmin_request_desc r, const char *method) get_keys(kadmin_request_desc r)
{ {
krb5_error_code ret;
if ((ret = validate_token(r)))
return ret; /* validate_token() calls bad_req() */
if (r->cname == NULL || r->cprinc == NULL) if (r->cname == NULL || r->cprinc == NULL)
return bad_403(r, EINVAL, return bad_401(r, "Could not extract principal name from token");
"Could not extract principal name from token"); return get_keysN(r); /* Sets an HTTP response */
return get_keysN(r, method); /* Sets an HTTP response */
} }
/* Implements GETs of /get-config */ /* Implements GETs of /get-config */
@@ -1795,11 +1860,8 @@ get_config(kadmin_request_desc r)
void *body = "include /etc/krb5.conf\n"; void *body = "include /etc/krb5.conf\n";
int freeit = 0; int freeit = 0;
if ((ret = validate_token(r)))
return ret; /* validate_token() calls bad_req() */
if (r->cname == NULL || r->cprinc == NULL) if (r->cname == NULL || r->cprinc == NULL)
return bad_403(r, EINVAL, return bad_401(r, "Could not extract principal name from token");
"Could not extract principal name from token");
/* /*
* No authorization needed -- configs are public. Though we do require * No authorization needed -- configs are public. Though we do require
* authentication (above). * authentication (above).
@@ -1832,7 +1894,7 @@ get_config(kadmin_request_desc r)
} }
} else { } else {
r->error_code = ret; r->error_code = ret;
return bad_404(r, "/get-config"); return bad_404(r, ret, "/get-config");
} }
} }
@@ -1840,7 +1902,7 @@ get_config(kadmin_request_desc r)
krb5_log_msg(r->context, logfac, 1, NULL, krb5_log_msg(r->context, logfac, 1, NULL,
"Returned krb5.conf contents to %s", r->cname); "Returned krb5.conf contents to %s", r->cname);
ret = resp(r, MHD_HTTP_OK, 0, MHD_RESPMEM_MUST_COPY, ret = resp(r, MHD_HTTP_OK, 0, MHD_RESPMEM_MUST_COPY,
"application/text", body, bodylen, NULL, NULL); "application/text", body, bodylen, NULL);
} else { } else {
ret = bad_503(r, ret, "Could not retrieve principal configuration"); ret = bad_503(r, ret, "Could not retrieve principal configuration");
} }
@@ -1866,7 +1928,9 @@ mac_csrf_token(kadmin_request_desc r, krb5_storage *sp)
memset(&princ, 0, sizeof(princ)); memset(&princ, 0, sizeof(princ));
ret = krb5_storage_to_data(sp, &data); ret = krb5_storage_to_data(sp, &data);
if (r->kadm_handle == NULL) if (r->kadm_handle == NULL)
ret = get_kadm_handle(r->context, r->realm, 0 /* want_write */, ret = get_kadm_handle(r->context,
r->realm ? r->realm : realm,
0 /* want_write */,
&r->kadm_handle); &r->kadm_handle);
if (ret == 0) if (ret == 0)
ret = krb5_make_principal(r->context, &p, ret = krb5_make_principal(r->context, &p,
@@ -1922,7 +1986,6 @@ make_csrf_token(kadmin_request_desc r,
char **token, char **token,
int64_t *age) int64_t *age)
{ {
static HEIMDAL_THREAD_LOCAL char tokenbuf[128]; /* See below, be sad */
krb5_error_code ret = 0; krb5_error_code ret = 0;
unsigned char given_decoded[128]; unsigned char given_decoded[128];
krb5_storage *sp = NULL; krb5_storage *sp = NULL;
@@ -1973,17 +2036,6 @@ make_csrf_token(kadmin_request_desc r,
if (ret == 0 && if (ret == 0 &&
(dlen = rk_base64_encode(data.data, data.length, token)) < 0) (dlen = rk_base64_encode(data.data, data.length, token)) < 0)
ret = errno; ret = errno;
if (ret == 0 && dlen >= sizeof(tokenbuf))
ret = ERANGE;
if (ret == 0) {
/*
* Work around for older versions of libmicrohttpd do not strdup()ing
* response header values.
*/
memcpy(tokenbuf, *token, dlen);
free(*token);
*token = tokenbuf;
}
krb5_storage_free(sp); krb5_storage_free(sp);
krb5_data_free(&data); krb5_data_free(&data);
return ret; return ret;
@@ -2000,11 +2052,28 @@ check_csrf(kadmin_request_desc r)
const char *given; const char *given;
int64_t age; int64_t age;
size_t givenlen, expectedlen; size_t givenlen, expectedlen;
char *expected = NULL;
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, given = MHD_lookup_connection_value(r->connection, MHD_HEADER_KIND,
"X-CSRF-Token"); "X-CSRF-Token");
ret = make_csrf_token(r, given, &expected, &age); ret = make_csrf_token(r, given, &r->csrf_token, &age);
if (ret) if (ret)
return bad_503(r, ret, "Could not create a CSRF token"); return bad_503(r, ret, "Could not create a CSRF token");
/* /*
@@ -2014,15 +2083,14 @@ check_csrf(kadmin_request_desc r)
if (given == NULL) { if (given == NULL) {
(void) resp(r, MHD_HTTP_FORBIDDEN, ENOSYS, MHD_RESPMEM_PERSISTENT, (void) resp(r, MHD_HTTP_FORBIDDEN, ENOSYS, MHD_RESPMEM_PERSISTENT,
NULL, "CSRF token needed; copy the X-CSRF-Token: response " NULL, "CSRF token needed; copy the X-CSRF-Token: response "
"header to your next POST", BODYLEN_IS_STRLEN, NULL, "header to your next POST", BODYLEN_IS_STRLEN, NULL);
expected);
return ENOSYS; return ENOSYS;
} }
/* Validate the CSRF token for this request */ /* Validate the CSRF token for this request */
givenlen = strlen(given); givenlen = strlen(given);
expectedlen = strlen(expected); expectedlen = strlen(r->csrf_token);
if (givenlen != expectedlen || ct_memcmp(given, expected, givenlen)) { if (givenlen != expectedlen || ct_memcmp(given, r->csrf_token, givenlen)) {
(void) bad_403(r, EACCES, "Invalid CSRF token"); (void) bad_403(r, EACCES, "Invalid CSRF token");
return EACCES; return EACCES;
} }
@@ -2038,15 +2106,60 @@ health(const char *method, kadmin_request_desc r)
{ {
if (strcmp(method, "HEAD") == 0) { if (strcmp(method, "HEAD") == 0) {
return resp(r, MHD_HTTP_OK, 0, MHD_RESPMEM_PERSISTENT, NULL, "", 0, return resp(r, MHD_HTTP_OK, 0, MHD_RESPMEM_PERSISTENT, NULL, "", 0,
NULL, NULL); NULL);
} }
return resp(r, MHD_HTTP_OK, 0, MHD_RESPMEM_PERSISTENT, NULL, return resp(r, MHD_HTTP_OK, 0, MHD_RESPMEM_PERSISTENT, NULL,
"To determine the health of the service, use the /get-config " "To determine the health of the service, use the /get-config "
"end-point.\n", BODYLEN_IS_STRLEN, NULL, NULL); "end-point.\n", BODYLEN_IS_STRLEN, NULL);
} }
/* Implements the entirety of this REST service */ 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)
{
kadmin_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;
(void)transfer_encoding;
(void)off; /* Offset in POST data */
/* We're going to MHD_set_connection_value(), but we need copies */
if (ftl == NULL || keydup == NULL || valdup == NULL) {
free(ftl);
free(keydup);
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 kadmin_request_desc *);
struct route {
const char *local_part;
handler h;
} routes[] = {
{ "/get-keys", get_keys },
{ "/get-config", get_config },
};
static heim_mhd_result static heim_mhd_result
route(void *cls, route(void *cls,
struct MHD_Connection *connection, struct MHD_Connection *connection,
@@ -2057,50 +2170,131 @@ route(void *cls,
size_t *upload_data_size, size_t *upload_data_size,
void **ctx) void **ctx)
{ {
static int aptr = 0; struct kadmin_request_desc *r = *ctx;
struct kadmin_request_desc r; size_t i;
int ret; int ret;
if (*ctx == NULL) { if (r == NULL) {
/* /*
* This is the first call, right after headers were read. * This is the first call, right after headers were read.
* *
* We must return quickly so that any 100-Continue might be sent with * We must return quickly so that any 100-Continue might be sent with
* celerity. * 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 handled * We'll get called again to really do the processing. If we're
* POSTs then we'd also get called with upload_data != NULL between the * handling a POST then we'll also get called with upload_data != NULL,
* first and last calls. We need to keep no state between the first * possibly multiple times.
* and last calls, but we do need to distinguish first and last call,
* so we use the ctx argument for this.
*/ */
*ctx = &aptr; if ((ret = set_req_desc(connection, method, url, &r))) {
return
bad_503(r, ret, "Could not initialize request state") == -1
? MHD_NO : MHD_YES;
}
*ctx = r;
/*
* All requests other than /health require authentication and CSRF
* protection.
*/
if (strcmp(url, "/health") == 0)
return MHD_YES;
/* Authenticate and do CSRF protection */
ret = validate_token(r);
if (ret == 0)
ret = check_csrf(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 (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; return MHD_YES;
} }
/* /*
* Note that because we attempt to connect to the HDB in set_req_desc(), * Either this is a HEAD, a GET, or a POST whose request body has now been
* this early 503 if we fail to serves to do all of what /health should do. * received completely and processed.
*/ */
if ((ret = set_req_desc(connection, method, url, &r)))
return bad_503(&r, ret, "Could not initialize request state"); /* Allow GET? */
if ((strcmp(method, "HEAD") == 0 || strcmp(method, "GET") == 0) && if (strcmp(method, "GET") == 0 && !allow_GET_flag) {
(strcmp(url, "/health") == 0 || strcmp(url, "/") == 0)) { /* No */
ret = health(method, &r); return bad_405(r, method) == -1 ? MHD_NO : MHD_YES;
} else if (strcmp(method, "GET") != 0 && strcmp(method, "POST") != 0) {
ret = bad_405(&r, method);
} else if (strcmp(url, "/get-keys") == 0) {
ret = get_keys(&r, method);
} else if (strcmp(url, "/get-config") == 0) {
if (strcmp(method, "GET") != 0)
ret = bad_405(&r, method);
else
ret = get_config(&r);
} else {
ret = bad_404(&r, url);
} }
clean_req_desc(&r); for (i = 0; i < sizeof(routes)/sizeof(routes[0]); i++) {
if (strcmp(url, routes[i].local_part) != 0)
continue;
if (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, 0, MHD_RESPMEM_PERSISTENT, NULL, "", 0,
NULL);
else
ret = routes[i].h(r);
return ret == -1 ? MHD_NO : MHD_YES;
}
ret = bad_404(r, ENOENT, url);
return ret == -1 ? MHD_NO : MHD_YES; return ret == -1 ? MHD_NO : MHD_YES;
} }
@@ -2109,6 +2303,10 @@ static struct getargs args[] = {
{ "version", '\0', arg_flag, &version_flag, "Print version", NULL }, { "version", '\0', arg_flag, &version_flag, "Print version", NULL },
{ NULL, 'H', arg_strings, &audiences, { NULL, 'H', arg_strings, &audiences,
"expected token audience(s) of the service", "HOSTNAME" }, "expected token audience(s) of the service", "HOSTNAME" },
{ "allow-GET", 0, arg_negative_flag,
&allow_GET_flag, NULL, NULL },
{ "csrf-header", 0, arg_string, &csrf_header,
"required request header", "HEADER-NAME" },
{ "daemon", 'd', arg_flag, &daemonize, "daemonize", "daemonize" }, { "daemon", 'd', arg_flag, &daemonize, "daemonize", "daemonize" },
{ "daemon-child", 0, arg_flag, &daemon_child_fd, NULL, NULL }, /* priv */ { "daemon-child", 0, arg_flag, &daemon_child_fd, NULL, NULL }, /* priv */
{ "reverse-proxied", 0, arg_flag, &reverse_proxied_flag, { "reverse-proxied", 0, arg_flag, &reverse_proxied_flag,
@@ -2222,6 +2420,67 @@ load_plugins(krb5_context context)
#endif #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 int
main(int argc, char **argv) main(int argc, char **argv)
{ {
@@ -2282,6 +2541,8 @@ main(int argc, char **argv)
if ((errno = get_krb5_context(&context))) if ((errno = get_krb5_context(&context)))
err(1, "Could not init krb5 context (config file issue?)"); err(1, "Could not init krb5 context (config file issue?)");
get_csrf_prot_type(context);
if (!realm) { if (!realm) {
char *s; char *s;
@@ -2295,6 +2556,7 @@ main(int argc, char **argv)
&kadm_handle))) &kadm_handle)))
err(1, "Could not connect to HDB"); err(1, "Could not connect to HDB");
kadm5_destroy(kadm_handle); kadm5_destroy(kadm_handle);
kadm_handle = NULL;
my_openlog(context, "httpkadmind", &logfac); my_openlog(context, "httpkadmind", &logfac);
load_plugins(context); load_plugins(context);
@@ -2419,11 +2681,18 @@ again:
sin.sin_family = AF_INET; sin.sin_family = AF_INET;
sin.sin_port = htons(port); sin.sin_port = htons(port);
current = MHD_start_daemon(flags, port, current = MHD_start_daemon(flags, port,
/*
* This is a connection access callback. We
* don't use it.
*/
NULL, NULL, NULL, NULL,
/* This is our request handler */
route, (char *)NULL, route, (char *)NULL,
MHD_OPTION_SOCK_ADDR, &sin, MHD_OPTION_SOCK_ADDR, &sin,
MHD_OPTION_CONNECTION_LIMIT, (unsigned int)200, MHD_OPTION_CONNECTION_LIMIT, (unsigned int)200,
MHD_OPTION_CONNECTION_TIMEOUT, (unsigned int)10, MHD_OPTION_CONNECTION_TIMEOUT, (unsigned int)10,
/* This is our request cleanup handler */
MHD_OPTION_NOTIFY_COMPLETED, cleanup_req, NULL,
MHD_OPTION_END); MHD_OPTION_END);
} else if (sock != MHD_INVALID_SOCKET) { } else if (sock != MHD_INVALID_SOCKET) {
/* /*
@@ -2438,6 +2707,7 @@ again:
MHD_OPTION_CONNECTION_LIMIT, (unsigned int)200, MHD_OPTION_CONNECTION_LIMIT, (unsigned int)200,
MHD_OPTION_CONNECTION_TIMEOUT, (unsigned int)10, MHD_OPTION_CONNECTION_TIMEOUT, (unsigned int)10,
MHD_OPTION_LISTEN_SOCKET, sock, MHD_OPTION_LISTEN_SOCKET, sock,
MHD_OPTION_NOTIFY_COMPLETED, cleanup_req, NULL,
MHD_OPTION_END); MHD_OPTION_END);
sock = MHD_INVALID_SOCKET; sock = MHD_INVALID_SOCKET;
} else { } else {
@@ -2448,6 +2718,7 @@ again:
MHD_OPTION_HTTPS_MEM_CERT, cert_pem, MHD_OPTION_HTTPS_MEM_CERT, cert_pem,
MHD_OPTION_CONNECTION_LIMIT, (unsigned int)200, MHD_OPTION_CONNECTION_LIMIT, (unsigned int)200,
MHD_OPTION_CONNECTION_TIMEOUT, (unsigned int)10, MHD_OPTION_CONNECTION_TIMEOUT, (unsigned int)10,
MHD_OPTION_NOTIFY_COMPLETED, cleanup_req, NULL,
MHD_OPTION_END); MHD_OPTION_END);
} }
if (current == NULL) if (current == NULL)

View File

@@ -697,7 +697,7 @@ ${hxtool} issue-certificate \
--lifetime=7d \ --lifetime=7d \
--certificate="FILE:pkinit-synthetic.crt" || --certificate="FILE:pkinit-synthetic.crt" ||
{ echo "Failed to make PKINIT client cert"; exit 1; } { echo "Failed to make PKINIT client cert"; exit 1; }
KRB5CCNAME=$admincache ${kadmin} get -s $p >/dev/null && KRB5CCNAME=$admincache ${kadmin} get -s $p >/dev/null 2>&1 &&
{ echo "Internal error -- $p exists too soon"; exit 1; } { echo "Internal error -- $p exists too soon"; exit 1; }
${kinit2} -C "FILE:${objdir}/pkinit-synthetic.crt,${keyfile2}" ${p}@${R} || \ ${kinit2} -C "FILE:${objdir}/pkinit-synthetic.crt,${keyfile2}" ${p}@${R} || \
{ echo "Failed to kinit with PKINIT client cert"; exit 1; } { echo "Failed to kinit with PKINIT client cert"; exit 1; }
@@ -727,7 +727,7 @@ ${hxtool} issue-certificate \
--lifetime=7d \ --lifetime=7d \
--certificate="FILE:pkinit-synthetic.crt" || --certificate="FILE:pkinit-synthetic.crt" ||
{ echo "Failed to make PKINIT client cert"; exit 1; } { echo "Failed to make PKINIT client cert"; exit 1; }
KRB5CCNAME=$admincache ${kadmin} get -s $p >/dev/null && KRB5CCNAME=$admincache ${kadmin} get -s $p >/dev/null 2>&1 &&
{ echo "Internal error -- $p exists too soon"; exit 1; } { echo "Internal error -- $p exists too soon"; exit 1; }
${kinit2} -C "FILE:${objdir}/pkinit-synthetic.crt,${keyfile2}" ${p}@${R} || \ ${kinit2} -C "FILE:${objdir}/pkinit-synthetic.crt,${keyfile2}" ${p}@${R} || \
{ echo "Failed to kinit with PKINIT client cert"; exit 1; } { echo "Failed to kinit with PKINIT client cert"; exit 1; }
@@ -757,7 +757,7 @@ ${hxtool} issue-certificate \
--lifetime=7d \ --lifetime=7d \
--certificate="FILE:pkinit-synthetic.crt" || --certificate="FILE:pkinit-synthetic.crt" ||
{ echo "Failed to make PKINIT client cert"; exit 1; } { echo "Failed to make PKINIT client cert"; exit 1; }
KRB5CCNAME=$admincache ${kadmin} get -s $p >/dev/null && KRB5CCNAME=$admincache ${kadmin} get -s $p >/dev/null 2>&1 &&
{ echo "Internal error -- $p exists too soon"; exit 1; } { echo "Internal error -- $p exists too soon"; exit 1; }
${kinit2} -C "FILE:${objdir}/pkinit-synthetic.crt,${keyfile2}" ${p}@${R} || \ ${kinit2} -C "FILE:${objdir}/pkinit-synthetic.crt,${keyfile2}" ${p}@${R} || \
{ echo "Failed to kinit with PKINIT client cert"; exit 1; } { echo "Failed to kinit with PKINIT client cert"; exit 1; }
@@ -787,7 +787,7 @@ ${hxtool} issue-certificate \
--lifetime=7d \ --lifetime=7d \
--certificate="FILE:pkinit-synthetic.crt" || --certificate="FILE:pkinit-synthetic.crt" ||
{ echo "Failed to make PKINIT client cert"; exit 1; } { echo "Failed to make PKINIT client cert"; exit 1; }
KRB5CCNAME=$admincache ${kadmin} get -s $p >/dev/null && KRB5CCNAME=$admincache ${kadmin} get -s $p >/dev/null 2>&1 &&
{ echo "Internal error -- $p exists too soon"; exit 1; } { echo "Internal error -- $p exists too soon"; exit 1; }
${kinit2} -C "FILE:${objdir}/pkinit-synthetic.crt,${keyfile2}" ${p}@${R} || \ ${kinit2} -C "FILE:${objdir}/pkinit-synthetic.crt,${keyfile2}" ${p}@${R} || \
{ echo "Failed to kinit with PKINIT client cert"; exit 1; } { echo "Failed to kinit with PKINIT client cert"; exit 1; }