Some time ago, we enabled IPv6 on our internal database webserver. The PHP application running bound its sessions to the client IP address. The local clients configured its IPv6 address using IPv6 auto-configuration with privacy-extensions enabled. This lead to IPv6 clients sometimes changing its IPv6 address used with the connection to the web server, so they could no longer use their session and thus were logged off.
The solution was to install SSL client certificates, so clients could reliably be identified even across different IPv6 privacy extension addresses. These client certificates were automatically generated and signed by a local CA using HTML <keygen> and PHPseclib and only served to identify the browser instance.
Now these client certificates started to expire.
As our Firefox users had not cleared the "Remember this decision" flag when choosing their single client certificate to connect with, Firefox 41 continued to connect using an expired certificate. The "auto select client certificate" in Firefox does not do any better, thought. When the server rejects an SSL client certificate with ssl_error_expired_cert_alert, Firefox shows an error message to the user but does not offer him to reselect an other client certificate. So users started to puzzle about how to fix this error. Even installing a renewed client certificate (same subject, issuer and public key) ahead of expiration into Firefox 41 does not help, as that does not remove the old certificate which is still chosen for connecting with.
So the only place to fix this is the server. On server side, the list of accepting CAs could be changed. Though Firefox code looks like in "auto select client certificate" mode this would work, the "use remembered client certificate" lacks any client certificate issuer check. So this does not look promising (see ClientAuthDataRunnable::RunOnTargetThread), though it has not been tested. Next, trying to configure apache 2.4 (Debian Jessie) to accept expired client certificates (while still requesting - at least optionally - one) turns out to be impossibly. There just is no such option. Thought, patching and recompiling apache2 was to be avoided as the server should still be able to install Debian security updates without much manual intervention.
After digging through the source of apache 2.4 mod_ssl, it appears that it is making heavy use of OpenSSL certificate validation routines to check client certificates - including the expiration checks (in contrast to https proxy server certificate validation, where expiration time is checked in mod_ssl itself). Specifically, it uses ssl_callback_SSLVerify with SSL_set_verify to ignore some verification errors already.
LD_PRELOAD is a mechanism to load a shared library early during linking (starting) of an applicaton (like apache2). The linker will the prefer the functions provided by LD_PRELOAD libraries over those libraries indicated by the application itself - so it can be used to hook into library function calls. By writing a specifically crafted LD_PRELOAD library, SSL_set_verify can be hooked and the callback replaced. The new callback will filter out any X509_V_ERR_CERT_HAS_EXPIRED error before passing back control to the original ssl_callback_SSLVerify callback.
LD_PRELOAD can the be configured in /etc/apache2/envvars (used by apache2ctl and thus Debian init scripts) to load the new library.
# mysslverify.c
// gcc -Wall -D_GNU_SOURCE -fPIC -DPIC -shared -ldl -o mysslverify.so mysslverify.c
// use with LD_PRELOAD=/.../mysslverify.so
#include <openssl/ssl.h>
#include <dlfcn.h>
void (*orig_SSL_CTX_set_verify)(SSL_CTX *ctx, int mode, int (*verify_callback)(int, X509_STORE_CTX *)) = NULL;
void (*orig_SSL_set_verify)(SSL *s, int mode, int (*verify_callback)(int, X509_STORE_CTX *)) = NULL;
int (*orig_verify_callback)(int, X509_STORE_CTX *) = NULL;
int verify_callback(int ok, X509_STORE_CTX *ctx) {
if (!ok && X509_STORE_CTX_get_error(ctx) == X509_V_ERR_CERT_HAS_EXPIRED) {
X509_STORE_CTX_set_error(ctx, X509_V_OK);
ok = !ok;
}
return orig_verify_callback(ok, ctx);
}
void SSL_CTX_set_verify(SSL_CTX *ctx, int mode, int (*cb)(int, X509_STORE_CTX *)) {
if (orig_verify_callback == NULL) orig_verify_callback = cb;
if (cb != NULL && cb == orig_verify_callback) cb = verify_callback;
if (!orig_SSL_CTX_set_verify) orig_SSL_CTX_set_verify = dlsym(RTLD_NEXT, "SSL_CTX_set_verify");
orig_SSL_CTX_set_verify(ctx, mode, cb);
}
void SSL_set_verify(SSL *s, int mode, int (*cb)(int, X509_STORE_CTX *)) {
if (orig_verify_callback == NULL) orig_verify_callback = cb;
if (cb != NULL && cb == orig_verify_callback) cb = verify_callback;
if (!orig_SSL_set_verify) orig_SSL_set_verify = dlsym(RTLD_NEXT, "SSL_set_verify");
orig_SSL_set_verify(s, mode, cb);
}