From: Arran Cudbard-Bell Date: Wed, 28 May 2025 14:22:39 +0000 (-0600) Subject: Basic CRL module X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=b3978a7d803b838b4e2c00cc74e3d8a919b397a3;p=thirdparty%2Ffreeradius-server.git Basic CRL module --- diff --git a/raddb/mods-available/crl b/raddb/mods-available/crl new file mode 100644 index 00000000000..e69de29bb2d diff --git a/share/dictionary/freeradius/dictionary b/share/dictionary/freeradius/dictionary index dc78bdd9bca..673d81a72c5 100644 --- a/share/dictionary/freeradius/dictionary +++ b/share/dictionary/freeradius/dictionary @@ -22,6 +22,9 @@ $INCLUDE dictionary.freeradius.internal.ippool # 5200-5299 SIM management attributes $INCLUDE dictionary.freeradius.internal.sim +# 6000-6001 CRL module attributes +$INCLUDE dictionary.freeradius.internal.crl + # # Include module-specific dictionaries. # diff --git a/share/dictionary/freeradius/dictionary.freeradius.internal.crl b/share/dictionary/freeradius/dictionary.freeradius.internal.crl new file mode 100644 index 00000000000..cdd2cbaf50d --- /dev/null +++ b/share/dictionary/freeradius/dictionary.freeradius.internal.crl @@ -0,0 +1,18 @@ +# -*- text -*- +# Copyright (C) 2022 The FreeRADIUS Server project and contributors +# This work is licensed under CC-BY version 4.0 https://creativecommons.org/licenses/by/4.0 +# Version $Id$ +# +# Attributes used by sim code (5200-5299) +# +# $Id$ +# + +# +# All of these attributes are internal. +# +FLAGS internal + +ATTRIBUTE CRL 6000 tlv +ATTRIBUTE Data .1 octets +ATTRIBUTE CDP-URL .2 string diff --git a/src/modules/rlm_crl/README.md b/src/modules/rlm_crl/README.md new file mode 100644 index 00000000000..a368ec78024 --- /dev/null +++ b/src/modules/rlm_crl/README.md @@ -0,0 +1,8 @@ +# rlm_dict +## Metadata +
+
category
datastore
+
+ +## Summary +Registers xlats and maps to access dictionary data diff --git a/src/modules/rlm_crl/all.mk b/src/modules/rlm_crl/all.mk new file mode 100644 index 00000000000..f743ef2117b --- /dev/null +++ b/src/modules/rlm_crl/all.mk @@ -0,0 +1,4 @@ +TARGETNAME := rlm_crl + +TARGET := $(TARGETNAME)$(L) +SOURCES := $(TARGETNAME).c diff --git a/src/modules/rlm_crl/rlm_crl.c b/src/modules/rlm_crl/rlm_crl.c new file mode 100644 index 00000000000..e0df79771de --- /dev/null +++ b/src/modules/rlm_crl/rlm_crl.c @@ -0,0 +1,385 @@ +/* + * This program is 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. + * + * 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. + * + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA + */ + +/** + * $Id$ + * @file rlm_crl.c + * @brief Check a certificate's serial number against a CRL + * + * @author Arran Cudbard-Bell (a.cudbardb@freeradius.org) + * @copyright 2025 Network RADIUS SAS (legal@networkradius.com) + */ +RCSID("$Id$") + +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include + +/** Global tree of CRLs + * + * Separate from the instance data because that's protected. + */ +typedef struct { + fr_rb_tree_t *crls; //!< A tree of CRLs organised by CDP URL. + fr_timer_list_t *timer_list; //!< The timer list to use for CRL expiry. + ///< This gets serviced by the main loop. + pthread_mutex_t mutex; +} rlm_crl_mutable_t; + +typedef struct { + CONF_SECTION *virtual_server; //!< Virtual server to use when retrieving CRLs + fr_time_delta_t force_expiry; //!< Force expiry of CRLs after this time + rlm_crl_mutable_t *mutable; //!< Mutable data that's shared between all threads. +} rlm_crl_t; + +/** A single CRL in the global list of CRLs */ +typedef struct { + X509_CRL *crl; //!< The CRL. + char const *cdp_url; //!< The URL of the CRL. + fr_timer_t *ev; //!< When to expire the CRL + fr_rb_node_t node; //!< The node in the tree + rlm_crl_t *inst; //!< The instance of the CRL module. +} crl_entry_t; + +typedef struct { + fr_value_box_t *cdp_url; //!< The URL we're currently attempting to load. + fr_value_box_list_t crl_data; //!< Data from CRL expansion. +} rlm_crl_rctx_t; + +static fr_dict_t const *dict_freeradius; + +extern fr_dict_autoload_t rlm_files_dict[]; +fr_dict_autoload_t rlm_files_dict[] = { + { .out = &dict_freeradius, .proto = "freeradius" }, + { NULL } +}; + +static fr_dict_attr_t const *attr_crl_data; +static fr_dict_attr_t const *attr_crl_cdp_url; + +extern fr_dict_attr_autoload_t rlm_files_dict_attr[]; +fr_dict_attr_autoload_t rlm_files_dict_attr[] = { + { .out = &attr_crl_data, .name = "CRL.Data", .type = FR_TYPE_OCTETS, .dict = &dict_freeradius }, + { .out = &attr_crl_cdp_url, .name = "CRL.CDP-URL", .type = FR_TYPE_STRING, .dict = &dict_freeradius }, + { NULL } +}; + +typedef struct { + tmpl_t *exp; //!< The xlat expansion use to retrieve the CRL. + fr_value_box_t serial; //!< The serial to check + fr_value_box_list_head_t *cdp; //!< The CRL distribution points +} rlm_crl_env_t; + +typedef enum { + CRL_ERROR = -1, //!< Unspecified error ocurred. + CRL_ENTRY_NOT_FOUND = 0, //!< Serial not found in this CRL. + CRL_ENTRY_FOUND = 1, //!< Serial was found in this CRL. + CRL_NOT_FOUND = 2, //!< No CRL found, need to load it from the CDP URL +} crl_ret_t; + +static const call_env_method_t crl_env = { + FR_CALL_ENV_METHOD_OUT(rlm_crl_env_t), + .env = (call_env_parser_t[]){ + { FR_CALL_ENV_PARSE_ONLY_OFFSET("crl", FR_TYPE_OCTETS, 0, rlm_crl_env_t, exp )}, + { FR_CALL_ENV_OFFSET("serial", FR_TYPE_STRING, CALL_ENV_FLAG_REQUIRED | CALL_ENV_FLAG_SINGLE, rlm_crl_env_t, serial), + .pair.dflt = "Session-State.TLS-Certificate.Serial", .pair.dflt_quote = T_BARE_WORD }, + { FR_CALL_ENV_OFFSET("cdp", FR_TYPE_STRING, CALL_ENV_FLAG_REQUIRED | CALL_ENV_FLAG_MULTI, rlm_crl_env_t, cdp), + .pair.dflt = "Session-State.TLS-Certificate.CRL-Distribution-Points[*]", .pair.dflt_quote = T_BARE_WORD }, + CALL_ENV_TERMINATOR + }, +}; + +static int8_t crl_cmp(void const *a, void const *b) +{ + crl_entry_t const *crl_a = (crl_entry_t const *)a; + crl_entry_t const *crl_b = (crl_entry_t const *)b; + + return CMP(strcmp(crl_a->cdp_url, crl_b->cdp_url), 0); +} + +static void crl_free(void *data) +{ + talloc_free(data); +} + +static void crl_expire(UNUSED fr_timer_list_t *tl, UNUSED fr_time_t now, UNUSED void *uctx) +{ + crl_entry_t *crl = talloc_get_type_abort(uctx, crl_entry_t); + + DEBUG2("CRL associated with CDP %s expired", crl->cdp_url); + pthread_mutex_lock(&crl->inst->mutable->mutex); + fr_rb_remove(crl->inst->mutable->crls, &crl->node); + pthread_mutex_unlock(&crl->inst->mutable->mutex); +} + +/** Make sure we don't lock up the server if a request is cancelled + */ +static void crl_signal(module_ctx_t const *mctx, UNUSED request_t *request, fr_signal_t action) +{ + rlm_crl_t const *inst = talloc_get_type_abort_const(mctx->mi->data, rlm_crl_t); + + if (action == FR_SIGNAL_CANCEL) { + pthread_mutex_unlock(&inst->mutable->mutex); + pair_delete_request(attr_crl_cdp_url); + } +} + +/** See if a particular serial is present in a CRL list + * + */ +static crl_ret_t crl_check_entry(crl_entry_t *crl_entry, request_t *request, uint8_t const *serial) +{ + X509_REVOKED *revoked; + ASN1_INTEGER *asn1_serial = NULL; + int ret; + + asn1_serial = d2i_ASN1_INTEGER(NULL, (unsigned char const **)&serial, talloc_array_length(serial)); + ret = X509_CRL_get0_by_serial(crl_entry->crl, &revoked, asn1_serial); + ASN1_INTEGER_free(asn1_serial); + switch (ret) { + case 0: + fr_tls_strerror_printf("Failed checking serial number against CRL %s", crl_entry->cdp_url); + RPERROR("Returning fail"); + return CRL_ERROR; + + case 1: + RDEBUG2("Certificate revoked by %s", crl_entry->cdp_url); + return CRL_ENTRY_FOUND; + + case 2: + RDEBUG2("Remove from CRL?"); + return CRL_ENTRY_NOT_FOUND; + } + + return CRL_ERROR; +} + +/** Resolve a cdp_url to a CRL entry, and check serial against it, if it exists + * + */ +static crl_ret_t crl_check_serial(fr_rb_tree_t *crls, request_t *request, char const *cdp_url, uint8_t const *serial) +{ + crl_entry_t *found, find = { .cdp_url = cdp_url}; + + found = fr_rb_find(crls, &find); + if (found == NULL) return CRL_NOT_FOUND; + + return crl_check_entry(found, request, serial); +} + +static int _crl_entry_free(crl_entry_t *crl_entry) +{ + X509_CRL_free(crl_entry->crl); + return 0; +} + +/** Add an entry to the cdp_url -> crl tree + * + * @note Must be called with the mutex held. + */ +static crl_entry_t *crl_entry_create(rlm_crl_t const *inst, fr_timer_list_t *tl, char const *url, uint8_t const *data) +{ + uint8_t const *our_data = data; + crl_entry_t *crl; + + MEM(crl = talloc_zero(inst, crl_entry_t)); + crl->cdp_url = talloc_bstrdup(crl, url); + crl->crl = d2i_X509_CRL(NULL, (const unsigned char **)&our_data, talloc_array_length(our_data)); + if (crl->crl == NULL) { + fr_tls_strerror_printf("Failed to parse CRL from %s", url); + talloc_free(crl); + return NULL; + } + talloc_set_destructor(crl, _crl_entry_free); + fr_timer_in(crl, tl, &crl->ev, inst->force_expiry, false, crl_expire, crl); + crl->ev = NULL; + + return crl; +} + +static unlang_action_t crl_by_url(rlm_rcode_t *p_result, module_ctx_t const *mctx, request_t *request); + +/** Process the response from evaluating the cdp_url -> crl_data expansion + * + * This is the resumption function when we yield to get CRL data associated with a URL + */ +static unlang_action_t crl_process_cdp_data(rlm_rcode_t *p_result, module_ctx_t const *mctx, request_t *request) +{ + rlm_crl_t const *inst = talloc_get_type_abort_const(mctx->mi->data, rlm_crl_t); + rlm_crl_env_t *env = talloc_get_type_abort(mctx->env_data, rlm_crl_env_t); + rlm_crl_rctx_t *rctx = talloc_get_type_abort(mctx->rctx, rlm_crl_rctx_t); + + switch (fr_value_box_list_num_elements(&rctx->crl_data)) { + case 0: + REDEBUG("No CRL data returned, failing"); + fail: + pthread_mutex_unlock(&inst->mutable->mutex); + fr_value_box_list_talloc_free(&rctx->crl_data); + pair_delete_request(attr_crl_cdp_url); + RETURN_MODULE_FAIL; + + case 1: + { + crl_entry_t *crl_entry; + + crl_entry = crl_entry_create(inst, unlang_interpret_event_list(request)->tl, + rctx->cdp_url->vb_strvalue, + fr_value_box_list_head(&rctx->crl_data)->vb_octets); + if (!crl_entry) goto fail; + + switch (crl_check_entry(crl_entry, request, env->serial.vb_octets)) { + case CRL_ENTRY_FOUND: + pthread_mutex_unlock(&inst->mutable->mutex); + RETURN_MODULE_REJECT; + + case CRL_ENTRY_NOT_FOUND: + /* + * We have a CRL, but the serial is not in it. + * check the rest of the CDPs, then return OK. + */ + pthread_mutex_unlock(&inst->mutable->mutex); + fr_value_box_list_talloc_free(&rctx->crl_data); /* Free the raw CRL data */ + pair_delete_request(attr_crl_cdp_url); + return crl_by_url(p_result, mctx, request); + + case CRL_ERROR: + goto fail; + + /* + * This should be return by crl_check_entry because we provided the entry! + */ + case CRL_NOT_FOUND: + fr_assert(0); + goto fail; + } + + } + break; + + default: + REDEBUG("Too many CRL values returned, failing"); + goto fail; + } +} + +static unlang_action_t crl_by_url(rlm_rcode_t *p_result, module_ctx_t const *mctx, request_t *request) +{ + rlm_crl_t const *inst = talloc_get_type_abort_const(mctx->mi->data, rlm_crl_t); + rlm_crl_env_t *env = talloc_get_type_abort(mctx->env_data, rlm_crl_env_t); + rlm_crl_rctx_t *rctx = mctx->rctx; + + if (!rctx) talloc_zero(unlang_interpret_frame_talloc_ctx(request), rlm_crl_rctx_t); + + pthread_mutex_lock(&inst->mutable->mutex); + + /* + * Fast path when we have all the CRLs + */ + while ((rctx->cdp_url = fr_value_box_list_next(env->cdp, rctx->cdp_url))) { + switch (crl_check_serial(inst->mutable->crls, request, + rctx->cdp_url->vb_strvalue, env->serial.vb_octets)) { + case CRL_ENTRY_FOUND: + pthread_mutex_unlock(&inst->mutable->mutex); + RETURN_MODULE_REJECT; + + case CRL_ENTRY_NOT_FOUND: + case CRL_ERROR: + continue; + + /* + * Need to convert the cdp_url to a CRL entry + * + * We yield to an expansion to allow this to happen, then parse the CRL data + * and check if the serial has an entry in the CRL. + */ + case CRL_NOT_FOUND: + { + fr_pair_t *vp; + + fr_value_box_list_init(&rctx->crl_data); + + MEM(pair_append_request(&vp, attr_crl_cdp_url) == 0); + MEM(fr_value_box_copy(vp, &vp->data, rctx->cdp_url) == 0); + + return unlang_module_yield_to_tmpl(rctx, &rctx->crl_data, request, env->exp, + NULL, crl_process_cdp_data, crl_signal, 0, rctx); + } + } + } + + pthread_mutex_unlock(&inst->mutable->mutex); + + RETURN_MODULE_OK; +} + +static int mod_mutable_free(rlm_crl_mutable_t *mutable) +{ + pthread_mutex_destroy(&mutable->mutex); + return 0; +} + +/* + * (Re-)read radiusd.conf into memory. + */ +static int mod_instantiate(module_inst_ctx_t const *mctx) +{ + rlm_crl_t *inst = talloc_get_type_abort(mctx->mi->data, rlm_crl_t); + + MEM(inst->mutable = talloc_zero(inst, rlm_crl_mutable_t)); + MEM(inst->mutable->crls = fr_rb_inline_talloc_alloc(inst->mutable, crl_entry_t, node, crl_cmp, crl_free)); + pthread_mutex_init(&inst->mutable->mutex, NULL); + talloc_set_destructor(inst->mutable, mod_mutable_free); + + return 0; +} + +extern module_rlm_t rlm_crl; +module_rlm_t rlm_crl = { + .common = { + .magic = MODULE_MAGIC_INIT, + .inst_size = sizeof(rlm_crl_t), + .instantiate = mod_instantiate, + .name = "crl", + }, + .method_group = { + .bindings = (module_method_binding_t[]){ + { .section = SECTION_NAME(CF_IDENT_ANY, CF_IDENT_ANY), .method = crl_by_url, .method_env = &crl_env }, + MODULE_BINDING_TERMINATOR + } + } +};