kdc: Add synthetic PKINIT principals option
This commit is contained in:
@@ -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;
|
||||
|
@@ -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;
|
||||
|
||||
|
@@ -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;
|
||||
|
@@ -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);
|
||||
|
150
kdc/misc.c
150
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);
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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).
|
||||
|
@@ -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"
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
Reference in New Issue
Block a user