I originally sent this patch out two weeks ago but didn't read properly and so didn't notice it was rejected due to me messing up my email addresses. Oops. On the upside I have some more information about this as the Let's Encrypt change has hit others that I would like to provide as context. (I didn't think it fit what git deems the commit message, and I haven't been around mailinglists enough to know proper etiquette on providing this kind of thing separate from the patch itself) Software generally has hit similar issues, such as ejabberd where XMPP uses the server certificates in much the same way (https://github.com/processone/ejabberd/issues/4392). Over there the issue is basically the same: "I am a server, and I want to authenticate as said server to another server" is met by a verification which checks for the certificate to be a client cert. This overall points at the general issue of s2s authentication using certificates being a valid paradigm, however software often still uses the defaults of the underlying library (here being OpenSSL which defaults to checking for client usage on certificates supplied by a client). Basically what I'm saying is that the approach of making it configurable (either in a library, or an application resembling a tacked on TLS library such as stunnel) is a genuine feature rather than just a workaround, at least in my opinion (and YMMV as always). On 3/17/26 10:42, stunnel@benary.org wrote:
From: benaryorg <binary@benary.org>
What this does in a nutshell is to allow to use client certificates for servers, and server certificates for clients.
This has become a relevant workaround for people using Let's Encrypt certificates for mutual TLS authentication (i.e. checkHost on both sides). Let's Encrypt, due to pressure from Google, have disabled client usage in their certificates, thereby causing issues in the latest renewals.
https://letsencrypt.org/2025/05/14/ending-tls-client-authentication
Any connections initiated by a client using a server certificate as to authenticate against a server cause a "unsuitable certificate purpose" on the server side and a TLS termination on the client side. This patch allows to simply override this check, for better or for worse.
An earlier version was written with `checkPurpose` being allowed to occur multiple times (like `checkHost` and friends). However the value set does not support binary OR (it is simply sequentially numbered).
Signed-off-by: benaryorg <binary@benary.org> --- doc/stunnel.8.in | 7 +++++++ doc/stunnel.html.in | 8 ++++++++ doc/stunnel.pod.in | 8 ++++++++ src/options.c | 35 +++++++++++++++++++++++++++++++++++ src/prototypes.h | 1 + src/verify.c | 3 +++ 6 files changed, 62 insertions(+)
diff --git a/doc/stunnel.8.in b/doc/stunnel.8.in index db02292..4450715 100644 --- a/doc/stunnel.8.in +++ b/doc/stunnel.8.in @@ -460,6 +460,13 @@ addresses specified with \fIcheckIP\fR. Multiple \fIcheckIP\fR options are allowed in a single service section. .Sp This option requires OpenSSL 1.0.2 or later. +.IP "\fBcheckPurpose\fR = default | any | client | server" 4 +.IX Item "\fBcheckPurpose\fR = default | any | client | server" +verify the Extended Key Usage of the peer certificate +.Sp +Certificate verification only succeeds if the certificate purpose, +specifically the Extended Key Usage, matches the parameters specified with +\fIcheckPurpose\fR. .IP "\fBciphers\fR = CIPHER_LIST" 4 .IX Item "ciphers = CIPHER_LIST" select permitted TLS ciphers (TLSv1.2 and below) diff --git a/doc/stunnel.html.in b/doc/stunnel.html.in index 90e8556..6eca48e 100644 --- a/doc/stunnel.html.in +++ b/doc/stunnel.html.in @@ -552,6 +552,14 @@
<p>This option requires OpenSSL 1.0.2 or later.</p>
+</dd> +<dt id="checkPurpose-default-any-client-server"><b>checkPurpose</b> = default | any | client | server</dt> +<dd> + +<p>verify the Extended Key Usage of the peer certificate</p> + +<p>Certificate verification only succeeds if the certificate purpose, specifically the Extended Key Usage, matches the parameters specified with <i>checkPurpose</i>.</p> + </dd> <dt id="ciphers-CIPHER_LIST"><b>ciphers</b> = CIPHER_LIST</dt> <dd> diff --git a/doc/stunnel.pod.in b/doc/stunnel.pod.in index 752359d..977a879 100644 --- a/doc/stunnel.pod.in +++ b/doc/stunnel.pod.in @@ -500,6 +500,14 @@ Multiple I<checkIP> options are allowed in a single service section.
This option requires OpenSSL 1.0.2 or later.
+=item B<checkPurpose> = default | any | client | server + +verify the Extended Key Usage of the peer certificate + +Certificate verification only succeeds if the certificate purpose, +specifically the Extended Key Usage, matches the parameters specified with +I<checkPurpose>. + =item B<ciphers> = CIPHER_LIST
select permitted TLS ciphers (TLSv1.2 and below) diff --git a/src/options.c b/src/options.c index 800d1f3..2ed4111 100644 --- a/src/options.c +++ b/src/options.c @@ -1854,6 +1854,41 @@ NOEXPORT const char *parse_service_option(CMD cmd, SERVICE_OPTIONS **section_ptr break; }
+ /* checkPurpose */ + switch(cmd) { + case CMD_SET_DEFAULTS: + section->check_purpose=X509_PURPOSE_DEFAULT_ANY; + break; + case CMD_SET_COPY: + section->check_purpose=new_service_options.check_purpose; + break; + case CMD_FREE: + break; + case CMD_SET_VALUE: + if(strcasecmp(opt, "checkPurpose")) + break; + if(!strcasecmp(arg, "default")) { + section->check_purpose=X509_PURPOSE_DEFAULT_ANY; + } else if(!strcasecmp(arg, "any")) { + section->check_purpose=X509_PURPOSE_ANY; + } else if(!strcasecmp(arg, "client")) { + section->check_purpose=X509_PURPOSE_SSL_CLIENT; + } else if(!strcasecmp(arg, "server")) { + section->check_purpose=X509_PURPOSE_SSL_SERVER; + } else { + return "The argument needs to be one of: default, any, client, server"; + } + return NULL; /* OK */ + case CMD_INITIALIZE: + break; + case CMD_PRINT_DEFAULTS: + break; + case CMD_PRINT_HELP: + s_log(LOG_NOTICE, "%-22s = client|server check for extended key usage", + "checkPurpose"); + break; + } + #endif /* OPENSSL_VERSION_NUMBER>=0x10002000L */
/* ciphers */ diff --git a/src/prototypes.h b/src/prototypes.h index 1142993..fde3da9 100644 --- a/src/prototypes.h +++ b/src/prototypes.h @@ -303,6 +303,7 @@ struct service_options_struct { NAME_LIST *check_host, *check_email, *check_ip; /* cert subject checks */ NAME_LIST *config; /* OpenSSL CONF options */ #endif /* OPENSSL_VERSION_NUMBER>=0x10002000L */ + int check_purpose; /* Extended Key Usage */
/* service-specific data for ctx.c */ char *cipher_list; diff --git a/src/verify.c b/src/verify.c index 7bab21e..e7dfe9f 100644 --- a/src/verify.c +++ b/src/verify.c @@ -78,6 +78,9 @@ int verify_init(SERVICE_OPTIONS *section) { verify_mode|=SSL_VERIFY_FAIL_IF_NO_PEER_CERT; } SSL_CTX_set_verify(section->ctx, verify_mode, verify_callback); + if(section->check_purpose) { + SSL_CTX_set_purpose(section->ctx, section->check_purpose); + }
auth_warnings(section);