diff --git a/kdc/default_config.c b/kdc/default_config.c index 87413663f..2a54f6c5b 100644 --- a/kdc/default_config.c +++ b/kdc/default_config.c @@ -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, diff --git a/kdc/kdc.8 b/kdc/kdc.8 index 312996434..150a3f18a 100644 --- a/kdc/kdc.8 +++ b/kdc/kdc.8 @@ -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. diff --git a/kdc/kdc.h b/kdc/kdc.h index 2a1671aa8..be4257ff2 100644 --- a/kdc/kdc.h +++ b/kdc/kdc.h @@ -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; diff --git a/kdc/kerberos5.c b/kdc/kerberos5.c index 8e07442ac..b98142d3c 100644 --- a/kdc/kerberos5.c +++ b/kdc/kerberos5.c @@ -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) diff --git a/kdc/pkinit.c b/kdc/pkinit.c index dbe833f07..67026ef33 100644 --- a/kdc/pkinit.c +++ b/kdc/pkinit.c @@ -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(""); diff --git a/kuser/kinit.1 b/kuser/kinit.1 index 65d733d96..bb3c90af1 100644 --- a/kuser/kinit.1 +++ b/kuser/kinit.1 @@ -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 diff --git a/kuser/kinit.c b/kuser/kinit.c index b8244f7e2..da62a509e 100644 --- a/kuser/kinit.c +++ b/kuser/kinit.c @@ -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 { diff --git a/lib/krb5/krb5.conf.5 b/lib/krb5/krb5.conf.5 index 4f7e9aee3..63ee8cfd7 100644 --- a/lib/krb5/krb5.conf.5 +++ b/lib/krb5/krb5.conf.5 @@ -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 diff --git a/lib/krb5/krb5.h b/lib/krb5/krb5.h index 315c0ae34..ea32052f0 100644 --- a/lib/krb5/krb5.h +++ b/lib/krb5/krb5.h @@ -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 ) /* * diff --git a/lib/krb5/principal.c b/lib/krb5/principal.c index c728e5673..c553244c6 100644 --- a/lib/krb5/principal.c +++ b/lib/krb5/principal.c @@ -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 diff --git a/lib/krb5/ticket.c b/lib/krb5/ticket.c index bf8b6a083..bb32e1141 100644 --- a/lib/krb5/ticket.c +++ b/lib/krb5/ticket.c @@ -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", ""));