From 31af49d62bb398f43d0e955ebf9a7157c38e2d56 Mon Sep 17 00:00:00 2001 From: Christopher Faulet Date: Tue, 9 Jun 2015 17:29:50 +0200 Subject: [PATCH] MEDIUM: ssl: Add options to forge SSL certificates With this patch, it is possible to configure HAProxy to forge the SSL certificate sent to a client using the SNI servername. We do it in the SNI callback. To enable this feature, you must pass following BIND options: * ca-sign-file : This is the PEM file containing the CA certitifacte and the CA private key to create and sign server's certificates. * (optionally) ca-sign-pass : This is the CA private key passphrase, if any. * generate-certificates: Enable the dynamic generation of certificates for a listener. Because generating certificates is expensive, there is a LRU cache to store them. Its size can be customized by setting the global parameter 'tune.ssl.ssl-ctx-cache-size'. --- doc/configuration.txt | 38 +++++ include/common/defaults.h | 4 + include/proto/ssl_sock.h | 3 +- include/types/global.h | 1 + include/types/listener.h | 7 + src/cfgparse.c | 22 +++ src/haproxy.c | 4 + src/ssl_sock.c | 284 +++++++++++++++++++++++++++++++++++++- 8 files changed, 361 insertions(+), 2 deletions(-) diff --git a/doc/configuration.txt b/doc/configuration.txt index 1e018451dd..897e28500a 100644 --- a/doc/configuration.txt +++ b/doc/configuration.txt @@ -581,6 +581,7 @@ The following keywords are supported in the "global" section : - tune.ssl.force-private-cache - tune.ssl.maxrecord - tune.ssl.default-dh-param + - tune.ssl.ssl-ctx-cache-size - tune.zlib.memlevel - tune.zlib.windowsize @@ -1278,6 +1279,12 @@ tune.ssl.default-dh-param used if static Diffie-Hellman parameters are supplied either directly in the certificate file or by using the ssl-dh-param-file parameter. +tune.ssl.ssl-ctx-cache-size + Sets the size of the cache used to store generated certificates to + entries. This is a LRU cache. Because generating a SSL certificate + dynamically is expensive, they are cached. The default cache size is set to + 1000 entries. + tune.zlib.memlevel Sets the memLevel parameter in zlib initialization for each session. It defines how much memory should be allocated for the internal compression @@ -9039,6 +9046,19 @@ ca-ignore-err [all|,...] If set to 'all', all errors are ignored. SSL handshake is not aborted if an error is ignored. +ca-sign-file + This setting is only available when support for OpenSSL was built in. It + designates a PEM file containing both the CA certificate and the CA private + key used to create and sign server's certificates. This is a mandatory + setting when the dynamic generation of certificates is enabled. See + 'generate-certificates' for details. + +ca-sign-passphrase + This setting is only available when support for OpenSSL was built in. It is + the CA private key passphrase. This setting is optional and used only when + the dynamic generation of certificates is enabled. See + 'generate-certificates' for details. + ciphers This setting is only available when support for OpenSSL was built in. It sets the string describing the list of cipher algorithms ("cipher suite") that are @@ -9164,6 +9184,24 @@ force-tlsv12 this listener. This option is also available on global statement "ssl-default-bind-options". See also "no-tlsv*", and "no-sslv3". +generate-certificates + This setting is only available when support for OpenSSL was built in. It + enables the dynamic SSL certificates generation. A CA certificate and its + private key are necessary (see 'ca-sign-file'). When HAProxy is configured as + a transparent forward proxy, SSL requests generate errors because of a common + name mismatch on the certificate presented to the client. With this option + enabled, HAProxy will try to forge a certificate using the SNI hostname + indicated by the client. This is done only if no certificate matches the SNI + hostname (see 'crt-list'). If an error occurs, the default certificate is + used, else the 'strict-sni' option is set. + It can also be used when HAProxy is configured as a reverse proxy to ease the + deployment of an architecture with many backends. + + Creating a SSL certificate is an expensive operation, so a LRU cache is used + to store forged certificates (see 'tune.ssl.ssl-ctx-cache-size'). It + increases the HAProxy's memroy footprint to reduce latency when the same + certificate is used many times. + gid Sets the group of the UNIX sockets to the designated system gid. It can also be set by default in the global section's "unix-bind" statement. Note that diff --git a/include/common/defaults.h b/include/common/defaults.h index 6193bdc73b..02962010e7 100644 --- a/include/common/defaults.h +++ b/include/common/defaults.h @@ -257,6 +257,10 @@ #define SSL_HANDSHAKE_MAX_COST (76*1024) // measured #endif +#ifndef DEFAULT_SSL_CTX_CACHE +#define DEFAULT_SSL_CTX_CACHE 1000 +#endif + /* approximate stream size (for maxconn estimate) */ #ifndef STREAM_MAX_COST #define STREAM_MAX_COST (sizeof(struct stream) + \ diff --git a/include/proto/ssl_sock.h b/include/proto/ssl_sock.h index 4db516e5ff..7a9e988233 100644 --- a/include/proto/ssl_sock.h +++ b/include/proto/ssl_sock.h @@ -44,10 +44,11 @@ int ssl_sock_is_ssl(struct connection *conn) int ssl_sock_handshake(struct connection *conn, unsigned int flag); int ssl_sock_prepare_ctx(struct bind_conf *bind_conf, SSL_CTX *ctx, struct proxy *proxy); -void ssl_sock_free_certs(struct bind_conf *bind_conf); int ssl_sock_prepare_all_ctx(struct bind_conf *bind_conf, struct proxy *px); int ssl_sock_prepare_srv_ctx(struct server *srv, struct proxy *px); void ssl_sock_free_all_ctx(struct bind_conf *bind_conf); +int ssl_sock_load_ca(struct bind_conf *bind_conf, struct proxy *px); +void ssl_sock_free_ca(struct bind_conf *bind_conf); const char *ssl_sock_get_cipher_name(struct connection *conn); const char *ssl_sock_get_proto_version(struct connection *conn); char *ssl_sock_get_version(struct connection *conn); diff --git a/include/types/global.h b/include/types/global.h index b3b96720c5..2996dda5c1 100644 --- a/include/types/global.h +++ b/include/types/global.h @@ -154,6 +154,7 @@ struct global { unsigned int ssllifetime; /* SSL session lifetime in seconds */ unsigned int ssl_max_record; /* SSL max record size */ unsigned int ssl_default_dh_param; /* SSL maximum DH parameter size */ + int ssl_ctx_cache; /* max number of entries in the ssl_ctx cache. */ #endif #ifdef USE_ZLIB int zlibmemlevel; /* zlib memlevel */ diff --git a/include/types/listener.h b/include/types/listener.h index 895cd00e09..4da6cacb47 100644 --- a/include/types/listener.h +++ b/include/types/listener.h @@ -133,8 +133,15 @@ struct bind_conf { struct eb_root sni_ctx; /* sni_ctx tree of all known certs full-names sorted by name */ struct eb_root sni_w_ctx; /* sni_ctx tree of all known certs wildcards sorted by name */ struct tls_keys_ref *keys_ref; /* TLS ticket keys reference */ + + char *ca_sign_file; /* CAFile used to generate and sign server certificates */ + char *ca_sign_pass; /* CAKey passphrase */ + + X509 *ca_sign_cert; /* CA certificate referenced by ca_file */ + EVP_PKEY *ca_sign_pkey; /* CA private key referenced by ca_key */ #endif int is_ssl; /* SSL is required for these listeners */ + int generate_certs; /* 1 if generate-certificates option is set, else 0 */ unsigned long bind_proc; /* bitmask of processes allowed to use these listeners */ struct { /* UNIX socket permissions */ uid_t uid; /* -1 to leave unchanged */ diff --git a/src/cfgparse.c b/src/cfgparse.c index 30d51c7823..3bfacade83 100644 --- a/src/cfgparse.c +++ b/src/cfgparse.c @@ -770,6 +770,22 @@ int cfg_parse_global(const char *file, int linenum, char **args, int kwm) } } #endif + else if (!strcmp(args[0], "tune.ssl.ssl-ctx-cache-size")) { + if (alertif_too_many_args(1, file, linenum, args, &err_code)) + goto out; + if (*(args[1]) == 0) { + Alert("parsing [%s:%d] : '%s' expects an integer argument.\n", file, linenum, args[0]); + err_code |= ERR_ALERT | ERR_FATAL; + goto out; + } + global.tune.ssl_ctx_cache = atoi(args[1]); + if (global.tune.ssl_ctx_cache < 0) { + Alert("parsing [%s:%d] : '%s' expects a positive numeric value\n", + file, linenum, args[0]); + err_code |= ERR_ALERT | ERR_FATAL; + goto out; + } + } #endif else if (!strcmp(args[0], "tune.buffers.limit")) { if (alertif_too_many_args(1, file, linenum, args, &err_code)) @@ -8219,6 +8235,9 @@ out_uri_auth_compat: /* initialize all certificate contexts */ cfgerr += ssl_sock_prepare_all_ctx(bind_conf, curproxy); + + /* initialize CA variables if the certificates generation is enabled */ + cfgerr += ssl_sock_load_ca(bind_conf, curproxy); } #endif /* USE_OPENSSL */ @@ -8287,8 +8306,11 @@ out_uri_auth_compat: if (bind_conf->is_ssl) continue; #ifdef USE_OPENSSL + ssl_sock_free_ca(bind_conf); ssl_sock_free_all_ctx(bind_conf); free(bind_conf->ca_file); + free(bind_conf->ca_sign_file); + free(bind_conf->ca_sign_pass); free(bind_conf->ciphers); free(bind_conf->ecdhe); free(bind_conf->crl_file); diff --git a/src/haproxy.c b/src/haproxy.c index c73d2e0d66..a17a65dbca 100644 --- a/src/haproxy.c +++ b/src/haproxy.c @@ -161,6 +161,7 @@ struct global global = { #ifdef DEFAULT_SSL_MAX_RECORD .ssl_max_record = DEFAULT_SSL_MAX_RECORD, #endif + .ssl_ctx_cache = DEFAULT_SSL_CTX_CACHE, #endif #ifdef USE_ZLIB .zlibmemlevel = 8, @@ -1456,8 +1457,11 @@ void deinit(void) /* Release unused SSL configs. */ list_for_each_entry_safe(bind_conf, bind_back, &p->conf.bind, by_fe) { #ifdef USE_OPENSSL + ssl_sock_free_ca(bind_conf); ssl_sock_free_all_ctx(bind_conf); free(bind_conf->ca_file); + free(bind_conf->ca_sign_file); + free(bind_conf->ca_sign_pass); free(bind_conf->ciphers); free(bind_conf->ecdhe); free(bind_conf->crl_file); diff --git a/src/ssl_sock.c b/src/ssl_sock.c index 3bd6fa2549..75876a26d9 100644 --- a/src/ssl_sock.c +++ b/src/ssl_sock.c @@ -35,7 +35,7 @@ #include #include #include - +#include #include #include @@ -51,6 +51,9 @@ #include #endif +#include +#include + #include #include #include @@ -75,6 +78,7 @@ #include #include #include +#include #include #include #include @@ -138,6 +142,27 @@ struct certificate_ocsp { long expire; }; +/* X509V3 Extensions that will be added on generated certificates */ +#define X509V3_EXT_SIZE 5 +static char *x509v3_ext_names[X509V3_EXT_SIZE] = { + "basicConstraints", + "nsComment", + "subjectKeyIdentifier", + "authorityKeyIdentifier", + "keyUsage", +}; +static char *x509v3_ext_values[X509V3_EXT_SIZE] = { + "CA:FALSE", + "\"OpenSSL Generated Certificate\"", + "hash", + "keyid,issuer:always", + "nonRepudiation,digitalSignature,keyEncipherment" +}; + +/* LRU cache to store generated certificate */ +static struct lru64_head *ssl_ctx_lru_tree = NULL; +static unsigned int ssl_ctx_lru_seed = 0; + /* * This function returns the number of seconds elapsed * since the Epoch, 1970-01-01 00:00:00 +0000 (UTC) and the @@ -978,6 +1003,134 @@ static int ssl_sock_advertise_alpn_protos(SSL *s, const unsigned char **out, } #endif +static SSL_CTX * +ssl_sock_create_cert(const char *servername, unsigned int serial, X509 *cacert, EVP_PKEY *capkey) +{ + SSL_CTX *ssl_ctx = NULL; + X509 *newcrt = NULL; + EVP_PKEY *pkey = NULL; + RSA *rsa; + X509_NAME *name; + const EVP_MD *digest; + X509V3_CTX ctx; + unsigned int i; + + /* Generate the public key */ + if (!(rsa = RSA_generate_key(2048, 3, NULL, NULL))) + goto mkcert_error; + if (!(pkey = EVP_PKEY_new())) + goto mkcert_error; + if (EVP_PKEY_assign_RSA(pkey, rsa) != 1) + goto mkcert_error; + + /* Create the certificate */ + if (!(newcrt = X509_new())) + goto mkcert_error; + + /* Set version number for the certificate (X509v3) and the serial + * number */ + if (X509_set_version(newcrt, 2L) != 1) + goto mkcert_error; + ASN1_INTEGER_set(X509_get_serialNumber(newcrt), serial); + + /* Set duration for the certificate */ + if (!X509_gmtime_adj(X509_get_notBefore(newcrt), (long)-60*60*24) || + !X509_gmtime_adj(X509_get_notAfter(newcrt),(long)60*60*24*365)) + goto mkcert_error; + + /* set public key in the certificate */ + if (X509_set_pubkey(newcrt, pkey) != 1) + goto mkcert_error; + + /* Set issuer name from the CA */ + if (!(name = X509_get_subject_name(cacert))) + goto mkcert_error; + if (X509_set_issuer_name(newcrt, name) != 1) + goto mkcert_error; + + /* Set the subject name using the same, but the CN */ + name = X509_NAME_dup(name); + if (X509_NAME_add_entry_by_txt(name, "CN", MBSTRING_ASC, + (const unsigned char *)servername, + -1, -1, 0) != 1) { + X509_NAME_free(name); + goto mkcert_error; + } + if (X509_set_subject_name(newcrt, name) != 1) { + X509_NAME_free(name); + goto mkcert_error; + } + X509_NAME_free(name); + + /* Add x509v3 extensions as specified */ + X509V3_set_ctx(&ctx, cacert, newcrt, NULL, NULL, 0); + for (i = 0; i < X509V3_EXT_SIZE; i++) { + X509_EXTENSION *ext; + + if (!(ext = X509V3_EXT_conf(NULL, &ctx, x509v3_ext_names[i], x509v3_ext_values[i]))) + goto mkcert_error; + if (!X509_add_ext(newcrt, ext, -1)) { + X509_EXTENSION_free(ext); + goto mkcert_error; + } + X509_EXTENSION_free(ext); + } + + /* Sign the certificate with the CA private key */ + if (EVP_PKEY_type(capkey->type) == EVP_PKEY_DSA) + digest = EVP_dss1(); + else if (EVP_PKEY_type (capkey->type) == EVP_PKEY_RSA) + digest = EVP_sha256(); + else + goto mkcert_error; + if (!(X509_sign(newcrt, capkey, digest))) + goto mkcert_error; + + /* Create and set the new SSL_CTX */ + if (!(ssl_ctx = SSL_CTX_new(SSLv23_server_method()))) + goto mkcert_error; + if (!SSL_CTX_use_PrivateKey(ssl_ctx, pkey)) + goto mkcert_error; + if (!SSL_CTX_use_certificate(ssl_ctx, newcrt)) + goto mkcert_error; + if (!SSL_CTX_check_private_key(ssl_ctx)) + goto mkcert_error; + + if (newcrt) X509_free(newcrt); + if (pkey) EVP_PKEY_free(pkey); + return ssl_ctx; + + mkcert_error: + if (ssl_ctx) SSL_CTX_free(ssl_ctx); + if (newcrt) X509_free(newcrt); + if (pkey) EVP_PKEY_free(pkey); + return NULL; +} + +static SSL_CTX * +ssl_sock_generate_certificate(const char *servername, struct bind_conf *bind_conf) +{ + X509 *cacert = bind_conf->ca_sign_cert; + EVP_PKEY *capkey = bind_conf->ca_sign_pkey; + SSL_CTX *ssl_ctx = NULL; + struct lru64 *lru = NULL; + unsigned int serial; + + serial = XXH32(servername, strlen(servername), ssl_ctx_lru_seed); + if (ssl_ctx_lru_tree) { + lru = lru64_get(serial, ssl_ctx_lru_tree, cacert, 0); + if (lru && lru->domain) + ssl_ctx = (SSL_CTX *)lru->data; + } + + if (!ssl_ctx) { + ssl_ctx = ssl_sock_create_cert(servername, serial, cacert, capkey); + if (lru) + lru64_commit(lru, ssl_ctx, cacert, 0, (void (*)(void *))SSL_CTX_free); + } + return ssl_ctx; +} + #ifdef SSL_CTRL_SET_TLSEXT_HOSTNAME /* Sets the SSL ctx of to match the advertised server name. Returns a * warning when no match is found, which implies the default (first) cert @@ -1022,6 +1175,14 @@ static int ssl_sock_switchctx_cbk(SSL *ssl, int *al, struct bind_conf *s) node = ebst_lookup(&s->sni_w_ctx, wildp); } if (!node || container_of(node, struct sni_ctx, name)->neg) { + SSL_CTX *ctx; + + if (s->generate_certs && + (ctx = ssl_sock_generate_certificate(servername, s))) { + /* switch ctx */ + SSL_set_SSL_CTX(ssl, ctx); + return SSL_TLSEXT_ERR_OK; + } return (s->strict_sni ? SSL_TLSEXT_ERR_ALERT_FATAL : SSL_TLSEXT_ERR_ALERT_WARNING); @@ -2245,6 +2406,75 @@ void ssl_sock_free_all_ctx(struct bind_conf *bind_conf) bind_conf->default_ctx = NULL; } +/* Load CA cert file and private key used to generate certificates */ +int +ssl_sock_load_ca(struct bind_conf *bind_conf, struct proxy *px) +{ + FILE *fp; + X509 *cacert = NULL; + EVP_PKEY *capkey = NULL; + int err = 0; + + if (!bind_conf || !bind_conf->generate_certs) + return err; + + if (!bind_conf->ca_sign_file) { + Alert("Proxy '%s': cannot enable certificate generation, " + "no CA certificate File configured at [%s:%d].\n", + px->id, bind_conf->file, bind_conf->line); + err++; + } + + if (err) + goto load_error; + + /* read in the CA certificate */ + if (!(fp = fopen(bind_conf->ca_sign_file, "r"))) { + Alert("Proxy '%s': Failed to read CA certificate file '%s' at [%s:%d].\n", + px->id, bind_conf->ca_sign_file, bind_conf->file, bind_conf->line); + err++; + goto load_error; + } + if (!(cacert = PEM_read_X509(fp, NULL, NULL, NULL))) { + Alert("Proxy '%s': Failed to read CA certificate file '%s' at [%s:%d].\n", + px->id, bind_conf->ca_sign_file, bind_conf->file, bind_conf->line); + fclose (fp); + err++; + goto load_error; + } + if (!(capkey = PEM_read_PrivateKey(fp, NULL, NULL, bind_conf->ca_sign_pass))) { + Alert("Proxy '%s': Failed to read CA private key file '%s' at [%s:%d].\n", + px->id, bind_conf->ca_sign_file, bind_conf->file, bind_conf->line); + fclose (fp); + err++; + goto load_error; + } + fclose (fp); + + bind_conf->ca_sign_cert = cacert; + bind_conf->ca_sign_pkey = capkey; + return err; + + load_error: + bind_conf->generate_certs = 0; + if (capkey) EVP_PKEY_free(capkey); + if (cacert) X509_free(cacert); + return err; +} + +/* Release CA cert and private key used to generate certificated */ +void +ssl_sock_free_ca(struct bind_conf *bind_conf) +{ + if (!bind_conf) + return; + + if (bind_conf->ca_sign_pkey) + EVP_PKEY_free(bind_conf->ca_sign_pkey); + if (bind_conf->ca_sign_cert) + X509_free(bind_conf->ca_sign_cert); +} + /* * This function is called if SSL * context is not yet allocated. The function * is designed to be called before any other data-layer operation and sets the @@ -3994,6 +4224,36 @@ static int bind_parse_ca_file(char **args, int cur_arg, struct proxy *px, struct return 0; } +/* parse the "ca-sign-file" bind keyword */ +static int bind_parse_ca_sign_file(char **args, int cur_arg, struct proxy *px, struct bind_conf *conf, char **err) +{ + if (!*args[cur_arg + 1]) { + if (err) + memprintf(err, "'%s' : missing CAfile path", args[cur_arg]); + return ERR_ALERT | ERR_FATAL; + } + + if ((*args[cur_arg + 1] != '/') && global.ca_base) + memprintf(&conf->ca_sign_file, "%s/%s", global.ca_base, args[cur_arg + 1]); + else + memprintf(&conf->ca_sign_file, "%s", args[cur_arg + 1]); + + return 0; +} + +/* parse the ca-sign-pass bind keyword */ + +static int bind_parse_ca_sign_pass(char **args, int cur_arg, struct proxy *px, struct bind_conf *conf, char **err) +{ + if (!*args[cur_arg + 1]) { + if (err) + memprintf(err, "'%s' : missing CAkey password", args[cur_arg]); + return ERR_ALERT | ERR_FATAL; + } + memprintf(&conf->ca_sign_pass, "%s", args[cur_arg + 1]); + return 0; +} + /* parse the "ciphers" bind keyword */ static int bind_parse_ciphers(char **args, int cur_arg, struct proxy *px, struct bind_conf *conf, char **err) { @@ -4326,6 +4586,18 @@ static int bind_parse_ssl(char **args, int cur_arg, struct proxy *px, struct bin return 0; } +/* parse the "generate-certificates" bind keyword */ +static int bind_parse_generate_certs(char **args, int cur_arg, struct proxy *px, struct bind_conf *conf, char **err) +{ +#ifdef SSL_CTRL_SET_TLSEXT_HOSTNAME + conf->generate_certs = 1; +#else + memprintf(err, "%sthis version of openssl cannot generate SSL certificates.\n", + err && *err ? *err : ""); +#endif + return 0; +} + /* parse the "strict-sni" bind keyword */ static int bind_parse_strict_sni(char **args, int cur_arg, struct proxy *px, struct bind_conf *conf, char **err) { @@ -4833,6 +5105,8 @@ static struct bind_kw_list bind_kws = { "SSL", { }, { { "alpn", bind_parse_alpn, 1 }, /* set ALPN supported protocols */ { "ca-file", bind_parse_ca_file, 1 }, /* set CAfile to process verify on client cert */ { "ca-ignore-err", bind_parse_ignore_err, 1 }, /* set error IDs to ignore on verify depth > 0 */ + { "ca-sign-file", bind_parse_ca_sign_file, 1 }, /* set CAFile used to generate and sign server certs */ + { "ca-sign-pass", bind_parse_ca_sign_pass, 1 }, /* set CAKey passphrase */ { "ciphers", bind_parse_ciphers, 1 }, /* set SSL cipher suite */ { "crl-file", bind_parse_crl_file, 1 }, /* set certificat revocation list file use on client cert verify */ { "crt", bind_parse_crt, 1 }, /* load SSL certificates from this location */ @@ -4843,6 +5117,7 @@ static struct bind_kw_list bind_kws = { "SSL", { }, { { "force-tlsv10", bind_parse_force_tlsv10, 0 }, /* force TLSv10 */ { "force-tlsv11", bind_parse_force_tlsv11, 0 }, /* force TLSv11 */ { "force-tlsv12", bind_parse_force_tlsv12, 0 }, /* force TLSv12 */ + { "generate-certificates", bind_parse_generate_certs, 0 }, /* enable the server certificates generation */ { "no-sslv3", bind_parse_no_sslv3, 0 }, /* disable SSLv3 */ { "no-tlsv10", bind_parse_no_tlsv10, 0 }, /* disable TLSv10 */ { "no-tlsv11", bind_parse_no_tlsv11, 0 }, /* disable TLSv11 */ @@ -4953,11 +5228,18 @@ static void __ssl_sock_init(void) #ifndef OPENSSL_NO_DH ssl_dh_ptr_index = SSL_CTX_get_ex_new_index(0, NULL, NULL, NULL, NULL); #endif + + /* Add a global parameter for the LRU cache size */ + if (global.tune.ssl_ctx_cache) + ssl_ctx_lru_tree = lru64_new(global.tune.ssl_ctx_cache); + ssl_ctx_lru_seed = (unsigned int)time(NULL); } __attribute__((destructor)) static void __ssl_sock_deinit(void) { + lru64_destroy(ssl_ctx_lru_tree); + #ifndef OPENSSL_NO_DH if (local_dh_1024) { DH_free(local_dh_1024); -- 2.39.5