From 00358252d3c5dbd8eeac8e25a6c98260335720ac Mon Sep 17 00:00:00 2001 From: Nicolas Williams Date: Mon, 28 Jun 2021 23:29:18 -0500 Subject: [PATCH] kdc: Add synthetic PKINIT principals option --- kdc/default_config.c | 20 +++++ kdc/kdc.h | 4 + kdc/kerberos5.c | 37 ++++++--- kdc/krb5tgs.c | 29 +++++++ kdc/misc.c | 150 ++++++++++++++++++++++++++-------- lib/hdb/hdb.asn1 | 1 + lib/hdb/hdb.h | 1 + lib/krb5/krb5.conf.5 | 11 +++ tests/kdc/check-pkinit.in | 19 ++++- tests/kdc/krb5-pkinit.conf.in | 1 + tests/kdc/krb5.conf.in | 1 + 11 files changed, 228 insertions(+), 46 deletions(-) diff --git a/kdc/default_config.c b/kdc/default_config.c index 08f7a3a77..496561d56 100644 --- a/kdc/default_config.c +++ b/kdc/default_config.c @@ -102,8 +102,11 @@ 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->synthetic_clients = FALSE; c->pkinit_max_life_from_cert_extension = FALSE; c->pkinit_max_life_bound = 0; + c->synthetic_clients_max_life = 300; + c->synthetic_clients_max_renew = 300; c->pkinit_dh_min_bits = 1024; c->db = NULL; c->num_db = 0; @@ -299,6 +302,13 @@ krb5_kdc_get_config(krb5_context context, krb5_kdc_configuration **config) "pkinit_max_life_from_cert_extension", NULL); + c->synthetic_clients = + krb5_config_get_bool_default(context, NULL, + c->synthetic_clients, + "kdc", + "synthetic_clients", + NULL); + c->pkinit_max_life_bound = krb5_config_get_time_default(context, NULL, 0, "kdc", "pkinit_max_life_bound", @@ -309,6 +319,16 @@ krb5_kdc_get_config(krb5_context context, krb5_kdc_configuration **config) "pkinit_max_life_from_cert", NULL); + c->synthetic_clients_max_life = + krb5_config_get_time_default(context, NULL, 300, "kdc", + "synthetic_clients_max_life", + NULL); + + c->synthetic_clients_max_renew = + krb5_config_get_time_default(context, NULL, 300, "kdc", + "synthetic_clients_max_renew", + NULL); + *config = c; return 0; diff --git a/kdc/kdc.h b/kdc/kdc.h index 96c3457b6..ad5e76433 100644 --- a/kdc/kdc.h +++ b/kdc/kdc.h @@ -84,11 +84,15 @@ typedef struct krb5_kdc_configuration { char **pkinit_kdc_cert_pool; char **pkinit_kdc_revoke; int pkinit_dh_min_bits; + /* XXX Turn these into bit-fields */ int pkinit_require_binding; int pkinit_allow_proxy_certs; + int synthetic_clients; int pkinit_max_life_from_cert_extension; krb5_timestamp pkinit_max_life_from_cert; krb5_timestamp pkinit_max_life_bound; + krb5_timestamp synthetic_clients_max_life; + krb5_timestamp synthetic_clients_max_renew; krb5_log_facility *logf; diff --git a/kdc/kerberos5.c b/kdc/kerberos5.c index d9e0d9ec0..7cd45a6b6 100644 --- a/kdc/kerberos5.c +++ b/kdc/kerberos5.c @@ -171,7 +171,11 @@ _kdc_find_etype(astgs_request_t r, uint32_t flags, Key *key = NULL; size_t i, k, m; - if (flags & KFE_USE_CLIENT) { + if (is_preauth && (flags & KFE_USE_CLIENT) && + r->client->entry.flags.synthetic) + return KRB5KDC_ERR_ETYPE_NOSUPP; + + if ((flags & KFE_USE_CLIENT) && !r->client->entry.flags.synthetic) { princ = r->client; request_princ = r->client_princ; } else { @@ -481,7 +485,8 @@ pa_pkinit_validate(astgs_request_t r, const PA_DATA *pa) } r->pa_endtime = _kdc_pk_endtime(pkp); - r->pa_max_life = _kdc_pk_max_life(pkp); + if (!r->client->entry.flags.synthetic) + 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); @@ -492,10 +497,8 @@ pa_pkinit_validate(astgs_request_t r, const PA_DATA *pa) _kdc_set_e_text(r, "Failed to build PK-INIT reply"); goto out; } -#if 0 ret = _kdc_add_initial_verified_cas(r->context, r->config, pkp, &r->et); -#endif out: if (pkp) _kdc_pk_free_client_param(r->context, pkp); @@ -864,13 +867,15 @@ struct kdc_patypes { unsigned int flags; #define PA_ANNOUNCE 1 #define PA_REQ_FAST 2 /* only use inside fast */ +#define PA_SYNTHETIC_OK 4 krb5_error_code (*validate)(astgs_request_t, const PA_DATA *pa); }; static const struct kdc_patypes pat[] = { #ifdef PKINIT { - KRB5_PADATA_PK_AS_REQ, "PK-INIT(ietf)", PA_ANNOUNCE, + KRB5_PADATA_PK_AS_REQ, "PK-INIT(ietf)", + PA_ANNOUNCE | PA_SYNTHETIC_OK, pa_pkinit_validate }, { @@ -1916,8 +1921,8 @@ _kdc_as_rep(astgs_request_t r) } ret = _kdc_db_fetch(context, config, r->client_princ, - HDB_F_GET_CLIENT | flags, NULL, - &r->clientdb, &r->client); + HDB_F_GET_CLIENT | HDB_F_SYNTHETIC_OK | flags, NULL, + &r->clientdb, &r->client); switch (ret) { case 0: /* Success */ break; @@ -1946,12 +1951,6 @@ _kdc_as_rep(astgs_request_t r) goto out; } default: - /* - * We could have an option to synthetically construct an HDB entry for - * the client from its certificate, if it used PKINIT and its cert has - * the PKINIT SAN. We could have a default HDB entry for this case to - * provide default field values. - */ msg = krb5_get_error_message(context, ret); kdc_log(context, config, 4, "UNKNOWN -- %s: %s", r->cname, msg); krb5_free_error_message(context, msg); @@ -2015,6 +2014,12 @@ _kdc_as_rep(astgs_request_t r) i = 0; pa = _kdc_find_padata(req, &i, pat[n].type); if (pa) { + if (r->client->entry.flags.synthetic && + !(pat[n].flags & PA_SYNTHETIC_OK)) { + kdc_log(context, config, 4, "UNKNOWN -- %s", r->cname); + ret = HDB_ERR_NOENTRY; + goto out; + } _kdc_audit_addkv((kdc_request_t)r, KDC_AUDIT_VIS, "pa", "%s", pat[n].name); ret = pat[n].validate(r, pa); @@ -2051,6 +2056,12 @@ _kdc_as_rep(astgs_request_t r) size_t n; krb5_boolean default_salt; + if (r->client->entry.flags.synthetic) { + kdc_log(context, config, 4, "UNKNOWN -- %s", r->cname); + ret = HDB_ERR_NOENTRY; + goto out; + } + for (n = 0; n < sizeof(pat) / sizeof(pat[0]); n++) { if ((pat[n].flags & PA_ANNOUNCE) == 0) continue; diff --git a/kdc/krb5tgs.c b/kdc/krb5tgs.c index c10d7d0a2..e6ea972c7 100644 --- a/kdc/krb5tgs.c +++ b/kdc/krb5tgs.c @@ -1977,8 +1977,28 @@ server_lookup: goto out; } + { + krb5_data verified_cas; + + /* + * If the client doesn't exist in the HDB but has a TGT and it's + * obtained with PKINIT then we assume it's a synthetic client -- that + * is, a client whose name was vouched for by a CA using a PKINIT SAN, + * but which doesn't exist in the HDB proper. We'll allow such a + * client to do TGT requests even though normally we'd reject all + * clients that don't exist in the HDB. + */ + ret = krb5_ticket_get_authorization_data_type(context, ticket, + KRB5_AUTHDATA_INITIAL_VERIFIED_CAS, + &verified_cas); + if (ret == 0) { + krb5_data_free(&verified_cas); + flags |= HDB_F_SYNTHETIC_OK; + } + } ret = _kdc_db_fetch(context, config, cp, HDB_F_GET_CLIENT | flags, NULL, &clientdb, &client); + flags &= ~HDB_F_SYNTHETIC_OK; priv->client = client; if(ret == HDB_ERR_NOT_FOUND_HERE) { /* This is OK, we are just trying to find out if they have @@ -2006,6 +2026,11 @@ server_lookup: msg = krb5_get_error_message(context, ret); kdc_log(context, config, 4, "Client not found in database: %s", msg); krb5_free_error_message(context, msg); + } else if (ret == 0 && + (client->entry.flags.invalid || !client->entry.flags.client)) { + kdc_log(context, config, 4, "Client has invalid attribute set"); + ret = KRB5KDC_ERR_POLICY; + goto out; } ret = check_PAC(context, config, cp, NULL, @@ -2139,6 +2164,10 @@ server_lookup: if (ret) goto out; + /* + * Note no HDB_F_SYNTHETIC_OK -- impersonating non-existent clients + * is probably not desirable! + */ ret = _kdc_db_fetch(context, config, tp, HDB_F_GET_CLIENT | flags, NULL, &s4u2self_impersonated_clientdb, &s4u2self_impersonated_client); diff --git a/kdc/misc.c b/kdc/misc.c index 02c612231..44393ec4c 100644 --- a/kdc/misc.c +++ b/kdc/misc.c @@ -51,6 +51,76 @@ name_type_ok(krb5_context context, struct timeval _kdc_now; +static krb5_error_code +synthesize_hdb_close(krb5_context context, struct HDB *db) +{ + (void) context; + (void) db; + return 0; +} + +/* + * Synthesize an HDB entry suitable for PKINIT and only PKINIT. + */ +static krb5_error_code +synthesize_client(krb5_context context, + krb5_kdc_configuration *config, + krb5_const_principal princ, + HDB **db, + hdb_entry_ex **h) +{ + static HDB null_db; + krb5_error_code ret; + hdb_entry_ex *e; + + /* Hope this works! */ + null_db.hdb_destroy = synthesize_hdb_close; + null_db.hdb_close = synthesize_hdb_close; + *db = &null_db; + + ret = (e = calloc(1, sizeof(*e))) ? 0 : krb5_enomem(context); + if (ret == 0) { + e->entry.flags.client = 1; + e->entry.flags.immutable = 1; + e->entry.flags.virtual = 1; + e->entry.flags.synthetic = 1; + e->entry.flags.do_not_store = 1; + e->entry.kvno = 1; + e->entry.keys.len = 0; + e->entry.keys.val = NULL; + e->entry.created_by.time = time(NULL); + e->entry.modified_by = NULL; + e->entry.valid_start = NULL; + e->entry.valid_end = NULL; + e->entry.pw_end = NULL; + e->entry.etypes = NULL; + e->entry.generation = NULL; + e->entry.extensions = NULL; + } + if (ret == 0) + ret = (e->entry.max_renew = calloc(1, sizeof(e->entry.max_renew))) ? + 0 : krb5_enomem(context); + if (ret == 0) + ret = (e->entry.max_life = calloc(1, sizeof(e->entry.max_life))) ? + 0 : krb5_enomem(context); + if (ret == 0) + ret = krb5_copy_principal(context, princ, &e->entry.principal); + if (ret == 0) + ret = krb5_copy_principal(context, princ, &e->entry.created_by.principal); + if (ret == 0) { + /* + * We can't check OCSP in the TGS path, so we can't let tickets for + * synthetic principals live very long. + */ + *(e->entry.max_renew) = config->synthetic_clients_max_renew; + *(e->entry.max_life) = config->synthetic_clients_max_life; + *h = e; + } else { + hdb_free_entry(context, e); + } + return ret; +} + krb5_error_code _kdc_db_fetch(krb5_context context, krb5_kdc_configuration *config, @@ -70,7 +140,7 @@ _kdc_db_fetch(krb5_context context, *h = NULL; if (!name_type_ok(context, config, principal)) - goto out2; + return HDB_ERR_NOENTRY; flags |= HDB_F_DECRYPT; if (kvno_ptr != NULL && *kvno_ptr != 0) { @@ -102,6 +172,9 @@ _kdc_db_fetch(krb5_context context, for (i = 0; i < config->num_db; i++) { HDB *curdb = config->db[i]; + if (db) + *db = curdb; + ret = curdb->hdb_open(context, curdb, O_RDONLY, 0); if (ret) { const char *msg = krb5_get_error_message(context, ret); @@ -117,41 +190,54 @@ _kdc_db_fetch(krb5_context context, ret = hdb_fetch_kvno(context, curdb, princ, flags, 0, 0, kvno, ent); curdb->hdb_close(context, curdb); - switch (ret) { - case HDB_ERR_WRONG_REALM: - /* - * the ent->entry.principal just contains hints for the client - * to retry. This is important for enterprise principal routing - * between trusts. - */ - /* fall through */ - case 0: - if (db) - *db = curdb; - *h = ent; - ent = NULL; - goto out; + if (ret == HDB_ERR_NOENTRY) + continue; /* Check the other databases */ - case HDB_ERR_NOENTRY: - /* Check the other databases */ - continue; - - default: - /* - * This is really important, because errors like - * HDB_ERR_NOT_FOUND_HERE (used to indicate to Samba that - * the RODC on which this code is running does not have - * the key we need, and so a proxy to the KDC is required) - * have specific meaning, and need to be propogated up. - */ - goto out; - } + /* + * This is really important, because errors like + * HDB_ERR_NOT_FOUND_HERE (used to indicate to Samba that + * the RODC on which this code is running does not have + * the key we need, and so a proxy to the KDC is required) + * have specific meaning, and need to be propogated up. + */ + break; } -out2: - if (ret == HDB_ERR_NOENTRY) { - krb5_set_error_message(context, ret, "no such entry found in hdb"); + switch (ret) { + case HDB_ERR_WRONG_REALM: + case 0: + /* + * the ent->entry.principal just contains hints for the client + * to retry. This is important for enterprise principal routing + * between trusts. + */ + *h = ent; + ent = NULL; + break; + + case HDB_ERR_NOENTRY: + if (db) + *db = NULL; + if ((flags & HDB_F_GET_CLIENT) && (flags & HDB_F_SYNTHETIC_OK) && + config->synthetic_clients) { + ret = synthesize_client(context, config, principal, db, h); + if (ret) { + krb5_set_error_message(context, ret, "could not synthesize " + "HDB client principal entry"); + ret = HDB_ERR_NOENTRY; + krb5_prepend_error_message(context, ret, "no such entry found in hdb"); + } + } else { + krb5_set_error_message(context, ret, "no such entry found in hdb"); + } + break; + + default: + if (db) + *db = NULL; + break; } + out: krb5_free_principal(context, enterprise_principal); free(ent); diff --git a/lib/hdb/hdb.asn1 b/lib/hdb/hdb.asn1 index 682aa6e7b..2011f7ab2 100644 --- a/lib/hdb/hdb.asn1 +++ b/lib/hdb/hdb.asn1 @@ -53,6 +53,7 @@ HDBFlags ::= BIT STRING { materialize(19), -- store even if within virtual namespace virtual-keys(20), -- entry stored; keys mostly derived virtual(21), -- entry not stored; keys always derived + synthetic(22), -- entry not stored; for PKINIT force-canonicalize(30), -- force the KDC to return the canonical -- principal irrespective of the setting diff --git a/lib/hdb/hdb.h b/lib/hdb/hdb.h index 558210f06..bc2a3bdc5 100644 --- a/lib/hdb/hdb.h +++ b/lib/hdb/hdb.h @@ -70,6 +70,7 @@ enum hdb_lockop{ HDB_RLOCK, HDB_WLOCK }; #define HDB_F_FOR_TGS_REQ 8192 /* fetch is for a TGS REQ */ #define HDB_F_PRECHECK 16384 /* check that the operation would succeed */ #define HDB_F_DELAY_NEW_KEYS 32768 /* apply [hdb] new_service_key_delay */ +#define HDB_F_SYNTHETIC_OK 65536 /* synthetic principal for PKINIT OK */ /* hdb_capability_flags */ #define HDB_CAP_F_HANDLE_ENTERPRISE_PRINCIPAL 1 diff --git a/lib/krb5/krb5.conf.5 b/lib/krb5/krb5.conf.5 index 1707a992f..8f9e41959 100644 --- a/lib/krb5/krb5.conf.5 +++ b/lib/krb5/krb5.conf.5 @@ -781,6 +781,17 @@ Allow address-less tickets. .\" XXX .It Li allow-anonymous = Va BOOL If the kdc is allowed to hand out anonymous tickets. +.It Li synthetic_clients = Va BOOL +If enabled then the KDC will issue tickets for clients that don't +exist in the HDB provided that they use PKINIT, that PKINIT is +enabled, and that the client's have certificates with PKINIT +subject alternative names (SANs). +.It Li synthetic_clients_max_life = Va TIME +Maximum ticket lifetime for synthetic clients. +Default: 5 minutes. +.It Li synthetic_clients_max_renew = Va TIME +Maximum ticket renewable lifetime for synthetic clients. +Default: 5 minutes. .It Li pkinit_identity = Va HX509-STORE This is an HX509 store containing the KDC's PKINIT credential (private key and end-entity certificate). diff --git a/tests/kdc/check-pkinit.in b/tests/kdc/check-pkinit.in index 71a304c49..c06858a07 100644 --- a/tests/kdc/check-pkinit.in +++ b/tests/kdc/check-pkinit.in @@ -156,6 +156,15 @@ ${hxtool} issue-certificate \ --lifetime=7d \ --certificate="FILE:pkinit.crt" || exit 1 +echo "issue user certificate (pkinit san; synthetic principal)" +${hxtool} issue-certificate \ + --ca-certificate=FILE:$objdir/ca.crt,${keyfile} \ + --type="pkinit-client" \ + --pk-init-principal="synthetized@TEST.H5L.SE" \ + --req="PKCS10:req-pkinit.der" \ + --lifetime=7d \ + --certificate="FILE:pkinit-synthetic.crt" || exit 1 + echo "issue user 2 certificate (no san)" ${hxtool} issue-certificate \ --ca-certificate=FILE:$objdir/ca.crt,${keyfile} \ @@ -193,7 +202,7 @@ 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 pkinit-synthetic.crt; exit 1;' EXIT ec=0 @@ -212,6 +221,14 @@ if jq --version >/dev/null 2>&1 && jq -ne true >/dev/null 2>&1; then fi ${kdestroy} +echo "Trying pk-init (principal in cert; synthetic)"; > messages.log +base="${objdir}" +${kinit} --lifetime=5d -C FILE:${base}/pkinit-synthetic.crt,${keyfile2} synthetized@${R} || \ + { ec=1 ; eval "${testfailed}"; } +${kgetcred} ${server}@${R} || { ec=1 ; eval "${testfailed}"; } +${klist} +${kdestroy} + echo "Restarting kdc ($kdcpid)" sh ${leaks_kill} kdc $kdcpid || ec=1 KRB5_CONFIG="${objdir}/krb5-pkinit.conf" diff --git a/tests/kdc/krb5-pkinit.conf.in b/tests/kdc/krb5-pkinit.conf.in index 9c48cd6d3..fbc21277a 100644 --- a/tests/kdc/krb5-pkinit.conf.in +++ b/tests/kdc/krb5-pkinit.conf.in @@ -15,6 +15,7 @@ [kdc] strict-nametypes = true + synthetic_clients = true enable-pkinit = true pkinit_identity = FILE:@objdir@/kdc.crt,@srcdir@/../../lib/hx509/data/key2.der pkinit_anchors = FILE:@objdir@/ca.crt diff --git a/tests/kdc/krb5.conf.in b/tests/kdc/krb5.conf.in index 4376573a5..3e6e810df 100644 --- a/tests/kdc/krb5.conf.in +++ b/tests/kdc/krb5.conf.in @@ -82,6 +82,7 @@ enable-http = true + synthetic_clients = true enable-pkinit = true pkinit_identity = FILE:@srcdir@/../../lib/hx509/data/kdc.crt,@srcdir@/../../lib/hx509/data/kdc.key pkinit_anchors = FILE:@srcdir@/../../lib/hx509/data/ca.crt