bx509d: Implement /get-tgt end-point

This commit is contained in:
Nicolas Williams
2021-04-06 13:55:52 -05:00
parent d72c4af635
commit 6633f6e525
5 changed files with 260 additions and 54 deletions

View File

@@ -194,6 +194,37 @@ header is included in the request, then the response will be a
redirect to that URI with the Negotiate token in an redirect to that URI with the Negotiate token in an
.Va Authorization .Va Authorization
header that the user-agent should copy to the redirected request. header that the user-agent should copy to the redirected request.
.Sh TGT HTTP API
This service provides an HTTP-based "kinit" service.
The protocol consists of a
.Ar GET
of
.Ar /get-tgt
with an optional
.Ar cname = Ar principal-name
query parameter.
.Pp
In a successful query, the response body will contain a TGT and
its session key encoded as a "ccache" file contents.
.Pp
Authentication is required.
Unauthenticated requests will elicit a 401 response.
.Pp
Supported query parameters (separated by ampersands)
.Bl -tag -width Ds -offset indent
.It Li cname = Va principal
.El
.Pp
Authentication is required
Unauthenticated requests will elicit a 401 response.
.Pp
Authorization is required, where the authorization check is the
same as for
.Va /get-cert
by the authenticated client principal to get a certificate with
a PKINIT SAN for itself or the requested principal if a
.Va cname
query parameter was included.
.Sh ENVIRONMENT .Sh ENVIRONMENT
.Bl -tag -width Ds .Bl -tag -width Ds
.It Ev KRB5_CONFIG .It Ev KRB5_CONFIG

View File

@@ -1062,29 +1062,16 @@ find_ccache(krb5_context context, const char *princ, char **ccname)
return ret ? ret : ENOENT; return ret ? ret : ENOENT;
} }
/* enum k5_creds_kind { K5_CREDS_EPHEMERAL, K5_CREDS_CACHED };
* 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 static krb5_error_code
do_pkinit(struct bx509_request_desc *r) get_ccache(struct bx509_request_desc *r, krb5_ccache *cc, int *won)
{ {
krb5_get_init_creds_opt *opt = NULL;
krb5_init_creds_context ctx = NULL;
krb5_error_code ret = 0; krb5_error_code ret = 0;
krb5_ccache temp_cc = NULL;
krb5_ccache cc = NULL;
krb5_principal p = NULL;
struct stat st1, st2; struct stat st1, st2;
time_t life;
const char *crealm;
const char *fn = NULL;
char *temp_ccname = NULL; char *temp_ccname = NULL;
const char *fn = NULL;
time_t life;
int fd = -1; int fd = -1;
/* /*
@@ -1103,6 +1090,8 @@ do_pkinit(struct bx509_request_desc *r)
* FILE ccache would take care to mkstemp() and rename() into place. * FILE ccache would take care to mkstemp() and rename() into place.
* fcc_open() basically does a similar thing. * fcc_open() basically does a similar thing.
*/ */
*cc = NULL;
*won = -1;
if (asprintf(&temp_ccname, "%s.ccnew", r->ccname) == -1 || if (asprintf(&temp_ccname, "%s.ccnew", r->ccname) == -1 ||
temp_ccname == NULL) temp_ccname == NULL)
ret = ENOMEM; ret = ENOMEM;
@@ -1138,30 +1127,70 @@ do_pkinit(struct bx509_request_desc *r)
/* Check if we lost any race to acquire Kerberos creds */ /* Check if we lost any race to acquire Kerberos creds */
if (ret == 0) if (ret == 0)
ret = krb5_cc_resolve(r->context, temp_ccname, &temp_cc); ret = krb5_cc_resolve(r->context, temp_ccname, cc);
if (ret == 0) if (ret == 0) {
ret = krb5_cc_get_lifetime(r->context, temp_cc, &life); ret = krb5_cc_get_lifetime(r->context, *cc, &life);
if (ret == 0 && life > 60) if (ret == 0 && life > 60)
goto out; /* We lost the race, but we win: we get to do less work */ *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;
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);
}
/*
* We won the race. 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.
*/
ret = krb5_parse_name(r->context, r->cname, &p); ret = krb5_parse_name(r->context, r->cname, &p);
if (ret == 0) if (ret == 0)
crealm = krb5_principal_get_realm(r->context, p); crealm = krb5_principal_get_realm(r->context, p);
if (ret == 0 && if (ret == 0)
(ret = krb5_get_init_creds_opt_alloc(r->context, &opt)) == 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, krb5_get_init_creds_opt_set_default_flags(r->context, "kinit", crealm,
opt); opt);
if (ret == 0 && if (ret == 0)
(ret = krb5_get_init_creds_opt_set_addressless(r->context, ret = krb5_get_init_creds_opt_set_addressless(r->context, opt, 1);
opt, 1)) == 0) if (ret == 0)
ret = krb5_get_init_creds_opt_set_pkinit(r->context, opt, p, ret = krb5_get_init_creds_opt_set_pkinit(r->context, opt, p,
r->pkix_store, r->pkix_store,
NULL, /* pkinit_anchor */ NULL, /* pkinit_anchor */
@@ -1183,12 +1212,20 @@ do_pkinit(struct bx509_request_desc *r)
* into temp_cc, and rename into place. Note that krb5_cc_move() closes * 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. * the source ccache, so we set temp_cc = NULL if it succeeds.
*/ */
if (ret == 0 && if (ret == 0)
(ret = krb5_init_creds_get(r->context, ctx)) == 0 && ret = krb5_init_creds_get(r->context, ctx);
(ret = krb5_init_creds_store(r->context, ctx, temp_cc)) == 0 && if (ret == 0)
(ret = krb5_cc_resolve(r->context, r->ccname, &cc)) == 0 && ret = krb5_init_creds_store(r->context, ctx, temp_cc);
(ret = krb5_cc_move(r->context, temp_cc, cc)) == 0) if (kind == K5_CREDS_CACHED) {
temp_cc = NULL; 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: out:
if (ctx) if (ctx)
@@ -1197,9 +1234,6 @@ out:
krb5_free_principal(r->context, p); krb5_free_principal(r->context, p);
krb5_cc_close(r->context, temp_cc); krb5_cc_close(r->context, temp_cc);
krb5_cc_close(r->context, cc); krb5_cc_close(r->context, cc);
free(temp_ccname);
if (fd != -1)
(void) close(fd); /* Drops the flock */
return ret; return ret;
} }
@@ -1230,7 +1264,7 @@ load_priv_key(krb5_context context, const char *fn, hx509_private_key *key)
} }
static krb5_error_code static krb5_error_code
bnegotiate_do_CA(struct bx509_request_desc *r) k5_do_CA(struct bx509_request_desc *r)
{ {
SubjectPublicKeyInfo spki; SubjectPublicKeyInfo spki;
hx509_private_key key = NULL; hx509_private_key key = NULL;
@@ -1272,7 +1306,7 @@ bnegotiate_do_CA(struct bx509_request_desc *r)
/* Issue the certificate */ /* Issue the certificate */
if (ret == 0) if (ret == 0)
ret = kdc_issue_certificate(r->context, "bx509", logfac, req, p, ret = kdc_issue_certificate(r->context, "getTGT", logfac, req, p,
&r->token_times, 0, &r->token_times, 0,
1 /* send_chain */, &certs); 1 /* send_chain */, &certs);
krb5_free_principal(r->context, p); krb5_free_principal(r->context, p);
@@ -1307,22 +1341,23 @@ bnegotiate_do_CA(struct bx509_request_desc *r)
/* Get impersonated Kerberos credentials for `cprinc' */ /* Get impersonated Kerberos credentials for `cprinc' */
static krb5_error_code static krb5_error_code
bnegotiate_get_creds(struct bx509_request_desc *r) k5_get_creds(struct bx509_request_desc *r, enum k5_creds_kind kind)
{ {
krb5_error_code ret; krb5_error_code ret;
/* If we have a live ccache for `cprinc', we're done */ /* If we have a live ccache for `cprinc', we're done */
if ((ret = find_ccache(r->context, r->cname, &r->ccname)) == 0) if (kind == K5_CREDS_CACHED &&
(ret = find_ccache(r->context, r->cname, &r->ccname)) == 0)
return ret; /* Success */ return ret; /* Success */
/* /*
* Else we have to acquire a credential for them using their bearer token * Else we have to acquire a credential for them using their bearer token
* for authentication (and our keytab / initiator credentials perhaps). * for authentication (and our keytab / initiator credentials perhaps).
*/ */
if ((ret = bnegotiate_do_CA(r))) if ((ret = k5_do_CA(r)))
return ret; /* bnegotiate_do_CA() calls bad_req() */ return ret; /* k5_do_CA() calls bad_req() */
if (ret == 0 && (ret = do_pkinit(r))) if (ret == 0 && (ret = do_pkinit(r, kind)))
ret = bad_403(r, ret, ret = bad_403(r, ret,
"Could not acquire Kerberos credentials using PKINIT"); "Could not acquire Kerberos credentials using PKINIT");
return ret; return ret;
@@ -1598,7 +1633,7 @@ bnegotiate(struct bx509_request_desc *r)
* Perhaps we could use S4U instead, which would speed up the slow path a * Perhaps we could use S4U instead, which would speed up the slow path a
* bit. * bit.
*/ */
ret = bnegotiate_get_creds(r); ret = k5_get_creds(r, K5_CREDS_CACHED);
/* Acquire the Negotiate token and output it */ /* Acquire the Negotiate token and output it */
if (ret == 0 && r->ccname != NULL) if (ret == 0 && r->ccname != NULL)
@@ -1618,6 +1653,72 @@ bnegotiate(struct bx509_request_desc *r)
return ret; return ret;
} }
static krb5_error_code
authorize_TGT_REQ(struct bx509_request_desc *r, const char *cname)
{
krb5_principal p = NULL;
krb5_error_code ret;
ret = krb5_parse_name(r->context, r->cname, &p);
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", cname);
ret = hx509_request_add_pkinit(r->context->hx509ctx, r->req, cname);
if (ret == 0)
ret = kdc_authorize_csr(r->context, "getTGT", r->req, p);
krb5_free_principal(r->context, p);
hx509_request_free(&r->req);
if (ret)
return bad_403(r, ret, "Not authorized to requested TGT");
return ret;
}
/*
* Implements /get-tgt end-point.
*
* Query parameters (mutually exclusive):
*
* - cname=<name> (client principal name, if not the same as the authenticated
* name, then this will be impersonated if allowed)
*/
static krb5_error_code
get_tgt(struct bx509_request_desc *r)
{
krb5_error_code ret;
size_t bodylen;
const char *cname = NULL;
const char *fn;
void *body;
cname = MHD_lookup_connection_value(r->connection, MHD_GET_ARGUMENT_KIND,
"cname");
ret = validate_token(r);
if (ret == 0)
ret = authorize_TGT_REQ(r, cname ? cname : r->cname);
/* validate_token() and authorize_TGT_REQ() call bad_req() */
if (ret)
return ret;
ret = k5_get_creds(r, K5_CREDS_EPHEMERAL);
if (ret)
return bad_503(r, ret, "Could not get TGT");
fn = strchr(r->ccname, ':');
if (fn == NULL)
return bad_500(r, ret, "Impossible error");
fn++;
if ((errno = rk_undumpdata(fn, &body, &bodylen))) {
(void) unlink(fn);
return bad_503(r, ret, "Could not get TGT");
}
ret = resp(r, MHD_HTTP_OK, MHD_RESPMEM_MUST_COPY, body, bodylen, NULL);
free(body);
return ret;
}
static krb5_error_code static krb5_error_code
health(const char *method, struct bx509_request_desc *r) health(const char *method, struct bx509_request_desc *r)
{ {
@@ -1676,6 +1777,8 @@ route(void *cls,
else if (strcmp(url, "/get-negotiate-token") == 0 || else if (strcmp(url, "/get-negotiate-token") == 0 ||
strcmp(url, "/bnegotiate") == 0) /* old name */ strcmp(url, "/bnegotiate") == 0) /* old name */
ret = bnegotiate(&r); ret = bnegotiate(&r);
else if (strcmp(url, "/get-tgt") == 0)
ret = get_tgt(&r);
else else
ret = bad_404(&r, url); ret = bad_404(&r, url);

View File

@@ -317,6 +317,7 @@ CLEANFILES= \
barpassword \ barpassword \
ca.crt \ ca.crt \
cache.krb5 \ cache.krb5 \
cache2.krb5 \
cdigest-reply \ cdigest-reply \
client-cache \ client-cache \
curlheaders \ curlheaders \
@@ -339,8 +340,10 @@ CLEANFILES= \
krb5-cc.conf \ krb5-cc.conf \
krb5-cccol.conf \ krb5-cccol.conf \
krb5-hdb-mitdb.conf \ krb5-hdb-mitdb.conf \
krb5-master2.conf \
krb5-pkinit-win.conf \ krb5-pkinit-win.conf \
krb5-pkinit.conf \ krb5-pkinit.conf \
krb5-pkinit2.conf \
krb5-bx509.conf \ krb5-bx509.conf \
krb5-httpkadmind.conf \ krb5-httpkadmind.conf \
krb5-slave2.conf \ krb5-slave2.conf \
@@ -354,12 +357,14 @@ CLEANFILES= \
malloc-log \ malloc-log \
malloc-log-master \ malloc-log-master \
malloc-log-slave \ malloc-log-slave \
messages.log2 \
negotiate-token \ negotiate-token \
notfoopassword \ notfoopassword \
o2cache.krb5 \ o2cache.krb5 \
o2digest-reply \ o2digest-reply \
ocache.krb5 \ ocache.krb5 \
out-log \ out-log \
req \
response-headers \ response-headers \
s2digest-reply \ s2digest-reply \
sdb \ sdb \

View File

@@ -54,7 +54,10 @@ kdc="${kdc} --addresses=localhost -P $port"
server=datan.test.h5l.se server=datan.test.h5l.se
otherserver=other.test.h5l.se otherserver=other.test.h5l.se
cache="FILE:${objdir}/cache.krb5" cachefile="${objdir}/cache.krb5"
cache="FILE:${cachefile}"
cachefile2="${objdir}/cache2.krb5"
cache2="FILE:${cachefile2}"
keyfile="${hx509_data}/key.der" keyfile="${hx509_data}/key.der"
keyfile2="${hx509_data}/key2.der" keyfile2="${hx509_data}/key2.der"
kt=${objdir}/kt kt=${objdir}/kt
@@ -63,6 +66,7 @@ ukt=${objdir}/ukt
ukeytab=FILE:${ukt} ukeytab=FILE:${ukt}
kinit="${kinit} -c $cache ${afs_no_afslog}" kinit="${kinit} -c $cache ${afs_no_afslog}"
klist2="${klist} --hidden -v -c $cache2"
klist="${klist} --hidden -v -c $cache" klist="${klist} --hidden -v -c $cache"
kgetcred="${kgetcred} -c $cache" kgetcred="${kgetcred} -c $cache"
kdestroy="${kdestroy} -c $cache ${afs_no_unlog}" kdestroy="${kdestroy} -c $cache ${afs_no_unlog}"
@@ -429,6 +433,30 @@ trap "kill -9 ${kdcpid} ${bx509pid}; echo signal killing kdc and bx509d; exit 1;
${kinit} -kt $ukeytab foo@${R} || exit 1 ${kinit} -kt $ukeytab foo@${R} || exit 1
$klist || { echo "failed to setup kimpersonate credentials"; exit 2; } $klist || { echo "failed to setup kimpersonate credentials"; exit 2; }
echo "Fetch TGT"
(set -vx; csr_grant pkinit foo@${R} foo@${R})
token=$(KRB5CCNAME=$cache $gsstoken HTTP@$server)
if ! (set -vx;
curl -o "${cachefile2}" -Lgsf \
--resolve ${server}:${bx509port}:127.0.0.1 \
-H "Authorization: Negotiate $token" \
"http://${server}:${bx509port}/get-tgt"); then
echo "Failed to get a TGT with /get-tgt end-point"
exit 2
fi
echo "Fetch TGT (inception)"
${kdestroy}
token=$(KRB5CCNAME=$cache2 $gsstoken HTTP@$server)
if ! (set -vx;
curl -o "${cachefile}" -Lgsf \
--resolve ${server}:${bx509port}:127.0.0.1 \
-H "Authorization: Negotiate $token" \
"http://${server}:${bx509port}/get-tgt"); then
echo "Failed to get a TGT with /get-tgt end-point"
exit 2
fi
echo "Fetch negotiate token (pre-test)" echo "Fetch negotiate token (pre-test)"
# Do what /bnegotiate does, roughly, prior to testing /bnegotiate # Do what /bnegotiate does, roughly, prior to testing /bnegotiate
$hxtool request-create --subject='' --generate-key=rsa --key-bits=1024 \ $hxtool request-create --subject='' --generate-key=rsa --key-bits=1024 \

View File

@@ -121,6 +121,45 @@
} }
} }
[getTGT]
simple_csr_authorizer_directory = @objdir@/simple_csr_authz
realms = {
TEST.H5L.SE = {
# Default (no cert exts requested)
user = {
# Use an issuer for user certs:
ca = PEM-FILE:@objdir@/user-issuer.pem
subject_name = CN=${principal-name-without-realm},DC=test,DC=h5l,DC=se
ekus = 1.3.6.1.5.5.7.3.2
include_pkinit_san = true
}
hostbased_service = {
# Only for HTTP services
HTTP = {
# Use an issuer for server certs:
ca = PEM-FILE:@objdir@/server-issuer.pem
include_dnsname_san = true
# Don't bother with a template
}
}
# Non-default certs (extensions requested)
#
# Use no templates -- get empty subject names,
# use SANs.
#
# Use appropriate issuers.
client = {
ca = PEM-FILE:@objdir@/user-issuer.pem
}
server = {
ca = PEM-FILE:@objdir@/server-issuer.pem
}
mixed = {
ca = PEM-FILE:@objdir@/mixed-issuer.pem
}
}
}
[logging] [logging]
kdc = 0-/FILE:@objdir@/messages.log kdc = 0-/FILE:@objdir@/messages.log
bx509d = 0-/FILE:@objdir@/messages.log bx509d = 0-/FILE:@objdir@/messages.log