Hello Juan,
Thank you, this looks good to me. I pushed the code in master.
I only made a small changes to the patches to disable the fetch when building
with WolfSSL because the API is not available with it.
Regards,
On Thu, Aug 06, 2026 at 05:07:55PM +0200, Juan Pablo Mora wrote:
> Subject: [PATCH 1/2] MINOR: ssl: add ssl_c_policies sample fetch
> Until now there was no way in HAProxy to inspect the "Certificate
> Policies" X509v3 extension of a client certificate presented during
> mTLS client auth. This is needed to take routing/access decisions
> based on the policy under which the certificate was issued, e.g. to
> tell apart eIDAS qualified certificates whose private key is held in
> a QSCD (policy OID 0.4.0.1862.1.4, id-etsi-qcp-legal-qscd) from other
> client certificates.
>
> This adds ssl_c_policies([<oid>]), following the same extraction
> pattern already used by ssl_c_san (X509_get_ext_d2i() +
> comma-separated list built in a trash chunk):
>
> - with no argument, it returns the full comma-separated list of
> policy OIDs (numeric dotted form) found in the certificate ;
> - with an <oid> argument, it only returns a sample when this
> specific OID is present among the certificate's policies, which
> allows using the "found" match method to take a decision, eg:
>
> acl qualified_qscd ssl_c_policies(0.4.0.1862.1.4) -m found
> http-request deny unless qualified_qscd
>
> doc/configuration.txt is updated accordingly.
>
> This is a pure addition, it does not touch any existing code path.
> Built with -Wall -Wextra -Werror (no warnings) and validated against
> doc/coding-style.txt's checkpatch.pl invocation: clean except for one
> expected hit on the missing space in "ARG1(0,STR)", which matches the
> pre-existing convention used by the other 22 entries of the same
> sample_fetch_keywords table in this file.
> ---
> doc/configuration.txt | 19 +++++++++++
> src/ssl_sample.c | 79 +++++++++++++++++++++++++++++++++++++++++++
> 2 files changed, 98 insertions(+)
>
> diff --git a/doc/configuration.txt b/doc/configuration.txt
> index 0656a7783..06b45b852 100644
> --- a/doc/configuration.txt
> +++ b/doc/configuration.txt
> @@ -25975,6 +25975,7 @@ ssl_c_i_dn([<entry>[,<occ>[,<format>]]])
> string
> ssl_c_key_alg string
> ssl_c_notafter string
> ssl_c_notbefore string
> +ssl_c_policies([<oid>]) string
> ssl_c_r_dn([<entry>[,<occ>[,<format>]]]) string
> ssl_c_s_dn([<entry>[,<occ>[,<format>]]]) string
> ssl_c_san string
> @@ -26376,6 +26377,24 @@ ssl_c_notbefore : string
> YYMMDDhhmmss[Z] when the incoming connection was made over an SSL/TLS
> transport layer.
>
> +ssl_c_policies([<oid>]) : string
> + When the incoming connection was made over an SSL/TLS transport layer, and
> + a client certificate was provided, returns a comma separated list of the
> + OIDs found in the "Certificate Policies" X509v3 extension of this
> + certificate.
> +
> + If the optional <oid> argument is given, in its numeric dotted form (eg:
> + "0.4.0.1862.1.4"), the fetch does not build the list anymore and instead
> + only checks whether this specific policy OID is present among the
> + certificate's policies. In that case it only returns a sample when the OID
> + is found, which makes it convenient to use with the "found" match method to
> + take a decision based on the presence of a given certificate policy.
> +
> + Example:
> +
> + acl qualified_qscd ssl_c_policies(0.4.0.1862.1.4) -m found
> + http-request deny unless qualified_qscd
> +
> ssl_c_r_dn([<entry>[,<occ>[,<format>]]]) : string
> When the incoming connection was made over an SSL/TLS transport layer, and
> is
> successfully validated with the configured ca-file, returns the full
> diff --git a/src/ssl_sample.c b/src/ssl_sample.c
> index f35dc9e18..64135109a 100644
> --- a/src/ssl_sample.c
> +++ b/src/ssl_sample.c
> @@ -1262,6 +1262,84 @@ smp_fetch_ssl_x_san(const struct arg *args, struct
> sample *smp, const char *kw,
> return ret;
> }
>
> +/* string, returns a comma separated list of the OIDs found in the
> + * "Certificate Policies" X509v3 extension of the certificate presented by
> + * the client. If the optional <oid> argument is given, instead of building
> + * the list, it only reports whether this specific policy OID is present,
> + * which allows the "found" match method to be used to take decisions based
> + * on it, eg: "http-request deny unless { ssl_c_policies(0.4.0.1862.1.4) -m
> found }"
> + * (id-etsi-qcp-legal-qscd, i.e. the certificate's private key is held in a
> + * QSCD).
> + */
> +static int
> +smp_fetch_ssl_c_policies(const struct arg *args, struct sample *smp, const
> char *kw, void *private)
> +{
> + CERTIFICATEPOLICIES *policies;
> + X509 *crt = NULL;
> + int ret = 0;
> + int filter = (args[0].type == ARGT_STR && args[0].data.str.data > 0);
> + struct buffer *smp_trash;
> + struct connection *conn;
> + SSL *ssl;
> + int i;
> +
> + conn = objt_conn(smp->sess->origin);
> + ssl = ssl_sock_get_ssl_object(conn);
> + if (!ssl)
> + return 0;
> +
> + if (conn->flags & CO_FL_WAIT_XPRT && !conn->err_code) {
> + smp->flags |= SMP_F_MAY_CHANGE;
> + return 0;
> + }
> +
> + crt = ssl_sock_get_peer_certificate(ssl);
> + if (!crt)
> + goto out;
> +
> + policies = X509_get_ext_d2i(crt, NID_certificate_policies, NULL, NULL);
> + if (!policies)
> + goto out;
> +
> + smp_trash = get_trash_chunk();
> +
> + for (i = 0; i < sk_POLICYINFO_num(policies); i++) {
> + POLICYINFO *policy = sk_POLICYINFO_value(policies, i);
> + char oid_str[128];
> + int len;
> +
> + len = OBJ_obj2txt(oid_str, sizeof(oid_str), policy->policyid,
> 1);
> + if (len <= 0 || len >= sizeof(oid_str))
> + continue;
> +
> + if (filter) {
> + if ((size_t)len != args[0].data.str.data ||
> + memcmp(oid_str, args[0].data.str.area, len) != 0)
> + continue;
> + chunk_appendf(smp_trash, "%s", oid_str);
> + break;
> + }
> +
> + if (smp_trash->data)
> + chunk_appendf(smp_trash, ", ");
> + chunk_appendf(smp_trash, "%s", oid_str);
> + }
> +
> + sk_POLICYINFO_pop_free(policies, POLICYINFO_free);
> +
> + if (smp_trash->data) {
> + smp->flags = SMP_F_VOL_SESS;
> + smp->data.type = SMP_T_STR;
> + smp->data.u.str = *smp_trash;
> + ret = 1;
> + }
> +out:
> + /* SSL_get_peer_certificate, it increase X509 * ref count */
> + if (crt)
> + X509_free(crt);
> + return ret;
> +}
> +
> /* string, returns notbefore date in ASN1_UTCTIME format.
> * The 5th keyword char is used to know if SSL_get_certificate or
> SSL_get_peer_certificate
> * should be use.
> @@ -2752,6 +2830,7 @@ static struct sample_fetch_kw_list
> sample_fetch_keywords = {ILH, {
> { "ssl_c_key_alg", smp_fetch_ssl_x_key_alg, 0,
> NULL, SMP_T_STR, SMP_USE_L5CLI },
> { "ssl_c_notafter", smp_fetch_ssl_x_notafter, 0,
> NULL, SMP_T_STR, SMP_USE_L5CLI },
> { "ssl_c_notbefore", smp_fetch_ssl_x_notbefore, 0,
> NULL, SMP_T_STR, SMP_USE_L5CLI },
> + { "ssl_c_policies", smp_fetch_ssl_c_policies, ARG1(0,STR),
> NULL, SMP_T_STR, SMP_USE_L5CLI },
> #ifdef HAVE_SSL_get0_verified_chain
> { "ssl_c_r_dn", smp_fetch_ssl_r_dn,
> ARG3(0,STR,SINT,STR),val_dnfmt, SMP_T_STR, SMP_USE_L5CLI },
> #endif
> --
> 2.50.1 (Apple Git-155)
>
>
--
William Lallemand