From dfada0ccad773f2a9bde854051bd5d9521465bcb Mon Sep 17 00:00:00 2001 From: Nicolas Williams Date: Mon, 15 Jul 2019 23:27:30 -0500 Subject: [PATCH] kx509: Add CSR support This commit adds support for proof of posession to the kx509 protocol by using PKCS#10 CSRs. This allows conveyance of extReq CSR attributes requesting desired Certificate Extensions. --- kdc/kx509.c | 599 ++++++++++++++++------ kuser/Makefile.am | 6 +- kuser/NTMakefile | 1 + kuser/heimtools-commands.in | 59 ++- kuser/klist.1 | 13 - kuser/klist.c | 102 +--- kuser/kx509.1 | 128 +++++ kuser/kx509.c | 256 ++++++++++ lib/asn1/krb5.asn1 | 6 +- lib/asn1/kx509.asn1 | 74 ++- lib/krb5/krb5.h | 1 + lib/krb5/kx509.c | 863 ++++++++++++++++++++++++++------ lib/krb5/libkrb5-exports.def.in | 17 + lib/krb5/version-script.map | 13 + tests/bin/setup-env.in | 1 + tests/kdc/check-pkinit.in | 8 +- 16 files changed, 1690 insertions(+), 457 deletions(-) create mode 100644 kuser/kx509.1 create mode 100644 kuser/kx509.c diff --git a/kdc/kx509.c b/kdc/kx509.c index 9dd097c08..7d56d5a75 100644 --- a/kdc/kx509.c +++ b/kdc/kx509.c @@ -43,7 +43,7 @@ * This file implements the kx509 service. * * The protocol, its shortcomings, and its future are described in - * lib/krb5/hx509.c. + * lib/krb5/hx509.c. See also lib/asn1/kx509.asn1. * * The service handles requests, decides whether to issue a certificate, and * does so by populating a "template" to generate a TBSCertificate and signing @@ -57,8 +57,8 @@ * Besides future protocol improvements described in lib/krb5/hx509.c, here is * a list of KDC functionality we'd like to add: * - * - support templates as strings in configuration? - * - lookup an hx509 template for the client principal in its HDB entry + * - support templates as strings (rather than filenames) in configuration? + * - lookup an hx509 template for the client principal in its HDB entry? * - lookup subjectName, SANs for a principal in its HDB entry * - lookup a host-based client principal's HDB entry and add its canonical * name / aliases as dNSName SANs @@ -76,6 +76,23 @@ static const unsigned char version_2_0[4] = {0 , 0, 2, 0}; +typedef struct kx509_req_context { + krb5_kdc_configuration *config; + const struct Kx509Request *req; + Kx509CSRPlus csr_plus; + krb5_auth_context ac; + const char *realm; /* XXX Confusion: is this crealm or srealm? */ + char *sname; + char *cname; + struct sockaddr *addr; + const char *from; + krb5_keyblock *key; + hx509_request csr; + krb5_data *reply; + unsigned int have_auth_data:1; /* Relevant authz data in the AP-REQ */ + unsigned int send_chain:1; /* Client expects a full chain */ +} *kx509_req_context; + /* * Taste the request to see if it's a kx509 request. */ @@ -150,6 +167,37 @@ verify_req_hash(krb5_context context, return 0; } +/* Wrapper around kdc_log() that adds contextual information */ +static void +kx509_log(krb5_context context, + kx509_req_context reqctx, + int level, + const char *fmt, + ...) +{ + va_list ap; + char *msg; + + va_start(ap, fmt); + if (vasprintf(&msg, fmt, ap) == -1 || msg == NULL) { + kdc_log(context, reqctx->config, level, + "Out of memory while formatting log message"); + va_end(ap); + va_start(ap, fmt); + kdc_vlog(context, reqctx->config, level, fmt, ap); + va_end(ap); + return; + } + va_end(ap); + + kdc_log(context, reqctx->config, level, + "kx509 %s (from %s for %s, service %s)", msg, + reqctx->from ? reqctx->from : "", + reqctx->cname ? reqctx->cname : "", + reqctx->sname ? reqctx->sname : ""); + free(msg); +} + /* * Set the HMAC in the response. */ @@ -206,6 +254,22 @@ calculate_reply_hash(krb5_context context, return 0; } +/* + * Lookup the principal's HDB entry, authorize the requested extensions, add + * authorized extensions to the `tbs', and indicate whether to add any of the + * EKUs/SANs we'd normally add automatically. + */ +static krb5_error_code +get_hdb_ekus_and_sans(krb5_context context, + kx509_req_context reqctx, + krb5_principal principal, + hx509_ca_tbs tbs, + int *add_auto_exts) +{ + *add_auto_exts = 1; + return ENOTSUP; +} + /* * Finds a template in the configuration that is appropriate to the form of the * client principal. Also sets some variables in `env' and adds some SANs to @@ -222,7 +286,7 @@ calculate_reply_hash(krb5_context context, */ static krb5_error_code get_template(krb5_context context, - krb5_kdc_configuration *config, + kx509_req_context reqctx, krb5_principal principal, const char *princ_no_realm, const char *princ, @@ -230,13 +294,20 @@ get_template(krb5_context context, hx509_env *env, hx509_ca_tbs tbs) { + krb5_error_code ret = KRB5KDC_ERR_POLICY; unsigned int ncomp = krb5_principal_get_num_comp(context, principal); const char *crealm = krb5_principal_get_realm(context, principal); const char *kx509_template = NULL; const char *comp0, *comp1, *comp2; char *domain = NULL; char *email = NULL; - krb5_error_code ret = KRB5KDC_ERR_POLICY; + int add_auto_exts = 1; + + /* Populate extensions from CSR / HDB entry as requested and permitted */ + ret = get_hdb_ekus_and_sans(context, reqctx, principal, tbs, + &add_auto_exts); + if (ret != 0 && ret != ENOTSUP) + return ret; if (ncomp == 1) { /* 1-component, user principal */ @@ -246,7 +317,7 @@ get_template(krb5_context context, crealm, "kx509_template", NULL); if (kx509_template == NULL) - kx509_template = config->kx509_template; + kx509_template = reqctx->config->kx509_template; if (kx509_template == NULL) goto out; @@ -264,7 +335,7 @@ get_template(krb5_context context, * XXX Dicey feature! Maybe this should be a string param whose value * is the domainname to use for the email address. */ - if (ret == 0 && + if (ret == 0 && add_auto_exts && get_bool_param(context, FALSE, crealm, "kx509_include_email_san")) { char *p; @@ -319,7 +390,7 @@ get_template(krb5_context context, if (ret == 0 && ncomp == 3) ret = hx509_env_add(context->hx509ctx, env, "principal-domain-name", comp2); - if (ret == 0 && + if (ret == 0 && add_auto_exts && get_bool_param(context, FALSE, crealm, "kx509_include_dnsname_san")) { ret = hx509_ca_tbs_add_san_hostname(context->hx509ctx, tbs, comp1); @@ -342,13 +413,13 @@ get_template(krb5_context context, "kx509_templates", config_label, comp0, NULL); if (kx509_template == NULL) { - kdc_log(context, config, 0, "kx509 template not found for %s", + kdc_log(context, reqctx->config, 0, "kx509 template not found for %s", princ); ret = KRB5KDC_ERR_POLICY; goto out; } } else { - kdc_log(context, config, 0, "kx509 client %s has too many components!", + kdc_log(context, reqctx->config, 0, "kx509 client %s has too many components!", princ); ret = KRB5KDC_ERR_POLICY; } @@ -360,13 +431,60 @@ out: return ret; } +static int +chain_add1_func(hx509_context context, void *d, hx509_cert c) +{ + heim_octet_string os; + Certificates *cs = d; + Certificate c2; + int ret; + + ret = hx509_cert_binary(context, c, &os); + if (ret) + return ret; + ret = decode_Certificate(os.data, os.length, &c2, NULL); + der_free_octet_string(&os); + if (ret) + return ret; + ret = add_Certificates(cs, &c2); + free_Certificate(&c2); + return ret; +} + +static krb5_error_code +encode_cert_and_chain(hx509_context hx509ctx, + hx509_cert cert, + const char *chain_store, + krb5_data *out) +{ + krb5_error_code ret; + Certificates cs; + hx509_certs certs = NULL; + size_t len; + + cs.len = 0; + cs.val = 0; + + ret = chain_add1_func(hx509ctx, &cs, cert); + if (ret == 0) + ret = hx509_certs_init(hx509ctx, chain_store, 0, NULL, &certs); + if (ret == 0) + ret = hx509_certs_iter_f(hx509ctx, certs, chain_add1_func, &cs); + hx509_certs_free(&certs); + if (ret == 0) + ASN1_MALLOC_ENCODE(Certificates, out->data, out->length, + &cs, &len, ret); + free_Certificates(&cs); + return ret; +} + + /* * Build a certifate for `principal´ that will expire at `endtime´. */ static krb5_error_code build_certificate(krb5_context context, - krb5_kdc_configuration *config, - const krb5_data *key, + kx509_req_context reqctx, time_t endtime, krb5_principal principal, krb5_data *certificate) @@ -390,11 +508,11 @@ build_certificate(krb5_context context, kx509_ca = krb5_config_get_string(context, NULL, "kdc", "realms", crealm, "kx509_ca", NULL); if (kx509_ca == NULL) - kx509_ca = config->kx509_ca; + kx509_ca = reqctx->config->kx509_ca; if (kx509_ca == NULL) { ret = KRB5KDC_ERR_POLICY; - kdc_log(context, config, 0, "No kx509 CA credential specified for " - "realm %s", crealm); + kdc_log(context, reqctx->config, 0, + "No kx509 CA credential specified for realm %s", crealm); goto out; } @@ -409,18 +527,19 @@ build_certificate(krb5_context context, goto out; /* Get a template and set things in `env' and `tbs' as appropriate */ - ret = get_template(context, config, principal, name, princ, + ret = get_template(context, reqctx, principal, name, princ, &kx509_template, &env, tbs); if (ret) goto out; if (kx509_template == NULL) { - kdc_log(context, config, 0, "No kx509 certificate template specified"); + kdc_log(context, reqctx->config, 0, + "No kx509 certificate template specified"); ret = KRB5KDC_ERR_POLICY; goto out; } - kdc_log(context, config, 0, "Issuing kx509 certificate to %s using " - "template %s", princ, kx509_template); + kdc_log(context, reqctx->config, 0, "Issuing kx509 certificate to %s " + "using template %s", princ, kx509_template); /* * Populate additional template "env" variables @@ -442,7 +561,8 @@ build_certificate(krb5_context context, ret = hx509_certs_init(context->hx509ctx, kx509_ca, 0, NULL, &certs); if (ret) { - kdc_log(context, config, 0, "Failed to load CA %s", kx509_ca); + kdc_log(context, reqctx->config, 0, + "Failed to load CA %s", kx509_ca); goto out; } ret = hx509_query_alloc(context->hx509ctx, &q); @@ -458,7 +578,8 @@ build_certificate(krb5_context context, hx509_query_free(context->hx509ctx, q); hx509_certs_free(&certs); if (ret) { - kdc_log(context, config, 0, "Failed to find a CA in %s", kx509_ca); + kdc_log(context, reqctx->config, 0, + "Failed to find a CA in %s", kx509_ca); goto out; } } @@ -466,22 +587,13 @@ build_certificate(krb5_context context, /* Populate the subject public key in the TBS context */ { SubjectPublicKeyInfo spki; - heim_any any; - memset(&spki, 0, sizeof(spki)); - - spki.subjectPublicKey.data = key->data; - spki.subjectPublicKey.length = key->length * 8; - - ret = der_copy_oid(&asn1_oid_id_pkcs1_rsaEncryption, - &spki.algorithm.algorithm); - - any.data = "\x05\x00"; - any.length = 2; - spki.algorithm.parameters = &any; - - ret = hx509_ca_tbs_set_spki(context->hx509ctx, tbs, &spki); - der_free_oid(&spki.algorithm.algorithm); + ret = hx509_request_get_SubjectPublicKeyInfo(context->hx509ctx, + reqctx->csr, + &spki); + if (ret == 0) + ret = hx509_ca_tbs_set_spki(context->hx509ctx, tbs, &spki); + free_SubjectPublicKeyInfo(&spki); if (ret) goto out; } @@ -497,18 +609,18 @@ build_certificate(krb5_context context, ret = hx509_get_one_cert(context->hx509ctx, certs, &template); hx509_certs_free(&certs); if (ret) { - kdc_log(context, config, 0, "Failed to load template from %s", - kx509_template); + kdc_log(context, reqctx->config, 0, + "Failed to load template from %s", kx509_template); goto out; } - + /* * Only take the subjectName, the keyUsage, and EKUs from the template * certificate. */ ret = hx509_ca_tbs_set_template(context->hx509ctx, tbs, - HX509_CA_TEMPLATE_SUBJECT| - HX509_CA_TEMPLATE_KU| + HX509_CA_TEMPLATE_SUBJECT | + HX509_CA_TEMPLATE_KU | HX509_CA_TEMPLATE_EKU, template); hx509_cert_free(template); @@ -518,7 +630,7 @@ build_certificate(krb5_context context, /* * Add other SANs. - * + * * Adding an id-pkinit-san means the client can use the certificate to * initiate PKINIT. That might seem odd, but it enables a sort of PKIX * credential delegation by allowing forwarded Kerberos tickets to be @@ -528,6 +640,9 @@ build_certificate(krb5_context context, * PKIX (w/ softtoken) -> Kerberos -> * PKIX (w/ softtoken) -> Kerberos -> * ... + * + * Note that we may not have added the PKINIT EKU -- that depends on the + * template, and host-based service templates might well not include it. */ if (ret == 0 && get_bool_param(context, TRUE, crealm, "kx509_include_pkinit_san")) { @@ -536,22 +651,30 @@ build_certificate(krb5_context context, goto out; } + /* + * Note that we set the certificate's end time to the client's *Ticket*'s + * end time. For server certs this may not always be appropriate. We + * might want to have a configurable setting for this, in which case maybe + * we should move this to get_template(). + */ hx509_ca_tbs_set_notAfter(context->hx509ctx, tbs, endtime); - /* Finally, expand the subjectName in the TBS context and sign to issue */ + /* Expand the subjectName template in the TBS */ hx509_ca_tbs_subject_expand(context->hx509ctx, tbs, env); hx509_env_free(&env); + + /* All done with the TBS, sign/issue the certificate */ ret = hx509_ca_sign(context->hx509ctx, tbs, signer, &cert); if (ret) goto out; /* Encode and output the certificate */ - ret = hx509_cert_binary(context->hx509ctx, cert, certificate); + if (reqctx->send_chain) + ret = encode_cert_and_chain(context->hx509ctx, cert, kx509_ca, certificate); + else + ret = hx509_cert_binary(context->hx509ctx, cert, certificate); out: - if (ret) - kdc_log(context, config, 0, "Failed to build a certificate for %s", - princ); krb5_xfree(name); krb5_xfree(princ); if (env) @@ -568,7 +691,7 @@ out: /* Check that a krbtgt's second component is a local realm */ static krb5_error_code is_local_realm(krb5_context context, - krb5_kdc_configuration *config, + kx509_req_context reqctx, const char *realm) { krb5_error_code ret; @@ -580,8 +703,8 @@ is_local_realm(krb5_context context, if (ret) return ret; if (ret == 0) - ret = _kdc_db_fetch(context, config, tgs, HDB_F_GET_KRBTGT, NULL, NULL, - &ent); + ret = _kdc_db_fetch(context, reqctx->config, tgs, HDB_F_GET_KRBTGT, + NULL, NULL, &ent); if (ent) _kdc_free_ent(context, ent); krb5_free_principal(context, tgs); @@ -598,7 +721,7 @@ is_local_realm(krb5_context context, * * We allow cross-realm requests. * - * XXX Maybe x-realm support should be configurable. Requiring INITIAL tickets + * Maybe x-realm support should be configurable. Requiring INITIAL tickets * does NOT preclude x-realm support! (Cross-realm TGTs can be INITIAL.) * * Support for specific client realms is configurable by configuring issuer @@ -606,10 +729,9 @@ is_local_realm(krb5_context context, * default. But maybe we should have an explicit configuration parameter * to enable support for clients from different realms than the service. */ -krb5_error_code +static krb5_error_code kdc_kx509_verify_service_principal(krb5_context context, - krb5_kdc_configuration *config, - const char *cname, + kx509_req_context reqctx, krb5_principal sprincipal) { krb5_error_code ret = 0; @@ -624,8 +746,8 @@ kdc_kx509_verify_service_principal(krb5_context context, if (strcmp(krb5_principal_get_comp_string(context, sprincipal, 0), KRB5_TGS_NAME) == 0) { const char *r = krb5_principal_get_comp_string(context, sprincipal, 1); - if ((ret = is_local_realm(context, config, r))) - kdc_log(context, config, 0, "client used wrong krbtgt for kx509"); + if ((ret = is_local_realm(context, reqctx, r))) + kx509_log(context, reqctx, 0, "client used wrong krbtgt for kx509"); goto out; } @@ -653,10 +775,8 @@ err: goto out; ret = KRB5KDC_ERR_SERVER_NOMATCH; - krb5_set_error_message(context, ret, - "User %s used wrong Kx509 service " - "principal, expected: %s", - cname, expected); + kx509_log(context, reqctx, 0, "client used wrong kx509 service principal " + "(expected %s)", expected); out: krb5_xfree(expected); @@ -667,39 +787,37 @@ out: static krb5_error_code encode_reply(krb5_context context, - krb5_kdc_configuration *config, - krb5_data *reply, + kx509_req_context reqctx, Kx509Response *r) { krb5_error_code ret; krb5_data data; size_t size; - reply->data = NULL; - reply->length = 0; + reqctx->reply->data = NULL; + reqctx->reply->length = 0; ASN1_MALLOC_ENCODE(Kx509Response, data.data, data.length, r, &size, ret); if (ret) { - kdc_log(context, config, 0, "Failed to encode kx509 reply"); + kdc_log(context, reqctx->config, 0, "Failed to encode kx509 reply"); return ret; } if (size != data.length) krb5_abortx(context, "ASN1 internal error"); - ret = krb5_data_alloc(reply, data.length + sizeof(version_2_0)); + ret = krb5_data_alloc(reqctx->reply, data.length + sizeof(version_2_0)); if (ret == 0) { - memcpy(reply->data, version_2_0, sizeof(version_2_0)); - memcpy(((unsigned char *)reply->data) + sizeof(version_2_0), + memcpy(reqctx->reply->data, version_2_0, sizeof(version_2_0)); + memcpy(((unsigned char *)reqctx->reply->data) + sizeof(version_2_0), data.data, data.length); } free(data.data); return ret; } +/* Make an error response, and log the error message as well */ static krb5_error_code mk_error_response(krb5_context context, - krb5_kdc_configuration *config, - krb5_keyblock *key, - krb5_data *reply, + kx509_req_context reqctx, int32_t code, const char *fmt, ...) @@ -712,7 +830,7 @@ mk_error_response(krb5_context context, char *freeme1 = NULL; va_list ap; - if (!config->enable_kx509) + if (!reqctx->config->enable_kx509) code = KRB5KDC_ERR_POLICY; /* Make sure we only send RFC4120 and friends wire protocol error codes */ @@ -736,13 +854,13 @@ mk_error_response(krb5_context context, msg = freeme0; va_end(ap); - if (!config->enable_kx509 && + if (!reqctx->config->enable_kx509 && asprintf(&freeme1, "kx509 service is disabled (%s)", msg) > -1 && freeme1 != NULL) { msg = freeme1; } - kdc_log(context, config, 0, "%s", msg); + kdc_log(context, reqctx->config, 0, "%s", msg); rep.hash = NULL; rep.certificate = NULL; @@ -750,15 +868,15 @@ mk_error_response(krb5_context context, if (ALLOC(rep.e_text)) *rep.e_text = (void *)(uintptr_t)msg; - if (key) { + if (reqctx->key) { if (ALLOC(rep.hash) != NULL && - calculate_reply_hash(context, key, &rep)) { + calculate_reply_hash(context, reqctx->key, &rep)) { free(rep.hash); rep.hash = NULL; } } - if ((ret2 = encode_reply(context, config, reply, &rep))) + if ((ret2 = encode_reply(context, reqctx, &rep))) ret = ret2; if (rep.hash) krb5_data_free(rep.hash); @@ -769,6 +887,159 @@ mk_error_response(krb5_context context, return ret; } +/* Wrap a bare public (RSA) key with a CSR (not signed it, since we can't) */ +static krb5_error_code +make_csr(krb5_context context, kx509_req_context reqctx, krb5_data *key) +{ + krb5_error_code ret; + SubjectPublicKeyInfo spki; + heim_any any; + + ret = hx509_request_init(context->hx509ctx, &reqctx->csr); + if (ret) + return ret; + + memset(&spki, 0, sizeof(spki)); + spki.subjectPublicKey.data = key->data; + spki.subjectPublicKey.length = key->length * 8; + + ret = der_copy_oid(&asn1_oid_id_pkcs1_rsaEncryption, + &spki.algorithm.algorithm); + + any.data = "\x05\x00"; + any.length = 2; + spki.algorithm.parameters = &any; + + if (ret == 0) + ret = hx509_request_set_SubjectPublicKeyInfo(context->hx509ctx, + reqctx->csr, &spki); + der_free_oid(&spki.algorithm.algorithm); + if (ret) + hx509_request_free(&reqctx->csr); + + /* + * TODO: Move a lot of the templating stuff here so we can let clients + * leave out extensions they don't want. + */ + return ret; +} + +/* Update a CSR with desired Certificate Extensions */ +static krb5_error_code +update_csr(krb5_context context, kx509_req_context reqctx, Extensions *exts) +{ + krb5_error_code ret = 0; + size_t i, k; + + if (exts == NULL) + return 0; + + for (i = 0; ret == 0 && i < exts->len; i++) { + Extension *e = &exts->val[i]; + + if (der_heim_oid_cmp(&e->extnID, &asn1_oid_id_x509_ce_keyUsage) == 0) { + KeyUsage ku; + + ret = decode_KeyUsage(e->extnValue.data, e->extnValue.length, &ku, + NULL); + if (ret) + return ret; + ret = hx509_request_set_ku(context->hx509ctx, reqctx->csr, ku); + } else if (der_heim_oid_cmp(&e->extnID, + &asn1_oid_id_x509_ce_extKeyUsage) == 0) { + ExtKeyUsage eku; + + ret = decode_ExtKeyUsage(e->extnValue.data, e->extnValue.length, + &eku, NULL); + for (k = 0; ret == 0 && k < eku.len; k++) { + ret = hx509_request_add_eku(context->hx509ctx, reqctx->csr, + &eku.val[k]); + } + free_ExtKeyUsage(&eku); + } else if (der_heim_oid_cmp(&e->extnID, + &asn1_oid_id_x509_ce_subjectAltName) == 0) { + GeneralNames san; + + ret = decode_GeneralNames(e->extnValue.data, e->extnValue.length, + &san, NULL); + for (k = 0; ret == 0 && k < san.len; k++) + ret = hx509_request_add_GeneralName(context->hx509ctx, + reqctx->csr, &san.val[k]); + free_GeneralNames(&san); + } + } + if (ret) + kx509_log(context, reqctx, 0, + "request has bad desired certificate extensions"); + return ret; +} + + +/* + * Parse the `pk_key' from the request as a CSR or raw public key, and if the + * latter, wrap it in a non-signed CSR. + */ +static krb5_error_code +get_csr(krb5_context context, kx509_req_context reqctx) +{ + krb5_error_code ret; + RSAPublicKey rsapkey; + heim_octet_string pk_key = reqctx->req->pk_key; + size_t size; + + ret = decode_Kx509CSRPlus(pk_key.data, pk_key.length, &reqctx->csr_plus, + &size); + if (ret == 0) { + reqctx->send_chain = 1; + if (reqctx->csr_plus.authz_datas.len) + reqctx->have_auth_data = 1; + + /* Parse CSR */ + ret = hx509_request_parse_der(context->hx509ctx, &reqctx->csr_plus.csr, + &reqctx->csr); + if (ret) + kx509_log(context, reqctx, 0, "invalid CSR"); + + /* + * Handle any additional Certificate Extensions requested out of band + * of the CSR. + */ + if (ret == 0) + return update_csr(context, reqctx, reqctx->csr_plus.exts); + return ret; + } + reqctx->send_chain = 0; + + /* Check if proof of possession is required by configuration */ + if (!get_bool_param(context, FALSE, reqctx->realm, "require_csr")) + return mk_error_response(context, reqctx, KX509_STATUS_CLIENT_USE_CSR, + "CSRs required but client did not send one"); + + /* Attempt to decode pk_key as RSAPublicKey */ + ret = decode_RSAPublicKey(reqctx->req->pk_key.data, + reqctx->req->pk_key.length, + &rsapkey, &size); + free_RSAPublicKey(&rsapkey); + if (ret == 0 && size == reqctx->req->pk_key.length) + return make_csr(context, reqctx, &pk_key); /* Make pretend CSR */ + + /* Not an RSAPublicKey or garbage follows it */ + if (ret == 0) + kx509_log(context, reqctx, 0, "request has garbage after key"); + return mk_error_response(context, reqctx, KRB5KDC_ERR_NULL_KEY, + "Could not decode CSR or RSA subject public key"); +} + +/* Stub for later work */ +static krb5_error_code +verify_auth_data(krb5_context context, + struct kx509_req_context *reqctx, + krb5_principal cprincipal, + krb5_principal *actual_cprincipal) +{ + return EACCES; +} + /* * Process a request, produce a reply. */ @@ -782,13 +1053,29 @@ _kdc_do_kx509(krb5_context context, krb5_error_code ret; krb5_ticket *ticket = NULL; krb5_flags ap_req_options; - krb5_auth_context ac = NULL; + krb5_principal actual_cprincipal = NULL; + krb5_principal cprincipal = NULL; + krb5_principal sprincipal = NULL; krb5_keytab id = NULL; - krb5_principal sprincipal = NULL, cprincipal = NULL; - char *sname = NULL; - char *cname = NULL; Kx509Response rep; - krb5_keyblock *key = NULL; + struct kx509_req_context reqctx; + int is_probe = 0; + + memset(&reqctx, 0, sizeof(reqctx)); + reqctx.csr_plus.authz_datas.val = NULL; + reqctx.csr_plus.csr.data = NULL; + reqctx.csr_plus.exts = NULL; + reqctx.config = config; + reqctx.sname = NULL; + reqctx.cname = NULL; + reqctx.realm = NULL; + reqctx.reply = reply; + reqctx.from = from; + reqctx.addr = addr; + reqctx.key = NULL; + reqctx.csr = NULL; + reqctx.req = req; + reqctx.ac = NULL; /* * In order to support authenticated error messages we defer checking @@ -798,7 +1085,6 @@ _kdc_do_kx509(krb5_context context, krb5_data_zero(reply); memset(&rep, 0, sizeof(rep)); - kdc_log(context, config, 0, "Kx509 request from %s", from); if (req->authenticator.length == 0) { /* @@ -807,8 +1093,9 @@ _kdc_do_kx509(krb5_context context, * mk_error_response() will check whether the service is enabled and * possibly change the error code and message. */ - ret = mk_error_response(context, config, key, reply, - KRB5KDC_ERR_NULL_KEY, + is_probe = 1; + kx509_log(context, &reqctx, 0, "unauthenticated probe request"); + ret = mk_error_response(context, &reqctx, KRB5KDC_ERR_NULL_KEY, "kx509 service is available"); goto out; } @@ -816,22 +1103,22 @@ _kdc_do_kx509(krb5_context context, /* Consume the AP-REQ */ ret = krb5_kt_resolve(context, "HDBGET:", &id); if (ret) { - mk_error_response(context, config, key, reply, - KRB5KDC_ERR_S_PRINCIPAL_UNKNOWN, - "Can't open database for digest"); + ret = mk_error_response(context, &reqctx, + KRB5KDC_ERR_S_PRINCIPAL_UNKNOWN, + "Can't open HDB/keytab for kx509"); goto out; } ret = krb5_rd_req(context, - &ac, + &reqctx.ac, &req->authenticator, NULL, id, &ap_req_options, &ticket); if (ret == 0) - ret = krb5_auth_con_getkey(context, ac, &key); - if (ret == 0 && key == NULL) + ret = krb5_auth_con_getkey(context, reqctx.ac, &reqctx.key); + if (ret == 0 && reqctx.key == NULL) ret = KRB5KDC_ERR_NULL_KEY; /* * Provided we got the session key, errors past this point will be @@ -840,9 +1127,8 @@ _kdc_do_kx509(krb5_context context, if (ret == 0) ret = krb5_ticket_get_client(context, ticket, &cprincipal); if (ret) { - mk_error_response(context, config, key, reply, ret, - "Could not get Ticket client principal from %s", - from); + ret = mk_error_response(context, &reqctx, ret, + "authentication failed"); goto out; } @@ -852,40 +1138,34 @@ _kdc_do_kx509(krb5_context context, !get_bool_param(context, TRUE, krb5_principal_get_realm(context, cprincipal), "require_initial_kca_tickets")) { - ret = mk_error_response(context, config, key, reply, - KRB5KDC_ERR_POLICY, /* XXX */ - "kx509 client %s used non-INITIAL tickets, " - "but kx509 service is configured to require " - "INITIAL tickets", from); + ret = mk_error_response(context, &reqctx, KRB5KDC_ERR_POLICY, /* XXX */ + "client used non-INITIAL tickets, but kx509" + "kx509 service is configured to require " + "INITIAL tickets"); goto out; } - ret = krb5_unparse_name(context, cprincipal, &cname); + ret = krb5_unparse_name(context, cprincipal, &reqctx.cname); /* Check that the service name is a valid kx509 service name */ if (ret == 0) ret = krb5_ticket_get_server(context, ticket, &sprincipal); if (ret == 0) - ret = krb5_unparse_name(context, sprincipal, &sname); + reqctx.realm = krb5_principal_get_realm(context, sprincipal); if (ret == 0) + ret = krb5_unparse_name(context, sprincipal, &reqctx.sname); if (ret == 0) - ret = kdc_kx509_verify_service_principal(context, config, cname, - sprincipal); + ret = kdc_kx509_verify_service_principal(context, &reqctx, sprincipal); if (ret) { - mk_error_response(context, config, key, reply, ret, "kx509 client %s from " - "%s used incorrect service name (%s) for kx509 " - "service", - cname ? cname : "", from, - sname ? sname : ""); + mk_error_response(context, &reqctx, ret, + "client used incorrect service name"); goto out; } /* Authenticate the rest of the request */ - ret = verify_req_hash(context, req, key); + ret = verify_req_hash(context, req, reqctx.key); if (ret) { - mk_error_response(context, config, key, reply, ret, "Incorrect HMAC " - "for kx509 request for client %s from %s for %s", - cname, from, sname); + mk_error_response(context, &reqctx, ret, "Incorrect request HMAC"); goto out; } @@ -896,35 +1176,23 @@ _kdc_do_kx509(krb5_context context, * mk_error_response() will check whether the service is enabled and * possibly change the error code and message. */ - ret = mk_error_response(context, config, key, reply, 0, - "kx509 probe request"); + is_probe = 1; + ret = mk_error_response(context, &reqctx, 0, + "kx509 authenticated probe request"); goto out; } - /* - * Verify that the key is a DER-encoded RSA key - * - * TODO: Try decoding `req->pk_key' as a DER-encoded CSR, or as a - * DER-encoded Certificate (and check that the subject key signed the - * thing). - * - * That will add proof-of-possesion. - * - * That will also add algorithm agility. - */ - { - RSAPublicKey rsapkey; - size_t rsapkeysize; + /* Extract and parse CSR or a DER-encoded RSA public key */ + ret = get_csr(context, &reqctx); + if (ret) + goto out; - ret = decode_RSAPublicKey(req->pk_key.data, req->pk_key.length, - &rsapkey, &rsapkeysize); - free_RSAPublicKey(&rsapkey); - if (ret || rsapkeysize != req->pk_key.length) { - ret = KRB5KDC_ERR_NULL_KEY; - mk_error_response(context, config, key, reply, ret, - "Could not decode RSA subject public key for " - "kx509 client %s from %s for %s", cname, from, - sname); + if (reqctx.have_auth_data) { + ret = verify_auth_data(context, &reqctx, cprincipal, + &actual_cprincipal); + if (ret) { + ret = mk_error_response(context, &reqctx, ret, + "authorization data validation failure"); goto out; } } @@ -932,49 +1200,44 @@ _kdc_do_kx509(krb5_context context, ALLOC(rep.hash); ALLOC(rep.certificate); if (rep.certificate == NULL || rep.hash == NULL) { - ret = mk_error_response(context, config, key, reply, ENOMEM, - "Could allocate memory for response for kx509 " - "client %s from %s for %s", - cname, from, sname); + ret = mk_error_response(context, &reqctx, ENOMEM, + "could allocate memory for response"); goto out; } /* Issue the certificate */ krb5_data_zero(rep.hash); krb5_data_zero(rep.certificate); - ret = build_certificate(context, config, &req->pk_key, + ret = build_certificate(context, &reqctx, krb5_ticket_get_endtime(context, ticket), - cprincipal, rep.certificate); + actual_cprincipal ? actual_cprincipal : cprincipal, + rep.certificate); if (ret) { - mk_error_response(context, config, key, reply, ret, "Failed to build " - "certificate for kx509 client %s from %s for %s", - cname, from, sname); + mk_error_response(context, &reqctx, ret, "Failed to build certificate"); goto out; } /* Authenticate the response */ - ret = calculate_reply_hash(context, key, &rep); + ret = calculate_reply_hash(context, reqctx.key, &rep); if (ret) { - mk_error_response(context, config, key, reply, ret, "Failed to HMAC " - "certificate for kx509 client %s from %s for %s", - cname, from, sname); + mk_error_response(context, &reqctx, ret, + "Failed to compute response HMAC"); goto out; } /* Encode and output reply */ - ret = encode_reply(context, config, reply, &rep); + ret = encode_reply(context, &reqctx, &rep); if (ret) - mk_error_response(context, config, key, reply, ret, "Could not encode " - "kx509 response to client %s from %s for %s", cname, - from, sname); + /* Can't send an error message either in this case, surely */ + kx509_log(context, &reqctx, 0, "Could not encode response"); out: - if (ret == 0) - kdc_log(context, config, 0, "Successful Kx509 request for %s", cname); - if (ac) - krb5_auth_con_free(context, ac); - if (ret) - krb5_warn(context, ret, "Kx509 request from %s failed", from); + if (ret == 0 && !is_probe) + kx509_log(context, &reqctx, 0, "Issued certificate"); + else + kx509_log(context, &reqctx, 0, "Did not issue certificate"); + if (reqctx.ac) + krb5_auth_con_free(context, reqctx.ac); if (ticket) krb5_free_ticket(context, ticket); if (id) @@ -983,12 +1246,16 @@ out: krb5_free_principal(context, sprincipal); if (cprincipal) krb5_free_principal(context, cprincipal); - if (key) - krb5_free_keyblock (context, key); - if (sname) - free(sname); - if (cname) - free(cname); + if (actual_cprincipal) + krb5_free_principal(context, actual_cprincipal); + if (reqctx.key) + krb5_free_keyblock (context, reqctx.key); + if (reqctx.sname) + free(reqctx.sname); + if (reqctx.cname) + free(reqctx.cname); + hx509_request_free(&reqctx.csr); + free_Kx509CSRPlus(&reqctx.csr_plus); free_Kx509Response(&rep); return ret; diff --git a/kuser/Makefile.am b/kuser/Makefile.am index 75ba9c19c..8ad4c3488 100644 --- a/kuser/Makefile.am +++ b/kuser/Makefile.am @@ -17,7 +17,8 @@ man_MANS = \ kswitch.1 \ kdigest.8 \ kgetcred.1 \ - kimpersonate.8 + kimpersonate.8 \ + kx509.1 bin_PROGRAMS = kinit kdestroy kgetcred heimtools libexec_PROGRAMS = kdigest kimpersonate @@ -45,7 +46,7 @@ heimtools_LDADD = \ $(LIB_readline) \ $(LIB_hx509) -dist_heimtools_SOURCES = heimtools.c klist.c kswitch.c copy_cred_cache.c +dist_heimtools_SOURCES = heimtools.c klist.c kx509.c kswitch.c copy_cred_cache.c nodist_heimtools_SOURCES = heimtools-commands.c $(heimtools_OBJECTS): heimtools-commands.h @@ -94,5 +95,6 @@ EXTRA_DIST = NTMakefile $(man_MANS) \ # make sure install-exec-hook doesn't have any commands in Makefile.am.common install-exec-hook: (cd $(DESTDIR)$(bindir) && rm -f klist && $(LN_S) heimtools klist) + (cd $(DESTDIR)$(bindir) && rm -f kx509 && $(LN_S) heimtools kx509) (cd $(DESTDIR)$(bindir) && rm -f kswitch && $(LN_S) heimtools kswitch) diff --git a/kuser/NTMakefile b/kuser/NTMakefile index 3b12ffd44..8df4d888e 100644 --- a/kuser/NTMakefile +++ b/kuser/NTMakefile @@ -79,6 +79,7 @@ HEIMTOOLS_OBJS = \ $(OBJ)\heimtools.obj \ $(OBJ)\kswitch.obj \ $(OBJ)\klist.obj \ + $(OBJ)\kx509.obj \ $(OBJ)\copy_cred_cache.obj HEIMTOOLSLIBS=\ diff --git a/kuser/heimtools-commands.in b/kuser/heimtools-commands.in index 3d8a20684..97ef10923 100644 --- a/kuser/heimtools-commands.in +++ b/kuser/heimtools-commands.in @@ -114,11 +114,6 @@ command = { type = "flag" help = "Verbose output" } - option = { - long = "extract-kx509-cert" - type = "string" - help = "hx509 store for kx509 certificate and private key" - } } command = { name = "kgetcred" @@ -245,6 +240,60 @@ command = { help = "Copies credential caches" argument = "[source] destination" } +command = { + name = "kx509" + help = "Acquire or extract certificates" + option = { + long = "cache" + short = "c" + type = "string" + help = "Kerberos credential cache" + } + option = { + long = "save" + short = "s" + type = "flag" + help = "save the certificate and private key in the Kerberos credential cache" + } + option = { + long = "out" + short = "o" + type = "string" + help = "hx509 store for kx509 certificate and private key" + } + option = { + long = "extract" + short = "x" + type = "flag" + help = "extract certificate and private key from credential cache" + } + option = { + long = "test" + short = "t" + type = "flag" + help = "exit successfully if certificate and private key are in credential cache" + } + option = { + name = "private-key" + short = "K" + type = "string" + help = "hx509 store containing private key" + } + option = { + name = "csr" + short = "C" + type = "string" + help = "file containing DER-encoded PKCS#10 certificate request" + } + option = { + name = "realm" + short = "r" + type = "string" + help = "realm from which to acquire certificate" + } + min_args = "0" + max_args = "0" +} command = { name = "help" name = "?" diff --git a/kuser/klist.1 b/kuser/klist.1 index 17d888961..8ebad7d1b 100644 --- a/kuser/klist.1 +++ b/kuser/klist.1 @@ -44,8 +44,6 @@ .Fl Fl cache= Ns Ar cache .Xc .Oc -.Oo Fl Fl extract-kx509-cert= Ns Ar hx509-store -.Oc .Op Fl s | Fl t | Fl Fl test .Op Fl T | Fl Fl tokens .Op Fl 5 | Fl Fl v5 @@ -67,17 +65,6 @@ credential cache to list .It Fl s , Fl t , Fl Fl test Test for there being an active and valid TGT for the local realm of the user in the credential cache. -.It Fl Fl extract-kx509-cert= Ns Ar hx509-store -An hx509 store specification, such as -.Va DER-FILE:/path/to/der/file , -.Va PEM-FILE:/path/to/PEM/file , -.Va FILE:/path/to/PEM/file , -or -.Va PKCS12:/path/to/PKCS#12/file -into which to store any PKIX certificate and private key -(unencrypted) that may have been acquired with the kx509 protocol -and stored in the -.Ns Ar ccache. .It Fl T , Fl Fl tokens display AFS tokens .It Fl 5 , Fl Fl v5 diff --git a/kuser/klist.c b/kuser/klist.c index 73c2d90ed..876e4c861 100644 --- a/kuser/klist.c +++ b/kuser/klist.c @@ -36,10 +36,7 @@ #include "kuser_locl.h" #include "parse_units.h" #include "heimtools-commands.h" -#include #undef HC_DEPRECATED_CRYPTO -#include "../lib/hx509/hx_locl.h" -#include "hx509-private.h" static char* printable_time_internal(time_t t, int x) @@ -264,28 +261,15 @@ print_tickets(krb5_context context, int do_verbose, int do_flags, int do_hidden, - int do_json, - const char *hx509_store) + int do_json) { char *str, *name, *fullname; - char *kx509_realm = NULL; - hx509_private_key key = NULL; - hx509_context hx509ctx = NULL; - hx509_certs certs = NULL; - hx509_cert cert = NULL; - krb5_error_code kx509_ret = 0; krb5_error_code ret; krb5_cc_cursor cursor; krb5_creds creds; krb5_deltat sec; rtbl_t ct = NULL; int print_comma = 0; - int kx509_disabled = 0; - int cert_stored = 0; - int cert_seen = 0; - - if (hx509_store) - kx509_ret = hx509_context_init(&hx509ctx); ret = krb5_unparse_name (context, principal, &str); if (ret) @@ -358,45 +342,6 @@ print_tickets(krb5_context context, if (do_verbose && do_json) printf("\"tickets\" : ["); while ((ret = krb5_cc_next_cred(context, ccache, &cursor, &creds)) == 0) { - if (krb5_is_config_principal(context, creds.server) && - krb5_principal_get_num_comp(context, creds.server) == 2 && - hx509_store) { - const char *s; - - s = krb5_principal_get_comp_string(context, creds.server, 1); - if (strcmp(s, "kx509_service_status") == 0) { - kx509_disabled = 1; - } else if (strcmp(s, "kx509_service_realm") == 0) { - kx509_realm = strndup(creds.ticket.data, creds.ticket.length); - } else if (strcmp(s, "kx509cert") == 0) { - cert = hx509_cert_init_data(hx509ctx, creds.ticket.data, - creds.ticket.length, NULL); - } else if (strcmp(s, "kx509key") == 0) { - (void) hx509_parse_private_key(hx509ctx, NULL, - creds.ticket.data, - creds.ticket.length, - HX509_KEY_FORMAT_PKCS8, &key); - } - if (hx509ctx && cert && key && !cert_seen) { - /* Now store the cert and key into the given hx509 store */ - (void) _hx509_cert_assign_key(cert, key); - kx509_ret = hx509_certs_init(hx509ctx, hx509_store, - HX509_CERTS_CREATE, NULL, &certs); - if (kx509_ret == 0) - kx509_ret = hx509_certs_add(hx509ctx, certs, cert); - if (kx509_ret == 0) - kx509_ret = hx509_certs_store(hx509ctx, certs, 0, NULL); - - /* - * Wait till we're done listing the ccache to complain about - * failing to extract the cert and priv key. - */ - cert_seen = 1; - if (kx509_ret == 0) - cert_stored = 1; - } - } - if (!do_hidden && krb5_is_config_principal(context, creds.server)) { ; } else if (do_verbose) { @@ -415,40 +360,6 @@ print_tickets(krb5_context context, if (ret) krb5_err(context, 1, ret, "krb5_cc_end_seq_get"); - /* Finish kx509 extraction error checking */ - if (hx509_store && cert_seen && !cert_stored) { - if (!hx509ctx) - krb5_err(context, 1, kx509_ret, - N_("Failed to store certificate and private key " - "in %s due to failure to initialize context: %s", ""), - hx509_store, hx509_get_error_string(hx509ctx, kx509_ret)); - if (!cert || !key) - krb5_err(context, 1, kx509_ret, - N_("Failed to store certificate and private key " - "in %s due to failure to parse them: %s", ""), - hx509_store, hx509_get_error_string(hx509ctx, kx509_ret)); - krb5_err(context, 1, kx509_ret, N_("Failed to store certificate and " - "private key in %s", ""), - hx509_store); - } - if (hx509_store && !cert_seen) { - /* No PKIX creds in ccache, but maybe we can run kx509 now */ - if (kx509_disabled) - krb5_errx(context, 1, N_("The kx509 protocol is disabled at the " - "KDC for realm %s", ""), - kx509_realm ? kx509_realm : ""); - ret = krb5_kx509_ext(context, ccache, NULL, NULL, NULL, 0, hx509_store, - NULL); - if (ret) - krb5_err(context, 1, ret, N_("Failed to acquire certificate and " - "store it and private key in %s", ""), - hx509_store); - } - hx509_private_key_free(&key); - hx509_certs_free(&certs); - hx509_cert_free(cert); - hx509_context_free(&hx509ctx); - print_comma = 0; if(!do_verbose) { rtbl_format(ct, stdout); @@ -566,7 +477,7 @@ static int display_v5_ccache (krb5_context context, krb5_ccache ccache, int do_test, int do_verbose, int do_flags, int do_hidden, - int do_json, const char *hx509_store) + int do_json) { krb5_error_code ret; krb5_principal principal; @@ -591,7 +502,7 @@ display_v5_ccache (krb5_context context, krb5_ccache ccache, exit_status = check_expiration(context, ccache, NULL); else print_tickets (context, ccache, principal, do_verbose, - do_flags, do_hidden, do_json, hx509_store); + do_flags, do_hidden, do_json); ret = krb5_cc_close (context, ccache); if (ret) @@ -738,8 +649,8 @@ klist(struct klist_options *opt, int argc, char **argv) exit_status |= display_v5_ccache(heimtools_context, id, do_test, do_verbose, opt->flags_flag, - opt->hidden_flag, opt->json_flag, - opt->extract_kx509_cert_string); + opt->hidden_flag, + opt->json_flag); if (!opt->json_flag) printf("\n\n"); @@ -760,8 +671,7 @@ klist(struct klist_options *opt, int argc, char **argv) } exit_status = display_v5_ccache(heimtools_context, id, do_test, do_verbose, opt->flags_flag, - opt->hidden_flag, opt->json_flag, - opt->extract_kx509_cert_string); + opt->hidden_flag, opt->json_flag); } } diff --git a/kuser/kx509.1 b/kuser/kx509.1 new file mode 100644 index 000000000..76be02259 --- /dev/null +++ b/kuser/kx509.1 @@ -0,0 +1,128 @@ +.\" Copyright (c) 2019 Kungliga Tekniska Högskolan +.\" (Royal Institute of Technology, Stockholm, Sweden). +.\" All rights reserved. +.\" +.\" Redistribution and use in source and binary forms, with or without +.\" modification, are permitted provided that the following conditions +.\" are met: +.\" +.\" 1. Redistributions of source code must retain the above copyright +.\" notice, this list of conditions and the following disclaimer. +.\" +.\" 2. Redistributions in binary form must reproduce the above copyright +.\" notice, this list of conditions and the following disclaimer in the +.\" documentation and/or other materials provided with the distribution. +.\" +.\" 3. Neither the name of the Institute nor the names of its contributors +.\" may be used to endorse or promote products derived from this software +.\" without specific prior written permission. +.\" +.\" THIS SOFTWARE IS PROVIDED BY THE INSTITUTE AND CONTRIBUTORS ``AS IS'' AND +.\" ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +.\" IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +.\" ARE DISCLAIMED. IN NO EVENT SHALL THE INSTITUTE OR CONTRIBUTORS BE LIABLE +.\" FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +.\" DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS +.\" OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) +.\" HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT +.\" LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY +.\" OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF +.\" SUCH DAMAGE. +.\" +.\" $Id$ +.\" +.Dd October 6, 2005 +.Dt KLIST 1 +.Os HEIMDAL +.Sh NAME +.Nm kx509 +.Nd acquire or extract certificates using Kerberos credentials +.Sh SYNOPSIS +.Nm +.Bk -words +.Oo Fl c Ar cache \*(Ba Xo +.Fl Fl cache= Ns Ar cache +.Xc +.Oc +.Oo Fl s \*(Ba Xo +.Fl Fl save +.Xc +.Oc +.Oo Fl o Ar store \*(Ba Xo +.Fl Fl out= Ns Ar store +.Xc +.Oc +.Oo Fl x \*(Ba Xo +.Fl Fl extract +.Xc +.Oc +.Oo Fl t \*(Ba Xo +.Fl Fl test +.Xc +.Oc +.Oo Fl C Ar PKCS10:filename \*(Ba Xo +.Fl Fl csr= Ns Ar PKCS10:filename +.Xc +.Oc +.Oo Fl C Ar PKCS10:filename \*(Ba Xo +.Fl Fl csr= Ns Ar PKCS10:filename +.Xc +.Oc +.Oo Fl K Ar hx509-store \*(Ba Xo +.Fl Fl private-key= Ns Ar hx509-store +.Xc +.Oc +.Oo Fl r Ar realm \*(Ba Xo +.Fl Fl realm= Ns Ar realm +.Xc +.Oc +.Op Fl Fl help +.Ek +.Sh DESCRIPTION +.Nm +acquires PKIX credentials from a credential cache using the kx509 +protocol, or extracts PKIX credentials stored in a credential +cache. +.Pp +Options supported: +.Bl -tag -width Ds +.It Fl c Ar cache , Fl Fl cache= Ns Ar cache +credential cache to use (if not given, then the default will be +used). +.It Fl t , Fl Fl test +Test for there being an active and valid certificate in the +credential cache. +.It Fl x , Fl Fl extract +Extract, rather than acquire credentials. +.It Fl s , Fl Fl save +save the acquired certificate and the private key used in the +given credential cache. +.It Fl o , Fl Fl out= Ns Ar hx509-store +An hx509 store specification, such as +.Va DER-FILE:/path/to/der/file , +.Va PEM-FILE:/path/to/PEM/file , +.Va FILE:/path/to/PEM/file , +or +.Va PKCS12:/path/to/PKCS#12/file +into which to store any PKIX certificate and private key +(unencrypted) that may have been acquired with the kx509 protocol +and stored in the +.Ns Ar ccache. +.It Fl r Ar realm, Fl Fl realm= Ns Ar realm +specify the name of the realm whose kx509 service to use. +.It Fl K Ar store, Fl Fl private-key= Ns Ar store +use the private key from the given hx509 store for requesting a +certificate. +.It Fl C Ar csr, Fl Fl csr= Ns Ar certificate-request +specify a CSR to use, which must be a string of the form +PKCS10:filename and which must contain the DER encoding of a +PKCS#10 certification request. +.El +.Pp +The +.Nm hxtool(1) +command can be used to create private keys and CSRs. +.Sh SEE ALSO +.Xr kdestroy 1 , +.Xr kinit 1 , +.Xr hxtool 1 diff --git a/kuser/kx509.c b/kuser/kx509.c new file mode 100644 index 000000000..4ec2de2c5 --- /dev/null +++ b/kuser/kx509.c @@ -0,0 +1,256 @@ +/* + * Copyright (c) 2019 Kungliga Tekniska Högskolan + * (Royal Institute of Technology, Stockholm, Sweden). + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * 3. Neither the name of the Institute nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE INSTITUTE AND CONTRIBUTORS ``AS IS'' AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE INSTITUTE OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS + * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) + * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY + * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF + * SUCH DAMAGE. + */ + +#include "kuser_locl.h" +#include "heimtools-commands.h" +#include +#undef HC_DEPRECATED_CRYPTO +#include "../lib/hx509/hx_locl.h" +#include "../lib/krb5/krb5_locl.h" +#include "hx509-private.h" + +static void +validate(krb5_context context, + const char *hx509_store, + krb5_data *der_cert, + krb5_data *pkcs8_priv_key) +{ + hx509_context hx509ctx = NULL; + hx509_private_key key = NULL; + hx509_cert cert; + krb5_error_code ret; + + ret = hx509_context_init(&hx509ctx); + if (ret) + krb5_err(context, 1, ret, "hx509 context init"); + + cert = hx509_cert_init_data(hx509ctx, der_cert->data, + der_cert->length, NULL); + if (cert == NULL) + krb5_err(context, 1, errno, "certificate could not be loaded"); + ret = hx509_parse_private_key(hx509ctx, NULL, pkcs8_priv_key->data, + pkcs8_priv_key->length, + HX509_KEY_FORMAT_PKCS8, &key); + if (ret) + krb5_err(context, 1, ret, "certificate could not be loaded"); + if (hx509_cert_get_notAfter(cert) < time(NULL)) + krb5_errx(context, 1, "certificate is expired"); + hx509_private_key_free(&key); + hx509_cert_free(cert); + hx509_context_free(&hx509ctx); +} + +static krb5_error_code +add1_2chain(hx509_context hx509ctx, void *d, hx509_cert cert) +{ + heim_octet_string os; + krb5_error_code ret; + Certificates *cs = d; + Certificate c; + size_t len; + + + ret = hx509_cert_binary(hx509ctx, cert, &os); + if (ret == 0) + ASN1_MALLOC_ENCODE(Certificate, os.data, os.length, &c, &len, ret); + der_free_octet_string(&os); + if (ret == 0) { + add_Certificates(cs, &c); + free_Certificate(&c); + } + return ret; +} + +static krb5_error_code +add_chain(hx509_context hx509ctx, hx509_certs certs, krb5_data *chain) +{ + krb5_error_code ret; + Certificates cs; + size_t len; + + ret = decode_Certificates(chain->data, chain->length, &cs, &len); + if (ret == 0) { + ret = hx509_certs_iter_f(hx509ctx, certs, add1_2chain, &cs); + free_Certificates(&cs); + } + return ret; +} + +static void +store(krb5_context context, + const char *hx509_store, + krb5_data *der_cert, + krb5_data *pkcs8_priv_key, + krb5_data *chain) +{ + hx509_context hx509ctx = NULL; + hx509_private_key key = NULL; + hx509_certs certs; + hx509_cert cert; + char *store_exp = NULL; + krb5_error_code ret; + + if (hx509_store == NULL) { + hx509_store = krb5_config_get_string(context, NULL, "libdefaults", + "kx509_store", NULL); + if (hx509_store) { + ret = _krb5_expand_path_tokens(context, hx509_store, 1, &store_exp); + if (ret) + krb5_err(context, 1, ret, "expanding tokens in default " + "hx509 store"); + hx509_store = store_exp; + } + } + if (hx509_store == NULL) + krb5_errx(context, 1, "no hx509 store given and no default hx509 " + "store configured"); + + ret = hx509_context_init(&hx509ctx); + if (ret) + krb5_err(context, 1, ret, "hx509 context init"); + + cert = hx509_cert_init_data(hx509ctx, der_cert->data, + der_cert->length, NULL); + if (cert == NULL) + krb5_err(context, 1, errno, "certificate could not be loaded"); + ret = hx509_parse_private_key(hx509ctx, NULL, pkcs8_priv_key->data, + pkcs8_priv_key->length, + HX509_KEY_FORMAT_PKCS8, &key); + if (ret) + krb5_err(context, 1, ret, "certificate could not be loaded"); + (void) _hx509_cert_assign_key(cert, key); + + ret = hx509_certs_init(hx509ctx, hx509_store, HX509_CERTS_CREATE, NULL, + &certs); + if (ret == 0) + ret = hx509_certs_add(hx509ctx, certs, cert); + if (ret == 0) + add_chain(hx509ctx, certs, chain); + if (ret == 0) + ret = hx509_certs_store(hx509ctx, certs, 0, NULL); + if (ret) + krb5_err(context, 1, ret, "certificate could not be stored"); + + hx509_private_key_free(&key); + hx509_certs_free(&certs); + hx509_cert_free(cert); + hx509_context_free(&hx509ctx); + free(store_exp); +} + +static void +set_csr(krb5_context context, krb5_kx509_req_ctx req, const char *csr_file) +{ + krb5_error_code ret; + krb5_data d; + + if (strncmp(csr_file, "PKCS10:", sizeof("PKCS10:") - 1) != 0) + krb5_errx(context, 1, "CSR filename must start with \"PKCS10:\""); + ret = rk_undumpdata(csr_file + sizeof("PKCS10:") - 1, &d.data, &d.length); + if (ret) + krb5_err(context, 1, ret, "could not read CSR"); + ret = krb5_kx509_ctx_set_csr_der(context, req, &d); + if (ret) + krb5_err(context, 1, ret, "hx509 context init"); +} + +int +kx509(struct kx509_options *opt, int argc, char **argv) +{ + krb5_kx509_req_ctx req = NULL; + krb5_context context = heimtools_context; + krb5_error_code ret; + krb5_ccache ccout = NULL; + krb5_ccache cc = NULL; + + if (opt->cache_string) + ret = krb5_cc_resolve(context, opt->cache_string, &cc); + else if (opt->save_flag || opt->extract_flag) + ret = krb5_cc_default(context, &cc); + if (ret) + krb5_err(context, 1, ret, "no input credential cache"); + if (opt->save_flag) + ccout = cc; + + if (opt->test_flag && + (opt->extract_flag || opt->csr_string || opt->private_key_string)) + krb5_errx(context, 1, "--test is exclusive of --extract, --csr, and " + "--private-key"); + + if (opt->extract_flag && (opt->csr_string || opt->private_key_string)) + krb5_errx(context, 1, "--extract is exclusive of --csr and --private-key"); + + if (opt->test_flag || opt->extract_flag) { + krb5_data der_cert, pkcs8_key, chain; + + der_cert.data = pkcs8_key.data = chain.data = NULL; + der_cert.length = pkcs8_key.length = chain.length = 0; + ret = krb5_cc_get_config(context, cc, NULL, "kx509cert", &der_cert); + if (ret == 0) + ret = krb5_cc_get_config(context, cc, NULL, "kx509key", &pkcs8_key); + if (ret == 0) + ret = krb5_cc_get_config(context, cc, NULL, "kx509cert-chain", &chain); + if (ret) + krb5_err(context, 1, ret, "no certificate in credential cache"); + if (opt->test_flag) + validate(context, opt->out_string, &der_cert, &pkcs8_key); + else + store(context, opt->out_string, &der_cert, &pkcs8_key, &chain); + krb5_data_free(&pkcs8_key); + krb5_data_free(&der_cert); + krb5_data_free(&chain); + } else { + /* + * XXX We should delete any cc configs that indicate that kx509 is + * disabled. + */ + ret = krb5_kx509_ctx_init(context, &req); + if (ret == 0 && opt->realm_string) + ret = krb5_kx509_ctx_set_realm(context, req, opt->realm_string); + if (ret == 0 && opt->csr_string) + set_csr(context, req, opt->csr_string); + if (ret == 0 && opt->private_key_string) + ret = krb5_kx509_ctx_set_key(context, req, opt->private_key_string); + if (ret) + krb5_err(context, 1, ret, "could not setup kx509 request options"); + + ret = krb5_kx509_ext(context, req, cc, opt->out_string, ccout); + if (ret) + krb5_err(context, 1, ret, "could not acquire certificate with kx509"); + krb5_kx509_ctx_free(context, &req); + } + + krb5_cc_close(context, cc); + + return 0; +} diff --git a/lib/asn1/krb5.asn1 b/lib/asn1/krb5.asn1 index 9183fc19a..560109997 100644 --- a/lib/asn1/krb5.asn1 +++ b/lib/asn1/krb5.asn1 @@ -212,7 +212,11 @@ AUTHDATA-TYPE ::= INTEGER { KRB5-AUTHDATA-GSS-API-ETYPE-NEGOTIATION(129), -- Authenticator only KRB5-AUTHDATA-SIGNTICKET-OLDER(-17), KRB5-AUTHDATA-SIGNTICKET-OLD(142), - KRB5-AUTHDATA-SIGNTICKET(512) + KRB5-AUTHDATA-SIGNTICKET(512), + -- N.B. these assignments have not been confirmed yet. + -- + -- DO NOT USE in production yet! + KRB5-AUTHDATA-ON-BEHALF-OF(580) -- UTF8String princ name } -- checksumtypes diff --git a/lib/asn1/kx509.asn1 b/lib/asn1/kx509.asn1 index 9357ab970..5451df3d9 100644 --- a/lib/asn1/kx509.asn1 +++ b/lib/asn1/kx509.asn1 @@ -1,9 +1,13 @@ -- $Id$ --- The kx509 protocol is documented in RFC6717. +-- Version 2 of the kx509 protocol is documented in RFC6717. +-- +-- Our version here has extensions without changing the version number on the +-- wire. -KX509 DEFINITIONS ::= -BEGIN +KX509 DEFINITIONS ::= BEGIN +IMPORTS Extensions FROM rfc2459 + AUTHDATA-TYPE FROM krb5; KX509-ERROR-CODE ::= INTEGER { KX509-STATUS-GOOD(0), @@ -13,15 +17,68 @@ KX509-ERROR-CODE ::= INTEGER { KX509-STATUS-SERVER-BAD(4), KX509-STATUS-SERVER-TEMP(5), -- 6 is used internally in the umich client, avoid that - KX509-STATUS-SERVER-KEY(7) + KX509-STATUS-SERVER-KEY(7), + -- CSR use negotiation: + KX509-STATUS-CLIENT-USE-CSR(8) -- Let us reserve 1000+ for Kebreros protocol wire error codes -Nico } --- Version 2, which has no proof of possession +-- Originally kx509 requests carried only a public key. We'd like to have +-- proof of possession, and the ability to carry additional options, both, in +-- cleartext and otherwise. +-- +-- We'll use a CSR for proof of posession and desired certificate extensions. +-- +-- We'll also provide a non-CSR-based method of conveying desired certificate +-- extensions. The reason for this is simply that we may want to have a [e.g., +-- RESTful HTTP] proxy for the kx509 service, and we want clients to be able to +-- be as simple as possible -cargo-culted even- with support for attributes +-- (desired certificate extensions) as parameters outside the CSR that the +-- proxy can encode without having the private key for the CSR (naturally). +-- +-- I.e., ultimately we'll have a REST endpoint, /kx509, say, with query +-- parameters like: +-- +-- - csr= +-- - eku= +-- - ku= +-- - rfc822Name= +-- - xMPPName= +-- - dNSName= +-- - dNSSrv= +-- - registeredID= +-- - principalName= +-- +-- with exactly one CSR and zero, one, or more of the other parameters. +-- +-- We'll even have a way to convey a bearer token from the REST proxy so that +-- we may have a way to get PKIX credentials using bearer tokens. And then, +-- using PKINIT, we may have a way to get Kerberos credentials using bearer +-- tokens. +-- +-- To do this we define a Kx509CSRPlus that we can use in the `pk-key' field of +-- Kx509Request (see below): +Kx509CSRPlus ::= [APPLICATION 35] SEQUENCE { + -- PKCS#10, DER-encoded CSR, with or without meaningful attributes + csr OCTET STRING, + -- The AP-REQ's Authenticator may contain authz-data of interest here + -- for carrying confidential payloads. E.g., a bearer token for a user + -- to impersonate. This sequence tells the server what authz-data + -- elements there might be, effectively making them critical even if + -- they are in AD-IF-RELEVANT containers. + authz-datas SEQUENCE OF AUTHDATA-TYPE, + -- Desired certificate Extensions such as KeyUsage, ExtKeyUsage, or + -- subjectAlternativeName (SAN) + exts Extensions OPTIONAL +} + +-- Version 2 Kx509Request ::= SEQUENCE { authenticator OCTET STRING, pk-hash OCTET STRING, -- HMAC(ticket_session_key, pk-key) - pk-key OCTET STRING -- the public key, DER-encoded (RSA, basically) + pk-key OCTET STRING -- one of: + -- - the public key, DER-encoded (RSA, basically) + -- - a Kx509CSRPlus } -- Kx509ErrorCode is a Heimdal-specific enhancement with no change on the wire, @@ -31,7 +88,10 @@ Kx509ErrorCode ::= INTEGER (-2147483648..2147483647) Kx509Response ::= SEQUENCE { error-code[0] Kx509ErrorCode DEFAULT 0, hash[1] OCTET STRING OPTIONAL, -- HMAC(session_key, ...) - certificate[2] OCTET STRING OPTIONAL, -- Certificates (plural) + certificate[2] OCTET STRING OPTIONAL, -- DER-encoded Certificate + -- if client sent raw RSA SPK + -- or DER-encoded Certificates + -- (i.e., SEQ. OF Certificate) -- if client used a -- Kx509CSRPlus e-text[3] VisibleString OPTIONAL diff --git a/lib/krb5/krb5.h b/lib/krb5/krb5.h index 1fbbc00f4..e41a8d565 100644 --- a/lib/krb5/krb5.h +++ b/lib/krb5/krb5.h @@ -989,6 +989,7 @@ typedef struct krb5_name_canon_iterator_data *krb5_name_canon_iterator; */ struct hx509_certs_data; +typedef struct krb5_kx509_req_ctx_data *krb5_kx509_req_ctx; #include diff --git a/lib/krb5/kx509.c b/lib/krb5/kx509.c index 19a1f1abd..0ca1eafb6 100644 --- a/lib/krb5/kx509.c +++ b/lib/krb5/kx509.c @@ -96,16 +96,302 @@ static const unsigned char version_2_0[4] = {0 , 0, 2, 0}; -struct kx509_ctx_data { - char *send_to_realm; /* realm to which to send request */ - krb5_keyblock *hmac_key; /* For HMAC validation */ - hx509_private_key *keys; +struct krb5_kx509_req_ctx_data { + krb5_auth_context ac; + krb5_data given_csr; + hx509_request csr; + Kx509CSRPlus csr_plus; + char *realm; /* Realm to which to send request */ + krb5_keyblock *hmac_key; /* For HMAC validation */ + hx509_private_key *keys; hx509_private_key priv_key; + unsigned int expect_chain; }; +/** + * Create a kx509 request context. + * + * @param context The Kerberos library context + * @param out Where to place the kx509 request context + * + * @return A krb5 error code. + */ +krb5_error_code +krb5_kx509_ctx_init(krb5_context context, krb5_kx509_req_ctx *out) +{ + krb5_kx509_req_ctx ctx; + krb5_error_code ret; + hx509_name name = NULL; + + ALLOC(ctx, 1); + if (ctx == NULL) + return krb5_enomem(context); + ctx->given_csr.data = NULL; + ctx->priv_key = NULL; + ctx->hmac_key = NULL; + ctx->realm = NULL; + ctx->keys = NULL; + ctx->csr = NULL; + ret = hx509_request_init(context->hx509ctx, &ctx->csr); + if (ret == 0) + ret = hx509_parse_name(context->hx509ctx, "", &name); + if (ret == 0) + ret = hx509_request_set_name(context->hx509ctx, ctx->csr, name); + if (ret == 0) + ret = krb5_auth_con_init(context, &ctx->ac); + if (name) + hx509_name_free(&name); + if (ret == 0) + *out = ctx; + else + krb5_kx509_ctx_free(context, &ctx); + return ret; +} + +/** + * Free a kx509 request context. + * + * @param context The Kerberos library context + * @param ctxp Pointer to krb5 request context to free + * + * @return A krb5 error code. + */ +void +krb5_kx509_ctx_free(krb5_context context, krb5_kx509_req_ctx *ctxp) +{ + krb5_kx509_req_ctx ctx = *ctxp; + + *ctxp = NULL; + if (ctx == NULL) + return; + krb5_free_keyblock(context, ctx->hmac_key); + krb5_auth_con_free(context, ctx->ac); + free_Kx509CSRPlus(&ctx->csr_plus); + free(ctx->realm); + hx509_request_free(&ctx->csr); + krb5_data_free(&ctx->given_csr); + hx509_private_key_free(&ctx->priv_key); + _hx509_certs_keys_free(context->hx509ctx, ctx->keys); + free(ctx); +} + +/** + * Set a realm to send kx509 request to, if different from the client's. + * + * @param context The Kerberos library context + * @param ctx The kx509 request context + * @param realm Realm name + * + * @return A krb5 error code. + */ +krb5_error_code +krb5_kx509_ctx_set_realm(krb5_context context, + krb5_kx509_req_ctx kx509_ctx, + const char *realm) +{ + return ((kx509_ctx->realm = strdup(realm)) == NULL) ? + krb5_enomem(context) : 0; +} + +/** + * Sets a CSR for a kx509 request. + * + * Normally kx509 will generate a CSR (and even a private key for it) + * automatically. If a CSR is given then kx509 will use it instead of + * generating one. + * + * @param context The Kerberos library context + * @param ctx The kx509 request context + * @param csr_der A DER-encoded PKCS#10 CSR + * + * @return A krb5 error code. + */ +krb5_error_code +krb5_kx509_ctx_set_csr_der(krb5_context context, + krb5_kx509_req_ctx ctx, + krb5_data *csr_der) +{ + krb5_data_free(&ctx->given_csr); + return krb5_data_copy(&ctx->given_csr, csr_der->data, csr_der->length); +} + +/** + * Adds an EKU as an additional desired Certificate Extension or in the CSR if + * the caller does not set a CSR. + * + * @param context The Kerberos library context + * @param ctx The kx509 request context + * @param oids A string representation of an OID + * + * @return A krb5 error code. + */ +krb5_error_code +krb5_kx509_ctx_add_eku(krb5_context context, + krb5_kx509_req_ctx kx509_ctx, + const char *oids) +{ + krb5_error_code ret; + heim_oid oid; + + ret = der_parse_heim_oid(oids, NULL, &oid); + if (ret == 0) + hx509_request_add_eku(context->hx509ctx, kx509_ctx->csr, &oid); + der_free_oid(&oid); + return ret; +} + +/** + * Adds a dNSName SAN (domainname, hostname) as an additional desired + * Certificate Extension or in the CSR if the caller does not set a CSR. + * + * @param context The Kerberos library context + * @param ctx The kx509 request context + * @param dname A string containing a DNS domainname + * + * @return A krb5 error code. + */ +krb5_error_code +krb5_kx509_ctx_add_san_dns_name(krb5_context context, + krb5_kx509_req_ctx kx509_ctx, + const char *dname) +{ + return hx509_request_add_dns_name(context->hx509ctx, kx509_ctx->csr, + dname); +} + +/** + * Adds an xmppAddr SAN (jabber address) as an additional desired Certificate + * Extension or in the CSR if the caller does not set a CSR. + * + * @param context The Kerberos library context + * @param ctx The kx509 request context + * @param jid A string containing a Jabber address + * + * @return A krb5 error code. + */ +krb5_error_code +krb5_kx509_ctx_add_san_xmpp(krb5_context context, + krb5_kx509_req_ctx kx509_ctx, + const char *jid) +{ + return hx509_request_add_xmpp_name(context->hx509ctx, kx509_ctx->csr, jid); +} + +/** + * Adds an rfc822Name SAN (e-mail address) as an additional desired Certificate + * Extension or in the CSR if the caller does not set a CSR. + * + * @param context The Kerberos library context + * @param ctx The kx509 request context + * @param email A string containing an e-mail address + * + * @return A krb5 error code. + */ +krb5_error_code +krb5_kx509_ctx_add_san_rfc822Name(krb5_context context, + krb5_kx509_req_ctx kx509_ctx, + const char *email) +{ + return hx509_request_add_email(context->hx509ctx, kx509_ctx->csr, email); +} + +/** + * Adds an pkinit SAN (Kerberos principal name) as an additional desired + * Certificate Extension or in the CSR if the caller does not set a CSR. + * + * @param context The Kerberos library context + * @param ctx The kx509 request context + * @param pname A string containing a representation of a Kerberos principal + * name + * + * @return A krb5 error code. + */ +krb5_error_code +krb5_kx509_ctx_add_san_pkinit(krb5_context context, + krb5_kx509_req_ctx kx509_ctx, + const char *pname) +{ + return hx509_request_add_pkinit(context->hx509ctx, kx509_ctx->csr, pname); +} + +/** + * Adds a Microsoft-style UPN (user principal name) as an additional desired + * Certificate Extension or in the CSR if the caller does not set a CSR. + * + * @param context The Kerberos library context + * @param ctx The kx509 request context + * @param upn A string containing a representation of a UPN + * + * @return A krb5 error code. + */ +krb5_error_code +krb5_kx509_ctx_add_san_ms_upn(krb5_context context, + krb5_kx509_req_ctx kx509_ctx, + const char *upn) +{ + return hx509_request_add_ms_upn_name(context->hx509ctx, kx509_ctx->csr, + upn); +} + +/** + * Adds an registeredID SAN (OID) as an additional desired Certificate + * Extension or in the CSR if the caller does not set a CSR. + * + * @param context The Kerberos library context + * @param ctx The kx509 request context + * @param oids A string representation of an OID + * + * @return A krb5 error code. + */ +krb5_error_code +krb5_kx509_ctx_add_san_registeredID(krb5_context context, + krb5_kx509_req_ctx kx509_ctx, + const char *oids) +{ + krb5_error_code ret; + heim_oid oid; + + ret = der_parse_heim_oid(oids, NULL, &oid); + if (ret == 0) + hx509_request_add_registered(context->hx509ctx, kx509_ctx->csr, &oid); + der_free_oid(&oid); + return ret; +} + +/** + * Adds authorization data to a kx509 request context. + * + * @param context The Kerberos library context + * @param ctx The kx509 request context + * @param ad_type The authorization data type + * @param ad_data The authorization data + * + * @return A krb5 error code. + */ +krb5_error_code +krb5_kx509_ctx_add_auth_data(krb5_context context, + krb5_kx509_req_ctx kx509_ctx, + krb5int32 ad_type, + krb5_data *ad_data) +{ + AUTHDATA_TYPE *tmp; + Kx509CSRPlus *p = &kx509_ctx->csr_plus; + + tmp = realloc(p->authz_datas.val, + sizeof(p->authz_datas.val[0]) * (p->authz_datas.len + 1)); + if (tmp == NULL) + return krb5_enomem(context); + p->authz_datas.val = tmp; + p->authz_datas.val[p->authz_datas.len++] = ad_type; + + return krb5_auth_con_add_AuthorizationDataIfRelevant(context, + kx509_ctx->ac, + ad_type, ad_data); +} + static krb5_error_code load_priv_key(krb5_context context, - struct kx509_ctx_data *kx509_ctx, + krb5_kx509_req_ctx kx509_ctx, const char *fn) { hx509_private_key *keys = NULL; @@ -129,6 +415,38 @@ load_priv_key(krb5_context context, return ret; } +/** + * Set a private key. + * + * @param context The Kerberos library context + * @param ctx The kx509 request context + * @param store The name of a PKIX credential store + * + * @return A krb5 error code. + */ +krb5_error_code +krb5_kx509_ctx_set_key(krb5_context context, + krb5_kx509_req_ctx kx509_ctx, + const char *store) +{ + SubjectPublicKeyInfo key; + krb5_error_code ret; + + memset(&key, 0, sizeof(key)); + hx509_private_key_free(&kx509_ctx->priv_key); + _hx509_certs_keys_free(context->hx509ctx, kx509_ctx->keys); + kx509_ctx->keys = NULL; + ret = load_priv_key(context, kx509_ctx, store); + if (ret == 0) + ret = hx509_private_key2SPKI(context->hx509ctx, kx509_ctx->priv_key, + &key); + if (ret == 0) + ret = hx509_request_set_SubjectPublicKeyInfo(context->hx509ctx, + kx509_ctx->csr, &key); + free_SubjectPublicKeyInfo(&key); + return ret; +} + static krb5_error_code gen_priv_key(krb5_context context, const char *gen_type, @@ -163,6 +481,58 @@ gen_priv_key(krb5_context context, return ret; } +/** + * Generate a private key. + * + * @param context The Kerberos library context + * @param ctx The kx509 request context + * @param gen_type The type of key (default: rsa) + * @param gen_bits The size of the key (for non-ECC, really, for RSA) + * + * @return A krb5 error code. + */ +krb5_error_code +krb5_kx509_ctx_gen_key(krb5_context context, + krb5_kx509_req_ctx kx509_ctx, + const char *gen_type, + int gen_bits) +{ + SubjectPublicKeyInfo key; + krb5_error_code ret; + + memset(&key, 0, sizeof(key)); + + if (gen_type == NULL) { + gen_type = krb5_config_get_string_default(context, NULL, "rsa", + "libdefaults", + "kx509_gen_key_type", NULL); + } + if (gen_bits == 0) { + /* + * The key size is really only for non-ECC, of which we'll only support + * RSA. For ECC key sizes will either be implied by the `key_type' or + * will have to be a magic value that allows us to pick from some small + * set of curves (e.g., 255 == Curve25519). + */ + gen_bits = krb5_config_get_int_default(context, NULL, 2048, + "libdefaults", + "kx509_gen_rsa_key_size", NULL); + } + hx509_private_key_free(&kx509_ctx->priv_key); + _hx509_certs_keys_free(context->hx509ctx, kx509_ctx->keys); + kx509_ctx->keys = NULL; + + ret = gen_priv_key(context, gen_type, gen_bits, &kx509_ctx->priv_key); + if (ret == 0) + ret = hx509_private_key2SPKI(context->hx509ctx, kx509_ctx->priv_key, + &key); + if (ret == 0) + ret = hx509_request_set_SubjectPublicKeyInfo(context->hx509ctx, + kx509_ctx->csr, &key); + free_SubjectPublicKeyInfo(&key); + return ret; +} + /* Set a cc config entry indicating that the kx509 service is not available */ static void store_kx509_disabled(krb5_context context, const char *realm, krb5_ccache cc) @@ -180,6 +550,42 @@ store_kx509_disabled(krb5_context context, const char *realm, krb5_ccache cc) krb5_cc_set_config(context, cc, NULL, "kx509_service_status", &data); } +static int +certs_export_func(hx509_context context, void *d, hx509_cert c) +{ + heim_octet_string os; + Certificates *cs = d; + Certificate c2; + int ret; + + ret = hx509_cert_binary(context, c, &os); + if (ret) + return ret; + ret = decode_Certificate(os.data, os.length, &c2, NULL); + if (ret) + return ret; + der_free_octet_string(&os); + ret = add_Certificates(cs, &c2); + free_Certificate(&c2); + return ret; +} + +static krb5_error_code +certs_export(hx509_context context, hx509_certs certs, heim_octet_string *out) +{ + Certificates cs; + size_t len; + int ret; + + cs.len = 0; + cs.val = 0; + ret = hx509_certs_iter_f(context, certs, certs_export_func, &cs); + if (ret == 0) + ASN1_MALLOC_ENCODE(Certificates, out->data, out->length, &cs, &len, ret); + free_Certificates(&cs); + return ret; +} + /* Store the private key and certificate where requested */ static krb5_error_code store(krb5_context context, @@ -187,7 +593,8 @@ store(krb5_context context, const char *realm, krb5_ccache cc, hx509_private_key key, - hx509_cert cert) + hx509_cert cert, + hx509_certs chain) { heim_octet_string hdata; krb5_error_code ret = 0; @@ -203,42 +610,50 @@ store(krb5_context context, /* Serialize and store the certificate in the ccache */ ret = hx509_cert_binary(context->hx509ctx, cert, &hdata); - data.data = hdata.data; - data.length = hdata.length; if (ret == 0) - ret = krb5_cc_set_config(context, cc, NULL, "kx509cert", &data); - free(hdata.data); + ret = krb5_cc_set_config(context, cc, NULL, "kx509cert", &hdata); + der_free_octet_string(&hdata); - /* - * Serialized and store the key in the ccache. Use PKCS#8 so that we - * store the algorithm OID too, which is needed in order to be able to - * read the private key back. - */ - if (ret == 0) - ret = _hx509_private_key_export(context->hx509ctx, key, - HX509_KEY_FORMAT_PKCS8, &hdata); - data.data = hdata.data; - data.length = hdata.length; - if (ret == 0) - ret = krb5_cc_set_config(context, cc, NULL, "kx509key", &data); - free(hdata.data); - if (ret) - krb5_set_error_message(context, ret, "Could not store kx509 " - "private key and certificate in ccache %s", - krb5_cc_get_name(context, cc)); + if (ret == 0 && key) { + /* + * Serialized and store the key in the ccache. Use PKCS#8 so that we + * store the algorithm OID too, which is needed in order to be able to + * read the private key back. + */ + if (ret == 0) + ret = _hx509_private_key_export(context->hx509ctx, key, + HX509_KEY_FORMAT_PKCS8, &hdata); + if (ret == 0) + ret = krb5_cc_set_config(context, cc, NULL, "kx509key", &hdata); + der_free_octet_string(&hdata); + if (ret) + krb5_set_error_message(context, ret, "Could not store kx509 " + "private key and certificate in ccache %s", + krb5_cc_get_name(context, cc)); + } + + if (ret == 0 && chain) { + ret = certs_export(context->hx509ctx, chain, &hdata); + if (ret == 0) + ret = krb5_cc_set_config(context, cc, NULL, "kx509cert-chain", + &hdata); + der_free_octet_string(&hdata); + } } - /* Store the private key and cert in an hx509 store */ if (hx509_store != NULL) { hx509_certs certs; - _hx509_cert_assign_key(cert, key); /* store both in the same store */ + if (key) + _hx509_cert_assign_key(cert, key); /* store both in the same store */ ret = hx509_certs_init(context->hx509ctx, hx509_store, HX509_CERTS_CREATE, NULL, &certs); if (ret == 0) ret = hx509_certs_add(context->hx509ctx, certs, cert); + if (ret == 0 && chain != NULL) + ret = hx509_certs_merge(context->hx509ctx, certs, chain); if (ret == 0) ret = hx509_certs_store(context->hx509ctx, certs, 0, NULL); hx509_certs_free(&certs); @@ -257,25 +672,100 @@ store(krb5_context context, return ret; } -static void -init_kx509_ctx(struct kx509_ctx_data *ctx) +/* Make a Kx509CSRPlus or a raw SPKI */ +static krb5_error_code +mk_kx509_req_body(krb5_context context, + krb5_kx509_req_ctx kx509_ctx, + krb5_data *out) { - memset(ctx, 0, sizeof(*ctx)); - ctx->send_to_realm = NULL; - ctx->hmac_key = NULL; - ctx->keys = NULL; - ctx->priv_key = NULL; + krb5_error_code ret; + size_t len; + + if (krb5_config_get_bool_default(context, NULL, FALSE, + "realms", kx509_ctx->realm, + "kx509_req_use_raw_spki", NULL)) { + SubjectPublicKeyInfo spki; + + /* Interop with old kx509 servers, send a raw SPKI, not a CSR */ + out->data = NULL; + out->length = 0; + memset(&spki, 0, sizeof(spki)); + ret = hx509_private_key2SPKI(context->hx509ctx, + kx509_ctx->priv_key, &spki); + if (ret == 0) { + out->length = spki.subjectPublicKey.length >> 3; + out->data = spki.subjectPublicKey.data; + } + kx509_ctx->expect_chain = 0; + return ret; + } + + /* + * New kx509 servers use a CSR for proof of possession, and send back a + * chain of certificates, with the issued certificate first. + */ + kx509_ctx->expect_chain = 1; + + if (kx509_ctx->given_csr.length) { + krb5_data exts_der; + + exts_der.data = NULL; + exts_der.length = 0; + + /* Use the given CSR */ + ret = der_copy_octet_string(&kx509_ctx->given_csr, + &kx509_ctx->csr_plus.csr); + + /* + * Extract the desired Certificate Extensions from our internal + * as-yet-unsigned CSR, then decode them into place in the + * Kx509CSRPlus. + */ + if (ret == 0) + ret = hx509_request_get_exts(context->hx509ctx, + kx509_ctx->csr, + &exts_der); + if (ret == 0 && exts_der.data && exts_der.length && + (kx509_ctx->csr_plus.exts = + calloc(1, sizeof (kx509_ctx->csr_plus.exts[0]))) == NULL) + ret = krb5_enomem(context); + if (ret == 0 && exts_der.data && exts_der.length) + ret = decode_Extensions(exts_der.data, exts_der.length, + kx509_ctx->csr_plus.exts, NULL); + krb5_data_free(&exts_der); + } else { + /* + * Sign and use our internal CSR, which will carry all our desired + * Certificate Extensions as an extReq CSR Attribute. + */ + ret = hx509_request_to_pkcs10(context->hx509ctx, + kx509_ctx->csr, + kx509_ctx->priv_key, + &kx509_ctx->csr_plus.csr); + } + if (ret == 0) + ASN1_MALLOC_ENCODE(Kx509CSRPlus, out->data, out->length, + &kx509_ctx->csr_plus, &len, ret); + return ret; } -static void -free_kx509_ctx(krb5_context context, struct kx509_ctx_data *ctx) +static krb5_error_code +get_start_realm(krb5_context context, + krb5_ccache cc, + krb5_principal princ, + char **out) { - krb5_free_keyblock(context, ctx->hmac_key); - free(ctx->send_to_realm); - hx509_private_key_free(&ctx->priv_key); - if (ctx->keys) - _hx509_certs_keys_free(context->hx509ctx, ctx->keys); - init_kx509_ctx(ctx); + krb5_error_code ret; + krb5_data d; + + ret = krb5_cc_get_config(context, cc, NULL, "start_realm", &d); + if (ret == 0) { + *out = strndup(d.data, d.length); + krb5_data_free(&d); + } else { + *out = strdup(krb5_principal_get_realm(context, princ)); + } + return (*out) ? 0 : krb5_enomem(context); } /* @@ -286,9 +776,8 @@ free_kx509_ctx(krb5_context context, struct kx509_ctx_data *ctx) */ static krb5_error_code mk_kx509_req(krb5_context context, - struct kx509_ctx_data *kx509_ctx, + krb5_kx509_req_ctx kx509_ctx, krb5_ccache incc, - const char *realm, hx509_private_key private_key, krb5_data *req) { @@ -296,12 +785,12 @@ mk_kx509_req(krb5_context context, SubjectPublicKeyInfo spki; struct Kx509Request kx509_req; krb5_data pre_req; - krb5_auth_context ac = NULL; krb5_error_code ret = 0; krb5_creds this_cred; krb5_creds *cred = NULL; HMAC_CTX ctx; const char *hostname; + char *start_realm = NULL; size_t len; krb5_data_zero(&pre_req); @@ -311,21 +800,21 @@ mk_kx509_req(krb5_context context, kx509_req.pk_hash.data = digest; kx509_req.pk_hash.length = SHA_DIGEST_LENGTH; - if (private_key) { - /* Encode the public key for use in the request */ - ret = hx509_private_key2SPKI(context->hx509ctx, private_key, &spki); - kx509_req.pk_key.data = spki.subjectPublicKey.data; - kx509_req.pk_key.length = spki.subjectPublicKey.length >> 3; + if (private_key || kx509_ctx->given_csr.data) { + /* Encode the CSR or public key for use in the request */ + ret = mk_kx509_req_body(context, kx509_ctx, &kx509_req.pk_key); } else { /* Probe */ kx509_req.pk_key.data = NULL; kx509_req.pk_key.length = 0; } - if (ret == 0) - ret = krb5_auth_con_init(context, &ac); if (ret == 0) ret = krb5_cc_get_principal(context, incc, &this_cred.client); + if (ret == 0) + ret = get_start_realm(context, incc, this_cred.client, &start_realm); + if (ret == 0 && kx509_ctx->realm == NULL) + ret = krb5_kx509_ctx_set_realm(context, kx509_ctx, start_realm); if (ret == 0) { /* * The kx509 protocol as deployed uses kca_service/kdc_hostname, but @@ -344,9 +833,9 @@ mk_kx509_req(krb5_context context, * that already unless there's no start_realm cc config, in which case * we'll use the ccache's default client principal's realm. */ - realm = realm ? realm : this_cred.client->realm; - hostname = krb5_config_get_string(context, NULL, "realm", realm, - "kx509_hostname", NULL); + hostname = krb5_config_get_string(context, NULL, "realm", + kx509_ctx->realm, "kx509_hostname", + NULL); if (hostname == NULL) hostname = krb5_config_get_string(context, NULL, "libdefaults", "kx509_hostname", NULL); @@ -355,10 +844,12 @@ mk_kx509_req(krb5_context context, KRB5_NT_SRV_HST, &this_cred.server); if (ret == 0) ret = krb5_principal_set_realm(context, this_cred.server, - realm); + kx509_ctx->realm); } else { - ret = krb5_make_principal(context, &this_cred.server, realm, - KRB5_TGS_NAME, this_cred.client->realm, + ret = krb5_make_principal(context, &this_cred.server, + start_realm, + KRB5_TGS_NAME, + kx509_ctx->realm, NULL); } } @@ -367,20 +858,13 @@ mk_kx509_req(krb5_context context, if (ret == 0) ret = krb5_get_credentials(context, 0, incc, &this_cred, &cred); if (ret == 0) - ret = krb5_mk_req_extended(context, &ac, AP_OPTS_USE_SUBKEY, NULL, cred, - &kx509_req.authenticator); + ret = krb5_mk_req_extended(context, &kx509_ctx->ac, AP_OPTS_USE_SUBKEY, + NULL, cred, &kx509_req.authenticator); krb5_free_keyblock(context, kx509_ctx->hmac_key); kx509_ctx->hmac_key = NULL; if (ret == 0) - ret = krb5_auth_con_getkey(context, ac, &kx509_ctx->hmac_key); - - /* Save the realm to send to */ - free(kx509_ctx->send_to_realm); - kx509_ctx->send_to_realm = NULL; - if (ret == 0 && - (kx509_ctx->send_to_realm = - strdup(krb5_principal_get_realm(context, cred->server))) == NULL) - ret = krb5_enomem(context); + ret = krb5_auth_con_getkey(context, kx509_ctx->ac, + &kx509_ctx->hmac_key); if (ret) goto out; @@ -390,7 +874,7 @@ mk_kx509_req(krb5_context context, HMAC_Init_ex(&ctx, kx509_ctx->hmac_key->keyvalue.data, kx509_ctx->hmac_key->keyvalue.length, EVP_sha1(), NULL); HMAC_Update(&ctx, version_2_0, sizeof(version_2_0)); - if (private_key) { + if (private_key || kx509_ctx->given_csr.data) { HMAC_Update(&ctx, kx509_req.pk_key.data, kx509_req.pk_key.length); } else { /* Probe */ @@ -409,12 +893,14 @@ mk_kx509_req(krb5_context context, } out: + free(start_realm); free(pre_req.data); krb5_free_creds(context, cred); - krb5_xfree(kx509_req.authenticator.data); + kx509_req.pk_hash.data = NULL; + kx509_req.pk_hash.length = 0; + free_Kx509Request(&kx509_req); free_SubjectPublicKeyInfo(&spki); krb5_free_cred_contents(context, &this_cred); - krb5_auth_con_free(context, ac); if (ret == 0 && req->length != len + sizeof(version_2_0)) { krb5_data_free(req); krb5_set_error_message(context, ret = ERANGE, @@ -423,12 +909,71 @@ out: return ret; } +static krb5_error_code +rd_chain(krb5_context context, + heim_octet_string *d, + hx509_cert *cert, + hx509_certs *chain, + heim_error_t *herr) +{ + krb5_error_code ret; + Certificates certs; + size_t i, len; + + *cert = NULL; + *chain = NULL; + + if ((ret = decode_Certificates(d->data, d->length, &certs, &len))) + return ret; + if (certs.len == 0) { + *herr = heim_error_create(EINVAL, "Server sent empty Certificate list"); + return EINVAL; + } + *cert = hx509_cert_init(context->hx509ctx, &certs.val[0], herr); + if (*cert == NULL) { + free_Certificates(&certs); + return errno; + } + if (certs.len == 1) + _krb5_debug(context, 1, "kx509 server sent certificate but no chain"); + else + _krb5_debug(context, 1, "kx509 server sent %llu certificates", + (unsigned long long)certs.len); + + ret = hx509_certs_init(context->hx509ctx, "MEMORY:anonymous", + HX509_CERTS_CREATE, NULL, chain); + if (ret) { + hx509_cert_free(*cert); + *cert = NULL; + free_Certificates(&certs); + return ret; + } + + for (i = 1; ret == 0 && i < certs.len; i++) { + hx509_cert c = hx509_cert_init(context->hx509ctx, &certs.val[i], herr); + + if (c == NULL) + ret = errno; + else + ret = hx509_certs_add(context->hx509ctx, *chain, c); + hx509_cert_free(c); + } + free_Certificates(&certs); + if (ret) { + hx509_certs_free(chain); + hx509_cert_free(*cert); + *cert = NULL; + } + return ret; +} + /* Parse and validate a kx509 reply */ static krb5_error_code rd_kx509_resp(krb5_context context, - struct kx509_ctx_data *kx509_ctx, + krb5_kx509_req_ctx kx509_ctx, krb5_data *rep, - hx509_cert *cert) + hx509_cert *cert, + hx509_certs *chain) { unsigned char digest[SHA_DIGEST_LENGTH]; Kx509Response r; @@ -442,6 +987,7 @@ rd_kx509_resp(krb5_context context, size_t len; *cert = NULL; + *chain = NULL; /* Strip `version_2_0' prefix */ if (rep->length < hdr_len || memcmp(rep->data, version_2_0, hdr_len)) { @@ -554,12 +1100,18 @@ rd_kx509_resp(krb5_context context, free_Kx509Response(&r); if (code != KRB5KDC_ERR_POLICY && kx509_ctx->priv_key == NULL) return 0; /* Probe success */ - return code; + return code ? code : KRB5KDC_ERR_POLICY; /* Not a probe -> must fail */ } /* Import the certificate payload */ - *cert = hx509_cert_init_data(context->hx509ctx, r.certificate->data, - r.certificate->length, &herr); + if (kx509_ctx->expect_chain) { + ret = rd_chain(context, r.certificate, cert, chain, &herr); + } else { + *cert = hx509_cert_init_data(context->hx509ctx, r.certificate->data, + r.certificate->length, &herr); + if (!*cert) + ret = errno; + } free_Kx509Response(&r); if (cert) { heim_release(herr); @@ -584,13 +1136,13 @@ rd_kx509_resp(krb5_context context, */ static krb5_error_code kx509_core(krb5_context context, - struct kx509_ctx_data *kx509_ctx, + krb5_kx509_req_ctx kx509_ctx, krb5_ccache incc, - const char *realm, const char *hx509_store, krb5_ccache outcc) { krb5_error_code ret; + hx509_certs chain = NULL; hx509_cert cert = NULL; krb5_data req, resp; @@ -598,24 +1150,24 @@ kx509_core(krb5_context context, krb5_data_zero(&resp); /* Make the kx509 request */ - ret = mk_kx509_req(context, kx509_ctx, incc, realm, kx509_ctx->priv_key, - &req); + ret = mk_kx509_req(context, kx509_ctx, incc, kx509_ctx->priv_key, &req); /* Send the kx509 request and get the response */ if (ret == 0) ret = krb5_sendto_context(context, NULL, &req, - kx509_ctx->send_to_realm, &resp); + kx509_ctx->realm, &resp); if (ret == 0) - ret = rd_kx509_resp(context, kx509_ctx, &resp, &cert); + ret = rd_kx509_resp(context, kx509_ctx, &resp, &cert, &chain); /* Store the key and cert! */ - if (ret == 0 && kx509_ctx->priv_key) - ret = store(context, hx509_store, kx509_ctx->send_to_realm, outcc, - kx509_ctx->priv_key, cert); - else if (ret == KRB5KDC_ERR_POLICY || ret == KRB5_KDC_UNREACH) - /* Probe failed -> Record that the realm does not support kx509 */ - store_kx509_disabled(context, kx509_ctx->send_to_realm, outcc); + if (ret == 0 && cert && (kx509_ctx->priv_key || kx509_ctx->given_csr.data)) + ret = store(context, hx509_store, kx509_ctx->realm, outcc, + kx509_ctx->priv_key, cert, chain); + else if (ret == KRB5KDC_ERR_POLICY) + /* Probe failed -> record that the realm does not support kx509 */ + store_kx509_disabled(context, kx509_ctx->realm, outcc); + hx509_certs_free(&chain); hx509_cert_free(cert); krb5_data_free(&resp); krb5_data_free(&req); @@ -628,88 +1180,60 @@ kx509_core(krb5_context context, * Given a private key this function will get a certificate. If no private key * is given, one will be generated. * - * The private key and certificate will be stored in the given hx509 store - * (e.g, "PEM-FILE:/path/to/file.pem") and/or given output ccache. When stored - * in a ccache, the DER-encoded Certificate will be stored as the data payload - * of a "cc config" named "kx509cert", while the key will be stored as a - * DER-encoded PKCS#8 PrivateKeyInfo in a cc config named "kx509key". + * The private key and certificate will be stored in the given PKIX credential + * store (e.g, "PEM-FILE:/path/to/file.pem") and/or given output ccache. When + * stored in a ccache, the DER-encoded Certificate will be stored as the data + * payload of a "cc config" named "kx509cert", while the key will be stored as + * a DER-encoded PKCS#8 PrivateKeyInfo in a cc config named "kx509key". * * @param context The Kerberos library context - * @param incc A credential cache - * @param realm A realm from which to get the certificate (uses the client - * principal's realm if NULL) - * @param use_priv_key_store An hx509 store containing a private key to certify - * (if NULL, a key will be generated) - * @param gen_type The public key algorithm for which to generate a private key - * @param gen_bits The size of the public key to generate, in bits - * @param hx509_store An hx509 store into which to store the private key and - * certificate (e.g, "PEM-FILE:/path/to/file.pem") + * @param kx509_ctx A kx509 request context + * @param incc A credential cache (if NULL use default ccache) + * @param hx509_store An PKIX credential store into which to store the private + * key and certificate (e.g, "PEM-FILE:/path/to/file.pem") * @param outcc A ccache into which to store the private key and certificate + * (mandatory) * * @return A krb5 error code. */ KRB5_LIB_FUNCTION krb5_error_code KRB5_LIB_CALL krb5_kx509_ext(krb5_context context, + krb5_kx509_req_ctx kx509_ctx, krb5_ccache incc, - const char *realm, - const char *use_priv_key_store, - const char *gen_type, - int gen_bits, const char *hx509_store, krb5_ccache outcc) { - struct kx509_ctx_data kx509_ctx; + krb5_ccache def_cc = NULL; krb5_error_code ret; - char *freeme = NULL; - /* TODO: Eventually switch to ECDSA, and eventually to ed25519 or ed448 */ - if (gen_type == NULL) { - gen_type = krb5_config_get_string_default(context, NULL, "rsa", - "libdefaults", - "kx509_gen_key_type", NULL); + if (incc == NULL) { + if ((ret = krb5_cc_default(context, &def_cc))) + return ret; + incc = def_cc; } - if (gen_bits == 0) { - /* - * The key size is really only for non-ECC, of which we'll only support - * RSA. For ECC key sizes will either be implied by the `key_type' or - * will have to be a magic value that allows us to pick from some small - * set of curves (e.g., 255 == Curve25519). - */ - gen_bits = krb5_config_get_int_default(context, NULL, 2048, - "libdefaults", - "kx509_gen_rsa_key_size", NULL); - } - - init_kx509_ctx(&kx509_ctx); - - if (realm == NULL) { + if (kx509_ctx->realm == NULL) { krb5_data data; ret = krb5_cc_get_config(context, incc, NULL, "start_realm", &data); if (ret == 0) { - if ((freeme = strndup(data.data, data.length)) == NULL) - return krb5_enomem(context); - realm = freeme; + if ((kx509_ctx->realm = strndup(data.data, data.length)) == NULL) + ret = krb5_enomem(context); + krb5_data_free(&data); + } + if (ret) { + if (def_cc) + krb5_cc_close(context, def_cc); + return ret; } } - if (use_priv_key_store) { - /* Get the given private key if it exists, and use it */ - ret = load_priv_key(context, &kx509_ctx, use_priv_key_store); - if (ret == 0) { - ret = kx509_core(context, &kx509_ctx, incc, realm, hx509_store, - outcc); - free_kx509_ctx(context, &kx509_ctx); - free(freeme); - return ret; - } - if (ret != ENOENT) { - free_kx509_ctx(context, &kx509_ctx); - free(freeme); - return ret; - } - /* Key store doesn't exist or has no keys, fall through */ + if (kx509_ctx->priv_key || kx509_ctx->given_csr.data) { + /* If given a private key, use it */ + ret = kx509_core(context, kx509_ctx, incc, hx509_store, outcc); + if (def_cc) + krb5_cc_close(context, def_cc); + return ret; } /* @@ -720,13 +1244,14 @@ krb5_kx509_ext(krb5_context context, */ /* Probe == call kx509_core() w/o a private key */ - ret = kx509_core(context, &kx509_ctx, incc, realm, NULL, outcc); + ret = kx509_core(context, kx509_ctx, incc, NULL, outcc); + if (ret == 0 && kx509_ctx->given_csr.data == NULL) + ret = krb5_kx509_ctx_gen_key(context, kx509_ctx, NULL, 0); if (ret == 0) - ret = gen_priv_key(context, gen_type, gen_bits, &kx509_ctx.priv_key); - if (ret == 0) - ret = kx509_core(context, &kx509_ctx, incc, realm, hx509_store, outcc); - free_kx509_ctx(context, &kx509_ctx); - free(freeme); + ret = kx509_core(context, kx509_ctx, incc, hx509_store, outcc); + + if (def_cc) + krb5_cc_close(context, def_cc); return ret; } @@ -735,8 +1260,8 @@ krb5_kx509_ext(krb5_context context, * for that key and the client principal's subject name. * * The private key and certificate will be stored in the given ccache, and also - * in a corresponding hx509 store if one is configured via [libdefaults] - * kx509_store. + * in a corresponding PKIX credential store if one is configured via + * [libdefaults] kx509_store. * * XXX NOTE: Dicey feature here... Review carefully! * @@ -750,11 +1275,16 @@ krb5_kx509_ext(krb5_context context, KRB5_LIB_FUNCTION krb5_error_code KRB5_LIB_CALL krb5_kx509(krb5_context context, krb5_ccache cc, const char *realm) { - krb5_error_code ret = 0; + krb5_kx509_req_ctx kx509_ctx; + krb5_error_code ret; const char *defcc; char *ccache_full_name = NULL; char *store_exp = NULL; + ret = krb5_kx509_ctx_init(context, &kx509_ctx); + if (ret == 0 && realm) + ret = krb5_kx509_ctx_set_realm(context, kx509_ctx, realm); + /* * The idea is that IF we are asked to do kx509 w/ creds from a default * ccache THEN we should store the kx509 certificate (if we get one) and @@ -771,7 +1301,8 @@ krb5_kx509(krb5_context context, krb5_ccache cc, const char *realm) * ccache name, and if so we get the [libdefaults] kx509_store string and * expand it, then use it. */ - if ((defcc = krb5_cc_configured_default_name(context)) && + if (ret == 0 && + (defcc = krb5_cc_configured_default_name(context)) && krb5_cc_get_full_name(context, cc, &ccache_full_name) == 0 && strcmp(defcc, ccache_full_name) == 0) { @@ -781,6 +1312,13 @@ krb5_kx509(krb5_context context, krb5_ccache cc, const char *realm) "kx509_store", NULL); if (store) ret = _krb5_expand_path_tokens(context, store, 1, &store_exp); + + /* + * If there's a private key in the store already, we'll use it, else + * we'll let krb5_kx509_ext() generate one, so we ignore this return + * value: + */ + (void) krb5_kx509_ctx_set_key(context, kx509_ctx, store); } /* @@ -788,8 +1326,9 @@ krb5_kx509(krb5_context context, krb5_ccache cc, const char *realm) * private key from (if it exists) as well as for storing the certificate * (and private key) into, which may save us some key generation cycles. */ - ret = krb5_kx509_ext(context, cc, realm, store_exp, NULL, 0, - store_exp, cc); + if (ret == 0) + ret = krb5_kx509_ext(context, kx509_ctx, cc, store_exp, cc); + krb5_kx509_ctx_free(context, &kx509_ctx); free(ccache_full_name); free(store_exp); return ret; diff --git a/lib/krb5/libkrb5-exports.def.in b/lib/krb5/libkrb5-exports.def.in index e68bdb8d6..0e5e72a2b 100644 --- a/lib/krb5/libkrb5-exports.def.in +++ b/lib/krb5/libkrb5-exports.def.in @@ -438,7 +438,24 @@ EXPORTS krb5_kt_resolve krb5_kt_start_seq_get krb5_kuserok + krb5_kx509 krb5_kx509 + krb5_kx509_ctx_add_auth_data + krb5_kx509_ctx_add_eku + krb5_kx509_ctx_add_san_dns_name + krb5_kx509_ctx_add_san_ms_upn + krb5_kx509_ctx_add_san_pkinit + krb5_kx509_ctx_add_san_registeredID + krb5_kx509_ctx_add_san_rfc822Name + krb5_kx509_ctx_add_san_xmpp + krb5_kx509_ctx_free + krb5_kx509_ctx_free + krb5_kx509_ctx_init + krb5_kx509_ctx_init + krb5_kx509_ctx_set_csr_der + krb5_kx509_ctx_set_key + krb5_kx509_ctx_set_realm + krb5_kx509_ext krb5_kx509_ext krb5_log krb5_log_msg diff --git a/lib/krb5/version-script.map b/lib/krb5/version-script.map index 869d91b6f..f27259de3 100644 --- a/lib/krb5/version-script.map +++ b/lib/krb5/version-script.map @@ -432,6 +432,19 @@ HEIMDAL_KRB5_2.0 { krb5_kt_start_seq_get; krb5_kuserok; krb5_kx509; + krb5_kx509_ctx_add_auth_data; + krb5_kx509_ctx_add_eku; + krb5_kx509_ctx_add_san_dns_name; + krb5_kx509_ctx_add_san_ms_upn; + krb5_kx509_ctx_add_san_pkinit; + krb5_kx509_ctx_add_san_registeredID; + krb5_kx509_ctx_add_san_rfc822Name; + krb5_kx509_ctx_add_san_xmpp; + krb5_kx509_ctx_free; + krb5_kx509_ctx_init; + krb5_kx509_ctx_set_csr_der; + krb5_kx509_ctx_set_key; + krb5_kx509_ctx_set_realm; krb5_kx509_ext; krb5_log; krb5_log_msg; diff --git a/tests/bin/setup-env.in b/tests/bin/setup-env.in index 5c5de1265..03e6fa123 100644 --- a/tests/bin/setup-env.in +++ b/tests/bin/setup-env.in @@ -32,6 +32,7 @@ klist="${TESTS_ENVIRONMENT} ${top_builddir}/kuser/heimtools klist" kpasswd="${TESTS_ENVIRONMENT} ${top_builddir}/kpasswd/kpasswd" kpasswdd="${TESTS_ENVIRONMENT} ${top_builddir}/kpasswd/kpasswdd" kswitch="${TESTS_ENVIRONMENT} ${top_builddir}/kuser/heimtools kswitch" +kx509="${TESTS_ENVIRONMENT} ${top_builddir}/kuser/heimtools kx509" ktutil="${TESTS_ENVIRONMENT} ${top_builddir}/admin/ktutil" gsstool="${TESTS_ENVIRONMENT} ${top_builddir}/lib/gssapi/gsstool" diff --git a/tests/kdc/check-pkinit.in b/tests/kdc/check-pkinit.in index a1affac03..d0fb77ba3 100644 --- a/tests/kdc/check-pkinit.in +++ b/tests/kdc/check-pkinit.in @@ -58,10 +58,7 @@ kinit="${kinit} -c $cache ${afs_no_afslog}" klist="${klist} --hidden -v -c $cache" kgetcred="${kgetcred} -c $cache" kdestroy="${kdestroy} -c $cache ${afs_no_unlog}" - -kextract() { - ${klist} --extract-kx509-cert="$1" -} +kx509="${kx509} -c $cache" KRB5_CONFIG="${objdir}/krb5-pkinit.conf" export KRB5_CONFIG @@ -202,7 +199,8 @@ ${kgetcred} ${server}@${R} || { ec=1 ; eval "${testfailed}"; } ${klist} echo "Check kx509 certificate acquisition" -kextract PEM-FILE:${objdir}/kx509.pem || { ec=1 ; eval "${testfailed}"; } +${kx509} -s || { ec=1 ; eval "${testfailed}"; } +${kx509} -o PEM-FILE:${objdir}/kx509.pem || { ec=1 ; eval "${testfailed}"; } ${kdestroy} echo "Check PKINIT w/ kx509 certificate"