kdc: Add synthetic PKINIT principals option

This commit is contained in:
Nicolas Williams
2021-06-28 23:29:18 -05:00
parent 4a5fc6bcde
commit 00358252d3
11 changed files with 228 additions and 46 deletions

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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);

View File

@@ -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);

View File

@@ -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

View File

@@ -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

View File

@@ -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).

View File

@@ -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"

View File

@@ -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

View File

@@ -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