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); -- 2.53.0