Merge branch 'revocation-fetcher'

Combines concurrent requests for the same CRL URI by multiple threads.
So only the first thread actually fetches it, the others wait for that
result.  This is particularly helpful if the CRL can currently not
be fetched due to DNS or HTTP/LDAP timeouts as it prevents each thread
from having to wait for the complete timeouts, which reduces the number
of SAs that can concurrently be established.

A negative result is cached for a while (currently 3 times the fetch
timeout, i.e. 30 seconds by default) so requests can fail quickly and
threads can continue establishing SAs if they use a relaxed revocation
policy.

Closes strongswan/strongswan#2918
This commit is contained in:
Tobias Brunner
2025-11-13 18:56:57 +01:00
4 changed files with 424 additions and 125 deletions
@@ -12,6 +12,7 @@ endif
libstrongswan_revocation_la_SOURCES = \
revocation_plugin.h revocation_plugin.c \
revocation_fetcher.h revocation_fetcher.c \
revocation_validator.h revocation_validator.c
libstrongswan_revocation_la_LDFLAGS = -module -avoid-version
@@ -0,0 +1,312 @@
/*
* 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;
}
@@ -0,0 +1,70 @@
/*
* Copyright (C) 2025 Martin Willi
*
* 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.
*/
/**
* @defgroup revocation_fetcher revocation_fetcher
* @{ @ingroup revocation
*/
#ifndef REVOCATION_FETCHER_H_
#define REVOCATION_FETCHER_H_
#include <credentials/certificates/certificate.h>
typedef struct revocation_fetcher_t revocation_fetcher_t;
/**
* Certificate fetcher performing the CRL/OCSP transfer.
*/
struct revocation_fetcher_t {
/**
* Fetch a CRL from given URL.
*
* @param this revocation fetcher
* @param url URL to retrieve the CRL from
* @param timeout timeout in seconds for the fetch operation
* @return fetched CRL or NULL on error
*/
certificate_t *(*fetch_crl)(revocation_fetcher_t *this, char *url,
u_int timeout);
/**
* Fetch an OCSP response from given URL.
*
* @param this revocation fetcher
* @param url URL to retrieve the OCSP response from
* @param subject subject to request OSCP status for
* @param issuer issuer of the subject
* @param timeout timeout in seconds for the fetch operation
* @return fetched OCSP response or NULL on error
*/
certificate_t *(*fetch_ocsp)(revocation_fetcher_t *this, char *url,
certificate_t *subject, certificate_t *issuer,
u_int timeout);
/**
* Destroy a revocation_fetcher_t.
*/
void (*destroy)(revocation_fetcher_t *this);
};
/**
* Create a revocation_fetcher instance.
*/
revocation_fetcher_t *revocation_fetcher_create();
#endif /** REVOCATION_FETCHER_H_ @}*/
@@ -23,12 +23,13 @@
#include <utils/debug.h>
#include <credentials/certificates/x509.h>
#include <credentials/certificates/crl.h>
#include <credentials/certificates/ocsp_request.h>
#include <credentials/certificates/ocsp_response.h>
#include <credentials/sets/ocsp_response_wrapper.h>
#include <selectors/traffic_selector.h>
#include <threading/spinlock.h>
#include "revocation_fetcher.h"
/**
* Default timeout in seconds when fetching OCSP/CRL.
*/
@@ -46,6 +47,11 @@ struct private_revocation_validator_t {
*/
revocation_validator_t public;
/**
* Fetch helper for CRL/OCSP.
*/
revocation_fetcher_t *fetcher;
/**
* Enable OCSP validation
*/
@@ -67,80 +73,6 @@ struct private_revocation_validator_t {
spinlock_t *lock;
};
/**
* Do an OCSP request
*/
static certificate_t *fetch_ocsp(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;
}
/**
* Verify OCSP response signature
*/
@@ -327,7 +259,8 @@ static certificate_t *get_better_ocsp(certificate_t *cand, certificate_t *best,
/**
* validate a x509 certificate using OCSP
*/
static cert_validation_t check_ocsp(x509_t *subject, x509_t *issuer,
static cert_validation_t check_ocsp(private_revocation_validator_t *this,
x509_t *subject, x509_t *issuer,
auth_cfg_t *auth, u_int timeout,
certificate_t **response)
{
@@ -368,8 +301,9 @@ static cert_validation_t check_ocsp(x509_t *subject, x509_t *issuer,
CERT_X509_OCSP_RESPONSE, keyid);
while (enumerator->enumerate(enumerator, &uri))
{
current = fetch_ocsp(uri, &subject->interface, &issuer->interface,
timeout);
current = this->fetcher->fetch_ocsp(this->fetcher, uri,
&subject->interface,
&issuer->interface, timeout);
if (current)
{
best = get_better_ocsp(current, best, subject, issuer,
@@ -391,8 +325,9 @@ static cert_validation_t check_ocsp(x509_t *subject, x509_t *issuer,
enumerator = subject->create_ocsp_uri_enumerator(subject);
while (enumerator->enumerate(enumerator, &uri))
{
current = fetch_ocsp(uri, &subject->interface, &issuer->interface,
timeout);
current = this->fetcher->fetch_ocsp(this->fetcher, uri,
&subject->interface,
&issuer->interface, timeout);
if (current)
{
best = get_better_ocsp(current, best, subject, issuer,
@@ -427,34 +362,6 @@ static cert_validation_t check_ocsp(x509_t *subject, x509_t *issuer,
return valid;
}
/**
* fetch a CRL from an URL
*/
static certificate_t* fetch_crl(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;
}
/**
* check the signature of an CRL
*/
@@ -615,7 +522,8 @@ static certificate_t *get_better_crl(certificate_t *cand, certificate_t *best,
/**
* Find or fetch a certificate for a given crlIssuer
*/
static cert_validation_t find_crl(x509_t *subject, identification_t *issuer,
static cert_validation_t find_crl(private_revocation_validator_t *this,
x509_t *subject, identification_t *issuer,
crl_t *base, certificate_t **best,
bool *uri_found, u_int timeout)
{
@@ -647,7 +555,7 @@ static cert_validation_t find_crl(x509_t *subject, identification_t *issuer,
while (enumerator->enumerate(enumerator, &uri))
{
*uri_found = TRUE;
current = fetch_crl(uri, timeout);
current = this->fetcher->fetch_crl(this->fetcher, uri, timeout);
if (current)
{
if (!current->has_issuer(current, issuer))
@@ -698,7 +606,8 @@ static bool check_issuer(certificate_t *crl, x509_t *issuer, x509_cdp_t *cdp)
/**
* Look for a delta CRL for a given base CRL
*/
static cert_validation_t check_delta_crl(x509_t *subject, x509_t *issuer,
static cert_validation_t check_delta_crl(private_revocation_validator_t *this,
x509_t *subject, x509_t *issuer,
crl_t *base, cert_validation_t base_valid,
u_int timeout)
{
@@ -716,7 +625,7 @@ static cert_validation_t check_delta_crl(x509_t *subject, x509_t *issuer,
if (chunk.len)
{
id = identification_create_from_encoding(ID_KEY_ID, chunk);
valid = find_crl(subject, id, base, &best, &uri, timeout);
valid = find_crl(this, subject, id, base, &best, &uri, timeout);
id->destroy(id);
}
@@ -727,7 +636,8 @@ static cert_validation_t check_delta_crl(x509_t *subject, x509_t *issuer,
{
if (cdp->issuer)
{
valid = find_crl(subject, cdp->issuer, base, &best, &uri, timeout);
valid = find_crl(this, subject, cdp->issuer, base, &best, &uri,
timeout);
}
}
enumerator->destroy(enumerator);
@@ -737,7 +647,7 @@ static cert_validation_t check_delta_crl(x509_t *subject, x509_t *issuer,
while (valid != VALIDATION_GOOD && valid != VALIDATION_REVOKED &&
enumerator->enumerate(enumerator, &cdp))
{
current = fetch_crl(cdp->uri, timeout);
current = this->fetcher->fetch_crl(this->fetcher, cdp->uri, timeout);
if (current)
{
if (!check_issuer(current, issuer, cdp))
@@ -769,7 +679,8 @@ static cert_validation_t check_delta_crl(x509_t *subject, x509_t *issuer,
/**
* validate a x509 certificate using CRL
*/
static cert_validation_t check_crl(x509_t *subject, x509_t *issuer,
static cert_validation_t check_crl(private_revocation_validator_t *this,
x509_t *subject, x509_t *issuer,
auth_cfg_t *auth, u_int timeout)
{
cert_validation_t valid = VALIDATION_SKIPPED;
@@ -786,7 +697,7 @@ static cert_validation_t check_crl(x509_t *subject, x509_t *issuer,
if (chunk.len)
{
id = identification_create_from_encoding(ID_KEY_ID, chunk);
valid = find_crl(subject, id, NULL, &best, &uri_found, timeout);
valid = find_crl(this, subject, id, NULL, &best, &uri_found, timeout);
id->destroy(id);
}
@@ -797,8 +708,8 @@ static cert_validation_t check_crl(x509_t *subject, x509_t *issuer,
{
if (cdp->issuer)
{
valid = find_crl(subject, cdp->issuer, NULL, &best, &uri_found,
timeout);
valid = find_crl(this, subject, cdp->issuer, NULL, &best,
&uri_found, timeout);
}
}
enumerator->destroy(enumerator);
@@ -810,7 +721,8 @@ static cert_validation_t check_crl(x509_t *subject, x509_t *issuer,
while (enumerator->enumerate(enumerator, &cdp))
{
uri_found = TRUE;
current = fetch_crl(cdp->uri, timeout);
current = this->fetcher->fetch_crl(this->fetcher, cdp->uri,
timeout);
if (current)
{
if (!check_issuer(current, issuer, cdp))
@@ -836,7 +748,8 @@ static cert_validation_t check_crl(x509_t *subject, x509_t *issuer,
/* look for delta CRLs */
if (best && (valid == VALIDATION_GOOD || valid == VALIDATION_STALE))
{
valid = check_delta_crl(subject, issuer, (crl_t*)best, valid, timeout);
valid = check_delta_crl(this, subject, issuer, (crl_t*)best, valid,
timeout);
}
/* an uri was found, but no result. switch validation state to failed */
@@ -880,8 +793,8 @@ METHOD(cert_validator_t, validate_online, bool,
if (enable_ocsp)
{
switch (check_ocsp((x509_t*)subject, (x509_t*)issuer, auth, timeout,
NULL))
switch (check_ocsp(this, (x509_t*)subject, (x509_t*)issuer,
auth, timeout, NULL))
{
case VALIDATION_GOOD:
DBG1(DBG_CFG, "certificate status is good");
@@ -910,7 +823,8 @@ METHOD(cert_validator_t, validate_online, bool,
if (enable_crl)
{
switch (check_crl((x509_t*)subject, (x509_t*)issuer, auth, timeout))
switch (check_crl(this, (x509_t*)subject, (x509_t*)issuer, auth,
timeout))
{
case VALIDATION_GOOD:
DBG1(DBG_CFG, "certificate status is good");
@@ -964,8 +878,8 @@ METHOD (cert_validator_t, ocsp, certificate_t *,
subject->get_subject(subject));
auth = auth_cfg_create();
switch (check_ocsp((x509_t*)subject, (x509_t*)issuer, auth, timeout,
&response))
switch (check_ocsp(this, (x509_t*)subject, (x509_t*)issuer,
auth, timeout, &response))
{
case VALIDATION_GOOD:
case VALIDATION_ON_HOLD:
@@ -1016,6 +930,7 @@ METHOD(revocation_validator_t, reload, void,
METHOD(revocation_validator_t, destroy, void,
private_revocation_validator_t *this)
{
this->fetcher->destroy(this->fetcher);
this->lock->destroy(this->lock);
free(this);
}
@@ -1034,6 +949,7 @@ revocation_validator_t *revocation_validator_create()
.reload = _reload,
.destroy = _destroy,
},
.fetcher = revocation_fetcher_create(),
.lock = spinlock_create(),
);