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


Reply via email to