MEDIUM: ssl: allow multiple fallback certificate to allow ECDSA/RSA selection

This patch changes the default certificate mechanism.

Since the beginning of SSL in HAProxy, the default certificate was the first
certificate of a bind line. This allowed to fallback on this certificate
when no servername extension was sent by the server, or when no SAN nor
CN was available in the certificate.

When using a multi-certificate bundle (ecdsa+rsa), it was possible to
have both certificates as the fallback one, leting openssl chose the
right one. This was possible because a multi-certificate bundle
was generating a unique SSL_CTX for both certificates.

When the haproxy and openssl architecture evolved, we decided to
use multiple SSL_CTX for a multi-cert bundle, in order to simplify the
code and allow updates over the CLI.

However only one default_ctx was allowed, so we lost the ability to
chose between ECDSA and RSA for the default certificate.

This patch allows to use a '*' filter for a certificate, which allow to
lookup between multiple '*' filter, and have one in RSA and another one
in ECDSA. It replaces the default_ctx mechanism in the ClientHello
callback and use the standard algorithm to look for a default cert and
chose between ECDSA and RSA.

/!\ This patch breaks the automatic setting of the default certificate, which
will be introduce in the next patch. So the first certificate of a bind
line won't be used as a defaullt anymore.

To use this feature, one could use crt-list with '*' filters:

$ cat foo.crtlist
foobar.pem.rsa   *
foobar.pem.ecdsa *

In order to test the feature, it's easy to send a request without
the servername extension and use ECDSA or RSA compatible ciphers:

$ openssl s_client -connect localhost:8443 -tls1_2 -cipher ECDHE-RSA-AES256-GCM-SHA384
$ openssl s_client -connect localhost:8443 -tls1_2 -cipher ECDHE-ECDSA-AES256-GCM-SHA384
This commit is contained in:
William Lallemand 2024-01-10 14:05:59 +01:00
parent 333f2cabab
commit 30592168e5

View File

@ -2346,6 +2346,9 @@ static __maybe_unused struct sni_ctx *ssl_sock_chose_sni_ctx(struct bind_conf *s
break;
}
}
/* if the servername is empty look for the default in the wildcard list */
if (!*servername)
wildp = servername;
/* Look for an ECDSA, RSA and DSA certificate, first in the single
* name and if not found in the wildcard */
@ -2444,7 +2447,8 @@ int ssl_sock_switchctx_cbk(SSL *ssl, int *al, void *arg)
int has_rsa_sig = 0, has_ecdsa_sig = 0;
struct sni_ctx *sni_ctx;
const char *servername;
size_t servername_len;
size_t servername_len = 0;
int default_lookup = 0; /* did we lookup for a default yet? */
int allow_early = 0;
int i;
@ -2532,14 +2536,16 @@ int ssl_sock_switchctx_cbk(SSL *ssl, int *al, void *arg)
goto allow_early;
}
#endif
/* without SNI extension, is the default_ctx (need SSL_TLSEXT_ERR_NOACK) */
if (!s->strict_sni) {
HA_RWLOCK_RDLOCK(SNI_LOCK, &s->sni_lock);
ssl_sock_switchctx_set(ssl, s->default_ctx);
HA_RWLOCK_RDUNLOCK(SNI_LOCK, &s->sni_lock);
goto allow_early;
}
/* no servername field is not compatible with strict-sni */
if (s->strict_sni)
goto abort;
/* without servername extension, look for the defaults which is
* defined by an empty servername string */
servername = "";
servername_len = 0;
default_lookup = 1;
}
/* extract/check clientHello information */
@ -2615,14 +2621,14 @@ int ssl_sock_switchctx_cbk(SSL *ssl, int *al, void *arg)
}
}
sni_lookup:
/* we need to transform this a NULL-ended string in lowecase */
for (i = 0; i < trash.size && i < servername_len; i++)
trash.area[i] = tolower(servername[i]);
trash.area[i] = 0;
servername = trash.area;
HA_RWLOCK_RDLOCK(SNI_LOCK, &s->sni_lock);
sni_ctx = ssl_sock_chose_sni_ctx(s, servername, has_rsa_sig, has_ecdsa_sig);
sni_ctx = ssl_sock_chose_sni_ctx(s, trash.area, has_rsa_sig, has_ecdsa_sig);
if (sni_ctx) {
/* switch ctx */
struct ssl_bind_conf *conf = sni_ctx->conf;
@ -2639,17 +2645,20 @@ int ssl_sock_switchctx_cbk(SSL *ssl, int *al, void *arg)
HA_RWLOCK_RDUNLOCK(SNI_LOCK, &s->sni_lock);
#if (!defined SSL_NO_GENERATE_CERTIFICATES)
if (s->options & BC_O_GENERATE_CERTS && ssl_sock_generate_certificate(servername, s, ssl)) {
if (s->options & BC_O_GENERATE_CERTS && ssl_sock_generate_certificate(trash.area, s, ssl)) {
/* switch ctx done in ssl_sock_generate_certificate */
goto allow_early;
}
#endif
if (!s->strict_sni) {
/* no certificate match, is the default_ctx */
HA_RWLOCK_RDLOCK(SNI_LOCK, &s->sni_lock);
ssl_sock_switchctx_set(ssl, s->default_ctx);
HA_RWLOCK_RDUNLOCK(SNI_LOCK, &s->sni_lock);
goto allow_early;
if (!s->strict_sni && !default_lookup) {
/* we didn't find a SNI, and we didn't look for a default
* look again to find a matching default cert */
servername = "";
servername_len = 0;
default_lookup = 1;
goto sni_lookup;
}
/* We are about to raise an handshake error so the servername extension
@ -2703,6 +2712,7 @@ int ssl_sock_switchctx_cbk(SSL *ssl, int *al, void *priv)
const char *wildp = NULL;
struct ebmb_node *node, *n;
struct bind_conf *s = priv;
int default_lookup = 0; /* did we lookup for a default yet? */
#ifdef USE_QUIC
const uint8_t *extension_data;
size_t extension_len;
@ -2742,12 +2752,15 @@ int ssl_sock_switchctx_cbk(SSL *ssl, int *al, void *priv)
#endif
if (s->strict_sni)
return SSL_TLSEXT_ERR_ALERT_FATAL;
HA_RWLOCK_RDLOCK(SNI_LOCK, &s->sni_lock);
ssl_sock_switchctx_set(ssl, s->default_ctx);
HA_RWLOCK_RDUNLOCK(SNI_LOCK, &s->sni_lock);
return SSL_TLSEXT_ERR_NOACK;
/* without servername extension, look for the defaults which is
* defined by an empty servername string */
servername = "";
default_lookup = 1;
}
sni_lookup:
for (i = 0; i < trash.size; i++) {
if (!servername[i])
break;
@ -2756,6 +2769,8 @@ int ssl_sock_switchctx_cbk(SSL *ssl, int *al, void *priv)
wildp = &trash.area[i];
}
trash.area[i] = 0;
if(!*trash.area) /* handle the default which in wildcard tree */
wildp = trash.area;
HA_RWLOCK_RDLOCK(SNI_LOCK, &s->sni_lock);
node = NULL;
@ -2785,13 +2800,17 @@ int ssl_sock_switchctx_cbk(SSL *ssl, int *al, void *priv)
return SSL_TLSEXT_ERR_OK;
}
#endif
if (s->strict_sni) {
HA_RWLOCK_RDUNLOCK(SNI_LOCK, &s->sni_lock);
return SSL_TLSEXT_ERR_ALERT_FATAL;
if (!s->strict_sni && !default_lookup) {
/* we didn't find a SNI, and we didn't look for a default
* look again to find a matching default cert */
servername = "";
default_lookup = 1;
goto sni_lookup;
}
ssl_sock_switchctx_set(ssl, s->default_ctx);
HA_RWLOCK_RDUNLOCK(SNI_LOCK, &s->sni_lock);
return SSL_TLSEXT_ERR_OK;
return SSL_TLSEXT_ERR_ALERT_FATAL;
}
/* switch ctx */
@ -2814,6 +2833,7 @@ static int ssl_sock_switchctx_wolfSSL_cbk(WOLFSSL* ssl, void* arg)
struct bind_conf *s = arg;
int has_rsa_sig = 0, has_ecdsa_sig = 0;
const char *servername;
int default_lookup = 0;
struct sni_ctx *sni_ctx;
int i;
@ -2825,14 +2845,13 @@ static int ssl_sock_switchctx_wolfSSL_cbk(WOLFSSL* ssl, void* arg)
servername = SSL_get_servername(ssl, TLSEXT_NAMETYPE_host_name);
if (!servername) {
/* without SNI extension, is the default_ctx (need SSL_TLSEXT_ERR_NOACK) */
if (!s->strict_sni) {
HA_RWLOCK_RDLOCK(SNI_LOCK, &s->sni_lock);
ssl_sock_switchctx_set(ssl, s->default_ctx);
HA_RWLOCK_RDUNLOCK(SNI_LOCK, &s->sni_lock);
goto allow_early;
}
if (s->strict_sni)
goto abort;
/* without servername extension, look for the defaults which is
* defined by an empty servername string */
servername = "";
default_lookup = 1;
}
/* extract sigalgs and ciphers */
@ -2876,6 +2895,8 @@ static int ssl_sock_switchctx_wolfSSL_cbk(WOLFSSL* ssl, void* arg)
}
}
sni_lookup:
/* we need to transform this into a NULL-ended string in lowecase */
for (i = 0; i < trash.size && servername[i] != '\0'; i++)
trash.area[i] = tolower(servername[i]);
@ -2897,12 +2918,13 @@ static int ssl_sock_switchctx_wolfSSL_cbk(WOLFSSL* ssl, void* arg)
}
HA_RWLOCK_RDUNLOCK(SNI_LOCK, &s->sni_lock);
if (!s->strict_sni) {
/* no certificate match, is the default_ctx */
HA_RWLOCK_RDLOCK(SNI_LOCK, &s->sni_lock);
ssl_sock_switchctx_set(ssl, s->default_ctx);
HA_RWLOCK_RDUNLOCK(SNI_LOCK, &s->sni_lock);
goto allow_early;
if (!s->strict_sni && !default_lookup) {
/* we didn't find a SNI, and we didn't look for a default
* look again to find a matching default cert */
servername = "";
default_lookup = 1;
goto sni_lookup;
}
/* We are about to raise an handshake error so the servername extension
@ -3316,7 +3338,7 @@ static int ckch_inst_add_cert_sni(SSL_CTX *ctx, struct ckch_inst *ckch_inst,
struct pkey_info kinfo, char *name, int order)
{
struct sni_ctx *sc;
int wild = 0, neg = 0;
int wild = 0, neg = 0, default_crt = 0;
if (*name == '!') {
neg = 1;
@ -3325,11 +3347,14 @@ static int ckch_inst_add_cert_sni(SSL_CTX *ctx, struct ckch_inst *ckch_inst,
if (*name == '*') {
wild = 1;
name++;
/* if this was only a '*' filter, this is a default cert */
if (!*name)
default_crt = 1;
}
/* !* filter is a nop */
if (neg && wild)
return order;
if (*name) {
if (*name || default_crt) {
int j, len;
len = strlen(name);
for (j = 0; j < len && j < trash.size; j++)
@ -3401,14 +3426,6 @@ void ssl_sock_load_cert_sni(struct ckch_inst *ckch_inst, struct bind_conf *bind_
else
ebst_insert(&bind_conf->sni_ctx, &sc0->name);
}
/* replace the default_ctx if required with the instance's ctx. */
if (ckch_inst->is_default) {
SSL_CTX_free(bind_conf->default_ctx);
SSL_CTX_up_ref(ckch_inst->ctx);
bind_conf->default_ctx = ckch_inst->ctx;
bind_conf->default_inst = ckch_inst;
}
}
/*