Optional backwards-compatible anon-pkinit behaviour

* Anonymous pkinit responses from the KDC where the name
  type is not well-known (as issued by 7.5 KDCs and earlier)
  are accepted by the client.  There is no need for the client
  to strictly enforce the name type.

* With historical_anon_pkinit = true, the kinit(1) client's
  "--anonymous" option only performs anon pkinit, and does
  not require an '@' prefix for the realm argument.

* With historical_anon_realm = true, the KDC issues anon
  pkinit tickets with the legacy pre-7.0 "real" realm.
This commit is contained in:
Viktor Dukhovni
2019-07-14 23:02:57 -04:00
committed by Viktor Dukhovni
parent f40d393c83
commit fae8df3839
11 changed files with 141 additions and 27 deletions

View File

@@ -59,6 +59,7 @@ krb5_kdc_get_config(krb5_context context, krb5_kdc_configuration **config)
c->check_ticket_addresses = TRUE;
c->allow_null_ticket_addresses = TRUE;
c->allow_anonymous = FALSE;
c->historical_anon_realm = FALSE;
c->strict_nametypes = FALSE;
c->trpolicy = TRPOLICY_ALWAYS_CHECK;
c->enable_pkinit = FALSE;
@@ -162,6 +163,12 @@ krb5_kdc_get_config(krb5_context context, krb5_kdc_configuration **config)
"kdc",
"allow-anonymous", NULL);
c->historical_anon_realm =
krb5_config_get_bool_default(context, NULL,
c->historical_anon_realm,
"kdc",
"historical_anon_realm", NULL);
c->strict_nametypes =
krb5_config_get_bool_default(context, NULL,
c->strict_nametypes,

View File

@@ -141,6 +141,19 @@ Permit tickets with no addresses.
This option is only relevant when check-ticket-addresses is TRUE.
.It Li allow-anonymous = Va boolean
Permit anonymous tickets with no addresses.
.It Li historical_anon_realm = Va boolean
Enables pre-7.0 non-RFC-comformant KDC behavior.
With this option set to
.Li true
the client realm in anonymous pkinit AS replies will be the requested realm,
rather than the RFC-conformant
.Li WELLKNOWN:ANONYMOUS
realm.
This can have a security impact on servers that expect to grant access to
anonymous-but-authenticated to the KDC users of the realm in question:
they would also grant access to unauthenticated anonymous users.
As such, it is not recommend to set this option to
.Li true.
.It Li max-kdc-datagram-reply-length = Va number
Maximum packet size the UDP rely that the KDC will transmit, instead
the KDC sends back a reply telling the client to use TCP instead.

View File

@@ -69,6 +69,7 @@ typedef struct krb5_kdc_configuration {
krb5_boolean check_ticket_addresses;
krb5_boolean allow_null_ticket_addresses;
krb5_boolean allow_anonymous;
krb5_boolean historical_anon_realm;
krb5_boolean strict_nametypes;
enum krb5_kdc_trpolicy trpolicy;

View File

@@ -117,10 +117,10 @@ is_default_salt_p(const krb5_salt *default_salt, const Key *key)
}
static krb5_boolean
is_anon_as_request_p(kdc_request_t r)
krb5_boolean
_kdc_is_anon_request(const KDC_REQ *req)
{
KDC_REQ_BODY *b = &r->req.req_body;
const KDC_REQ_BODY *b = &req->req_body;
/*
* Versions of Heimdal from 0.9rc1 through 1.50 use bit 14 instead
@@ -464,7 +464,7 @@ pa_enc_chal_validate(kdc_request_t r, const PA_DATA *pa)
heim_assert(r->armor_crypto != NULL, "ENC-CHAL called for non FAST");
if (is_anon_as_request_p(r)) {
if (_kdc_is_anon_request(&r->req)) {
ret = KRB5KRB_AP_ERR_BAD_INTEGRITY;
kdc_log(r->context, r->config, 0, "ENC-CHALL doesn't support anon");
return ret;
@@ -1795,7 +1795,7 @@ _kdc_as_rep(kdc_request_t r,
*/
if (_kdc_is_anonymous(context, r->client_princ) &&
!is_anon_as_request_p(r)) {
!_kdc_is_anon_request(&r->req)) {
kdc_log(context, config, 0, "Anonymous client w/o anonymous flag");
ret = KRB5KDC_ERR_BADOPTION;
goto out;
@@ -1969,7 +1969,7 @@ _kdc_as_rep(kdc_request_t r,
* send requre preauth is its required or anon is requested,
* anon is today only allowed via preauth mechanisms.
*/
if (require_preauth_p(r) || is_anon_as_request_p(r)) {
if (require_preauth_p(r) || _kdc_is_anon_request(&r->req)) {
ret = KRB5KDC_ERR_PREAUTH_REQUIRED;
_kdc_set_e_text(r, "Need to use PA-ENC-TIMESTAMP/PA-PK-AS-REQ");
goto out;
@@ -2002,7 +2002,7 @@ _kdc_as_rep(kdc_request_t r,
if(ret)
goto out;
if (is_anon_as_request_p(r)) {
if (_kdc_is_anon_request(&r->req)) {
ret = _kdc_check_anon_policy(context, config, r->client, r->server);
if (ret) {
_kdc_set_e_text(r, "Anonymous ticket requests are disabled");
@@ -2036,7 +2036,8 @@ _kdc_as_rep(kdc_request_t r,
rep.pvno = 5;
rep.msg_type = krb_as_rep;
if (_kdc_is_anonymous(context, r->client_princ)) {
if (!config->historical_anon_realm &&
_kdc_is_anonymous(context, r->client_princ)) {
Realm anon_realm = KRB5_ANON_REALM;
ret = copy_Realm(&anon_realm, &rep.crealm);
} else if (f.canonicalize || r->client->entry.flags.force_canonicalize)

View File

@@ -623,7 +623,8 @@ _kdc_pk_rd_padata(krb5_context context,
hx509_certs signer_certs;
int flags = HX509_CMS_VS_ALLOW_DATA_OID_MISMATCH; /* BTMM */
if (_kdc_is_anonymous(context, client->entry.principal))
if (_kdc_is_anonymous(context, client->entry.principal)
|| (config->historical_anon_realm && _kdc_is_anon_request(req)))
flags |= HX509_CMS_VS_ALLOW_ZERO_SIGNER;
ret = hx509_cms_verify_signed(context->hx509ctx,
@@ -1676,7 +1677,8 @@ _kdc_pk_check_client(krb5_context context,
size_t i;
if (cp->cert == NULL) {
if (!_kdc_is_anonymous(context, client->entry.principal))
if (!_kdc_is_anonymous(context, client->entry.principal)
&& !config->historical_anon_realm)
return KRB5KDC_ERR_BADOPTION;
*subject_name = strdup("<unauthenticated anonymous client>");

View File

@@ -166,11 +166,21 @@ in
.It Fl A , Fl Fl no-addresses
Request a ticket with no addresses.
.It Fl n , Fl Fl anonymous
Request an anonymous ticket. If the principal is specified as @REALM, then
Request an anonymous ticket.
With the default (false) setting of the
.Ar historical_anon_pkinit
configuration parameter, if the principal is specified as @REALM, then
anonymous PKINIT will be used to acquire an unauthenticated anonymous ticket
and both the client name and realm in the returned ticket will be anonymized.
and both the client name and (with fully RFC-comformant KDCs) realm in the
returned ticket will be anonymized.
Otherwise, authentication proceeds as normal and the anonymous ticket will have
only the client name anonymized.
With
.Ar historical_anon_pkinit
set to
.Li true ,
the principal is interpreted as a realm even without an at-sign prefix, and it
is not possible to obtain authenticated anonymized tickets.
.It Fl Fl enterprise
Parse principal as a enterprise (KRB5-NT-ENTERPRISE) name. Enterprise
names are email like principals that are stored in the name part of

View File

@@ -669,7 +669,7 @@ get_new_tickets(krb5_context context,
}
} else if (pk_user_id || ent_user_id ||
krb5_principal_is_anonymous(context, principal, KRB5_ANON_MATCH_ANY)) {
/* nop */;
} else if (!interactive && passwd[0] == '\0') {
static int already_warned = 0;
@@ -1272,6 +1272,7 @@ main(int argc, char **argv)
struct sigaction sa;
#endif
krb5_boolean unique_ccache = FALSE;
krb5_boolean historical_anon_pkinit = FALSE;
int anonymous_pkinit = FALSE;
setprogname(argv[0]);
@@ -1300,6 +1301,9 @@ main(int argc, char **argv)
argc -= optidx;
argv += optidx;
krb5_appdefault_boolean(context, "kinit", NULL, "historical_anon_pkinit",
FALSE, &historical_anon_pkinit);
/*
* Open the keytab now, we use the keytab to determine the principal's
* realm when the requested principal has no realm.
@@ -1332,6 +1336,16 @@ main(int argc, char **argv)
krb5_err(context, 1, ret, "krb5_make_principal");
krb5_principal_set_type(context, principal, KRB5_NT_WELLKNOWN);
anonymous_pkinit = TRUE;
} else if (anonymous_flag && historical_anon_pkinit) {
char *realm = argc == 0 ? get_default_realm(context) :
argv[0][0] == '@' ? &argv[0][1] : argv[0];
ret = krb5_make_principal(context, &principal, realm,
KRB5_WELLKNOWN_NAME, KRB5_ANON_NAME, NULL);
if (ret)
krb5_err(context, 1, ret, "krb5_make_principal");
krb5_principal_set_type(context, principal, KRB5_NT_WELLKNOWN);
anonymous_pkinit = TRUE;
} else if (use_keytab || keytab_str) {
get_princ_kt(context, &principal, argv[0]);
} else {

View File

@@ -148,6 +148,19 @@ Forward credentials to remote host (for
.Xr rsh 1 ,
.Xr telnet 1 ,
etc).
.It Li historical_anon_pkinit = Va boolean
Enable legacy anonymous pkinit command-line syntax.
With this option set to
.Li true,
the
.Xr kinit 1
.Fl Fl anonymous
command with no principal argument specified will request an anonymous pkinit
ticket from the default realm.
If a principal argument is specified, it is used as an explicit realm name for
anonymous pkinit even without an
.Li @
prefix.
.El
.It Li [libdefaults]
.Bl -tag -width "xxx" -offset indent
@@ -654,8 +667,21 @@ Allow address-less tickets.
.\" XXX
.It Li allow-anonymous = Va BOOL
If the kdc is allowed to hand out anonymous tickets.
.It Li historical_anon_realm = Va boolean
Enables pre-7.0 non-RFC-comformant KDC behavior.
With this option set to
.Li true
the client realm in anonymous pkinit AS replies will be the requested realm,
rather than the RFC-conformant
.Li WELLKNOWN:ANONYMOUS
realm.
This can have a security impact on servers that expect to grant access to
anonymous-but-authenticated to the KDC users of the realm in question:
they would also grant access to unauthenticated anonymous users.
As such, it is not recommend to set this option to
.Li true.
.It Li encode_as_rep_as_tgs_rep = Va BOOL
Encode as-rep as tgs-rep tobe compatible with mistakes older DCE secd did.
Encode as-rep as tgs-rep to be compatible with mistakes older DCE secd did.
.\" XXX
.It Li kdc_warn_pwexpire = Va TIME
The time before expiration that the user should be warned that her

View File

@@ -960,8 +960,11 @@ typedef struct krb5_name_canon_iterator_data *krb5_name_canon_iterator;
*/
#define KRB5_ANON_MATCH_AUTHENTICATED 1 /* authenticated with anon flag */
#define KRB5_ANON_MATCH_UNAUTHENTICATED 2 /* anonymous PKINIT */
#define KRB5_ANON_MATCH_ANY ( KRB5_ANON_MATCH_AUTHENTICATED | KRB5_ANON_MATCH_UNAUTHENTICATED )
#define KRB5_ANON_IGNORE_NAME_TYPE 4 /* don't check the name type */
#define KRB5_ANON_MATCH_ANY ( KRB5_ANON_MATCH_AUTHENTICATED | \
KRB5_ANON_MATCH_UNAUTHENTICATED )
#define KRB5_ANON_MATCH_ANY_NONT ( KRB5_ANON_MATCH_ANY | \
KRB5_ANON_IGNORE_NAME_TYPE )
/*
*

View File

@@ -1258,19 +1258,43 @@ krb5_principal_is_anonymous(krb5_context context,
krb5_const_principal p,
unsigned int flags)
{
int anon_realm;
/*
* Heimdal versions 7.5 and below left the name-type at KRB5_NT_PRINCIPAL
* even with anonymous pkinit responses. To retain interoperability with
* legacy KDCs, the name-type is not checked by the client after requesting
* a fully anonymous ticket.
*/
if (!(flags & KRB5_ANON_IGNORE_NAME_TYPE) &&
p->name.name_type != KRB5_NT_WELLKNOWN &&
p->name.name_type != KRB5_NT_UNKNOWN)
return FALSE;
if ((p->name.name_type != KRB5_NT_WELLKNOWN &&
p->name.name_type != KRB5_NT_UNKNOWN) ||
p->name.name_string.len != 2 ||
if (p->name.name_string.len != 2 ||
strcmp(p->name.name_string.val[0], KRB5_WELLKNOWN_NAME) != 0 ||
strcmp(p->name.name_string.val[1], KRB5_ANON_NAME) != 0)
return FALSE;
anon_realm = strcmp(p->realm, KRB5_ANON_REALM) == 0;
/*
* While unauthenticated clients SHOULD get "WELLKNOWN:ANONYMOUS" as their
* realm, Heimdal KDCs prior to 7.0 returned the requested realm. While
* such tickets might lead *servers* to unwittingly grant access to fully
* anonymous clients, trusting that the client was authenticated to the
* realm in question, doing it right is the KDC's job, the client should
* not refuse such a ticket.
*
* If we ever do decide to enforce WELLKNOWN:ANONYMOUS for unauthenticated
* clients, it is essential that calls that pass KRB5_ANON_MATCH_ANY still
* ignore the realm, as in that case either case matches one of the two
* possible conditions.
*/
if (flags & KRB5_ANON_MATCH_UNAUTHENTICATED)
return TRUE;
return ((flags & KRB5_ANON_MATCH_AUTHENTICATED) && !anon_realm) ||
((flags & KRB5_ANON_MATCH_UNAUTHENTICATED) && anon_realm);
/*
* Finally, authenticated clients that asked to be only anonymized do
* legitimately expect a non-anon realm.
*/
return strcmp(p->realm, KRB5_ANON_REALM) != 0;
}
static int

View File

@@ -541,10 +541,22 @@ check_client_anonymous(krb5_context context,
if (!rep->enc_part.flags.anonymous)
return KRB5KDC_ERR_BADOPTION;
/*
* Here we must validate that the AS returned a ticket of the expected type
* for either a fully anonymous request, or authenticated request for an
* anonymous ticket. If this is a TGS request, we're done. Then if the
* 'requested' principal was anonymous, we'll check the 'mapped' principal
* accordingly (without enforcing the name type and perhaps the realm).
* Finally, if the 'requested' principal was not anonymous, well check
* that the 'mapped' principal has an anonymous name and type, in a
* non-anonymous realm. (Should we also be checking for a realm match
* between the request and the mapped name in this case?)
*/
if (is_tgs_rep)
flags = KRB5_ANON_MATCH_ANY;
else if (krb5_principal_is_anonymous(context, requested, KRB5_ANON_MATCH_ANY))
flags = KRB5_ANON_MATCH_UNAUTHENTICATED;
flags = KRB5_ANON_MATCH_ANY_NONT;
else if (krb5_principal_is_anonymous(context, requested,
KRB5_ANON_MATCH_ANY_NONT))
flags = KRB5_ANON_MATCH_UNAUTHENTICATED | KRB5_ANON_IGNORE_NAME_TYPE;
else
flags = KRB5_ANON_MATCH_AUTHENTICATED;
@@ -566,7 +578,8 @@ check_client_mismatch(krb5_context context,
krb5_keyblock const * key)
{
if (rep->enc_part.flags.anonymous) {
if (!krb5_principal_is_anonymous(context, mapped, KRB5_ANON_MATCH_ANY)) {
if (!krb5_principal_is_anonymous(context, mapped,
KRB5_ANON_MATCH_ANY_NONT)) {
krb5_set_error_message(context, KRB5KRB_AP_ERR_MODIFIED,
N_("Anonymous ticket does not contain anonymous "
"principal", ""));