If a CRL server is unresponsive, all threads trying to fetch this CRL will block execution. If a recent previous attempt to fetch the CRL failed, it is likely that it will fail again. While it makes sense to retry fetching the CRL on demand with one thread, it hardly does to block additional threads while the first one is blocked during the fetch. So remember the timestamp of the last CRL fetch failure per URL, and do not block more than one thread in the CRL fetch for some time. This time is a multiple of the configured fetch timeout, so that it works well for any configured value. With the default configuration, a failing CRL fetch will impact concurrent CRL fetches for the same URL for 30s.
313 lines
7.4 KiB
C
313 lines
7.4 KiB
C
/*
|
|
* Copyright (C) 2025 Martin Willi
|
|
* Copyright (C) 2015-2018 Tobias Brunner
|
|
* Copyright (C) 2009-2022 Andreas Steffen
|
|
*
|
|
* Copyright (C) secunet Security Networks AG
|
|
*
|
|
* This program is free software; you can redistribute it and/or modify it
|
|
* under the terms of the GNU General Public License as published by the
|
|
* Free Software Foundation; either version 2 of the License, or (at your
|
|
* option) any later version. See <http://www.fsf.org/copyleft/gpl.txt>.
|
|
*
|
|
* This program is distributed in the hope that it will be useful, but
|
|
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
|
|
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
|
|
* for more details.
|
|
*/
|
|
|
|
#include "revocation_fetcher.h"
|
|
|
|
#include <utils/debug.h>
|
|
#include <threading/mutex.h>
|
|
#include <threading/condvar.h>
|
|
#include <collections/hashtable.h>
|
|
#include <credentials/certificates/crl.h>
|
|
#include <credentials/certificates/ocsp_request.h>
|
|
#include <credentials/certificates/ocsp_response.h>
|
|
|
|
/* number of fetch timeouts to degrade a CRL fetch after a failure */
|
|
#define CRL_DEGRADATION_TIMES 3
|
|
|
|
typedef struct private_revocation_fetcher_t private_revocation_fetcher_t;
|
|
|
|
/**
|
|
* Private data of an revocation_fetcher_t object.
|
|
*/
|
|
struct private_revocation_fetcher_t {
|
|
|
|
/**
|
|
* Public revocation_fetcher_t interface.
|
|
*/
|
|
revocation_fetcher_t public;
|
|
|
|
/**
|
|
* Mutex to synchronize CRL fetches
|
|
*/
|
|
mutex_t *mutex;
|
|
|
|
/**
|
|
* Active/completed/failed CRL fetches, crl_fetch_t.
|
|
*/
|
|
hashtable_t *crls;
|
|
};
|
|
|
|
typedef struct crl_fetch_t crl_fetch_t;
|
|
|
|
/**
|
|
* Represents an active/completed/failed CRL fetch.
|
|
*/
|
|
struct crl_fetch_t {
|
|
|
|
/**
|
|
* URL of the CRL.
|
|
*/
|
|
char *url;
|
|
|
|
/**
|
|
* Condition variable to signal completion of the fetch.
|
|
*/
|
|
condvar_t *condvar;
|
|
|
|
/**
|
|
* Number of threads fetching this CRL.
|
|
*/
|
|
u_int fetchers;
|
|
|
|
/**
|
|
* Has the previous fetch failed, until when is this URL degraded?
|
|
*/
|
|
time_t failing;
|
|
|
|
/**
|
|
* CRL received in the currently active fetch.
|
|
*/
|
|
certificate_t *crl;
|
|
};
|
|
|
|
/**
|
|
* Perform the actual CRL fetch from the given URL.
|
|
*/
|
|
static certificate_t *do_crl_fetch(private_revocation_fetcher_t *this,
|
|
char *url, u_int timeout)
|
|
{
|
|
certificate_t *crl;
|
|
chunk_t chunk = chunk_empty;
|
|
|
|
DBG1(DBG_CFG, " fetching crl from '%s' ...", url);
|
|
if (lib->fetcher->fetch(lib->fetcher, url, &chunk,
|
|
FETCH_TIMEOUT, timeout,
|
|
FETCH_END) != SUCCESS)
|
|
{
|
|
DBG1(DBG_CFG, "crl fetching failed");
|
|
chunk_free(&chunk);
|
|
return NULL;
|
|
}
|
|
crl = lib->creds->create(lib->creds, CRED_CERTIFICATE, CERT_X509_CRL,
|
|
BUILD_BLOB_PEM, chunk, BUILD_END);
|
|
chunk_free(&chunk);
|
|
if (!crl)
|
|
{
|
|
DBG1(DBG_CFG, "crl fetched successfully but parsing failed");
|
|
return NULL;
|
|
}
|
|
return crl;
|
|
}
|
|
|
|
/**
|
|
* Start a new CRL fetch and signal completion to waiting threads.
|
|
*/
|
|
static certificate_t *start_crl_fetch(private_revocation_fetcher_t *this,
|
|
crl_fetch_t *fetch, u_int timeout)
|
|
{
|
|
certificate_t *crl;
|
|
|
|
fetch->fetchers++;
|
|
this->mutex->unlock(this->mutex);
|
|
crl = do_crl_fetch(this, fetch->url, timeout);
|
|
this->mutex->lock(this->mutex);
|
|
fetch->crl = crl;
|
|
if (crl)
|
|
{
|
|
fetch->failing = 0;
|
|
}
|
|
else
|
|
{
|
|
fetch->failing = time_monotonic(NULL) + timeout * CRL_DEGRADATION_TIMES;
|
|
}
|
|
while (fetch->fetchers > 1)
|
|
{
|
|
fetch->condvar->signal(fetch->condvar);
|
|
fetch->condvar->wait(fetch->condvar, this->mutex);
|
|
}
|
|
fetch->fetchers--;
|
|
fetch->crl = NULL;
|
|
return crl;
|
|
}
|
|
|
|
/**
|
|
* Wait for a CRL fetch performed by another thread to complete.
|
|
*/
|
|
static certificate_t *wait_for_crl(private_revocation_fetcher_t *this,
|
|
crl_fetch_t *fetch)
|
|
{
|
|
certificate_t *crl = NULL;
|
|
|
|
if (fetch->failing && fetch->failing > time_monotonic(NULL))
|
|
{
|
|
DBG1(DBG_CFG, " crl fetch from '%s' recently failed, skipping",
|
|
fetch->url);
|
|
return NULL;
|
|
}
|
|
DBG1(DBG_CFG, " waiting for crl fetch from '%s' ...", fetch->url);
|
|
if (fetch->crl)
|
|
{
|
|
/* fetch is already complete, no need to wait */
|
|
return fetch->crl->get_ref(fetch->crl);
|
|
}
|
|
fetch->fetchers++;
|
|
fetch->condvar->wait(fetch->condvar, this->mutex);
|
|
fetch->fetchers--;
|
|
if (fetch->crl)
|
|
{
|
|
crl = fetch->crl->get_ref(fetch->crl);
|
|
}
|
|
fetch->condvar->signal(fetch->condvar);
|
|
return crl;
|
|
}
|
|
|
|
METHOD(revocation_fetcher_t, fetch_crl, certificate_t*,
|
|
private_revocation_fetcher_t *this, char *url, u_int timeout)
|
|
{
|
|
certificate_t *crl;
|
|
crl_fetch_t *fetch;
|
|
|
|
this->mutex->lock(this->mutex);
|
|
fetch = this->crls->get(this->crls, url);
|
|
if (!fetch)
|
|
{
|
|
INIT(fetch,
|
|
.url = strdup(url),
|
|
.condvar = condvar_create(CONDVAR_TYPE_DEFAULT),
|
|
);
|
|
this->crls->put(this->crls, fetch->url, fetch);
|
|
}
|
|
if (fetch->fetchers)
|
|
{
|
|
crl = wait_for_crl(this, fetch);
|
|
}
|
|
else
|
|
{
|
|
crl = start_crl_fetch(this, fetch, timeout);
|
|
}
|
|
this->mutex->unlock(this->mutex);
|
|
return crl;
|
|
}
|
|
|
|
METHOD(revocation_fetcher_t, fetch_ocsp, certificate_t*,
|
|
private_revocation_fetcher_t *this, char *url,
|
|
certificate_t *subject, certificate_t *issuer, u_int timeout)
|
|
{
|
|
certificate_t *request, *response;
|
|
ocsp_request_t *ocsp_request;
|
|
ocsp_response_t *ocsp_response;
|
|
chunk_t send, receive = chunk_empty;
|
|
|
|
/* TODO: requestor name, signature */
|
|
request = lib->creds->create(lib->creds,
|
|
CRED_CERTIFICATE, CERT_X509_OCSP_REQUEST,
|
|
BUILD_CA_CERT, issuer,
|
|
BUILD_CERT, subject, BUILD_END);
|
|
if (!request)
|
|
{
|
|
DBG1(DBG_CFG, "generating ocsp request failed");
|
|
return NULL;
|
|
}
|
|
|
|
if (!request->get_encoding(request, CERT_ASN1_DER, &send))
|
|
{
|
|
DBG1(DBG_CFG, "encoding ocsp request failed");
|
|
request->destroy(request);
|
|
return NULL;
|
|
}
|
|
|
|
DBG1(DBG_CFG, " requesting ocsp status from '%s' ...", url);
|
|
if (lib->fetcher->fetch(lib->fetcher, url, &receive,
|
|
FETCH_REQUEST_DATA, send,
|
|
FETCH_REQUEST_TYPE, "application/ocsp-request",
|
|
FETCH_TIMEOUT, timeout,
|
|
FETCH_END) != SUCCESS)
|
|
{
|
|
DBG1(DBG_CFG, "ocsp request to %s failed", url);
|
|
request->destroy(request);
|
|
chunk_free(&receive);
|
|
chunk_free(&send);
|
|
return NULL;
|
|
}
|
|
chunk_free(&send);
|
|
|
|
response = lib->creds->create(lib->creds,
|
|
CRED_CERTIFICATE, CERT_X509_OCSP_RESPONSE,
|
|
BUILD_BLOB_ASN1_DER, receive, BUILD_END);
|
|
chunk_free(&receive);
|
|
if (!response)
|
|
{
|
|
DBG1(DBG_CFG, "parsing ocsp response failed");
|
|
request->destroy(request);
|
|
return NULL;
|
|
}
|
|
ocsp_response = (ocsp_response_t*)response;
|
|
if (ocsp_response->get_ocsp_status(ocsp_response) != OCSP_SUCCESSFUL)
|
|
{
|
|
response->destroy(response);
|
|
request->destroy(request);
|
|
return NULL;
|
|
}
|
|
ocsp_request = (ocsp_request_t*)request;
|
|
if (ocsp_response->get_nonce(ocsp_response).len &&
|
|
!chunk_equals_const(ocsp_request->get_nonce(ocsp_request),
|
|
ocsp_response->get_nonce(ocsp_response)))
|
|
{
|
|
DBG1(DBG_CFG, "nonce in ocsp response doesn't match");
|
|
request->destroy(request);
|
|
return NULL;
|
|
}
|
|
request->destroy(request);
|
|
return response;
|
|
}
|
|
|
|
CALLBACK(crl_fetch_destroy, void, crl_fetch_t *fetch, const void *key)
|
|
{
|
|
fetch->condvar->destroy(fetch->condvar);
|
|
free(fetch->url);
|
|
free(fetch);
|
|
}
|
|
|
|
METHOD(revocation_fetcher_t, destroy, void,
|
|
private_revocation_fetcher_t *this)
|
|
{
|
|
this->crls->destroy_function(this->crls, crl_fetch_destroy);
|
|
this->mutex->destroy(this->mutex);
|
|
free(this);
|
|
}
|
|
|
|
/**
|
|
* See header
|
|
*/
|
|
revocation_fetcher_t *revocation_fetcher_create()
|
|
{
|
|
private_revocation_fetcher_t *this;
|
|
|
|
INIT(this,
|
|
.public = {
|
|
.fetch_crl = _fetch_crl,
|
|
.fetch_ocsp = _fetch_ocsp,
|
|
.destroy = _destroy,
|
|
},
|
|
.mutex = mutex_create(MUTEX_TYPE_DEFAULT),
|
|
.crls = hashtable_create(hashtable_hash_str, hashtable_equals_str, 8),
|
|
);
|
|
|
|
return &this->public;
|
|
}
|