diff --git a/kdc/bx509d.c b/kdc/bx509d.c index b8b8bbbfe..93bbfb6cb 100644 --- a/kdc/bx509d.c +++ b/kdc/bx509d.c @@ -172,6 +172,7 @@ validate_token(struct MHD_Connection *connection, krb5_data tok; size_t host_len, brk, i; + *cprinc_from_token = NULL; memset(token_times, 0, sizeof(*token_times)); ret = get_krb5_context(&context); if (ret) @@ -257,8 +258,7 @@ generate_key(hx509_context context, if (ret == 0) ret = hx509_certs_add(context, certs, cert); if (ret == 0) - ret = hx509_certs_store(context, certs, - HX509_CERTS_STORE_NO_PRIVATE_KEYS, NULL); + ret = hx509_certs_store(context, certs, 0, NULL); if (ret) hx509_err(context, 1, ret, "Could not generate and save private key " "for %s", key_name); @@ -630,16 +630,33 @@ update_and_authorize_CSR(krb5_context context, return ret; } +/* + * hx509_certs_iter_f() callback to assign a private key to the first cert in a + * store. + */ +static int HX509_LIB_CALL +set_priv_key(hx509_context context, void *d, hx509_cert c) +{ + (void) _hx509_cert_assign_key(c, (hx509_private_key)d); + return -1; /* stop iteration */ +} + static krb5_error_code -store_certs(hx509_context context, const char *store, hx509_certs store_these) +store_certs(hx509_context context, + const char *store, + hx509_certs store_these, + hx509_private_key key) { krb5_error_code ret; hx509_certs certs = NULL; ret = hx509_certs_init(context, store, HX509_CERTS_CREATE, NULL, &certs); - if (ret == 0) + if (ret == 0) { + if (key) + (void) hx509_certs_iter_f(context, store_these, set_priv_key, key); hx509_certs_merge(context, certs, store_these); + } if (ret == 0) hx509_certs_store(context, certs, 0, NULL); hx509_certs_free(&certs); @@ -716,7 +733,7 @@ do_CA(krb5_context context, return bad_500(connection, ret, "Could not create PEM store for issued certificate"); - ret = store_certs(context->hx509ctx, *pkix_store, certs); + ret = store_certs(context->hx509ctx, *pkix_store, certs, NULL); hx509_certs_free(&certs); if (ret) { (void) unlink(strchr(*pkix_store, ':') + 1); @@ -873,11 +890,13 @@ find_ccache(krb5_context context, const char *princ, char **ccname) /* Check if we have a good enough credential */ if (ret == 0 && - (ret = krb5_cc_get_lifetime(context, cc, &life)) == 0 && life > 60) + (ret = krb5_cc_get_lifetime(context, cc, &life)) == 0 && life > 60) { + krb5_cc_close(context, cc); return 0; + } if (cc) krb5_cc_close(context, cc); - return ret; + return ret ? ret : ENOENT; } /* @@ -897,7 +916,7 @@ do_pkinit(krb5_context context, { krb5_get_init_creds_opt *opt = NULL; krb5_init_creds_context ctx = NULL; - krb5_error_code ret = ENOMEM; + krb5_error_code ret = 0; krb5_ccache temp_cc = NULL; krb5_ccache cc = NULL; krb5_principal p = NULL; @@ -908,12 +927,21 @@ do_pkinit(krb5_context context, char *temp_ccname = NULL; int fd = -1; - if ((ret = krb5_cc_resolve(context, ccname, &cc))) - return ret; - /* - * Avoid nasty race conditions and ccache file corruption, take an flock on - * temp_ccname and do the cleanup dance. + * Open and lock a .new ccache file. Use .new to avoid garbage files on + * crash. + * + * We can race with other threads to do this, so we loop until we + * definitively win or definitely lose the race. We win when we have a) an + * open FD that is b) flock'ed, and c) we observe with lstat() that the + * file we opened and locked is the same as on disk after locking. + * + * We don't close the FD until we're done. + * + * If we had a proper anon MEMORY ccache, we could instead use that for a + * temporary ccache, and then the initialization of and move to the final + * FILE ccache would take care to mkstemp() and rename() into place. + * fcc_open() basically does a similar thing. */ if (asprintf(&temp_ccname, "%s.ccnew", ccname) == -1 || temp_ccname == NULL) @@ -922,7 +950,7 @@ do_pkinit(krb5_context context, fn = temp_ccname + sizeof("FILE:") - 1; if (ret == 0) do { /* - * Open and flock the file. + * Open and flock the temp ccache file. * * XXX We should really a) use _krb5_xlock(), or move that into * lib/roken anyways, b) abstract this loop into a utility function in @@ -952,9 +980,9 @@ do_pkinit(krb5_context context, if (ret == 0) ret = krb5_cc_resolve(context, temp_ccname, &temp_cc); if (ret == 0) - ret = krb5_cc_get_lifetime(context, cc, &life); + ret = krb5_cc_get_lifetime(context, temp_cc, &life); if (ret == 0 && life > 60) - goto out; /* We lost the race, we get to do less work */ + goto out; /* We lost the race, but we win: we get to do less work */ /* * We won the race. Setup to acquire Kerberos creds with PKINIT. @@ -968,7 +996,6 @@ do_pkinit(krb5_context context, if (ret == 0) crealm = krb5_principal_get_realm(context, p); if (ret == 0 && - (ret = krb5_cc_initialize(context, temp_cc, p)) == 0 && (ret = krb5_get_init_creds_opt_alloc(context, &opt)) == 0) krb5_get_init_creds_opt_set_default_flags(context, "kinit", crealm, opt); @@ -976,9 +1003,9 @@ do_pkinit(krb5_context context, (ret = krb5_get_init_creds_opt_set_addressless(context, opt, 1)) == 0) ret = krb5_get_init_creds_opt_set_pkinit(context, opt, p, pkix_store, - NULL, /* XXX pkinit_anchor */ - NULL, /* XXX anchor_chain */ - NULL, /* XXX pkinit_crl */ + NULL, /* pkinit_anchor */ + NULL, /* anchor_chain */ + NULL, /* pkinit_crl */ 0, /* flags */ NULL, /* prompter */ NULL, /* prompter data */ @@ -992,11 +1019,14 @@ do_pkinit(krb5_context context, /* * Finally, do the AS exchange w/ PKINIT, extract the new Kerberos creds - * into temp_cc, and rename into place. + * into temp_cc, and rename into place. Note that krb5_cc_move() closes + * the source ccache, so we set temp_cc = NULL if it succeeds. */ if (ret == 0 && (ret = krb5_init_creds_get(context, ctx)) == 0 && + (ret = krb5_cc_initialize(context, temp_cc, p)) == 0 && (ret = krb5_init_creds_store(context, ctx, temp_cc)) == 0 && + (ret = krb5_cc_resolve(context, ccname, &cc)) == 0 && (ret = krb5_cc_move(context, temp_cc, cc)) == 0) temp_cc = NULL; @@ -1005,10 +1035,9 @@ out: krb5_init_creds_free(context, ctx); krb5_get_init_creds_opt_free(context, opt); krb5_free_principal(context, p); - if (temp_cc) - krb5_cc_close(context, temp_cc); - if (cc) - krb5_cc_close(context, cc); + krb5_cc_close(context, temp_cc); + krb5_cc_close(context, cc); + free(temp_ccname); if (fd != -1) (void) close(fd); /* Drops the flock */ return ret; @@ -1067,6 +1096,9 @@ bnegotiate_do_CA(krb5_context context, ret = krb5_parse_name(context, princ, &p); if (ret == 0) hx509_private_key2SPKI(context->hx509ctx, key, &spki); + if (ret == 0) + hx509_request_set_SubjectPublicKeyInfo(context->hx509ctx, req, &spki); + free_SubjectPublicKeyInfo(&spki); if (ret == 0) ret = hx509_request_add_pkinit(context->hx509ctx, req, princ); if (ret == 0) @@ -1085,23 +1117,29 @@ bnegotiate_do_CA(krb5_context context, if (ret == 0) ret = kdc_issue_certificate(context, kdc_config, req, p, token_times, 1 /* send_chain */, &certs); - hx509_private_key_free(&key); krb5_free_principal(context, p); hx509_request_free(&req); p = NULL; - if (ret == KRB5KDC_ERR_POLICY) + if (ret == KRB5KDC_ERR_POLICY) { + hx509_private_key_free(&key); return bad_500(connection, ret, "Certificate request denied for policy reasons"); - if (ret == ENOMEM) + } + if (ret == ENOMEM) { + hx509_private_key_free(&key); return bad_503(connection, ret, "Certificate issuance failed"); - if (ret) + } + if (ret) { + hx509_private_key_free(&key); return bad_500(connection, ret, "Certificate issuance failed"); + } /* Setup PKIX store and extract the certificate chain into it */ ret = mk_pkix_store(pkix_store); if (ret == 0) - ret = store_certs(context->hx509ctx, *pkix_store, certs); + ret = store_certs(context->hx509ctx, *pkix_store, certs, key); + hx509_private_key_free(&key); hx509_certs_free(&certs); if (ret) { (void) unlink(strchr(*pkix_store, ':') + 1); @@ -1145,7 +1183,7 @@ bnegotiate_get_creds(struct MHD_Connection *connection, if (ret == 0 && (ret = do_pkinit(context, subject_cprinc, pkix_store, *ccname))) ret = bad_403(connection, ret, - "Could not acquire PKIX credentials using PKINIT"); + "Could not acquire Kerberos credentials using PKINIT"); free(pkix_store); return ret; @@ -1223,22 +1261,6 @@ bad_req_gss(struct MHD_Connection *connection, return ret; } -static gss_OID -get_name_type(struct MHD_Connection *connection) -{ - const char *nt; - - nt = MHD_lookup_connection_value(connection, MHD_GET_ARGUMENT_KIND, - "nametype"); - if (nt == NULL || strcmp(nt, "hostbased-service") == 0) - return GSS_C_NT_HOSTBASED_SERVICE; - if (strcmp(nt, "exported-name") == 0) - return GSS_C_NT_EXPORT_NAME; - if (strcmp(nt, "krb5") == 0) - return GSS_KRB5_NT_PRINCIPAL_NAME; - return GSS_C_NO_OID; -} - /* Make an HTTP/Negotiate token */ static krb5_error_code mk_nego_tok(struct MHD_Connection *connection, @@ -1257,14 +1279,11 @@ mk_nego_tok(struct MHD_Connection *connection, gss_name_t iname = GSS_C_NO_NAME; gss_name_t aname = GSS_C_NO_NAME; OM_uint32 major, minor, junk; - gss_OID nt; krb5_error_code ret; /* More like a system error code here */ char *token_b64 = NULL; *nego_tok = NULL; *nego_toksz = 0; - if ((nt = get_name_type(connection)) == GSS_C_NO_OID) - return bad_400(connection, EINVAL, "unknown GSS name type in request"); /* Import initiator name */ name.length = strlen(cprinc); @@ -1279,7 +1298,7 @@ mk_nego_tok(struct MHD_Connection *connection, /* Import target acceptor name */ name.length = strlen(target); name.value = rk_UNCONST(target); - major = gss_import_name(&minor, &name, nt, &aname); + major = gss_import_name(&minor, &name, GSS_C_NT_HOSTBASED_SERVICE, &aname); if (major != GSS_S_COMPLETE) { (void) gss_release_name(&junk, &iname); return bad_req_gss(connection, major, minor, GSS_C_NO_OID, @@ -1304,6 +1323,7 @@ mk_nego_tok(struct MHD_Connection *connection, GSS_KRB5_MECHANISM, 0, GSS_C_INDEFINITE, NULL, GSS_C_NO_BUFFER, NULL, &token, NULL, NULL); + (void) gss_delete_sec_context(&junk, &ctx, GSS_C_NO_BUFFER); (void) gss_release_name(&junk, &aname); (void) gss_release_cred(&junk, &cred); if (major != GSS_S_COMPLETE) @@ -1325,14 +1345,103 @@ mk_nego_tok(struct MHD_Connection *connection, return 0; } +static krb5_error_code +bnegotiate_get_target(struct MHD_Connection *connection, + const char **out_target, + const char **out_redir, + char **freeme) +{ + const char *target; + const char *redir; + const char *referer; /* misspelled on the wire, misspelled here, FYI */ + const char *authority; + const char *local_part; + char *s1 = NULL; + char *s2 = NULL; + + *out_target = NULL; + *out_redir = NULL; + *freeme = NULL; + + target = MHD_lookup_connection_value(connection, MHD_GET_ARGUMENT_KIND, + "target"); + redir = MHD_lookup_connection_value(connection, MHD_GET_ARGUMENT_KIND, + "redirect"); + referer = MHD_lookup_connection_value(connection, MHD_HEADER_KIND, + "referer"); + if (target != NULL && redir == NULL) { + *out_target = target; + return 0; + } + if (target == NULL && redir == NULL) + return bad_400(connection, EINVAL, + "Query missing 'target' or 'redirect' parameter value"); + if (target != NULL && redir != NULL) + return bad_403(connection, EACCES, + "Only one of 'target' or 'redirect' parameter allowed"); + if (redir != NULL && referer == NULL) + return bad_403(connection, EACCES, + "Redirect request without Referer header nor allowed"); + + if (strncmp(referer, "https://", sizeof("https://") - 1) || + strncmp(redir, "https://", sizeof("https://") - 1)) + return bad_403(connection, EACCES, + "Redirect requests permitted only for https referrers"); + + /* Parse out authority from each URI, redirect and referrer */ + authority = redir + sizeof("https://") - 1; + if ((local_part = strchr(authority, '/')) == NULL) + local_part = authority + strlen(authority); + if ((s1 = strndup(authority, local_part - authority)) == NULL) + return bad_enomem(connection, ENOMEM); + + authority = referer + sizeof("https://") - 1; + if ((local_part = strchr(authority, '/')) == NULL) + local_part = authority + strlen(authority); + if ((s2 = strndup(authority, local_part - authority)) == NULL) { + free(s1); + return bad_enomem(connection, ENOMEM); + } + + /* Both must match */ + if (strcasecmp(s1, s2)) { + free(s2); + free(s1); + return bad_403(connection, EACCES, + "Redirect request does not match referer"); + } + free(s2); + + if (strchr(s1, '@')) { + free(s1); + return bad_403(connection, EACCES, + "Redirect request authority has login information"); + } + + /* Extract hostname portion of authority and format GSS name */ + if (strchr(s1, ':')) + *strchr(s1, ':') = '\0'; + if (asprintf(freeme, "HTTP@%s", s1) == -1 || *freeme == NULL) { + free(s1); + return bad_enomem(connection, ENOMEM); + } + + *out_target = *freeme; + *out_redir = redir; + free(s1); + return 0; +} + /* * Implements /bnegotiate end-point. * - * Query parameters: + * Query parameters (mutually exclusive): * - * - target= (REQUIRED) - * - nametype=hostbased-service|exported-name|krb5 (OPTIONAL) - * - redirect= (OPTIONAL) + * - target= + * - redirect= + * + * If the redirect query parameter is set then the Referer: header must be as + * well, and the authority of the redirect and Referer URIs must be the same. */ static krb5_error_code bnegotiate(struct MHD_Connection *connection) @@ -1345,21 +1454,18 @@ bnegotiate(struct MHD_Connection *connection) char *nego_tok = NULL; char *cprinc_from_token = NULL; char *ccname = NULL; + char *freeme = NULL; - target = MHD_lookup_connection_value(connection, MHD_GET_ARGUMENT_KIND, - "target"); - if (target == NULL) - return bad_400(connection, EINVAL, - "Query missing 'target' parameter value"); - redir = MHD_lookup_connection_value(connection, MHD_GET_ARGUMENT_KIND, - "redirect"); + /* bnegotiate_get_target() calls bad_req() */ + ret = bnegotiate_get_target(connection, &target, &redir, &freeme); + if (ret) + return ret == -1 ? MHD_NO : MHD_YES; - if ((ret = validate_token(connection, &token_times, &cprinc_from_token))) + if ((ret = validate_token(connection, &token_times, + &cprinc_from_token))) { + free(freeme); return ret; /* validate_token() calls bad_req() */ - - if (cprinc_from_token == NULL) - return bad_400(connection, EINVAL, - "Could not extract principal name from token"); + } /* * Make sure we have Kerberos credentials for cprinc. If we have them @@ -1390,6 +1496,7 @@ bnegotiate(struct MHD_Connection *connection) free(cprinc_from_token); free(nego_tok); free(ccname); + free(freeme); return ret == -1 ? MHD_NO : MHD_YES; } diff --git a/lib/gssapi/gss-token.c b/lib/gssapi/gss-token.c index 48b215421..1e825d95d 100644 --- a/lib/gssapi/gss-token.c +++ b/lib/gssapi/gss-token.c @@ -378,6 +378,7 @@ read_one_token(gss_name_t service, const char *ccname, int negotiate) return -1; } in.length = ret; + ret = 0; out.length = 0; out.value = 0; diff --git a/tests/kdc/Makefile.am b/tests/kdc/Makefile.am index 609a78ed2..0aca0c9db 100644 --- a/tests/kdc/Makefile.am +++ b/tests/kdc/Makefile.am @@ -44,6 +44,7 @@ TESTS = $(SCRIPT_TESTS) port = 49188 admport = 49189 pwport = 49190 +bx509port = 49191 if HAVE_DLOPEN do_dlopen = -e 's,[@]DLOPEN[@],true,g' @@ -57,6 +58,7 @@ do_subst = $(heim_verbose)sed $(do_dlopen) \ -e 's,[@]srcdir[@],$(srcdir),g' \ -e 's,[@]port[@],$(port),g' \ -e 's,[@]admport[@],$(admport),g' \ + -e 's,[@]bx509port[@],$(bx509port),g' \ -e 's,[@]pwport[@],$(pwport),g' \ -e 's,[@]objdir[@],$(top_builddir)/tests/kdc,g' \ -e 's,[@]top_builddir[@],$(top_builddir),g' \ diff --git a/tests/kdc/check-bx509.in b/tests/kdc/check-bx509.in index bf3630b40..78c5151f2 100644 --- a/tests/kdc/check-bx509.in +++ b/tests/kdc/check-bx509.in @@ -44,19 +44,23 @@ ${have_db} || exit 77 R=TEST.H5L.SE DCs="DC=test,DC=h5l,DC=se" -H=datan.test.h5l.se port=@port@ +bx509port=@bx509port@ -#kadmin="${kadmin} -l -r $R" -bx509="${bx509} --reverse-proxied -p $port" +kadmin="${kadmin} -l -r $R" +bx509="${bx509} --reverse-proxied -p $bx509port" +kdc="${kdc} --addresses=localhost -P $port" server=datan.test.h5l.se +otherserver=other.test.h5l.se cache="FILE:${objdir}/cache.krb5" keyfile="${hx509_data}/key.der" keyfile2="${hx509_data}/key2.der" -keytab=FILE:${objdir}/kt kt=${objdir}/kt +keytab=FILE:${kt} +ukt=${objdir}/ukt +ukeytab=FILE:${ukt} kinit="${kinit} -c $cache ${afs_no_afslog}" klist="${klist} --hidden -v -c $cache" @@ -96,9 +100,10 @@ mkdir -p simple_csr_authz > messages.log -# We'll avoid using a KDC. We only need one for Negotiate tokens, and we'll -# use ktutil and kimpersonate to make it possible to create and accept those -# without a KDC. +# We'll avoid using a KDC for now. For testing /bx509 we only need keys for +# Negotiate tokens, and we'll use ktutil and kimpersonate to make it possible +# to create and accept those without a KDC. When we test /bnegotiate, however, +# we'll start a KDC. # csr_grant ext-type value princ csr_grant() { @@ -114,21 +119,21 @@ csr_revoke() { # get_cert "" curl-opts # get_cert "&qparams" curl-opts get_cert() { - url="http://${server}:${port}/bx509?csr=$csr${1}" + url="http://${server}:${bx509port}/bx509?csr=$csr${1}" shift - curl -g --connect-to ${server}:${port}:localhost:${port} \ + curl -g --connect-to ${server}:${bx509port}:localhost:${bx509port} \ -H "Authorization: Negotiate $token" \ "$@" "$url" } -rm -f $kt +rm -f $kt $ukt $ktutil -k $keytab add -r -V 1 -e aes128-cts-hmac-sha1-96 \ - -p HTTP/datan.test.h5l.se@TEST.H5L.SE || + -p HTTP/datan.test.h5l.se@${R} || { echo "failed to setup kimpersonate credentials"; exit 2; } $ktutil -k $keytab list || { echo "failed to setup kimpersonate credentials"; exit 2; } $kimpersonate --ccache=$cache -k $keytab -R -t aes128-cts-hmac-sha1-96 \ - -c foo@TEST.H5L.SE -s HTTP/datan.test.h5l.se@TEST.H5L.SE || + -c foo@${R} -s HTTP/datan.test.h5l.se@${R} || { echo "failed to setup kimpersonate credentials"; exit 2; } $klist || { echo "failed to setup kimpersonate credentials"; exit 2; } @@ -162,11 +167,11 @@ $hxtool ca --issue-ca --self-signed --type=pkinit-kdc \ cp ${objdir}/user-issuer.pem ${objdir}/pkinit-anchor.pem # Put the cert alone in the trust anchors file -#ex "${objdir}/pkinit-anchor.pem" <<"EOF" -#/-----BEGIN CERTIFICATE----- -#1,.-1 d -#wq -#EOF +ex "${objdir}/pkinit-anchor.pem" <<"EOF" +/-----BEGIN CERTIFICATE----- +1,.-1 d +wq +EOF $hxtool ca --issue-ca --self-signed \ --ku=digitalSignature --ku=keyCertSign --ku=cRLSign \ @@ -184,7 +189,7 @@ $hxtool ca --issue-ca --self-signed \ $hxtool ca --issue-ca --type=https-negotiate-server \ --ca-certificate=PEM-FILE:"${objdir}/server-issuer.pem" \ - --ku=digitalSignature --pk-init-principal=HTTP/${H}@${R}\ + --ku=digitalSignature --pk-init-principal=HTTP/${server}@${R}\ --generate-key=rsa --key-bits=1024 --subject="" \ --certificate=PEM-FILE:"${objdir}/bx509.pem" || { echo "failed to setup CA certificate"; exit 2; } @@ -196,10 +201,10 @@ $hxtool ca --issue-ca --type=https-negotiate-server \ # - the KDC CA tester program works echo "Check gss-token and Negotiate token validator plugin" -token=$(KRB5CCNAME=$cache $gsstoken HTTP@$H | tr A B) +token=$(KRB5CCNAME=$cache $gsstoken HTTP@$server | tr A B) $test_token_validator -a datan.test.h5l.se Negotiate "$token" && { echo "Negotiate token validator accepted invalid token"; exit 2; } -token=$(KRB5CCNAME=$cache $gsstoken HTTP@$H) +token=$(KRB5CCNAME=$cache $gsstoken HTTP@$server) $test_token_validator -a datan.test.h5l.se Negotiate "$token" || { echo "Negotiate token validator failed to validate valid token"; exit 2; } @@ -211,11 +216,18 @@ $hxtool request-create --subject='' --generate-key=rsa --key-bits=1024 \ rm -f trivial.pem server.pem email.pem echo "Testing plain user cert issuance KDC CA" -$test_kdc_ca -a bx509 -A foo@TEST.H5L.SE PKCS10:${objdir}/req \ +$test_kdc_ca -a bx509 -A foo@${R} PKCS10:${objdir}/req \ PEM-FILE:${objdir}/trivial.pem || { echo "Trivial offline CA test failed"; exit 2; } $hxtool print --content PEM-FILE:${objdir}/trivial.pem || { echo "Trivial offline CA test failed"; exit 2; } +$hxtool acert --end-entity \ + --expr="%{certificate.subject} == \"CN=foo,$DCs\"" \ + -P "foo@${R}" "FILE:${objdir}/trivial.pem" || + { echo "Trivial offline CA test failed"; exit 2; } +$hxtool acert --expr="%{certificate.subject} == \"OU=Users,CN=KDC,$DCs\"" \ + --lacks-private-key "FILE:${objdir}/trivial.pem" || + { echo "Trivial offline CA test failed (issuer private keys included!!)"; exit 2; } echo "Testing other cert issuance KDC CA" csr_revoke @@ -225,32 +237,38 @@ $hxtool request-create --subject='' --generate-key=rsa --key-bits=1024 \ --eku=id_pkix_kp_serverAuth \ --dnsname=foo.test.h5l.se "${objdir}/req" || { echo "Failed to make a CSR with a dNSName SAN request"; exit 2; } -$test_kdc_ca -a bx509 foo@TEST.H5L.SE PKCS10:${objdir}/req \ +$test_kdc_ca -a bx509 foo@${R} PKCS10:${objdir}/req \ PEM-FILE:${objdir}/server.pem && { echo "Trivial offline CA test failed: unauthorized issuance (dNSName)"; exit 2; } -csr_grant dnsname foo.test.h5l.se foo@TEST.H5L.SE -csr_grant eku 1.3.6.1.5.5.7.3.1 foo@TEST.H5L.SE -$test_kdc_ca -a bx509 foo@TEST.H5L.SE PKCS10:${objdir}/req \ +csr_grant dnsname foo.test.h5l.se foo@${R} +csr_grant eku 1.3.6.1.5.5.7.3.1 foo@${R} +$test_kdc_ca -a bx509 foo@${R} PKCS10:${objdir}/req \ PEM-FILE:${objdir}/server.pem || { echo "Offline CA test failed for explicitly authorized dNSName"; exit 2; } $hxtool print --content PEM-FILE:${objdir}/server.pem || { echo "Offline CA test failed for explicitly authorized dNSName"; exit 2; } +$hxtool acert --expr="%{certificate.subject} == \"OU=Servers,CN=KDC,$DCs\"" \ + --lacks-private-key "FILE:${objdir}/server.pem" || + { echo "Trivial offline CA test failed (issuer private keys included!!)"; exit 2; } # email cert $hxtool request-create --subject='' --generate-key=rsa --key-bits=1024 \ --key=FILE:"${objdir}/k.der" \ --eku=id_pkix_kp_clientAuth \ --email=foo@test.h5l.se "${objdir}/req" || { echo "Failed to make a CSR with an rfc822Name SAN request"; exit 2; } -$test_kdc_ca -a bx509 foo@TEST.H5L.SE PKCS10:${objdir}/req \ +$test_kdc_ca -a bx509 foo@${R} PKCS10:${objdir}/req \ PEM-FILE:${objdir}/email.pem && - { echo "Trivial offline CA test failed: unauthorized issuance (dNSName)"; exit 2; } -csr_grant email foo@test.h5l.se foo@TEST.H5L.SE -csr_grant eku 1.3.6.1.5.5.7.3.2 foo@TEST.H5L.SE -$test_kdc_ca -a bx509 foo@TEST.H5L.SE PKCS10:${objdir}/req \ + { echo "Offline CA test failed: unauthorized issuance (dNSName)"; exit 2; } +csr_grant email foo@test.h5l.se foo@${R} +csr_grant eku 1.3.6.1.5.5.7.3.2 foo@${R} +$test_kdc_ca -a bx509 foo@${R} PKCS10:${objdir}/req \ PEM-FILE:${objdir}/email.pem || { echo "Offline CA test failed for explicitly authorized dNSName"; exit 2; } $hxtool print --content PEM-FILE:${objdir}/email.pem || { echo "Offline CA test failed for explicitly authorized dNSName"; exit 2; } +$hxtool acert --expr="%{certificate.subject} == \"OU=Users,CN=KDC,$DCs\"" \ + --lacks-private-key "FILE:${objdir}/email.pem" || + { echo "Offline CA test failed (issuer private keys included!!)"; exit 2; } if ! which curl; then echo "curl is not available -- not testing bx509d" @@ -262,12 +280,21 @@ if ! test -x ${objdir}/../../kdc/bx509d; then exit 0 fi +echo "Creating database" +${kadmin} init \ + --realm-max-ticket-life=1day \ + --realm-max-renewable-life=1month \ + ${R} || exit 1 +${kadmin} add -r --use-defaults foo@${R} || exit 1 +${kadmin} modify --pkinit-acl="CN=foo,DC=test,DC=h5l,DC=se" foo@${R} || exit 1 + + echo "Starting bx509d" -${bx509d} --reverse-proxied -H $H --cert=${objdir}/bx509.pem -t -p $port --daemon || +${bx509d} --reverse-proxied -H $server --cert=${objdir}/bx509.pem -t -p $bx509port --daemon || { echo "bx509 failed to start"; exit 2; } bx509pid=`getpid bx509d` -trap "kill -9 ${bx509pid}; echo signal killing bx509d; cat ca.crt kdc.crt pkinit.crt ;exit 1;" EXIT +trap "kill -9 ${bx509pid}; echo signal killing bx509d; exit 1;" EXIT ec=0 rm -f trivial.pem server.pem email.pem @@ -283,17 +310,21 @@ csr=$($rkbase64 -- ${objdir}/req | $rkvis -h --stdin) # Create a barebones bx509 HTTP/1.1 client test program? echo "Fetching a trivial user certificate" -token=$(KRB5CCNAME=$cache $gsstoken HTTP@$H) +token=$(KRB5CCNAME=$cache $gsstoken HTTP@$server) if (set -vx; get_cert '' -sf -o "${objdir}/trivial.pem"); then $hxtool print --content "FILE:${objdir}/trivial.pem" if $hxtool acert --end-entity \ --expr="%{certificate.subject} == \"CN=foo,$DCs\"" \ - -P "foo@TEST.H5L.SE" "FILE:${objdir}/trivial.pem"; then + -P "foo@${R}" "FILE:${objdir}/trivial.pem"; then echo 'Successfully obtained a trivial client certificate!' else echo 'FAIL: Obtained a trivial client certificate w/o expected PKINIT SAN)' exit 1 fi + if $hxtool acert --expr="%{certificate.subject} == \"OU=Users,$DCs\"" \ + --has-private-key "FILE:${objdir}/trivial.pem"; then + echo 'Successfully obtained a trivial client certificate!' + fi else echo 'Failed to get a certificate!' exit 1 @@ -319,11 +350,11 @@ else fi echo "Fetching a server certificate with one dNSName SAN" -csr_grant dnsname $server foo@TEST.H5L.SE +csr_grant dnsname $server foo@${R} if (set -vx; get_cert "&dNSName=$server" -sf -o "${objdir}/server.pem"); then $hxtool print --content "FILE:${objdir}/server.pem" if (set -vx; $hxtool acert --expr="%{certificate.subject} == \"\"" \ - --end-entity -P foo@TEST.H5L.SE \ + --end-entity -P foo@${R} \ "FILE:${objdir}/server.pem"); then echo 'Got a broken server certificate (has PKINIT SAN)' exit 1 @@ -339,13 +370,13 @@ else fi echo "Fetching a server certificate with two dNSName SANs" -csr_grant dnsname "second-$server" foo@TEST.H5L.SE +csr_grant dnsname "second-$server" foo@${R} if (set -vx; get_cert "&dNSName=${server}&dNSName=second-$server" -sf \ -o "${objdir}/server2.pem"); then $hxtool print --content "FILE:${objdir}/server2.pem" if $hxtool acert --expr="%{certificate.subject} == \"\"" \ - --end-entity -P foo@TEST.H5L.SE \ + --end-entity -P foo@${R} \ "FILE:${objdir}/server2.pem"; then echo 'Got a broken server certificate (has PKINIT SAN)' exit 1 @@ -363,10 +394,10 @@ else fi echo "Fetching an email certificate" -csr_grant email foo@bar.example foo@TEST.H5L.SE +csr_grant email foo@bar.example foo@${R} if (set -vx; get_cert "&rfc822Name=foo@bar.example" -sf -o "${objdir}/email.pem"); then $hxtool print --content "FILE:${objdir}/email.pem" - if $hxtool acert --end-entity -P "foo@TEST.H5L.SE" "FILE:${objdir}/email.pem"; then + if $hxtool acert --end-entity -P "foo@${R}" "FILE:${objdir}/email.pem"; then echo 'Got a broken email certificate (has PKINIT SAN)' exit 1 elif $hxtool acert --expr="%{certificate.subject} == \"\"" \ @@ -382,30 +413,113 @@ else exit 1 fi -if false not yet; then - # XXX Need to start a KDC to test this. - echo "Fetching a Negotiate token" - if (set -vx; - curl -o negotiate-token -Lgsf --connect-to ${server}:${port}:localhost:${port} \ - -H "Authorization: Negotiate $token" \ - "http://${server}:${port}/bnegotiate?target=HTTP%40${server}"); then - # bx509 sends us a token w/o a newline for now; we add one because - # gss-token expects it. - [[ -s negotiate-token ]] && echo >> negotiate-token - if [[ -s negotiate-token ]] && KRB5_KTNAME="${etc}/keytab.user" $gsstoken -Nr < negotiate-token; then - echo 'Successfully obtained a Negotiate token!' - else - echo 'Failed to get a Negotiate token!' - exit 1 - fi +# Need to start a KDC to test this. +rm -f $kt $ukt +${kdestroy} +${kadmin} add -r --use-defaults HTTP/${server}@${R} || exit 1 +${kadmin} ext_keytab -r -k $keytab HTTP/${server}@${R} || exit 1 +${kadmin} add -r --use-defaults HTTP/${otherserver}@${R} || exit 1 +${kadmin} ext_keytab -r -k $ukeytab foo@${R} || exit 1 + +echo "Starting kdc" ; > messages.log +${kdc} --detach --testing || { echo "kdc failed to start"; exit 1; } +kdcpid=`getpid kdc` +trap "kill -9 ${kdcpid} ${bx509pid}; echo signal killing kdc and bx509d; exit 1;" EXIT + +${kinit} -kt $ukeytab foo@${R} || exit 1 +$klist || { echo "failed to setup kimpersonate credentials"; exit 2; } + +echo "Fetch negotiate token (pre-test)" +# Do what /bnegotiate does, roughly, prior to testing /bnegotiate +$hxtool request-create --subject='' --generate-key=rsa --key-bits=1024 \ + --key=PEM-FILE:"${objdir}/k.pem" "${objdir}/req" || + { echo "Failed to make a CSR"; exit 2; } +$test_kdc_ca -a bx509 -A foo@${R} PKCS10:${objdir}/req \ + PEM-FILE:${objdir}/pkinit-test.pem || + { echo "Trivial offline CA test failed (CA)"; exit 2; } +cat ${objdir}/k.pem >> ${objdir}/pkinit-test.pem +${kinit} -C PEM-FILE:${objdir}/pkinit-test.pem foo@${R} || + { echo "Trivial offline CA test failed (PKINIT)"; exit 2; } +#${kgetcred} -H HTTP/${server}@${R} || +# { echo "Trivial offline CA test failed (TGS)"; exit 2; } +KRB5CCNAME=$cache $gsstoken HTTP@$server | KRB5_KTNAME="$keytab" $gsstoken -r || + { echo "Trivial offline CA test failed (gss-token)"; exit 2; } + +echo "Fetching a Negotiate token" +token=$(KRB5CCNAME=$cache $gsstoken HTTP@$server) +if (set -vx; + curl -o negotiate-token -Lgsf \ + --connect-to ${server}:${bx509port}:localhost:${bx509port} \ + -H "Authorization: Negotiate $token" \ + "http://${server}:${bx509port}/bnegotiate?target=HTTP%40${server}"); then + # bx509 sends us a token w/o a newline for now; we add one because + # gss-token expects it. + test -s negotiate-token && echo >> negotiate-token + if test -s negotiate-token && KRB5_KTNAME="$keytab" $gsstoken -Nr < negotiate-token; then + echo 'Successfully obtained a Negotiate token!' else - echo 'Failed to get a Negotiate token!' + echo 'Failed to get a Negotiate token (got an unacceptable token)!' exit 1 fi +else + echo 'Failed to get a Negotiate token!' + exit 1 fi -echo "killing bx509d (${bx509pid})" -sh ${leaks_kill} bx509 $bx509pid || ec=1 +referer=https://${otherserver}/blah +redirect=$(${rkvis} -h https://${otherserver}/blah?q=whatever) +if (set -vx; + curl -o negotiate-token -Lgsf \ + --connect-to ${server}:${bx509port}:localhost:${bx509port} \ + -H "Authorization: Negotiate $token" \ + "http://${server}:${bx509port}/bnegotiate?target=HTTP%40${server}&redirect=${redirect}"); then + echo "Error: /bnegotiate with target and redirect succeeded" + exit 1 +fi + +if (set -vx; + curl -o negotiate-token -Lgsf \ + --connect-to ${server}:${bx509port}:localhost:${bx509port} \ + -H "Authorization: Negotiate $token" \ + "http://${server}:${bx509port}/bnegotiate?redirect=${redirect}"); then + echo "Error: /bnegotiate with redirect but no Referer succeeded" + exit 1 +fi + +referer=http://${otherserver}/blah +redirect=$(${rkvis} -h http://${otherserver}/blah?q=whatever) +if (set -vx; + curl -gsf \ + --connect-to ${server}:${bx509port}:localhost:${bx509port} \ + -H "Authorization: Negotiate $token" \ + -H "Referer: $referer" \ + "http://${server}:${bx509port}/bnegotiate?redirect=${redirect}"); then + echo "Error: redirect for non-https referer" + exit 1 +fi + +referer=https://${otherserver}/blah +redirect=$(${rkvis} -h https://${otherserver}/blah?q=whatever) +if (set -vx; + curl -gfs -D curlheaders \ + --connect-to ${server}:${bx509port}:localhost:${bx509port} \ + -H "Authorization: Negotiate $token" \ + -H "Referer: $referer" \ + "http://${server}:${bx509port}/bnegotiate?redirect=${redirect}"); then + read junk code junk < curlheaders + if test "$code" = 307; then + echo "Got a proper redirect" + else + echo "Error: unexpected status code $code (wanted 307)" + fi +else + echo "Error: no redirect" + exit 1 +fi + +echo "killing kdc (${kdcpid}) and bx509d (${bx509pid})" +sh ${leaks_kill} kdc $kdcpid || ec=1 +sh ${leaks_kill} bx509d $bx509pid || ec=1 trap "" EXIT