From: sftcd Date: Fri, 26 Sep 2025 21:23:31 +0000 (+0100) Subject: MINOR: ssl/ech: key management via stats socket X-Git-Tag: v3.3-dev11~8 X-Git-Url: http://git.ipfire.org/gitweb.cgi?a=commitdiff_plain;h=9aacb684cd6671698a281fd29f4a6920243b8e9c;p=thirdparty%2Fhaproxy.git MINOR: ssl/ech: key management via stats socket 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 []" displays all ECH configurations or a specific one. - "add ssl ech " adds a new PEM-formatted ECH configuration. - "set ssl ech " replaces all existing ECH configurations. - "del ssl ech []" removes ECH configurations, optionally filtered by age. --- diff --git a/src/ech.c b/src/ech.c index 786a4e96d..717f18ba1 100644 --- a/src/ech.c +++ b/src/ech.c @@ -17,6 +17,16 @@ #include #include +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 .ech we find in the named @@ -74,6 +84,314 @@ end: return rv; } +/* + * should be in the format "frontend/@:" + * 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 < */ +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 "); + 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 */ +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 "); + 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 [] */ +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 "); + 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 [] : 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 : 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 : replace all ECH configs with that provided", cli_parse_set_ech, NULL, NULL, NULL, ACCESS_EXPERIMENTAL }, + { { "del", "ssl", "ech", NULL }, "del ssl ech [] : 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: