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 | 13 +++++++++++++ doc/stunnel.html.in | 7 +++++++ doc/stunnel.md | 8 ++++++++ src/options.c | 35 +++++++++++++++++++++++++++++++++++ src/prototypes.h | 1 + src/verify.c | 3 +++ 6 files changed, 67 insertions(+) diff --git a/doc/stunnel.8.in b/doc/stunnel.8.in index 4179430..239a402 100644 --- a/doc/stunnel.8.in +++ b/doc/stunnel.8.in @@ -577,6 +577,19 @@ section. This option requires OpenSSL 1.0.2 or later. .RE .IP \[bu] 2 +\f[B]checkPurpose\f[R] = default | any | client | server +.RS 2 +.PP +verify the Extended Key Usage of the end\-entity (leaf) peer certificate +subject +.PP +Certificates are accepted if no subject checks were specified, or the +Extended Key Usage of the end\-entity (leaf) peer certificate matches +the parameters specified with \f[I]checkPurpose\f[R]. +.PP +This option requires OpenSSL 1.0.2 or later. +.RE +.IP \[bu] 2 \f[B]ciphers\f[R] = CIPHER_LIST .RS 2 .PP diff --git a/doc/stunnel.html.in b/doc/stunnel.html.in index 2cf83ca..92d9d4f 100644 --- a/doc/stunnel.html.in +++ b/doc/stunnel.html.in @@ -526,6 +526,13 @@ IP addresses specified with <em>checkIP</em>.</p> <p>Multiple <em>checkIP</em> options are allowed in a single service section.</p> <p>This option requires OpenSSL 1.0.2 or later.</p></li> +<li><p><strong>checkPurpose</strong> = default | any | client | server</p> +<p>verify the Extended Key Usage of the end-entity (leaf) peer certificate +subject</p> +<p>Certificates are accepted if no subject checks were specified, or the +Extended Key Usage of the end-entity (leaf) peer certificate matches +the parameters specified with <em>checkPurpose</em>.</p> +<p>This option requires OpenSSL 1.0.2 or later.</p></li> <li><p><strong>ciphers</strong> = CIPHER_LIST</p> <p>select permitted TLS ciphers (TLSv1.2 and below)</p> <p>This option does not impact TLSv1.3 ciphersuites.</p> diff --git a/doc/stunnel.md b/doc/stunnel.md index 6ad562b..037105c 100644 --- a/doc/stunnel.md +++ b/doc/stunnel.md @@ -372,6 +372,14 @@ Note that if you wish to run **stunnel** in *inetd* mode (where it is provided a This option requires OpenSSL 1.0.2 or later. +- **checkPurpose** = default \| any \| client \| server + + verify the Extended Key Usage of the end\-entity (leaf) peer certificate subject + + Certificates are accepted if no subject checks were specified, or the Extended Key Usage of the end-entity (leaf) peer certificate matches the parameters specified with *checkPurpose*. + + This option requires OpenSSL 1.0.2 or later. + - **checkIP** = IP verify the IP address of the end-entity (leaf) peer certificate subject diff --git a/src/options.c b/src/options.c index 9a7b0aa..482291e 100644 --- a/src/options.c +++ b/src/options.c @@ -1899,6 +1899,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 1a56d3b..0a24eed 100644 --- a/src/prototypes.h +++ b/src/prototypes.h @@ -309,6 +309,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 80d0937..02f8751 100644 --- a/src/verify.c +++ b/src/verify.c @@ -82,6 +82,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