From 6633f6e5253665176953d0cbbe6506da9695907f Mon Sep 17 00:00:00 2001 From: Nicolas Williams Date: Tue, 6 Apr 2021 13:55:52 -0500 Subject: [PATCH] bx509d: Implement /get-tgt end-point --- kdc/bx509d.8 | 31 ++++++ kdc/bx509d.c | 209 ++++++++++++++++++++++++++--------- tests/kdc/Makefile.am | 5 + tests/kdc/check-bx509.in | 30 ++++- tests/kdc/krb5-bx509.conf.in | 39 +++++++ 5 files changed, 260 insertions(+), 54 deletions(-) diff --git a/kdc/bx509d.8 b/kdc/bx509d.8 index b0930f6c5..a521f1349 100644 --- a/kdc/bx509d.8 +++ b/kdc/bx509d.8 @@ -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 .Va Authorization 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 .Bl -tag -width Ds .It Ev KRB5_CONFIG diff --git a/kdc/bx509d.c b/kdc/bx509d.c index d5b36dfb4..2fc525fe0 100644 --- a/kdc/bx509d.c +++ b/kdc/bx509d.c @@ -1062,29 +1062,16 @@ find_ccache(krb5_context context, const char *princ, char **ccname) return ret ? ret : ENOENT; } -/* - * 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. - */ +enum k5_creds_kind { K5_CREDS_EPHEMERAL, K5_CREDS_CACHED }; + 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_ccache temp_cc = NULL; - krb5_ccache cc = NULL; - krb5_principal p = NULL; struct stat st1, st2; - time_t life; - const char *crealm; - const char *fn = NULL; char *temp_ccname = NULL; + const char *fn = NULL; + time_t life; 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. * 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; @@ -1138,30 +1127,70 @@ do_pkinit(struct bx509_request_desc *r) /* Check if we lost any race to acquire Kerberos creds */ if (ret == 0) - ret = krb5_cc_resolve(r->context, temp_ccname, &temp_cc); - if (ret == 0) - ret = krb5_cc_get_lifetime(r->context, temp_cc, &life); - if (ret == 0 && life > 60) - goto out; /* We lost the race, but we win: we get to do less work */ + 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; + + 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); if (ret == 0) crealm = krb5_principal_get_realm(r->context, p); - if (ret == 0 && - (ret = krb5_get_init_creds_opt_alloc(r->context, &opt)) == 0) + 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 && - (ret = krb5_get_init_creds_opt_set_addressless(r->context, - opt, 1)) == 0) + if (ret == 0) + ret = krb5_get_init_creds_opt_set_addressless(r->context, opt, 1); + if (ret == 0) ret = krb5_get_init_creds_opt_set_pkinit(r->context, opt, p, r->pkix_store, 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 * the source ccache, so we set temp_cc = NULL if it succeeds. */ - if (ret == 0 && - (ret = krb5_init_creds_get(r->context, ctx)) == 0 && - (ret = krb5_init_creds_store(r->context, ctx, temp_cc)) == 0 && - (ret = krb5_cc_resolve(r->context, r->ccname, &cc)) == 0 && - (ret = krb5_cc_move(r->context, temp_cc, cc)) == 0) - temp_cc = NULL; + 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) @@ -1197,9 +1234,6 @@ out: krb5_free_principal(r->context, p); krb5_cc_close(r->context, temp_cc); krb5_cc_close(r->context, cc); - free(temp_ccname); - if (fd != -1) - (void) close(fd); /* Drops the flock */ return ret; } @@ -1230,7 +1264,7 @@ load_priv_key(krb5_context context, const char *fn, hx509_private_key *key) } static krb5_error_code -bnegotiate_do_CA(struct bx509_request_desc *r) +k5_do_CA(struct bx509_request_desc *r) { SubjectPublicKeyInfo spki; hx509_private_key key = NULL; @@ -1272,7 +1306,7 @@ bnegotiate_do_CA(struct bx509_request_desc *r) /* Issue the certificate */ 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, 1 /* send_chain */, &certs); krb5_free_principal(r->context, p); @@ -1307,22 +1341,23 @@ bnegotiate_do_CA(struct bx509_request_desc *r) /* Get impersonated Kerberos credentials for `cprinc' */ 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; /* 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 */ /* * Else we have to acquire a credential for them using their bearer token * for authentication (and our keytab / initiator credentials perhaps). */ - if ((ret = bnegotiate_do_CA(r))) - return ret; /* bnegotiate_do_CA() calls bad_req() */ + if ((ret = k5_do_CA(r))) + 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, "Could not acquire Kerberos credentials using PKINIT"); 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 * bit. */ - ret = bnegotiate_get_creds(r); + ret = k5_get_creds(r, K5_CREDS_CACHED); /* Acquire the Negotiate token and output it */ if (ret == 0 && r->ccname != NULL) @@ -1618,6 +1653,72 @@ bnegotiate(struct bx509_request_desc *r) 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= (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 health(const char *method, struct bx509_request_desc *r) { @@ -1676,6 +1777,8 @@ route(void *cls, else if (strcmp(url, "/get-negotiate-token") == 0 || strcmp(url, "/bnegotiate") == 0) /* old name */ ret = bnegotiate(&r); + else if (strcmp(url, "/get-tgt") == 0) + ret = get_tgt(&r); else ret = bad_404(&r, url); diff --git a/tests/kdc/Makefile.am b/tests/kdc/Makefile.am index 425e8a2c7..a07f776eb 100644 --- a/tests/kdc/Makefile.am +++ b/tests/kdc/Makefile.am @@ -317,6 +317,7 @@ CLEANFILES= \ barpassword \ ca.crt \ cache.krb5 \ + cache2.krb5 \ cdigest-reply \ client-cache \ curlheaders \ @@ -339,8 +340,10 @@ CLEANFILES= \ krb5-cc.conf \ krb5-cccol.conf \ krb5-hdb-mitdb.conf \ + krb5-master2.conf \ krb5-pkinit-win.conf \ krb5-pkinit.conf \ + krb5-pkinit2.conf \ krb5-bx509.conf \ krb5-httpkadmind.conf \ krb5-slave2.conf \ @@ -354,12 +357,14 @@ CLEANFILES= \ malloc-log \ malloc-log-master \ malloc-log-slave \ + messages.log2 \ negotiate-token \ notfoopassword \ o2cache.krb5 \ o2digest-reply \ ocache.krb5 \ out-log \ + req \ response-headers \ s2digest-reply \ sdb \ diff --git a/tests/kdc/check-bx509.in b/tests/kdc/check-bx509.in index 875bb31d5..3870a38a7 100644 --- a/tests/kdc/check-bx509.in +++ b/tests/kdc/check-bx509.in @@ -54,7 +54,10 @@ kdc="${kdc} --addresses=localhost -P $port" server=datan.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" keyfile2="${hx509_data}/key2.der" kt=${objdir}/kt @@ -63,6 +66,7 @@ ukt=${objdir}/ukt ukeytab=FILE:${ukt} kinit="${kinit} -c $cache ${afs_no_afslog}" +klist2="${klist} --hidden -v -c $cache2" klist="${klist} --hidden -v -c $cache" kgetcred="${kgetcred} -c $cache" 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 $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)" # Do what /bnegotiate does, roughly, prior to testing /bnegotiate $hxtool request-create --subject='' --generate-key=rsa --key-bits=1024 \ diff --git a/tests/kdc/krb5-bx509.conf.in b/tests/kdc/krb5-bx509.conf.in index 787bf2dec..f55679a42 100644 --- a/tests/kdc/krb5-bx509.conf.in +++ b/tests/kdc/krb5-bx509.conf.in @@ -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] kdc = 0-/FILE:@objdir@/messages.log bx509d = 0-/FILE:@objdir@/messages.log