/* Licensed to Stichting The Commons Conservancy (TCC) under one or more * contributor license agreements. See the AUTHORS file distributed with * this work for additional information regarding copyright ownership. * TCC licenses this file to You under the Apache License, Version 2.0 * (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ /* * Module to verify permission to issue a certificate, and to store a * certificate once issued. * * Author: Graham Leggett * */ #include #include #if HAVE_APR_CRYPTO_CLEAR #include #endif #include #include #include #include #include #include "httpd.h" #include "http_config.h" #include "http_core.h" #include "http_log.h" #include "http_protocol.h" #include "http_request.h" #include "util_ldap.h" #include "util_script.h" #include "mod_ca.h" #if !APR_HAS_LDAP #error mod_ca_ldap requires APR-util to have LDAP support built in. To fix add --with-ldap to ./configure. #endif #define RETRIES 2 #define DELAY 1000000 #define TIMEOUT 5 #define MOD_CA_LDAP_LDC "mod_ca_ldap:ldc" #define DEFAULT_CA_DAYS 365*1 #define SERIAL_RAND_BITS 64 module AP_MODULE_DECLARE_DATA ca_ldap_module; static APR_OPTIONAL_FN_TYPE(uldap_connection_open) *util_ldap_connection_open; static APR_OPTIONAL_FN_TYPE(uldap_connection_close) *util_ldap_connection_close; static APR_OPTIONAL_FN_TYPE(uldap_connection_find) *util_ldap_connection_find; static APR_OPTIONAL_FN_TYPE(uldap_connection_unbind) *util_ldap_connection_unbind; typedef struct { /* These parameters are all derived from the CALdapUrl directive */ char *url; /* String representation of the URL */ char *host; /* Name of the LDAP server (or space separated list) */ int port; /* Port of the LDAP server */ char *basedn; /* Base DN to do all searches from */ char *attribute; /* Attribute to search for */ char **attributes; /* Array of all the attributes to return */ int scope; /* Scope of the search */ char *filter; /* Filter to further limit the search */ int secure; /* True if SSL connections are requested */ int url_set:1; /* Set if we have found an LDAP url */ const char *binddn; /* DN to bind to server (can be NULL) */ int binddn_set:1; const char *bindpw; /* Password to bind to server (can be NULL) */ int bindpw_set:1; const char *pass_attribute; /* Password attribute to remove */ int pass_attribute_set:1; const char *pass_objectclass; /* Password objectclass to remove */ int pass_objectclass_set:1; const char *cert_attribute; /* Certificate attribute to add */ int cert_attribute_set:1; const char *cert_objectclass; /* Certificate objectclass to add */ int cert_objectclass_set:1; const char *path_attribute; /* Path attribute to add */ int path_attribute_set:1; const char *path_objectclass; /* Path objectclass to add */ int path_objectclass_set:1; struct timeval *op_timeout; /* timeout to use */ int op_timeout_set:1; apr_hash_t *subject; /* map of cert elements to ldap elements */ int subject_set:1; apr_hash_t *subjectaltname; /* map of cert elements to ldap elements */ int subjectaltname_set:1; } ca_ldap_config_rec; typedef struct ca_ldap_ldc_rec { apr_pool_t *pool; util_ldap_connection_t *ldc; const char *dn; } ca_ldap_ldc_rec; struct ap_ca_instance_t { void *placeholder; }; static void log_message(request_rec *r, apr_status_t status, const char *message) { int len; BIO *mem = BIO_new(BIO_s_mem()); char *err = apr_palloc(r->pool, HUGE_STRING_LEN); ERR_print_errors(mem); len = BIO_gets(mem, err, HUGE_STRING_LEN - 1); if (len > -1) { err[len] = 0; } apr_table_setn(r->notes, "error-notes", apr_pstrcat(r->pool, "LDAP: ", ap_escape_html( r->pool, message), NULL)); /* Allow "error-notes" string to be printed by ap_send_error_response() */ apr_table_setn(r->notes, "verbose-error-to", "*"); if (len > 0) { ap_log_rerror( APLOG_MARK, APLOG_ERR, status, r, "mod_ca_ldap: " "%s (%s)", message, err); } else { ap_log_rerror(APLOG_MARK, APLOG_ERR, status, r, "mod_ca_ldap: " "%s", message); } BIO_free(mem); } const char *subjectaltnames[] = { "otherName", "rfc822Name", "dNSName", "x400Address", "directoryName", "ediPartyName", "uniformResourceIdentifier", "iPAddress", "registeredID" }; static const char *subjectaltname_from_type(int type) { if (type >= GEN_OTHERNAME && type <= GEN_RID) { return subjectaltnames[type]; } else { return "undefined"; } } static int type_from_subjectaltname(const char *arg) { char a = arg[0]; if (a == 'o' && !strcmp(arg, "otherName")) { return GEN_OTHERNAME; } else if (a == 'r' && !strcmp(arg, "rfc822Name")) { return GEN_EMAIL; } else if (a == 'd' && !strcmp(arg, "dNSName")) { return GEN_DNS; } else if (a == 'x' && !strcmp(arg, "x400Address")) { return GEN_X400; } else if (a == 'd' && !strcmp(arg, "directoryName")) { return GEN_DIRNAME; } else if (a == 'e' && !strcmp(arg, "ediPartyName")) { return GEN_EDIPARTY; } else if (a == 'u' && !strcmp(arg, "uniformResourceIdentifier")) { return GEN_URI; } else if (a == 'i' && !strcmp(arg, "iPAddress")) { return GEN_IPADD; } else if (a == 'r' && !strcmp(arg, "registeredID")) { return GEN_RID; } return -1; } static apr_status_t ca_PKCS7_cleanup(void *data) { PKCS7_free((PKCS7 *) data); return APR_SUCCESS; } static apr_status_t ca_X509_REQ_cleanup(void *data) { X509_REQ_free((X509_REQ *) data); return APR_SUCCESS; } static apr_status_t ca_ldc_cleanup(void *data) { util_ldap_connection_close((util_ldap_connection_t *) data); return APR_SUCCESS; } static const char c2x_table[] = "0123456789abcdef"; static APR_INLINE unsigned char *c2x(unsigned what, unsigned char prefix, unsigned char *where) { #if APR_CHARSET_EBCDIC what = apr_xlate_conv_byte(ap_hdrs_to_ascii, (unsigned char)what); #endif /*APR_CHARSET_EBCDIC*/ *where++ = prefix; *where++ = c2x_table[what >> 4]; *where++ = c2x_table[what & 0xf]; return where; } static const char *escape_ldap(apr_pool_t *p, const char *raw) { const char *from = raw; int found = 0; /* any need to escape? */ while (*from) { if (*from < ' ' || *from == '*' || *from == '(' || *from == ')' || *from == '\\') { found++; break; } from++; } /* escape if necessary */ if (found) { char *result; unsigned char *to; result = apr_palloc(p, found * 2 + strlen(raw) + 1); to = (unsigned char *) result; from = raw; while (*from) { if (*from < ' ' || *from == '*' || *from == '(' || *from == ')' || *from == '\\') { c2x((unsigned int) (*from++), '\\', to); to += 3; } else { *to++ = *from++; } } *to = 0; return result; } return raw; } static int uldap_ld_errno(util_ldap_connection_t *ldc) { int ldaprc; #ifdef LDAP_OPT_ERROR_NUMBER if (LDAP_SUCCESS == ldap_get_option(ldc->ldap, LDAP_OPT_ERROR_NUMBER, &ldaprc)) return ldaprc; #endif #ifdef LDAP_OPT_RESULT_CODE if (LDAP_SUCCESS == ldap_get_option(ldc->ldap, LDAP_OPT_RESULT_CODE, &ldaprc)) return ldaprc; #endif return LDAP_OTHER; } /* * Replacement function for ldap_simple_bind_s() with a timeout. * To do this in a portable way, we have to use ldap_simple_bind() and * ldap_result(). * * Returns LDAP_SUCCESS on success; and an error code on failure */ static int uldap_simple_bind(util_ldap_connection_t *ldc, char *binddn, char* bindpw, struct timeval *timeout) { LDAPMessage *result; int rc; int msgid = ldap_simple_bind(ldc->ldap, binddn, bindpw); if (msgid == -1) { ldc->reason = "LDAP: ldap_simple_bind() failed"; return uldap_ld_errno(ldc); } rc = ldap_result(ldc->ldap, msgid, 0, timeout, &result); if (rc == -1) { ldc->reason = "LDAP: ldap_simple_bind() result retrieval failed"; /* -1 is LDAP_SERVER_DOWN in openldap, use something else */ return uldap_ld_errno(ldc); } else if (rc == 0) { ldc->reason = "LDAP: ldap_simple_bind() timed out"; rc = LDAP_TIMEOUT; } else if (ldap_parse_result(ldc->ldap, result, &rc, NULL, NULL, NULL, NULL, 1) == -1) { ldc->reason = "LDAP: ldap_simple_bind() parse result failed"; return uldap_ld_errno(ldc); } return rc; } static int ca_reqauthz_ldap(request_rec *r, apr_hash_t *params, const unsigned char *buffer, apr_size_t len) { ca_ldap_ldc_rec *l = NULL; int failures = 0; int result = 0; char *dn; int count; LDAPMessage *res, *entry; struct timeval opTimeout; char *filter; const char *userPassword; int i = 0; unsigned char *buf; X509 *cert = NULL; X509_REQ *creq = NULL; X509_NAME *subject = NULL; X509_ATTRIBUTE *challenge = NULL; ASN1_STRING *str; int idx; ca_ldap_config_rec *conf = ap_get_module_config(r->per_dir_config, &ca_ldap_module); /* * Basic sanity checks before any LDAP operations even happen. */ if (!conf->url_set) { return DECLINED; } /* read in the certificate */ if (!d2i_X509_REQ(&creq, &buffer, len)) { log_message(r, APR_SUCCESS, "could not DER decode the certificate to be signed"); return HTTP_BAD_REQUEST; } apr_pool_cleanup_register(r->pool, cert, ca_X509_REQ_cleanup, apr_pool_cleanup_null); /* handle the challenge */ idx = X509_REQ_get_attr_by_NID(creq, OBJ_sn2nid("challengePassword"), -1); if (idx == -1) { log_message(r, APR_SUCCESS, "no challenge included in certificate request for signing"); return HTTP_FORBIDDEN; } challenge = X509_REQ_get_attr(creq, idx); if (X509_ATTRIBUTE_count(challenge) != 1) { log_message(r, APR_SUCCESS, "challenge included in certificate request was not single valued"); return HTTP_FORBIDDEN; } str = X509_ATTRIBUTE_get0_data(challenge, 0, V_ASN1_UTF8STRING, NULL); if (!str) { str = X509_ATTRIBUTE_get0_data(challenge, 0, V_ASN1_IA5STRING, NULL); if (!str) { str = X509_ATTRIBUTE_get0_data(challenge, 0, V_ASN1_PRINTABLESTRING, NULL); if (!str) { ASN1_TYPE *asn1 = X509_ATTRIBUTE_get0_type(challenge, 0); log_message(r, APR_SUCCESS, apr_psprintf(r->pool, "challenge included in certificate request was not V_ASN1_UTF8STRING, V_ASN1_IA5STRING, or V_ASN1_PRINTABLESTRING (%d instead)", asn1->type)); return HTTP_FORBIDDEN; } } } len = ASN1_STRING_to_UTF8(&buf, str); userPassword = apr_pstrndup(r->pool, (const char *) buf, len); if (!userPassword || !*userPassword) { log_message(r, APR_SUCCESS, "challenge included in certificate request was empty or missing"); return HTTP_FORBIDDEN; } /* There is a good CALdapUrl, right? */ if (conf->host) { const char *binddn = conf->binddn; const char *bindpw = conf->bindpw; l = apr_pcalloc(r->pool, sizeof(ca_ldap_ldc_rec)); apr_pool_create(&l->pool, r->pool); l->ldc = util_ldap_connection_find(r, conf->host, conf->port, binddn, bindpw, LDAP_DEREF_NEVER, conf->secure); apr_pool_cleanup_register(l->pool, l->ldc, ca_ldc_cleanup, apr_pool_cleanup_null); } else { ap_log_rerror( APLOG_MARK, APLOG_WARNING, 0, r, "mod_ca_ldap authn: no sec->host - weird...?"); return HTTP_INTERNAL_SERVER_ERROR; } ap_log_rerror( APLOG_MARK, APLOG_DEBUG, 0, r, "mod_ca_ldap authn: using URL %s", conf->url); /* construct the filter. * * We aren't as efficient as we could be with the filter string, we * keep copying it each time we append to it. We assume only one * or two attributes will be passed, so it is not worth getting too * bogged down with efficient buffer handling. */ subject = X509_REQ_get_subject_name(creq); if (!subject) { log_message(r, APR_SUCCESS, "certificate request had no subject"); return HTTP_BAD_REQUEST; } if (conf->filter) { filter = apr_psprintf(r->pool, "(&(%s)", conf->filter); } else { filter = apr_psprintf(r->pool, "(&"); } while (conf->attributes && conf->attributes[i]) { const char *name = apr_hash_get(conf->subject, conf->attributes[i], APR_HASH_KEY_STRING); const int *type = apr_hash_get(conf->subjectaltname, conf->attributes[i], APR_HASH_KEY_STRING); int k = 0, last = -1, j = 0; int found = 0; X509_NAME_ENTRY *tne = NULL; ASN1_OBJECT *ko; ASN1_STRING *val; if (!name && !type) { log_message(r, APR_SUCCESS, apr_psprintf(r->pool, "LDAP attribute '%s' not mapped to a subject or subjectAltName.", conf->attributes[i])); apr_pool_destroy(l->pool); return HTTP_INTERNAL_SERVER_ERROR; } if (name) { if ((k = OBJ_txt2nid(name)) == NID_undef) { log_message(r, APR_SUCCESS, apr_psprintf(r->pool, "Subject name '%s' not recognised.", name)); apr_pool_destroy(l->pool); return HTTP_INTERNAL_SERVER_ERROR; } ko = OBJ_nid2obj(k); while (j >= 0) { j = X509_NAME_get_index_by_OBJ(subject, ko, last); if (j >= 0) { tne = X509_NAME_get_entry(subject, j); val = X509_NAME_ENTRY_get_data(tne); if (V_ASN1_PRINTABLESTRING == val->type || V_ASN1_IA5STRING == val->type || V_ASN1_UTF8STRING == val->type) { filter = apr_pstrcat(r->pool, filter, "(", conf->attributes[i], "=", escape_ldap(r->pool, apr_pstrndup(r->pool, (const char *) val->data, val->length)), ")", NULL); found = 1; #if HAVE_APR_CRYPTO_CLEAR apr_crypto_clear(r->pool, filter, strlen(filter)); #endif } else { log_message(r, APR_SUCCESS, apr_psprintf(r->pool, "Subject name '%s' is not a utf8, printable or ia5string (%d instead).", name, val->type)); apr_pool_destroy(l->pool); return HTTP_FORBIDDEN; } } last = j; } if (!found) { log_message(r, APR_SUCCESS, apr_psprintf(r->pool, "Subject name '%s' was not found in request.", name)); apr_pool_destroy(l->pool); return HTTP_FORBIDDEN; } } if (type) { STACK_OF(X509_EXTENSION) *exts; exts = X509_REQ_get_extensions(creq); if (exts) { int j, idx = -1; GENERAL_NAMES *gens; GENERAL_NAME *gen; gens = X509V3_get_d2i(exts, NID_subject_alt_name, NULL, &idx); if (gens) { for (j = 0; j < sk_GENERAL_NAME_num(gens); j++) { gen = sk_GENERAL_NAME_value(gens, j); if (*type == gen->type) { switch (*type) { case GEN_EMAIL: case GEN_DNS: case GEN_URI: { ASN1_STRING *val = GENERAL_NAME_get0_value(gen, NULL); if (V_ASN1_PRINTABLESTRING == val->type) { filter = apr_pstrcat(r->pool, filter, "(", conf->attributes[i], "=", escape_ldap(r->pool, apr_pstrndup( r->pool, (const char *) val->data, val->length)), ")", NULL); found = 1; #if HAVE_APR_CRYPTO_CLEAR apr_crypto_clear(r->pool, filter, strlen(filter)); #endif } else { log_message(r, APR_SUCCESS, apr_psprintf(r->pool, "Subject name '%s' is not a printable string.", name)); apr_pool_destroy(l->pool); return HTTP_FORBIDDEN; } break; } default: { log_message(r, APR_SUCCESS, apr_psprintf(r->pool, "SubjectAltName name '%s' cannot be parsed.", subjectaltname_from_type( *type))); apr_pool_destroy(l->pool); return HTTP_INTERNAL_SERVER_ERROR; break; } } } } } X509V3_get_d2i(exts, NID_subject_alt_name, NULL, &idx); if (idx >= 0) { log_message(r, APR_SUCCESS, apr_psprintf(r->pool, "Multiple '%s' extensions in a request are not supported.", SN_subject_alt_name)); apr_pool_destroy(l->pool); return HTTP_FORBIDDEN; } } if (!found) { log_message(r, APR_SUCCESS, apr_psprintf(r->pool, "SubjectAltName name '%s' was not found in request.", subjectaltname_from_type(*type))); apr_pool_destroy(l->pool); return HTTP_FORBIDDEN; } } i++; } filter = apr_pstrcat(r->pool, filter, ")", NULL); #if HAVE_APR_CRYPTO_CLEAR apr_crypto_clear(r->pool, filter, strlen(filter)); #endif /* * If LDAP operation fails due to LDAP_SERVER_DOWN, control returns here. */ while (failures < RETRIES) { if (failures > 0 && DELAY > 0) { apr_sleep(DELAY); } if (LDAP_SUCCESS != (result = util_ldap_connection_open(r, l->ldc))) { break; } /* try do the search */ opTimeout.tv_sec = TIMEOUT; opTimeout.tv_usec = 0; result = ldap_search_ext_s(l->ldc->ldap, (char *) conf->basedn, conf->scope, (char *) filter, NULL, 0, NULL, NULL, &opTimeout, APR_LDAP_SIZELIMIT, &res); if (AP_LDAP_IS_SERVER_DOWN(result)) { l->ldc->reason = "ldap_search_ext_s() for user failed with server down"; util_ldap_connection_unbind(l->ldc); failures++; continue; } /* if there is an error (including LDAP_NO_SUCH_OBJECT) return now */ if (result != LDAP_SUCCESS) { l->ldc->reason = "ldap_search_ext_s() for user failed"; break; } /* * We should have found exactly one entry; to find a different * number is an error. */ count = ldap_count_entries(l->ldc->ldap, res); if (count != 1) { if (count == 0) l->ldc->reason = "Certificate not found"; else l->ldc->reason = "Certificate is not unique (search found two " "or more matches)"; ldap_msgfree(res); break; } entry = ldap_first_entry(l->ldc->ldap, res); /* Grab the dn, copy it into the pool, and free it again */ dn = ldap_get_dn(l->ldc->ldap, entry); l->dn = apr_pstrdup(l->pool, dn); ldap_memfree(dn); /* * Attempt to bind with the retrieved dn and the password. If the bind * fails, it means that the password is wrong (the dn obviously * exists, since we just retrieved it) */ result = uldap_simple_bind(l->ldc, (char *) l->dn, (char *) userPassword, conf->op_timeout); if (AP_LDAP_IS_SERVER_DOWN(result) || (result == LDAP_TIMEOUT && failures == 0)) { if (AP_LDAP_IS_SERVER_DOWN(result)) l->ldc->reason = "ldap_simple_bind() to check certificate " "failed with server down"; else l->ldc->reason = "ldap_simple_bind() to check certificate " "timed out"; ldap_msgfree(res); util_ldap_connection_unbind(l->ldc); failures++; continue; } /* failure? if so - return */ if (result != LDAP_SUCCESS) { l->ldc->reason = "ldap_simple_bind() to check certificate failed"; ldap_msgfree(res); util_ldap_connection_unbind(l->ldc); break; } else { /* * We have just bound the connection to a different user and password * combination, which might be reused unintentionally next time this * connection is used from the connection pool. To ensure no confusion, * we mark the connection as unbound. */ l->ldc->bound = 0; } break; } if (result != LDAP_SUCCESS) { ap_log_rerror( APLOG_MARK, APLOG_ERR, 0, r, "mod_ca_ldap authn: LDAP search for filter '%s' at base '%s' failed: %s (%s)", filter, conf->basedn, l->ldc->reason, ldap_err2string(result)); apr_pool_destroy(l->pool); return HTTP_FORBIDDEN; } else if (!l->dn) { ap_log_rerror( APLOG_MARK, APLOG_ERR, 0, r, "mod_ca_ldap authn: LDAP search for filter '%s' at base '%s' did not return a valid entry: %s", filter, conf->basedn, l->ldc->reason); apr_pool_destroy(l->pool); return HTTP_FORBIDDEN; } /* if we reach this point, our certificate is authorised, squirrel * away the connection so we can use it to write the certificate * in the next step. */ if (conf->cert_attribute) { apr_pool_userdata_setn(l, MOD_CA_LDAP_LDC, NULL, r->pool); } return OK; } static int ca_certstore_ldap(request_rec *r, apr_hash_t *params, const unsigned char *buffer, apr_size_t len) { void *data; ca_ldap_ldc_rec *l = NULL; int failures = 0; int result = 0; X509 *cert = NULL; PKCS7 *p7 = NULL; char *path = NULL; int path_len = 0; ca_ldap_config_rec *conf = ap_get_module_config(r->per_dir_config, &ca_ldap_module); if (!conf->url_set || !buffer || !len || !conf->cert_attribute) { return DECLINED; } /* fetch the DN, if we were here before */ apr_pool_userdata_get(&data, MOD_CA_LDAP_LDC, r->pool); if (data) { l = data; } else { return DECLINED; } /* read in the certificate */ if (!d2i_PKCS7(&p7, &buffer, len)) { log_message(r, APR_SUCCESS, "could not DER decode the signed certificates"); return HTTP_BAD_REQUEST; } apr_pool_cleanup_register(r->pool, p7, ca_PKCS7_cleanup, apr_pool_cleanup_null); /* grab the first certificate */ if (OBJ_obj2nid(p7->type) == NID_pkcs7_signed) { STACK_OF(X509) *certs = p7->d.sign->cert; if (sk_X509_num(certs)) { unsigned char *buf; BIO* bio; unsigned long flags = XN_FLAG_RFC2253; ASN1_INTEGER *serialNumber; /* extract the first certificate for saving */ cert = sk_X509_value(certs, 0); len = i2d_X509(cert, NULL); if (len <= 0) { log_message(r, APR_SUCCESS, "could not DER encode the signed X509"); return HTTP_INTERNAL_SERVER_ERROR; } buffer = buf = apr_palloc(r->pool, len); if (!i2d_X509(cert, &buf)) { log_message(r, APR_SUCCESS, "could not DER encode the signed X509"); return HTTP_INTERNAL_SERVER_ERROR; } /* extract the serial number and issuer for saving */ if (!(bio = BIO_new(BIO_s_mem()))) { log_message(r, APR_SUCCESS, "could not create a BIO"); return HTTP_INTERNAL_SERVER_ERROR; } serialNumber = X509_get_serialNumber(cert); if (serialNumber) { X509_NAME *subject = X509_get_issuer_name(cert); if (subject) { BIGNUM *bn = ASN1_INTEGER_to_BN(serialNumber, NULL); char *decimal = BN_bn2dec(bn); BIO_printf(bio, "{ serialNumber %s, issuer rdnSequence:\"", decimal); X509_NAME_print_ex(bio, subject, 0, flags); BIO_write(bio, "\" }", 3); OPENSSL_free(decimal); BN_free(bn); } } path_len = BIO_pending(bio); if (path_len > 0) { path = apr_palloc(r->pool, path_len + 1); path_len = BIO_read(bio, path, path_len); path[path_len] = 0; } BIO_free(bio); } else { log_message(r, APR_SUCCESS, "PKCS7 contained zero certificates, nothing to return"); return HTTP_BAD_REQUEST; } } else { log_message(r, APR_SUCCESS, "PKCS7 was not signedData, nothing to return"); return HTTP_BAD_REQUEST; } /* write the certificate at the DN */ /* * If LDAP operation fails due to LDAP_SERVER_DOWN, control returns here. */ while (failures < RETRIES) { int i = 0; LDAPMod *mods[7]; LDAPMod path_attribute_mod; struct berval *path_attribute_vals[2]; struct berval path_attribute_val; LDAPMod path_objectclass_mod; char *path_objectclass_vals[2]; LDAPMod cert_attribute_mod; struct berval *cert_attribute_vals[2]; struct berval cert_attribute_val; LDAPMod cert_objectclass_mod; char *cert_objectclass_vals[2]; LDAPMod pass_attribute_mod; LDAPMod pass_objectclass_mod; char *pass_objectclass_vals[2]; if (failures > 0 && DELAY > 0) { apr_sleep(DELAY); } /* set up the modification */ if (conf->path_attribute) { path_attribute_vals[0] = &path_attribute_val; path_attribute_vals[1] = NULL; path_attribute_val.bv_val = (char *) path; path_attribute_val.bv_len = path_len; path_attribute_mod.mod_op = LDAP_MOD_REPLACE | LDAP_MOD_BVALUES; path_attribute_mod.mod_type = (char *) conf->path_attribute; path_attribute_mod.mod_vals.modv_bvals = path_attribute_vals; mods[i++] = &path_attribute_mod; } if (conf->path_objectclass) { path_objectclass_vals[0] = (char *) conf->path_objectclass; path_objectclass_vals[1] = NULL; path_objectclass_mod.mod_op = LDAP_MOD_ADD; path_objectclass_mod.mod_type = "objectclass"; path_objectclass_mod.mod_vals.modv_strvals = path_objectclass_vals; mods[i++] = &path_objectclass_mod; } if (conf->cert_attribute) { cert_attribute_vals[0] = &cert_attribute_val; cert_attribute_vals[1] = NULL; cert_attribute_val.bv_val = (char *) buffer; cert_attribute_val.bv_len = len; cert_attribute_mod.mod_op = LDAP_MOD_REPLACE | LDAP_MOD_BVALUES; cert_attribute_mod.mod_type = (char *) conf->cert_attribute; cert_attribute_mod.mod_vals.modv_bvals = cert_attribute_vals; mods[i++] = &cert_attribute_mod; } if (conf->cert_objectclass) { cert_objectclass_vals[0] = (char *) conf->cert_objectclass; cert_objectclass_vals[1] = NULL; cert_objectclass_mod.mod_op = LDAP_MOD_ADD; cert_objectclass_mod.mod_type = "objectclass"; cert_objectclass_mod.mod_vals.modv_strvals = cert_objectclass_vals; mods[i++] = &cert_objectclass_mod; } if (conf->pass_attribute) { pass_attribute_mod.mod_op = LDAP_MOD_DELETE; pass_attribute_mod.mod_type = (char *) conf->pass_attribute; pass_attribute_mod.mod_vals.modv_strvals = NULL; mods[i++] = &pass_attribute_mod; } if (conf->pass_objectclass) { pass_objectclass_vals[0] = (char *) conf->pass_objectclass; pass_objectclass_vals[1] = NULL; pass_objectclass_mod.mod_op = LDAP_MOD_DELETE; pass_objectclass_mod.mod_type = "objectclass"; pass_objectclass_mod.mod_vals.modv_strvals = pass_objectclass_vals; mods[i++] = &pass_objectclass_mod; } mods[i] = NULL; /* try do the modify */ result = ldap_modify_ext_s(l->ldc->ldap, l->dn, mods, NULL, NULL); if (AP_LDAP_IS_SERVER_DOWN(result)) { l->ldc->reason = "ldap_modify_ext_s() for user failed with server down"; failures++; continue; } /* if there is an error (including LDAP_NO_SUCH_OBJECT) return now */ if (result != LDAP_SUCCESS) { l->ldc->reason = "ldap_modify_ext_s() for certificate failed"; } break; } if (result != LDAP_SUCCESS) { ap_log_rerror( APLOG_MARK, APLOG_ERR, 0, r, "mod_ca_ldap store: LDAP modify of '%s' failed: %s (%s)", l->dn, l->ldc->reason, ldap_err2string(result)); apr_pool_destroy(l->pool); return HTTP_FORBIDDEN; } /* terminate the connection as soon as we are finished using it */ apr_pool_destroy(l->pool); return OK; } static void *create_ca_dir_config(apr_pool_t *p, char *d) { ca_ldap_config_rec *conf = apr_pcalloc(p, sizeof(ca_ldap_config_rec)); conf->subject = apr_hash_make(p); conf->subjectaltname = apr_hash_make(p); conf->op_timeout = apr_pcalloc(p, sizeof(struct timeval)); conf->op_timeout->tv_sec = 60; return conf; } static void *merge_ca_dir_config(apr_pool_t *p, void *basev, void *addv) { ca_ldap_config_rec *new = (ca_ldap_config_rec *) apr_pcalloc(p, sizeof(ca_ldap_config_rec)); ca_ldap_config_rec *add = (ca_ldap_config_rec *) addv; ca_ldap_config_rec *base = (ca_ldap_config_rec *) basev; new->url = (add->url_set == 0) ? base->url : add->url; new->host = (add->url_set == 0) ? base->host : add->host; new->port = (add->url_set == 0) ? base->port : add->port; new->basedn = (add->url_set == 0) ? base->basedn : add->basedn; new->attribute = (add->url_set == 0) ? base->attribute : add->attribute; new->attributes = (add->url_set == 0) ? base->attributes : add->attributes; new->scope = (add->url_set == 0) ? base->scope : add->scope; new->filter = (add->url_set == 0) ? base->filter : add->filter; new->secure = (add->url_set == 0) ? base->secure : add->secure; new->url_set = add->url_set || base->url_set; new->binddn = (add->binddn_set == 0) ? base->binddn : add->binddn; new->binddn_set = add->binddn_set || base->binddn_set; new->bindpw = (add->bindpw_set == 0) ? base->bindpw : add->bindpw; new->bindpw_set = add->bindpw_set || base->bindpw_set; new->pass_attribute = (add->pass_attribute_set == 0) ? base->pass_attribute : add->pass_attribute; new->pass_attribute_set = add->pass_attribute_set || base->pass_attribute_set; new->pass_objectclass = (add->pass_objectclass_set == 0) ? base->pass_objectclass : add->pass_objectclass; new->pass_objectclass_set = add->pass_objectclass_set || base->pass_objectclass_set; new->cert_attribute = (add->cert_attribute_set == 0) ? base->cert_attribute : add->cert_attribute; new->cert_attribute_set = add->cert_attribute_set || base->cert_attribute_set; new->cert_objectclass = (add->cert_objectclass_set == 0) ? base->cert_objectclass : add->cert_objectclass; new->cert_objectclass_set = add->cert_objectclass_set || base->cert_objectclass_set; new->path_attribute = (add->path_attribute_set == 0) ? base->path_attribute : add->path_attribute; new->path_attribute_set = add->path_attribute_set || base->path_attribute_set; new->path_objectclass = (add->path_objectclass_set == 0) ? base->path_objectclass : add->path_objectclass; new->path_objectclass_set = add->path_objectclass_set || base->path_objectclass_set; new->subject = (add->subject_set == 0) ? base->subject : add->subject; new->subject_set = add->subject_set || base->subject_set; new->subjectaltname = (add->subjectaltname_set == 0) ? base->subjectaltname : add->subjectaltname; new->subjectaltname_set = add->subjectaltname_set || base->subjectaltname_set; new->op_timeout = (add->op_timeout_set == 0) ? base->op_timeout : add->op_timeout; new->op_timeout_set = add->op_timeout_set || base->op_timeout_set; return new; } /* * Use the ldap url parsing routines to break up the ldap url into * host and port. */ static const char *mod_ca_ldap_parse_url(cmd_parms *cmd, void *config, const char *url, const char *mode) { int rc; apr_ldap_url_desc_t *urld; apr_ldap_err_t *result; ca_ldap_config_rec *sec = config; rc = apr_ldap_url_parse(cmd->pool, url, &(urld), &(result)); if (rc != APR_SUCCESS) { return result->reason; } sec->url = apr_pstrdup(cmd->pool, url); /* Set all the values, or at least some sane defaults */ if (sec->host) { sec->host = apr_pstrcat(cmd->pool, urld->lud_host, " ", sec->host, NULL); } else { sec->host = urld->lud_host ? apr_pstrdup(cmd->pool, urld->lud_host) : "localhost"; } sec->basedn = urld->lud_dn ? apr_pstrdup(cmd->pool, urld->lud_dn) : ""; if (urld->lud_attrs && urld->lud_attrs[0]) { int i = 1; while (urld->lud_attrs[i]) { i++; } sec->attributes = apr_pcalloc(cmd->pool, sizeof(char *) * (i+1)); i = 0; while (urld->lud_attrs[i]) { sec->attributes[i] = apr_pstrdup(cmd->pool, urld->lud_attrs[i]); i++; } sec->attribute = sec->attributes[0]; } else { sec->attribute = "uid"; } sec->scope = urld->lud_scope == LDAP_SCOPE_ONELEVEL ? LDAP_SCOPE_ONELEVEL : LDAP_SCOPE_SUBTREE; if (urld->lud_filter) { if (urld->lud_filter[0] == '(') { /* * Get rid of the surrounding parens; later on when generating the * filter, they'll be put back. */ sec->filter = apr_pstrmemdup(cmd->pool, urld->lud_filter + 1, strlen(urld->lud_filter) - 2); } else { sec->filter = apr_pstrdup(cmd->pool, urld->lud_filter); } } else { sec->filter = "objectclass=*"; } if (mode) { if (0 == strcasecmp("NONE", mode)) { sec->secure = APR_LDAP_NONE; } else if (0 == strcasecmp("SSL", mode)) { sec->secure = APR_LDAP_SSL; } else if (0 == strcasecmp("TLS", mode) || 0 == strcasecmp("STARTTLS", mode)) { sec->secure = APR_LDAP_STARTTLS; } else { return "Invalid LDAP connection mode setting: must be one of NONE, " "SSL, or TLS/STARTTLS"; } } /* "ldaps" indicates secure ldap connections desired */ if (strncasecmp(url, "ldaps", 5) == 0) { sec->secure = APR_LDAP_SSL; sec->port = urld->lud_port ? urld->lud_port : LDAPS_PORT; } else { sec->port = urld->lud_port ? urld->lud_port : LDAP_PORT; } sec->url_set = 1; ap_log_error( APLOG_MARK, APLOG_DEBUG, 0, cmd->server, "mod_ca_ldap url parse: `%s', Host: %s, Port: %d, DN: %s, " "attrib: %s, scope: %s, filter: %s, connection mode: %s", url, urld->lud_host, urld->lud_port, urld->lud_dn, urld->lud_attrs? urld->lud_attrs[0] : "(null)", (urld->lud_scope == LDAP_SCOPE_SUBTREE? "subtree" : urld->lud_scope == LDAP_SCOPE_BASE? "base" : urld->lud_scope == LDAP_SCOPE_ONELEVEL? "onelevel" : "unknown"), urld->lud_filter, sec->secure == APR_LDAP_SSL ? "using SSL": "not using SSL"); return NULL; } static const char *set_cert_attribute(cmd_parms *cmd, void *dconf, const char *arg) { ca_ldap_config_rec *conf = dconf; conf->cert_attribute = arg; conf->cert_attribute_set = 1; return NULL; } static const char *set_cert_objectclass(cmd_parms *cmd, void *dconf, const char *arg) { ca_ldap_config_rec *conf = dconf; conf->cert_objectclass = arg; conf->cert_objectclass_set = 1; return NULL; } static const char *set_pass_attribute(cmd_parms *cmd, void *dconf, const char *arg) { ca_ldap_config_rec *conf = dconf; conf->pass_attribute = arg; conf->pass_attribute_set = 1; return NULL; } static const char *set_pass_objectclass(cmd_parms *cmd, void *dconf, const char *arg) { ca_ldap_config_rec *conf = dconf; conf->pass_objectclass = arg; conf->pass_objectclass_set = 1; return NULL; } static const char *set_path_attribute(cmd_parms *cmd, void *dconf, const char *arg) { ca_ldap_config_rec *conf = dconf; conf->path_attribute = arg; conf->path_attribute_set = 1; return NULL; } static const char *set_path_objectclass(cmd_parms *cmd, void *dconf, const char *arg) { ca_ldap_config_rec *conf = dconf; conf->path_objectclass = arg; conf->path_objectclass_set = 1; return NULL; } static const char *set_binddn(cmd_parms *cmd, void *dconf, const char *arg) { ca_ldap_config_rec *conf = dconf; conf->binddn = arg; conf->binddn_set = 1; return NULL; } static const char *set_bindpw(cmd_parms *cmd, void *dconf, const char *arg) { ca_ldap_config_rec *conf = dconf; conf->bindpw = arg; conf->bindpw_set = 1; return NULL; } static const char *set_subject(cmd_parms *cmd, void *dconf, const char *arg1, const char *arg2) { ca_ldap_config_rec *conf = dconf; apr_hash_set(conf->subject, arg1, strlen(arg1), arg2); conf->subject_set = 1; return NULL; } static const char *set_subjectaltname(cmd_parms *cmd, void *dconf, const char *arg1, const char *arg2) { ca_ldap_config_rec *conf = dconf; int *type = apr_palloc(cmd->pool, sizeof(int)); *type = type_from_subjectaltname(arg2); if (*type < 0) { return apr_psprintf(cmd->pool, "Argument '%s' was not one of otherName, rfc822Name, dNSName, x400Address, directoryName, ediPartyName, uniformResourceIdentifier, iPAddress or registeredID", arg2); } apr_hash_set(conf->subjectaltname, arg1, strlen(arg1), type); conf->subjectaltname_set = 1; return NULL; } static const char *set_op_timeout(cmd_parms *cmd, void *dconf, const char *val) { long timeout; char *endptr; ca_ldap_config_rec *conf = dconf; timeout = strtol(val, &endptr, 10); if ((val == endptr) || (*endptr != '\0')) { return "CALdapTimeout is not numeric"; } if (timeout < 0) { return "CALdapTimeout must be non-negative"; } if (timeout) { if (!conf->op_timeout) { conf->op_timeout = apr_pcalloc(cmd->pool, sizeof(struct timeval)); } conf->op_timeout->tv_sec = timeout; } else { conf->op_timeout = NULL; } #ifndef LDAP_OPT_TIMEOUT ap_log_error(APLOG_MARK, APLOG_DEBUG, 0, cmd->server, "LDAP: LDAP_OPT_TIMEOUT option not supported by the " "LDAP library in use. Using LDAPTimeout value as search " "timeout only." ); #endif return NULL; } static const command_rec ca_cmds[] = { AP_INIT_TAKE12("CALdapUrl", mod_ca_ldap_parse_url, NULL, OR_AUTHCFG, "URL to define LDAP connection. This should be an RFC 2255 compliant\n" "URL of the form ldap://host[:port]/basedn[?attrib[?scope[?filter]]].\n" "
    \n" "
  • Host is the name of the LDAP server. Use a space separated list of hosts \n" "to specify redundant servers.\n" "
  • Port is optional, and specifies the port to connect to.\n" "
  • basedn specifies the base DN to start searches from\n" "
  • Attrib specifies what attribute to search for in the directory. If not " "provided, it defaults to certificateUUID.\n" "
  • Scope is the scope of the search, and can be either sub or " "one. If not provided, the default is sub.\n" "
  • Filter is a filter to use in the search. If not provided, " "defaults to (objectClass=*).\n" "
\n" "Searches are performed using the attribute and the filter combined. " "For example, assume that the\n" "LDAP URL is ldap://ldap.airius.com/ou=People, o=Airius?certificateUUID?sub?(posixid=*). " "Searches will\n" "be done using the filter (&((posixid=*))(certificateUUID=uuid)), " "where uuid\n" "is the certificate UUID as specified. The search will be a subtree " "search on the branch ou=People, o=Airius."), AP_INIT_TAKE1("CALdapBindDN", set_binddn, NULL, OR_AUTHCFG, "DN to use to bind to LDAP server. If not provided, will do an anonymous bind."), AP_INIT_TAKE1("CALdapBindPassword", set_bindpw, NULL, OR_AUTHCFG, "Password to use to bind to LDAP server. If not provided, will do an anonymous bind."), AP_INIT_TAKE2("CALdapSubject", set_subject, NULL, OR_AUTHCFG, "Mapping from LDAP attribute to certificate subject element."), AP_INIT_TAKE2("CALdapSubjectAltName", set_subjectaltname, NULL, OR_AUTHCFG, "Mapping from LDAP attribute to certificate subjectAltName element."), AP_INIT_TAKE1("CALdapTimeout", set_op_timeout, NULL, RSRC_CONF, "Specify the LDAP bind/search timeout in seconds " "(0 = no limit). Default: 60"), AP_INIT_TAKE1("CALdapCertAttribute", set_cert_attribute, NULL, OR_AUTHCFG, "If specified, we attempt to save the certificate in this attribute."), AP_INIT_TAKE1("CALdapCertObjectClass", set_cert_objectclass, NULL, OR_AUTHCFG, "If specified, we attempt to add this objectclass along with the certificate."), AP_INIT_TAKE1("CALdapPasswordAttribute", set_pass_attribute, NULL, OR_AUTHCFG, "If specified, we attempt to remove the password in this attribute."), AP_INIT_TAKE1("CALdapPasswordObjectClass", set_pass_objectclass, NULL, OR_AUTHCFG, "If specified, we attempt to remove this objectclass along with the password."), AP_INIT_TAKE1("CALdapPathAttribute", set_path_attribute, NULL, OR_AUTHCFG, "If specified, we attempt to place the certificate path in this attribute."), AP_INIT_TAKE1("CALdapPathObjectClass", set_path_objectclass, NULL, OR_AUTHCFG, "If specified, we attempt to add this objectclass along with the path."), { NULL } }; static apr_status_t ca_cleanup(void *data) { ERR_free_strings(); EVP_cleanup(); return APR_SUCCESS; } static int ca_pre_config(apr_pool_t *pconf, apr_pool_t *plog, apr_pool_t *ptemp) { OpenSSL_add_all_algorithms(); ERR_load_crypto_strings(); apr_pool_cleanup_register(pconf, NULL, ca_cleanup, apr_pool_cleanup_null); return APR_SUCCESS; } static void ca_optional_fn_retrieve(void) { util_ldap_connection_open = APR_RETRIEVE_OPTIONAL_FN(uldap_connection_open); util_ldap_connection_close = APR_RETRIEVE_OPTIONAL_FN(uldap_connection_close); util_ldap_connection_find = APR_RETRIEVE_OPTIONAL_FN(uldap_connection_find); util_ldap_connection_unbind = APR_RETRIEVE_OPTIONAL_FN(uldap_connection_unbind); } static void register_hooks(apr_pool_t *p) { ap_hook_pre_config(ca_pre_config, NULL, NULL, APR_HOOK_MIDDLE); ap_hook_ca_reqauthz(ca_reqauthz_ldap, NULL, NULL, APR_HOOK_MIDDLE); ap_hook_ca_certstore(ca_certstore_ldap, NULL, NULL, APR_HOOK_MIDDLE); ap_hook_optional_fn_retrieve(ca_optional_fn_retrieve, NULL, NULL, APR_HOOK_MIDDLE); } AP_DECLARE_MODULE(ca_ldap) = { STANDARD20_MODULE_STUFF, create_ca_dir_config, /* dir config creater */ merge_ca_dir_config, /* dir merger --- default is to override */ NULL, /* server config */ NULL, /* merge server config */ ca_cmds, /* command apr_table_t */ register_hooks /* register hooks */ };