From dc74e9d00c15e178eaed6128d4d93e6a7c652eef Mon Sep 17 00:00:00 2001 From: Nicolas Williams Date: Wed, 24 Mar 2021 17:47:04 -0500 Subject: [PATCH] kdc: Add Heimdal cert ext for ticket max_life This adds support for using a Heimdal-specific PKIX extension to derive a maximum Kerberos ticket lifetime from a client's PKINIT certificate. KDC configuration parameters: - pkinit_max_life_from_cert_extension - pkinit_max_life_bound If `pkinit_max_life_from_cert_extension` is set to true then the certificate extension or EKU will be checked. If `pkinit_max_life_bound` is set to a positive relative time, then that will be the upper bound of maximum Kerberos ticket lifetime derived from these extensions. The KDC config `pkinit_ticket_max_life_from_cert` that was added earlier has been renamed to `pkinit_max_life_from_cert`. See lib/hx509 and lib/krb5/krb5.conf.5. --- kdc/default_config.c | 20 ++++++++++++++ kdc/kdc.h | 3 +++ kdc/kerberos5.c | 34 +++++++++++------------- kdc/pkinit.c | 22 ++++++++++++++- lib/krb5/krb5.conf.5 | 32 ++++++++++++++++++++-- tests/kdc/check-pkinit.in | 50 +++++++++++++++++++++++++++++------ tests/kdc/krb5-pkinit.conf.in | 3 ++- 7 files changed, 134 insertions(+), 30 deletions(-) diff --git a/kdc/default_config.c b/kdc/default_config.c index ed4d03b05..7ba08d123 100644 --- a/kdc/default_config.c +++ b/kdc/default_config.c @@ -101,6 +101,9 @@ krb5_kdc_get_config(krb5_context context, krb5_kdc_configuration **config) c->enable_pkinit = FALSE; c->pkinit_princ_in_cert = TRUE; c->pkinit_require_binding = TRUE; + c->pkinit_max_life_from_cert_extension = FALSE; + c->pkinit_max_life_bound = 0; + c->pkinit_dh_min_bits = 1024; c->db = NULL; c->num_db = 0; c->logf = NULL; @@ -283,6 +286,23 @@ krb5_kdc_get_config(krb5_context context, krb5_kdc_configuration **config) 0, "kdc", "pkinit_dh_min_bits", NULL); + c->pkinit_max_life_from_cert_extension = + krb5_config_get_bool_default(context, NULL, + c->pkinit_max_life_from_cert_extension, + "kdc", + "pkinit_max_life_from_cert_extension", + NULL); + + c->pkinit_max_life_bound = + krb5_config_get_time_default(context, NULL, 0, "kdc", + "pkinit_max_life_bound", + NULL); + + c->pkinit_max_life_from_cert = + krb5_config_get_time_default(context, NULL, 0, "kdc", + "pkinit_max_life_from_cert", + NULL); + *config = c; return 0; diff --git a/kdc/kdc.h b/kdc/kdc.h index e8c27cfed..84a5b4a05 100644 --- a/kdc/kdc.h +++ b/kdc/kdc.h @@ -85,6 +85,9 @@ typedef struct krb5_kdc_configuration { int pkinit_dh_min_bits; int pkinit_require_binding; int pkinit_allow_proxy_certs; + int pkinit_max_life_from_cert_extension; + krb5_timestamp pkinit_max_life_from_cert; + krb5_timestamp pkinit_max_life_bound; krb5_log_facility *logf; diff --git a/kdc/kerberos5.c b/kdc/kerberos5.c index 9d38d8524..0793c45e2 100644 --- a/kdc/kerberos5.c +++ b/kdc/kerberos5.c @@ -464,7 +464,6 @@ pa_pkinit_validate(astgs_request_t r, const PA_DATA *pa) pk_client_params *pkp = NULL; char *client_cert = NULL; krb5_error_code ret; - krb5_timestamp max_life; ret = _kdc_pk_rd_padata(r, pa, &pkp); if (ret || pkp == NULL) { @@ -481,13 +480,8 @@ pa_pkinit_validate(astgs_request_t r, const PA_DATA *pa) goto out; } - max_life = krb5_config_get_time_default(r->context, NULL, 0, "kdc", - "pkinit_ticket_max_life_from_cert", - NULL); - if (max_life > 0) { - r->pa_max_life = max_life; - r->pa_endtime = _kdc_pk_endtime(pkp); - } + r->pa_endtime = _kdc_pk_endtime(pkp); + r->pa_max_life = _kdc_pk_max_life(pkp); _kdc_r_log(r, 4, "PKINIT pre-authentication succeeded -- %s using %s", r->cname, client_cert); @@ -2231,19 +2225,23 @@ _kdc_as_rep(astgs_request_t r) /* be careful not overflowing */ - if (r->client->entry.max_life) { + /* + * Pre-auth can override r->client->entry.max_life if configured. + * + * See pre-auth methods, specifically PKINIT, which can get or derive + * this from the client's certificate. + */ + if (r->pa_max_life > 0) + t = start + min(t - start, r->pa_max_life); + else if (r->client->entry.max_life) t = start + min(t - start, *r->client->entry.max_life); - if (r->pa_max_life > 0 && - r->pa_endtime > 0 && - t < r->pa_endtime && - r->pa_max_life > *r->client->entry.max_life) - t = start + min(r->pa_endtime - start, r->pa_max_life); - } else if (r->pa_max_life > 0 && - r->pa_endtime > 0 && - t < r->pa_endtime) - t = start + min(r->pa_endtime - start, r->pa_max_life); + if (r->server->entry.max_life) t = start + min(t - start, *r->server->entry.max_life); + + /* Pre-auth can bound endtime as well */ + if (r->pa_endtime > 0) + t = start + min(t - start, r->pa_endtime); #if 0 t = min(t, start + realm->max_life); #endif diff --git a/kdc/pkinit.c b/kdc/pkinit.c index 1d065fa46..4feddd135 100644 --- a/kdc/pkinit.c +++ b/kdc/pkinit.c @@ -59,6 +59,8 @@ struct pk_client_params { } ecdh; } u; hx509_cert cert; + krb5_timestamp endtime; + krb5_timestamp max_life; unsigned nonce; EncryptionKey reply_key; char *dh_group_name; @@ -802,7 +804,13 @@ out: krb5_timestamp _kdc_pk_endtime(pk_client_params *pkp) { - return hx509_cert_get_notAfter(pkp->cert); + return pkp->endtime; +} + +krb5_timestamp +_kdc_pk_max_life(pk_client_params *pkp) +{ + return pkp->max_life; } /* @@ -1695,6 +1703,18 @@ _kdc_pk_check_client(astgs_request_t r, return 0; } + cp->endtime = hx509_cert_get_notAfter(cp->cert); + cp->max_life = 0; + if (config->pkinit_max_life_from_cert_extension) + cp->max_life = + hx509_cert_get_pkinit_max_life(context->hx509ctx, cp->cert, + config->pkinit_max_life_bound); + if (cp->max_life == 0 && config->pkinit_max_life_from_cert > 0) { + cp->max_life = cp->endtime - hx509_cert_get_notBefore(cp->cert); + if (cp->max_life > config->pkinit_max_life_from_cert) + cp->max_life = config->pkinit_max_life_from_cert; + } + ret = hx509_cert_get_base_subject(context->hx509ctx, cp->cert, &name); diff --git a/lib/krb5/krb5.conf.5 b/lib/krb5/krb5.conf.5 index c9fc7a66c..75b512735 100644 --- a/lib/krb5/krb5.conf.5 +++ b/lib/krb5/krb5.conf.5 @@ -840,14 +840,42 @@ Defaults to .It Li pkinit_dh_min_bits = Va NUMBER Minimum acceptable modular Diffie-Hellman public key size in bits. -.It Li pkinit_ticket_max_life_from_cert = Va TIME +.It Li pkinit_max_life_from_cert_extension = Va BOOL +If set to +.Va true +then the KDC will override the +.Va max_life +attribute of the client principal's HDB record with a maximum +ticket life taken from a certificate extension with OID +.Va { iso(1) member-body(2) se(752) su(43) heim-pkix(16) 4 } +and the DER encoding of an +.Va INTEGER +number of seconds. +Alternatively, if the extended key usage OID +.Va { iso(1) member-body(2) se(752) su(43) heim-pkix(16) 3 } +is included in the client's certificate, then the +.Va notAfter +minus the current time will be used. +.It Li pkinit_max_life_bound = Va TIME +If set, this will be a hard bound on the maximum ticket lifetime +taken from the client's certificate. +As usual, +.Va TIME +can be given as a number followed by a unit, such as +.Dq 2d +for +.Dq two days . +.It Li pkinit_max_life_from_cert = Va TIME If set, this will override the .Va max_life attribute of the client principal's HDB record with the .Va notAfter of the client's certificate minus the current time, bounded to the given relative -.Va TIME . +.Va TIME +unless the +.Li pkinit_max_life_from_cert_extension +parameter is set and the client's certificate has that extension. As usual, .Va TIME can be given as a number followed by a unit, such as diff --git a/tests/kdc/check-pkinit.in b/tests/kdc/check-pkinit.in index 55d7073ba..71a304c49 100644 --- a/tests/kdc/check-pkinit.in +++ b/tests/kdc/check-pkinit.in @@ -189,31 +189,65 @@ ${hxtool} issue-certificate \ echo foo > ${objdir}/foopassword echo Starting kdc ; > messages.log +KRB5_CONFIG="${objdir}/krb5-pkinit2.conf" ${kdc} --detach --testing || { echo "kdc failed to start"; exit 1; } kdcpid=`getpid kdc` -trap "kill -9 ${kdcpid}; echo signal killing kdc; cat ca.crt kdc.crt pkinit.crt ;exit 1;" EXIT +trap 'kill -9 ${kdcpid}; echo signal killing kdc; cat ca.crt kdc.crt pkinit.crt; exit 1;' EXIT ec=0 +echo "Trying pk-init (principal in cert; longer max_life)"; > messages.log +base="${objdir}" +${kinit} --lifetime=5d -C FILE:${base}/pkinit.crt,${keyfile2} bar@${R} || \ + { ec=1 ; eval "${testfailed}"; } +${kgetcred} ${server}@${R} || { ec=1 ; eval "${testfailed}"; } +${klist} +if jq --version >/dev/null 2>&1 && jq -ne true >/dev/null 2>&1; then + ${klistjson} | + jq -e '(((.tickets[0].Expires| + strptime("%b %d %H:%M:%S %Y")|mktime) - now) / 86400) | + (floor < 4)' >/dev/null && + { ec=1 ; eval "${testfailed}"; } +fi +${kdestroy} + +echo "Restarting kdc ($kdcpid)" +sh ${leaks_kill} kdc $kdcpid || ec=1 +KRB5_CONFIG="${objdir}/krb5-pkinit.conf" +${kdc} --detach --testing || { echo "kdc failed to start"; exit 1; } +kdcpid=`getpid kdc` + echo "Trying pk-init (principal in cert)"; > messages.log base="${objdir}" ${kinit} -C FILE:${base}/pkinit.crt,${keyfile2} bar@${R} || \ { ec=1 ; eval "${testfailed}"; } ${kgetcred} ${server}@${R} || { ec=1 ; eval "${testfailed}"; } ${klist} +if jq --version >/dev/null 2>&1 && jq -ne true >/dev/null 2>&1; then + ${klistjson} | + jq -e '(((.tickets[0].Expires| + strptime("%b %d %H:%M:%S %Y")|mktime) - now) / 86400) | + (floor > 1)' >/dev/null && + { ec=1 ; eval "${testfailed}"; } +fi ${kdestroy} -echo "Restarting kdc (${kdcpid}) for longer max_life test" -sh ${leaks_kill} kdc $kdcpid || ec=1 -KRB5_CONFIG="${objdir}/krb5-pkinit2.conf" -${kdc} --detach --testing || { echo "kdc failed to start"; exit 1; } -kdcpid=`getpid kdc` - -echo "Trying pk-init (principal in cert; longer max_life)"; > messages.log +echo "Trying pk-init (principal in cert; longer max_life from cert ext)"; > messages.log +# Re-issue cert with --pkinit-max-life=7d +${hxtool} issue-certificate \ + --ca-certificate=FILE:$objdir/ca.crt,${keyfile} \ + --type="pkinit-client" \ + --pk-init-principal="bar@TEST.H5L.SE" \ + --req="PKCS10:req-pkinit.der" \ + --lifetime=7d \ + --pkinit-max-life=7d \ + --certificate="FILE:pkinit.crt" || exit 1 base="${objdir}" +set -vx ${kinit} --lifetime=5d -C FILE:${base}/pkinit.crt,${keyfile2} bar@${R} || \ { ec=1 ; eval "${testfailed}"; } +set +vx ${kgetcred} ${server}@${R} || { ec=1 ; eval "${testfailed}"; } ${klist} if jq --version >/dev/null 2>&1 && jq -ne true >/dev/null 2>&1; then diff --git a/tests/kdc/krb5-pkinit.conf.in b/tests/kdc/krb5-pkinit.conf.in index 1795ce524..9c48cd6d3 100644 --- a/tests/kdc/krb5-pkinit.conf.in +++ b/tests/kdc/krb5-pkinit.conf.in @@ -19,7 +19,8 @@ pkinit_identity = FILE:@objdir@/kdc.crt,@srcdir@/../../lib/hx509/data/key2.der pkinit_anchors = FILE:@objdir@/ca.crt pkinit_mappings_file = @srcdir@/pki-mapping - pkinit_ticket_max_life_from_cert = @max_life_from_cert@ + pkinit_max_life_from_cert_extension = true + pkinit_max_life_from_cert = @max_life_from_cert@ plugin_dir = @objdir@/../../kdc/.libs