diff --git a/kdc/httpkadmind.8 b/kdc/httpkadmind.8 index 08edce41f..90b4f63aa 100644 --- a/kdc/httpkadmind.8 +++ b/kdc/httpkadmind.8 @@ -160,6 +160,23 @@ but using .Nm [ext_keytab] as the .Nm krb5.conf(5) section. +Clients with host-based principals for the the host service can +create and extract keys for their own service name, but otherwise +a number of service names are not denied: +.Bl -tag -width Ds -offset indent +.It Dq host +.It Dq root +.It Dq exceed +.El +as well as all the service names for Heimdal-specific services: +.Bl -tag -width Ds -offset indent +.It Dq krbtgt +.It Dq iprop +.It Dq kadmin +.It Dq hprop +.It Dq WELLKNOWN +.It Dq K +.El .Pp Supported options: .Bl -tag -width Ds -offset indent diff --git a/kdc/httpkadmind.c b/kdc/httpkadmind.c index 13b5528e1..b21d034eb 100644 --- a/kdc/httpkadmind.c +++ b/kdc/httpkadmind.c @@ -171,6 +171,7 @@ typedef struct kadmin_request_desc { unsigned int revoke:1; unsigned int create:1; unsigned int ro:1; + unsigned int is_self:1; char frombuf[128]; } *kadmin_request_desc; @@ -875,7 +876,7 @@ check_service_name(kadmin_request_desc r, const char *name) return 0; krb5_set_error_message(r->context, EACCES, "No one is allowed to fetch keys for " - "Heimdal service %s because of authorizer " + "service \"%s\" because of authorizer " "limitations", name); return EACCES; } @@ -916,6 +917,8 @@ param_cb(void *d, strcmp(key, "rotate") == 0 || strcmp(key, "create") == 0 || strcmp(key, "ro") == 0) { + heim_audit_addkv((heim_svc_req_desc)r, KDC_AUDIT_VIS, + "requested_option", "%s", key); if (!val || strcmp(val, "true") != 0) krb5_set_error_message(r->context, ret = EINVAL, "get-keys \"%s\" q-param accepts " @@ -930,28 +933,38 @@ param_cb(void *d, r->create = 1; else if (strcmp(key, "ro") == 0) r->ro = 1; - if (ret == 0) - heim_audit_addkv((heim_svc_req_desc)r, KDC_AUDIT_VIS, - "requested_option", "%s", key); } else if (strcmp(key, "dNSName") == 0 && val) { - s = heim_string_create(val); - if (!s) - ret = krb5_enomem(r->context); - else - ret = heim_array_append_value(r->hostnames, s); heim_audit_addkv((heim_svc_req_desc)r, KDC_AUDIT_VIS, "requested_dNSName", "%s", val); - ret = hx509_request_add_dns_name(r->context->hx509ctx, r->req, val); + if (r->is_self) { + krb5_set_error_message(r->context, ret = EACCES, + "only one service may be requested for self"); + } else if (strchr(val, '.') == NULL) { + krb5_set_error_message(r->context, ret = EACCES, + "dNSName must have at least one '.' in it"); + } else { + s = heim_string_create(val); + if (!s) + ret = krb5_enomem(r->context); + else + ret = heim_array_append_value(r->hostnames, s); + } + if (ret == 0) + ret = hx509_request_add_dns_name(r->context->hx509ctx, r->req, val); } else if (strcmp(key, "service") == 0 && val) { - ret = check_service_name(r, val); + heim_audit_addkv((heim_svc_req_desc)r, KDC_AUDIT_VIS, + "requested_service", "%s", val); + if (r->is_self) + krb5_set_error_message(r->context, ret = EACCES, + "use \"spn\" for self"); + else + ret = check_service_name(r, val); if (ret == 0) { s = heim_string_create(val); if (!s) ret = krb5_enomem(r->context); else ret = heim_array_append_value(r->service_names, s); - heim_audit_addkv((heim_svc_req_desc)r, KDC_AUDIT_VIS, - "requested_service", "%s", val); } } else if (strcmp(key, "enctypes") == 0 && val) { r->enctypes = strdup(val); @@ -959,6 +972,11 @@ param_cb(void *d, ret = krb5_enomem(r->context); heim_audit_addkv((heim_svc_req_desc)r, KDC_AUDIT_VIS, "requested_enctypes", "%s", val); + } else if (r->is_self && strcmp(key, "spn") == 0 && val) { + heim_audit_addkv((heim_svc_req_desc)r, KDC_AUDIT_VIS, + "requested_spn", "%s", val); + krb5_set_error_message(r->context, ret = EACCES, + "only one service may be requested for self"); } else if (strcmp(key, "spn") == 0 && val) { krb5_principal p = NULL; const char *hostname = ""; @@ -971,17 +989,44 @@ param_cb(void *d, if (ret == 0 && krb5_principal_get_realm(r->context, p) == NULL) ret = krb5_principal_set_realm(r->context, p, r->realm ? r->realm : realm); + + /* + * The SPN has to have two components. + * + * TODO: Support more components? Support AD-style NetBIOS computer + * account names? + */ if (ret == 0 && krb5_principal_get_num_comp(r->context, p) != 2) ret = ENOTSUP; - if (ret == 0) + + /* + * Allow only certain service names. Except that when + * the SPN == the requestor's principal name then allow the "host" + * service name. + */ + if (ret == 0) { + const char *service = + krb5_principal_get_comp_string(r->context, p, 0); + + if (strcmp(service, "host") == 0 && + krb5_principal_compare(r->context, p, r->cprinc) && + !r->is_self && + heim_array_get_length(r->hostnames) == 0 && + heim_array_get_length(r->spns) == 0) { + r->is_self = 1; + } else + ret = check_service_name(r, service); + } + if (ret == 0 && !krb5_principal_compare(r->context, p, r->cprinc)) ret = check_service_name(r, krb5_principal_get_comp_string(r->context, p, 0)); - if (ret == 0) + if (ret == 0) { hostname = krb5_principal_get_comp_string(r->context, p, 1); - if (!hostname || !strchr(hostname, '.')) - krb5_set_error_message(r->context, ret = ENOTSUP, - "Only host-based service names supported"); + if (!hostname || !strchr(hostname, '.')) + krb5_set_error_message(r->context, ret = ENOTSUP, + "Only host-based service names supported"); + } if (ret == 0 && r->realm) ret = krb5_principal_set_realm(r->context, p, r->realm); else if (ret == 0 && realm) @@ -1016,19 +1061,25 @@ authorize_req(kadmin_request_desc r) { krb5_error_code ret; + r->is_self = 0; ret = hx509_request_init(r->context->hx509ctx, &r->req); if (ret) return bad_enomem(r, ret); (void) MHD_get_connection_values(r->connection, MHD_GET_ARGUMENT_KIND, param_cb, r); ret = r->ret; + if (ret == EACCES) + return bad_403(r, ret, "Not authorized to requested principal(s)"); if (ret) return bad_req(r, ret, MHD_HTTP_SERVICE_UNAVAILABLE, "Could not handle query parameters"); - ret = kdc_authorize_csr(r->context, "ext_keytab", r->req, r->cprinc); + if (r->is_self) + ret = 0; + else + ret = kdc_authorize_csr(r->context, "ext_keytab", r->req, r->cprinc); if (ret == EACCES || ret == EINVAL || ret == ENOTSUP || ret == KRB5KDC_ERR_POLICY) - return bad_403(r, ret, "Not authorized to requested certificate"); + return bad_403(r, ret, "Not authorized to requested principal(s)"); if (ret) return bad_req(r, ret, MHD_HTTP_SERVICE_UNAVAILABLE, "Error checking authorization"); diff --git a/tests/kdc/check-httpkadmind.in b/tests/kdc/check-httpkadmind.in index 1962a3ff3..b593925a3 100644 --- a/tests/kdc/check-httpkadmind.in +++ b/tests/kdc/check-httpkadmind.in @@ -65,7 +65,8 @@ restport2=@restport2@ server=datan.test.h5l.se otherserver=other.test.h5l.se cache="FILE:${objdir}/cache.krb5" -admincache="FILE:${objdir}/cache2.krb5" +cache2="FILE:${objdir}/cache2.krb5" +admincache="FILE:${objdir}/cache3.krb5" keyfile="${hx509_data}/key.der" keyfile2="${hx509_data}/key2.der" kt=${objdir}/kt @@ -81,10 +82,14 @@ kadmind="${kadmind} --keytab=${keytab} --detach -p $admport" httpkadmind2="${httpkadmind} --reverse-proxied -T Negotiate -p $restport2" httpkadmind="${httpkadmind} --reverse-proxied -T Negotiate -p $restport1" +kinit2="${kinit} -c $cache2 ${afs_no_afslog}" kinit="${kinit} -c $cache ${afs_no_afslog}" adminklist="${klist} --hidden -v -c $admincache" +klist2="${klist} --hidden -v -c $cache2" klist="${klist} --hidden -v -c $cache" +kgetcred2="${kgetcred} -c $cache2" kgetcred="${kgetcred} -c $cache" +kdestroy2="${kdestroy} -c $cache2 ${afs_no_unlog}" kdestroy="${kdestroy} -c $cache ${afs_no_unlog}" kx509="${kx509} -c $cache" @@ -159,11 +164,11 @@ get_keytab_POST() { get_keytab "$q" -X POST --data-binary @/dev/null -f "$@" && { echo "POST succeeded w/o CSRF token!"; return 1; } get_keytab "$q" -X POST --data-binary @/dev/null -D response-headers "$@" - grep ^X-CSRF-Token: response-headers >/dev/null || - { echo "POST w/o CSRF token had response w/o CSRF token!"; return 1; } - get_keytab "$q" -X POST --data-binary @/dev/null -f \ + grep ^X-CSRF-Token: response-headers >/dev/null || return 1 + get_keytab "$q" -X POST --data-binary @/dev/null -D response-headers \ -H "$(sed -e 's/\r//' response-headers | grep ^X-CSRF-Token:)" "$@" - return $? + grep '^HTTP/1.1 200' response-headers >/dev/null || return $? + return 0 } get_keytab_POST_redir() { @@ -481,6 +486,38 @@ $kimpersonate --ticket-flags=initial --ccache=$admincache -k $keytab -t aes128-c $adminklist -t >/dev/null || { echo "failed to setup kimpersonate credentials"; exit 2; } + +echo "Making PKINIT certs for KDC" +${hxtool} issue-certificate \ + --self-signed \ + --issue-ca \ + --ca-private-key=FILE:${keyfile} \ + --subject="CN=CA,DC=test,DC=h5l,DC=se" \ + --certificate="FILE:ca.crt" || exit 1 +${hxtool} request-create \ + --subject="CN=kdc,DC=test,DC=h5l,DC=se" \ + --key=FILE:${keyfile2} \ + req-kdc.der || exit 1 +${hxtool} issue-certificate \ + --ca-certificate=FILE:$objdir/ca.crt,${keyfile} \ + --type="pkinit-kdc" \ + --pk-init-principal="krbtgt/TEST.H5L.SE@TEST.H5L.SE" \ + --req="PKCS10:req-kdc.der" \ + --certificate="FILE:kdc.crt" || exit 1 +${hxtool} request-create \ + --subject="CN=bar,DC=test,DC=h5l,DC=se" \ + --key=FILE:${keyfile2} \ + req-pkinit.der || + { echo "Failed to make CSR for PKINIT client cert"; exit 1; } +${hxtool} issue-certificate \ + --ca-certificate=FILE:$objdir/ca.crt,${keyfile} \ + --type="pkinit-client" \ + --pk-init-principal="host/synthesized.${domain}@$R" \ + --req="PKCS10:req-pkinit.der" \ + --lifetime=7d \ + --certificate="FILE:pkinit-synthetic.crt" || + { echo "Failed to make PKINIT client cert"; exit 1; } + echo "Starting kdc needed for httpkadmind authentication to kadmind" ${kdc} --detach --testing || { echo "kdc failed to start"; exit 1; } kdcpid=`getpid kdc` @@ -585,6 +622,50 @@ cmp extracted_keytab.rest1 extracted_keytab.rest2 > /dev/null && test "$(grep $p extracted_keytab.rest2 | wc -l)" -eq 3 || { echo "Wrong number of new keys!"; exit 1; } +echo "Checking that host services as clients can self-serve" +hn=synthesized.${domain} +p=host/$hn +KRB5CCNAME=$admincache ${kadmin} get -s $p && + { echo "Internal error -- $p exists too soon"; exit 1; } +${kinit2} -C "FILE:${objdir}/pkinit-synthetic.crt,${keyfile2}" ${p}@${R} || \ + { echo "Failed to kinit with PKINIT client cert"; exit 1; } +${kgetcred2} HTTP/localhost@$R || echo WAT +${klist2} +rm -f extracted_keytab* +KRB5CCNAME=$cache2 \ +get_keytab_POST "spn=$p&create=true" -s -o "${objdir}/extracted_keytab" || + { echo "Failed to create and extract host keys for self"; exit 1; } +${ktutil} -k "${objdir}/extracted_keytab" list || + { echo "Failed to create and extract host keys for self (bogus keytab)"; exit 1; } +KRB5CCNAME=$admincache ${kadmin} get -s $p || + { echo "Failed to create and extract host keys for self"; exit 1; } + +echo "Checking that host services can't get other host service principals" +hn=nonexistent.${domain} +p=host/$hn +KRB5CCNAME=$cache2 \ +get_keytab_POST "spn=$p&create=true" -s -o "${objdir}/extracted_keytab2" && + { echo "Failed to fail to create and extract host keys for other!"; exit 1; } +${ktutil} -k "${objdir}/extracted_keytab2" list || true +KRB5CCNAME=$admincache ${kadmin} get -s $p && + { echo "Failed to fail to create and extract host keys for other!"; exit 1; } + +echo "Checking that host services can't get keys for themselves and others" +hn=synthesized.${domain} +p=host/$hn +p2=host/nonexistent.${domain} +${kinit2} -C "FILE:${objdir}/pkinit-synthetic.crt,${keyfile2}" ${p}@${R} || \ + { echo "Failed to kinit with PKINIT client cert"; exit 1; } +${kgetcred2} HTTP/localhost@$R || echo WAT +${klist2} +rm -f extracted_keytab* +KRB5CCNAME=$cache2 \ +get_keytab_POST "spn=$p&spn=$p2&create=true" -s -o "${objdir}/extracted_keytab" && + { echo "Failed to fail to create and extract host keys for other!"; exit 1; } +${ktutil} -k "${objdir}/extracted_keytab2" list || true +KRB5CCNAME=$admincache ${kadmin} get -s $p2 && + { echo "Failed to fail to create and extract host keys for other!"; exit 1; } + grep 'Internal error' messages.log && { echo "Internal errors in log"; exit 1; } @@ -593,7 +674,10 @@ sh ${leaks_kill} kadmind $kadmindpid || ec=1 sh ${leaks_kill} kadmind $kadmind2pid || ec=1 sh ${leaks_kill} kdc $kdcpid || ec=1 -trap "" EXIT +if [ $ec = 0 ]; then + trap "" EXIT + echo "Success" +fi # TODO # diff --git a/tests/kdc/krb5-httpkadmind.conf.in b/tests/kdc/krb5-httpkadmind.conf.in index e3adbd2bb..4882d52f5 100644 --- a/tests/kdc/krb5-httpkadmind.conf.in +++ b/tests/kdc/krb5-httpkadmind.conf.in @@ -7,8 +7,8 @@ name_canon_rules = as-is:realm=TEST.H5L.SE [appdefaults] - pkinit_anchors = FILE:@objdir@/pkinit-anchor.pem - pkinit_pool = FILE:@objdir@/pkinit-anchor.pem + pkinit_anchors = FILE:@objdir@/ca.crt + pkinit_pool = FILE:@objdir@/ca.crt [realms] TEST.H5L.SE = { @@ -19,9 +19,10 @@ [kdc] num-kdc-processes = 1 strict-nametypes = true + synthetic_clients = true enable-pkinit = true - pkinit_identity = PEM-FILE:@objdir@/user-issuer.pem - pkinit_anchors = PEM-FILE:@objdir@/pkinit-anchor.pem + pkinit_identity = FILE:@objdir@/kdc.crt,@srcdir@/../../lib/hx509/data/key2.der + pkinit_anchors = FILE:@objdir@/ca.crt pkinit_mappings_file = @srcdir@/pki-mapping # Locate kdc plugins for testing @@ -29,11 +30,6 @@ # Configure kdc plugins for testing simple_csr_authorizer_directory = @objdir@/simple_csr_authz - - enable-pkinit = true - pkinit_identity = PEM-FILE:@objdir@/user-issuer.pem - pkinit_anchors = PEM-FILE:@objdir@/pkinit-anchor.pem - pkinit_mappings_file = @srcdir@/pki-mapping database = { dbname = @objdir@/current-db