]> git.ipfire.org Git - thirdparty/haproxy.git/commitdiff
MINOR: ssl/ech: key management via stats socket
authorsftcd <stephen.farrell@cs.tcd.ie>
Fri, 26 Sep 2025 21:23:31 +0000 (22:23 +0100)
committerWilliam Lallemand <wlallemand@haproxy.com>
Thu, 30 Oct 2025 09:38:31 +0000 (10:38 +0100)
This patch extends the ECH support by adding runtime CLI commands to
view and modify ECH configurations.

New commands are added to the HAProxy CLI:
- "show ssl ech [<name>]" displays all ECH configurations or a specific
  one.
- "add ssl ech <name> <payload>" adds a new PEM-formatted ECH
  configuration.
- "set ssl ech <name> <payload>" replaces all existing ECH
  configurations.
- "del ssl ech <name> [<age-in-secs>]" removes ECH configurations,
  optionally filtered by age.

src/ech.c

index 786a4e96d40e1625d3bb46fd5118515adadf563c..717f18ba1f715dfde7744ce13bbe17d0585dd8e3 100644 (file)
--- a/src/ech.c
+++ b/src/ech.c
 #include <haproxy/proxy.h>
 #include <haproxy/ssl_sock-t.h>
 
+struct show_ech_ctx {
+       struct proxy *pp;
+       struct bind_conf *b;
+       SSL_CTX *specific_ctx;
+       char *specific_name;
+       enum {
+               SHOW_ECH_ALL = 0,
+               SHOW_ECH_SPECIFIC,
+       } state;                       /* phase of the current dump */
+};
 
 /*
  * load any key files called <name>.ech we find in the named
@@ -74,6 +84,314 @@ end:
        return rv;
 }
 
+/*
+ * <name> should be in the format "frontend/@<filename>:<linenum>"
+ * Example:
+ *   "http1/@haproxy.cfg:1234"
+ *
+ */
+
+/* find a named SSL_CTX, returns 1 if found */
+static int cli_find_ech_specific_ctx(char *name, SSL_CTX **sctx)
+{
+       struct proxy *p;
+       struct bind_conf *bind_conf;
+       char *pname; /* proxy name */
+       char *bname; /* bind_name */
+
+       if (!name || !sctx)
+               return 0;
+
+       for (pname = bname = name; *bname != '\0' && *bname != '/'; bname++)
+               ;
+
+       if (*bname) {
+               *bname = '\0'; /* replace / by '\0' */
+               bname++; /* there's a bind_conf name or id */
+       }
+
+       if (!*pname || !*bname)
+               return 0;
+
+       p = proxy_find_by_name(pname, PR_CAP_FE, 0);
+       if (!p)
+               return 0;
+
+       bind_conf = bind_conf_find_by_name(p, bname);
+       if (!bind_conf)
+               return 0;
+
+       if (bind_conf->initial_ctx) {
+               *sctx = bind_conf->initial_ctx;
+               return 1;
+       }
+       return 0;
+}
+
+/* parsing function for 'show ssl ech [echfile]' */
+static int cli_parse_show_ech(char **args, char *payload,
+                              struct appctx *appctx, void *private)
+{
+       struct show_ech_ctx *ctx = applet_reserve_svcctx(appctx, sizeof(*ctx));
+
+       /* no parameter, shows only file list */
+       if (*args[3]) {
+               SSL_CTX *sctx = NULL;
+               ctx->specific_name = strdup(args[3]);
+
+               if (cli_find_ech_specific_ctx(args[3], &sctx) != 1)
+                       return cli_err(appctx, "'show ssl ech' unable to locate referenced name\n");
+               ctx->specific_ctx = sctx;
+               ctx->state = SHOW_ECH_SPECIFIC;
+               ctx->pp = NULL;
+               ctx->b = NULL;
+       } else {
+               ctx->specific_name = NULL;
+               ctx->specific_ctx = NULL;
+               ctx->pp = proxies_list;
+               ctx->b = NULL;
+               ctx->state = SHOW_ECH_ALL;
+       }
+
+       return 0;
+}
+
+static void cli_print_ech_info(SSL_CTX *ctx, struct buffer *trash)
+{
+       int oi_ind, oi_cnt = 0;
+       OSSL_ECHSTORE *es = NULL;
+       BIO *out = NULL;
+
+       out = BIO_new(BIO_s_mem());
+       if (!out) {
+               chunk_appendf(trash, "error making BIO\n");
+               return;
+       }
+       if ((es = SSL_CTX_get1_echstore(ctx)) == NULL
+           || OSSL_ECHSTORE_num_entries(es, &oi_cnt) != 1) {
+               chunk_appendf(trash, "error accessing ECH store\n");
+               goto end;
+       }
+       if (oi_cnt <= 0)
+               chunk_appendf(trash, "no ECH config\n");
+       for (oi_ind = 0; oi_ind < oi_cnt; oi_ind++) {
+               time_t secs = 0;
+               char *pn = NULL, *ec = NULL;
+               int has_priv, for_retry, returned;
+               struct buffer *tmp = alloc_trash_chunk();
+
+               if (!tmp) {
+                       chunk_appendf(trash, "error making tmp buffer\n");
+                       goto end;
+               }
+               if (OSSL_ECHSTORE_get1_info(es, oi_ind, &secs, &pn, &ec,
+                                           &has_priv, &for_retry) != 1) {
+                       chunk_appendf(trash, "error printing ECH Info\n");
+                       OPENSSL_free(pn); /* just in case */
+                       OPENSSL_free(ec);
+                       goto end;
+               }
+               BIO_printf(out, "ECH entry: %d public_name: %s age: %lld%s\n",
+                          oi_ind, pn, (long long)secs,
+                          has_priv ? " (has private key)" : "");
+               BIO_printf(out, "\t%s\n", ec);
+               OPENSSL_free(pn);
+               OPENSSL_free(ec);
+               returned = BIO_read(out, tmp->area, tmp->size-1);
+               tmp->area[returned] = '\0';
+               chunk_appendf(trash, "\n%s", tmp->area);
+               free_trash_chunk(tmp);
+       }
+end:
+       BIO_free(out);
+       OSSL_ECHSTORE_free(es);
+       return;
+}
+
+/*
+ * Print out ECH details where they (might) exist
+ *
+ * The applet_putchk() calls will emit text to the "stats" socket
+ * which is more or less a command line UI. If that returns a -1
+ * then we should break off processing to allow other threads to
+ * do stuff. That's why all the "goto end" stuff and why the code
+ * is kind of re-entrant.
+ */
+
+static int cli_io_handler_ech_details(struct appctx *appctx)
+{
+       struct buffer *trash = get_trash_chunk();
+       struct show_ech_ctx *ctx = appctx->svcctx;
+       int ret = 0;
+       struct proxy *p;
+       struct bind_conf *bind_conf;
+       if (!ctx) return 1;
+
+       if (ctx->state == SHOW_ECH_SPECIFIC) {
+               chunk_appendf(trash, "***\nECH for %s ", ctx->specific_name);
+               cli_print_ech_info(ctx->specific_ctx, trash);
+               if (applet_putchk(appctx, trash) == -1)
+                       return 0;
+               return 1;
+       }
+
+       if (ctx->state == SHOW_ECH_ALL) {
+
+               bind_conf = ctx->b;
+               p = ctx->pp;
+
+               for (; p; p = p->next) {
+
+                       if (!(p->cap & PR_CAP_FE) || LIST_ISEMPTY(&p->conf.bind))
+                               continue;
+
+                       if (!bind_conf) {
+                               bind_conf = LIST_ELEM(p->conf.bind.n, typeof(bind_conf), by_fe);
+                               chunk_appendf(trash, "***\nfrontend: %s\n", bind_conf->frontend->id);
+                       }
+
+                       /* loop on binds */
+                       list_for_each_entry_from(bind_conf, &p->conf.bind, by_fe) {
+
+                               if (bind_conf->initial_ctx) {
+                                       /* print stuff */
+
+                                       chunk_appendf(trash, "\nbind: %s/@%s:%d\n", bind_conf->frontend->id, bind_conf->file, bind_conf->line);
+                                       cli_print_ech_info(bind_conf->initial_ctx, trash);
+                                       if (applet_putchk(appctx, trash) == -1) {
+                                               goto end;
+                                       }
+                               }
+                       }
+                       bind_conf =  NULL;
+               }
+               p = NULL;
+               ret = 1; /* we're all done */
+       }
+
+end:
+       ctx->pp = p;
+       ctx->b = bind_conf;
+       return ret;
+}
+
+#define ECH_SUCCESS_MSG_MAX 256
+
+/*
+ * For the add and set commands below one needs to provide the ECH PEM file
+ * content on the command line. That can be done via:
+ *
+ *          $ openssl ech -public_name htest.com -pemout htest.pem
+ *          $ echo -e "add ssl ech ECH-front <<EOF\n$(cat htest.pem)\nEOF\n" | socat /tmp/haproxy.sock -
+ *          added a new ECH config to ECH-front
+ *
+ */
+
+/* add ssl ech <name> <pemesni> */
+static int cli_parse_add_ech(char **args, char *payload, struct appctx *appctx, void *private)
+{
+       SSL_CTX *sctx = NULL;
+       char success_message[ECH_SUCCESS_MSG_MAX];
+       OSSL_ECHSTORE *es = NULL;
+       BIO *es_in = NULL;
+
+       if (!*args[3] || !payload)
+               return cli_err(appctx, "syntax: add ssl ech <name> <PEM file content>");
+       if (cli_find_ech_specific_ctx(args[3], &sctx) != 1)
+               return cli_err(appctx, "'add ssl ech' unable to locate referenced name\n");
+       if ((es_in = BIO_new_mem_buf(payload, strlen(payload))) == NULL
+           || (es = SSL_CTX_get1_echstore(sctx)) == NULL
+           || OSSL_ECHSTORE_read_pem(es, es_in, OSSL_ECH_FOR_RETRY) != 1
+           || SSL_CTX_set1_echstore(sctx, es) != 1) {
+               OSSL_ECHSTORE_free(es);
+               BIO_free_all(es_in);
+               return cli_err(appctx, "'add ssl ech' error adding provided PEM ECH value\n");
+       }
+       OSSL_ECHSTORE_free(es);
+       BIO_free_all(es_in);
+       snprintf(success_message, ECH_SUCCESS_MSG_MAX,
+                "added a new ECH config to %s", args[3]);
+       return cli_msg(appctx, LOG_INFO, success_message);
+}
+
+/* set ssl ech <name> <pemesni> */
+static int cli_parse_set_ech(char **args, char *payload, struct appctx *appctx, void *private)
+{
+       SSL_CTX *sctx = NULL;
+       char success_message[ECH_SUCCESS_MSG_MAX];
+       OSSL_ECHSTORE *es = NULL;
+       BIO *es_in = NULL;
+
+       if (!*args[3] || !payload)
+               return cli_err(appctx, "syntax: set ssl ech <name> <PEM file content>");
+       if (cli_find_ech_specific_ctx(args[3], &sctx) != 1)
+               return cli_err(appctx, "'set ssl ech' unable to locate referenced name\n");
+       if ((es_in = BIO_new_mem_buf(payload, strlen(payload))) == NULL
+           || (es = OSSL_ECHSTORE_new(NULL, NULL)) == NULL
+           || OSSL_ECHSTORE_read_pem(es, es_in, OSSL_ECH_FOR_RETRY) != 1
+           || SSL_CTX_set1_echstore(sctx, es) != 1) {
+               OSSL_ECHSTORE_free(es);
+               BIO_free_all(es_in);
+               return cli_err(appctx, "'set ssl ech' error adding provided PEM ECH value\n");
+       }
+       OSSL_ECHSTORE_free(es);
+       BIO_free_all(es_in);
+       snprintf(success_message, ECH_SUCCESS_MSG_MAX,
+                "set new ECH configs for %s", args[3]);
+       return cli_msg(appctx, LOG_INFO, success_message);
+}
+
+/* del ssl ech <name> [<age-in-secs>] */
+static int cli_parse_del_ech(char **args, char *payload, struct appctx *appctx, void *private)
+{
+       SSL_CTX *sctx = NULL;
+       time_t age = 0;
+       char success_message[ECH_SUCCESS_MSG_MAX];
+       OSSL_ECHSTORE *es = NULL;
+
+       if (!*args[3])
+               return cli_err(appctx, "syntax: del ssl ech <name>");
+       if (*args[4])
+               age = atoi(args[4]);
+       if (cli_find_ech_specific_ctx(args[3], &sctx) != 1)
+               return cli_err(appctx, "'del ssl ech' unable to locate referenced name\n");
+       if ((es = SSL_CTX_get1_echstore(sctx)) == NULL
+           || OSSL_ECHSTORE_flush_keys(es, age) != 1
+           || SSL_CTX_set1_echstore(sctx, es) != 1) {
+               OSSL_ECHSTORE_free(es);
+               return cli_err(appctx, "'del ssl ech' error removing old ECH values\n");
+       }
+       OSSL_ECHSTORE_free(es);
+       memset(success_message, 0, ECH_SUCCESS_MSG_MAX);
+       if (!age)
+               snprintf(success_message, ECH_SUCCESS_MSG_MAX,
+                        "deleted all ECH configs from %s", args[3]);
+       else
+               snprintf(success_message, ECH_SUCCESS_MSG_MAX,
+                        "deleted ECH configs older than %ld seconds from %s", age, args[3]);
+       return cli_msg(appctx, LOG_INFO, success_message);
+}
+
+
+static void cli_release_ech(struct appctx *appctx)
+{
+       struct show_ech_ctx *ctx = appctx->svcctx;
+
+       ha_free(&ctx->specific_name);
+}
+
+
+static struct cli_kw_list cli_kws = {{ },{
+    { { "show", "ssl", "ech", NULL},  "show ssl ech [<name>]                   : display a named ECH configuation or all",      cli_parse_show_ech, cli_io_handler_ech_details, cli_release_ech, NULL, ACCESS_EXPERIMENTAL },
+    { { "add", "ssl", "ech", NULL },  "add ssl ech <name> <payload>            : add a new PEM-formatted ECH config and key ",  cli_parse_add_ech, NULL, NULL, NULL, ACCESS_EXPERIMENTAL },
+    { { "set", "ssl", "ech", NULL },  "set ssl ech <name> <payload>            : replace all ECH configs with that provided",   cli_parse_set_ech, NULL, NULL, NULL, ACCESS_EXPERIMENTAL },
+    { { "del", "ssl", "ech", NULL },  "del ssl ech <name> [<age-in-secs>]      : delete ECH configs",                           cli_parse_del_ech, NULL, NULL, NULL, ACCESS_EXPERIMENTAL },
+    { { NULL }, NULL, NULL, NULL, NULL },
+
+}};
+
+INITCALL1(STG_REGISTER, cli_register_kw, &cli_kws);
+
 /*
  * Place an ECH status string into a trash buffer
  * ECH status string examples: