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.
This commit is contained in:
Nicolas Williams
2021-03-24 17:47:04 -05:00
parent 15b2094079
commit dc74e9d00c
7 changed files with 134 additions and 30 deletions

View File

@@ -101,6 +101,9 @@ krb5_kdc_get_config(krb5_context context, krb5_kdc_configuration **config)
c->enable_pkinit = FALSE; c->enable_pkinit = FALSE;
c->pkinit_princ_in_cert = TRUE; c->pkinit_princ_in_cert = TRUE;
c->pkinit_require_binding = 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->db = NULL;
c->num_db = 0; c->num_db = 0;
c->logf = NULL; c->logf = NULL;
@@ -283,6 +286,23 @@ krb5_kdc_get_config(krb5_context context, krb5_kdc_configuration **config)
0, 0,
"kdc", "pkinit_dh_min_bits", NULL); "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; *config = c;
return 0; return 0;

View File

@@ -85,6 +85,9 @@ typedef struct krb5_kdc_configuration {
int pkinit_dh_min_bits; int pkinit_dh_min_bits;
int pkinit_require_binding; int pkinit_require_binding;
int pkinit_allow_proxy_certs; 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; krb5_log_facility *logf;

View File

@@ -464,7 +464,6 @@ pa_pkinit_validate(astgs_request_t r, const PA_DATA *pa)
pk_client_params *pkp = NULL; pk_client_params *pkp = NULL;
char *client_cert = NULL; char *client_cert = NULL;
krb5_error_code ret; krb5_error_code ret;
krb5_timestamp max_life;
ret = _kdc_pk_rd_padata(r, pa, &pkp); ret = _kdc_pk_rd_padata(r, pa, &pkp);
if (ret || pkp == NULL) { if (ret || pkp == NULL) {
@@ -481,13 +480,8 @@ pa_pkinit_validate(astgs_request_t r, const PA_DATA *pa)
goto out; 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", _kdc_r_log(r, 4, "PKINIT pre-authentication succeeded -- %s using %s",
r->cname, client_cert); r->cname, client_cert);
@@ -2231,19 +2225,23 @@ _kdc_as_rep(astgs_request_t r)
/* be careful not overflowing */ /* 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); 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) if (r->server->entry.max_life)
t = start + min(t - start, *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 #if 0
t = min(t, start + realm->max_life); t = min(t, start + realm->max_life);
#endif #endif

View File

@@ -59,6 +59,8 @@ struct pk_client_params {
} ecdh; } ecdh;
} u; } u;
hx509_cert cert; hx509_cert cert;
krb5_timestamp endtime;
krb5_timestamp max_life;
unsigned nonce; unsigned nonce;
EncryptionKey reply_key; EncryptionKey reply_key;
char *dh_group_name; char *dh_group_name;
@@ -802,7 +804,13 @@ out:
krb5_timestamp krb5_timestamp
_kdc_pk_endtime(pk_client_params *pkp) _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; 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, ret = hx509_cert_get_base_subject(context->hx509ctx,
cp->cert, cp->cert,
&name); &name);

View File

@@ -840,14 +840,42 @@ Defaults to
.It Li pkinit_dh_min_bits = Va NUMBER .It Li pkinit_dh_min_bits = Va NUMBER
Minimum acceptable modular Diffie-Hellman public key size in Minimum acceptable modular Diffie-Hellman public key size in
bits. 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 If set, this will override the
.Va max_life .Va max_life
attribute of the client principal's HDB record with the attribute of the client principal's HDB record with the
.Va notAfter .Va notAfter
of the client's certificate minus the current time, bounded to of the client's certificate minus the current time, bounded to
the given relative 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, As usual,
.Va TIME .Va TIME
can be given as a number followed by a unit, such as can be given as a number followed by a unit, such as

View File

@@ -189,31 +189,65 @@ ${hxtool} issue-certificate \
echo foo > ${objdir}/foopassword echo foo > ${objdir}/foopassword
echo Starting kdc ; > messages.log echo Starting kdc ; > messages.log
KRB5_CONFIG="${objdir}/krb5-pkinit2.conf"
${kdc} --detach --testing || { echo "kdc failed to start"; exit 1; } ${kdc} --detach --testing || { echo "kdc failed to start"; exit 1; }
kdcpid=`getpid kdc` 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 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 echo "Trying pk-init (principal in cert)"; > messages.log
base="${objdir}" base="${objdir}"
${kinit} -C FILE:${base}/pkinit.crt,${keyfile2} bar@${R} || \ ${kinit} -C FILE:${base}/pkinit.crt,${keyfile2} bar@${R} || \
{ ec=1 ; eval "${testfailed}"; } { ec=1 ; eval "${testfailed}"; }
${kgetcred} ${server}@${R} || { ec=1 ; eval "${testfailed}"; } ${kgetcred} ${server}@${R} || { ec=1 ; eval "${testfailed}"; }
${klist} ${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} ${kdestroy}
echo "Restarting kdc (${kdcpid}) for longer max_life test" echo "Trying pk-init (principal in cert; longer max_life from cert ext)"; > messages.log
sh ${leaks_kill} kdc $kdcpid || ec=1 # Re-issue cert with --pkinit-max-life=7d
KRB5_CONFIG="${objdir}/krb5-pkinit2.conf" ${hxtool} issue-certificate \
${kdc} --detach --testing || { echo "kdc failed to start"; exit 1; } --ca-certificate=FILE:$objdir/ca.crt,${keyfile} \
kdcpid=`getpid kdc` --type="pkinit-client" \
--pk-init-principal="bar@TEST.H5L.SE" \
echo "Trying pk-init (principal in cert; longer max_life)"; > messages.log --req="PKCS10:req-pkinit.der" \
--lifetime=7d \
--pkinit-max-life=7d \
--certificate="FILE:pkinit.crt" || exit 1
base="${objdir}" base="${objdir}"
set -vx
${kinit} --lifetime=5d -C FILE:${base}/pkinit.crt,${keyfile2} bar@${R} || \ ${kinit} --lifetime=5d -C FILE:${base}/pkinit.crt,${keyfile2} bar@${R} || \
{ ec=1 ; eval "${testfailed}"; } { ec=1 ; eval "${testfailed}"; }
set +vx
${kgetcred} ${server}@${R} || { ec=1 ; eval "${testfailed}"; } ${kgetcred} ${server}@${R} || { ec=1 ; eval "${testfailed}"; }
${klist} ${klist}
if jq --version >/dev/null 2>&1 && jq -ne true >/dev/null 2>&1; then if jq --version >/dev/null 2>&1 && jq -ne true >/dev/null 2>&1; then

View File

@@ -19,7 +19,8 @@
pkinit_identity = FILE:@objdir@/kdc.crt,@srcdir@/../../lib/hx509/data/key2.der pkinit_identity = FILE:@objdir@/kdc.crt,@srcdir@/../../lib/hx509/data/key2.der
pkinit_anchors = FILE:@objdir@/ca.crt pkinit_anchors = FILE:@objdir@/ca.crt
pkinit_mappings_file = @srcdir@/pki-mapping 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 plugin_dir = @objdir@/../../kdc/.libs