]> git.ipfire.org Git - thirdparty/haproxy.git/commitdiff
MINOR: ssl: add ssl_c_policies sample fetch
authorJuan Pablo Mora <juan.mora.carbonell@gmail.com>
Thu, 6 Aug 2026 15:07:55 +0000 (17:07 +0200)
committerWilliam Lallemand <wlallemand@haproxy.com>
Mon, 10 Aug 2026 13:34:29 +0000 (15:34 +0200)
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
include/haproxy/openssl-compat.h
src/ssl_sample.c

index 1ca4cf34bf1612c4ce3cf6d51db58d223f40666c..2e961e51851c94d20806dfff373c8d0e9b723fc7 100644 (file)
@@ -25978,6 +25978,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
@@ -26379,6 +26380,26 @@ 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.
+
+  Note: not supported by wolfSSL.
+
+  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
index efdfee3435bd330199209382d99f6517a39ab062..8ffeebabaacdea6c3dc266ae4417c2cdaad1a551 100644 (file)
@@ -154,6 +154,11 @@ enum ssl_encryption_level_t {
 #define HAVE_SSL_get0_verified_chain
 #endif
 
+/* wolfSSL does not provide the CERTIFICATEPOLICIES API */
+#if !defined(USE_OPENSSL_WOLFSSL)
+#define HAVE_CERTIFICATEPOLICIES
+#endif
+
 #if defined(SSL_OP_NO_ANTI_REPLAY) || defined(OPENSSL_IS_BORINGSSL) || defined(OPENSSL_IS_AWSLC)
 #define HAVE_SSL_0RTT
 #endif
index 62088db3a040fdc51f53b75c9665d8759ae84f30..4f4070b508a7354f743321e7b4897ce4f36119ae 100644 (file)
@@ -1275,6 +1275,86 @@ out:
        return ret;
 }
 
+#ifdef HAVE_CERTIFICATEPOLICIES
+/* 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;
+}
+#endif /* HAVE_CERTIFICATEPOLICIES */
+
 /* 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.
@@ -2765,6 +2845,9 @@ 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 },
+#ifdef HAVE_CERTIFICATEPOLICIES
+       { "ssl_c_policies",         smp_fetch_ssl_c_policies,     ARG1(0,STR),         NULL,    SMP_T_STR,  SMP_USE_L5CLI },
+#endif
 #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