Repository: randombit/botan
Pull request #2367 (2020-06-09 13:44:51)

Fix various issues with certificate handling
All were reported by Mario Korth of Ruhr-Universität Bochum.


Patch:
diff --git a/src/bogo_shim/bogo_shim.cpp b/src/bogo_shim/bogo_shim.cpp
index 5a9c36076..5fd1f598f 100644
--- a/src/bogo_shim/bogo_shim.cpp
+++ b/src/bogo_shim/bogo_shim.cpp
@@ -89,6 +89,7 @@ std::string map_to_bogo_error(const std::string& e)
          { "Can't interleave application and handshake data", ":UNEXPECTED_RECORD:" },
          { "Certificate chain exceeds policy specified maximum size", ":EXCESSIVE_MESSAGE_SIZE:" },
          { "Certificate key type did not match ciphersuite", ":WRONG_CERTIFICATE_TYPE:" },
+         { "Certificate usage constraints do not allow this ciphersuite", ":KEY_USAGE_BIT_INCORRECT:" },
          { "Certificate: Message malformed", ":DECODE_ERROR:" },
          { "Channel::key_material_export cannot export during renegotiation", "failed to export keying material" },
          { "Client cert verify failed", ":BAD_SIGNATURE:" },
@@ -560,7 +561,7 @@ std::unique_ptr<Shim_Arguments> parse_options(char* argv[])
       "enable-grease",
       "enable-ocsp-stapling",
       "enable-signed-cert-timestamps",
-      //"enforce-rsa-key-usage",
+      "enforce-rsa-key-usage",
       //"expect-accept-early-data",
       "expect-extended-master-secret",
       "expect-no-offer-early-data",
diff --git a/src/bogo_shim/config.json b/src/bogo_shim/config.json
index ce4b28352..2ac81e969 100644
--- a/src/bogo_shim/config.json
+++ b/src/bogo_shim/config.json
@@ -123,7 +123,6 @@
         "PartialClientFinishedWithClientHello": "Need to check for buffered messages when CCS (bug)",
         "SendUnencryptedFinished-DTLS": "Need to check for buffered messages when CCS (bug)",
 
-        "ECDSAKeyUsage-TLS12": "Botan ignores KeyUsage (bug)",
-        "RSAKeyUsage-*": "Botan ignores KeyUsage (bug)"
+        "RSAKeyUsage-*-UnenforcedTLS*": "We always enforce key usage"
     }
 }
diff --git a/src/lib/asn1/alg_id.cpp b/src/lib/asn1/alg_id.cpp
index 4839303db..b564bf1fa 100644
--- a/src/lib/asn1/alg_id.cpp
+++ b/src/lib/asn1/alg_id.cpp
@@ -57,35 +57,28 @@ AlgorithmIdentifier::AlgorithmIdentifier(const std::string& alg_id,
       parameters.assign(DER_NULL, DER_NULL + 2);
    }
 
-/*
-* Compare two AlgorithmIdentifiers
-*/
-namespace {
-
-bool param_null_or_empty(const std::vector<uint8_t>& p)
+bool AlgorithmIdentifier::parameters_are_null() const
    {
-   if(p.size() == 2 && (p[0] == 0x05) && (p[1] == 0x00))
-      return true;
-   return p.empty();
+   return (parameters.size() == 2 && (parameters[0] == 0x05) && (parameters[1] == 0x00));
    }
 
-}
-
 bool operator==(const AlgorithmIdentifier& a1, const AlgorithmIdentifier& a2)
    {
    if(a1.get_oid() != a2.get_oid())
       return false;
 
-   if(param_null_or_empty(a1.get_parameters()) &&
-      param_null_or_empty(a2.get_parameters()))
+   /*
+   * Treat NULL and empty as equivalent
+   */
+   if(a1.parameters_are_null_or_empty() &&
+      a2.parameters_are_null_or_empty())
+      {
       return true;
+      }
 
    return (a1.get_parameters() == a2.get_parameters());
    }
 
-/*
-* Compare two AlgorithmIdentifiers
-*/
 bool operator!=(const AlgorithmIdentifier& a1, const AlgorithmIdentifier& a2)
    {
    return !(a1 == a2);
diff --git a/src/lib/asn1/alg_id.h b/src/lib/asn1/alg_id.h
index dc9ca2398..55e533d66 100644
--- a/src/lib/asn1/alg_id.h
+++ b/src/lib/asn1/alg_id.h
@@ -37,6 +37,14 @@ class BOTAN_PUBLIC_API(2,0) AlgorithmIdentifier final : public ASN1_Object
       const OID& get_oid() const { return oid; }
       const std::vector<uint8_t>& get_parameters() const { return parameters; }
 
+      bool parameters_are_null() const;
+      bool parameters_are_empty() const { return parameters.empty(); }
+
+      bool parameters_are_null_or_empty() const
+         {
+         return parameters_are_empty() || parameters_are_null();
+         }
+
    BOTAN_DEPRECATED_PUBLIC_MEMBER_VARIABLES:
       /*
       * These values are public for historical reasons, but in a future release
diff --git a/src/lib/tls/msg_certificate.cpp b/src/lib/tls/msg_certificate.cpp
index 1ec0766ce..25aeeefd2 100644
--- a/src/lib/tls/msg_certificate.cpp
+++ b/src/lib/tls/msg_certificate.cpp
@@ -1,12 +1,14 @@
 /*
 * Certificate Message
-* (C) 2004-2006,2012 Jack Lloyd
+* (C) 2004-2006,2012,2020 Jack Lloyd
 *
 * Botan is released under the Simplified BSD License (see license.txt)
 */
 
 #include <botan/tls_messages.h>
 #include <botan/tls_extensions.h>
+#include <botan/tls_exceptn.h>
+#include <botan/tls_alert.h>
 #include <botan/internal/tls_reader.h>
 #include <botan/internal/tls_handshake_io.h>
 #include <botan/internal/tls_handshake_hash.h>
@@ -62,6 +64,19 @@ Certificate::Certificate(const std::vector<uint8_t>& buf, const Policy& policy)
 
       certs += cert_size + 3;
       }
+
+   /*
+   * TLS 1.0 through 1.2 all seem to require that the certificate be
+   * precisely a v3 certificate. In fact the strict wording would seem
+   * to require that every certificate in the chain be v3. But often
+   * the intermediates are outside of the control of the server.
+   * But, require that the leaf certificate be v3
+   */
+   if(m_certs.size() > 0 && m_certs[0].x509_version() != 3)
+      {
+      throw TLS_Exception(Alert::BAD_CERTIFICATE,
+                          "The leaf certificate must be v3");
+      }
    }
 
 /**
diff --git a/src/lib/tls/tls_client.cpp b/src/lib/tls/tls_client.cpp
index 440dfb6c2..9354c4c4d 100644
--- a/src/lib/tls/tls_client.cpp
+++ b/src/lib/tls/tls_client.cpp
@@ -221,6 +221,26 @@ void Client::send_client_hello(Handshake_State& state_base,
    secure_renegotiation_check(state.client_hello());
    }
 
+namespace {
+
+bool key_usage_matches_ciphersuite(Key_Constraints usage,
+                                   Ciphersuite suite)
+   {
+   if(usage == NO_CONSTRAINTS)
+      return true; // anything goes ...
+
+   if(suite.kex_method() == Kex_Algo::STATIC_RSA)
+      {
+      return (usage & KEY_ENCIPHERMENT) | (usage & DATA_ENCIPHERMENT);
+      }
+   else
+      {
+      return (usage & DIGITAL_SIGNATURE) | (usage & NON_REPUDIATION);
+      }
+   }
+
+}
+
 /*
 * Process a handshake message
 */
@@ -507,6 +527,10 @@ void Client::process_handshake_msg(const Handshake_State* active_state,
          throw TLS_Exception(Alert::ILLEGAL_PARAMETER,
                              "Certificate key type did not match ciphersuite");
 
+      if(!key_usage_matches_ciphersuite(server_cert.constraints(), state.ciphersuite()))
+         throw TLS_Exception(Alert::BAD_CERTIFICATE,
+                             "Certificate usage constraints do not allow this ciphersuite");
+
       state.server_public_key.reset(peer_key.release());
 
       if(state.ciphersuite().kex_method() != Kex_Algo::STATIC_RSA)
diff --git a/src/lib/x509/asn1_alt_name.cpp b/src/lib/x509/asn1_alt_name.cpp
index 1e5611c8b..e0437b4a9 100644
--- a/src/lib/x509/asn1_alt_name.cpp
+++ b/src/lib/x509/asn1_alt_name.cpp
@@ -105,6 +105,20 @@ std::vector<std::string> AlternativeName::get_attribute(const std::string& attr)
    return results;
    }
 
+X509_DN AlternativeName::dn() const
+   {
+   X509_DN dn;
+   auto range = m_alt_info.equal_range("DN");
+
+   for(auto i = range.first; i != range.second; ++i)
+      {
+      std::istringstream strm(i->second);
+      strm >> dn;
+      }
+
+   return dn;
+   }
+
 /*
 * Return if this object has anything useful
 */
diff --git a/src/lib/x509/asn1_alt_name.h b/src/lib/x509/asn1_alt_name.h
index 83ac215ba..d35383815 100644
--- a/src/lib/x509/asn1_alt_name.h
+++ b/src/lib/x509/asn1_alt_name.h
@@ -11,6 +11,7 @@
 #include <botan/asn1_obj.h>
 #include <botan/asn1_str.h>
 #include <botan/asn1_oid.h>
+#include <botan/x509_dn.h>
 #include <map>
 
 namespace Botan {
@@ -44,6 +45,8 @@ class BOTAN_PUBLIC_API(2,0) AlternativeName final : public ASN1_Object
          return m_othernames;
          }
 
+      X509_DN dn() const;
+
       bool has_items() const;
 
       AlternativeName(const std::string& email_addr = "",
diff --git a/src/lib/x509/name_constraint.cpp b/src/lib/x509/name_constraint.cpp
index ab654ae5e..940c14aea 100644
--- a/src/lib/x509/name_constraint.cpp
+++ b/src/lib/x509/name_constraint.cpp
@@ -115,9 +115,13 @@ GeneralName::MatchResult GeneralName::matches(const X509_Certificate& cert) cons
       {
       match_fn = std::mem_fn(&GeneralName::matches_dn);
 
-      std::stringstream ss;
-      ss << dn;
-      nam.push_back(ss.str());
+      nam.push_back(dn.to_string());
+
+      const auto alt_dn = alt_name.dn();
+      if(alt_dn.empty() == false)
+         {
+         nam.push_back(alt_dn.to_string());
+         }
       }
    else if(type() == "IP")
       {
diff --git a/src/lib/x509/x509_obj.cpp b/src/lib/x509/x509_obj.cpp
index 644a1ca5e..2bb67629a 100644
--- a/src/lib/x509/x509_obj.cpp
+++ b/src/lib/x509/x509_obj.cpp
@@ -1,6 +1,6 @@
 /*
 * X.509 SIGNED Object
-* (C) 1999-2007 Jack Lloyd
+* (C) 1999-2007,2020 Jack Lloyd
 *
 * Botan is released under the Simplified BSD License (see license.txt)
 */
@@ -189,10 +189,11 @@ Certificate_Status_Code X509_Object::verify_signature(const Public_Key& pub_key)
    if(sig_info.size() < 1 || sig_info.size() > 2 || sig_info[0] != pub_key.algo_name())
       return Certificate_Status_Code::SIGNATURE_ALGO_BAD_PARAMS;
 
+   const std::string pub_key_algo = sig_info[0];
    std::string padding;
    if(sig_info.size() == 2)
       padding = sig_info[1];
-   else if(sig_info[0] == "Ed25519" || sig_info[0] == "XMSS")
+   else if(pub_key_algo == "Ed25519" || pub_key_algo == "XMSS")
       padding = "Pure";
    else
       return Certificate_Status_Code::SIGNATURE_ALGO_BAD_PARAMS;
@@ -240,6 +241,40 @@ Certificate_Status_Code X509_Object::verify_signature(const Public_Key& pub_key)
 
       padding += "(" + hash_algo + "," + mgf_algo + "," + std::to_string(pss_parameter.salt_len) + ")";
       }
+   else
+      {
+      /*
+      * For all other signature types the signature parameters should
+      * be either NULL or empty. In theory there is some distinction between
+      * these but in practice they seem to be used somewhat interchangeably.
+      *
+      * The various RFCs all have prescriptions of what is allowed:
+      * RSA - NULL (RFC 3279)
+      * DSA - empty (RFC 3279)
+      * ECDSA - empty (RFC 3279)
+      * GOST - empty (RFC 4491)
+      * Ed25519 - empty (RFC 8410)
+      * XMSS - empty (draft-vangeest-x509-hash-sigs)
+      *
+      * But in practice we find RSA with empty and ECDSA will NULL all
+      * over the place so it's not really possible to enforce. For Ed25519
+      * and XMSS because they are new we attempt to enforce.
+      */
+      if(pub_key_algo == "Ed25519" || pub_key_algo == "XMSS")
+         {
+         if(!signature_algorithm().parameters_are_empty())
+            {
+            return Certificate_Status_Code::SIGNATURE_ALGO_BAD_PARAMS;
+            }
+         }
+      else
+         {
+         if(!signature_algorithm().parameters_are_null_or_empty())
+            {
+            return Certificate_Status_Code::SIGNATURE_ALGO_BAD_PARAMS;
+            }
+         }
+      }
 
    try
       {
diff --git a/src/lib/x509/x509cert.cpp b/src/lib/x509/x509cert.cpp
index 579613c5d..c1ad38dce 100644
--- a/src/lib/x509/x509cert.cpp
+++ b/src/lib/x509/x509cert.cpp
@@ -215,6 +215,14 @@ std::unique_ptr<X509_Certificate_Data> parse_x509_cert_body(const X509_Object& o
    if(auto ext = data->m_v3_extensions.get_extension_object_as<Cert_Extension::Key_Usage>())
       {
       data->m_key_constraints = ext->get_constraints();
+      /*
+      RFC 5280: When the keyUsage extension appears in a certificate,
+      at least one of the bits MUST be set to 1.
+      */
+      if(data->m_key_constraints == NO_CONSTRAINTS)
+         {
+         throw Decoding_Error("Certificate has invalid encoding for KeyUsage");
+         }
       }
    else
       {


Test patch:
diff --git a/src/tests/data/x509/name_constraint_san/int.pem b/src/tests/data/x509/name_constraint_san/int.pem
new file mode 100644
index 000000000..7ad42865b
--- /dev/null
+++ b/src/tests/data/x509/name_constraint_san/int.pem
@@ -0,0 +1,3 @@
+-----BEGIN CERTIFICATE-----
+MIIDozCCAougAwIBAgIBfzANBgkqhkiG9w0BAQsFADBEMQswCQYDVQQGEwJERTEMMAoGA1UECgwDUlVCMScwJQYDVQQLDB5UTFMtU2Nhbm5lciBDQ0EgUlNBIFJPT1QtQ0EgdjMwHhcNMTkxMjEyMDAwMDAwWhcNMjAxMjE5MDAwMDAwWjBvMQswCQYDVQQGEwJERTEMMAoGA1UECgwDUlVCMVIwUAYDVQQLDElUTFMtU2Nhbm5lciBDQ0EgQ0EgMSB2MyAoUk9PVHYzX0NBdjNfTmFtZUNvbnN0cmFpbnRzX0xFQUZfUlNBdjNfU0FOMkNyaXQpMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA0pCd6NhAKvY5R1RF55ZddAZJiSORJJhOrRlS754HS/9SFM/owFq/GNlOIA0VRx1TYZafS17EgY3CSxefo5CwzJjED/Gx9WEYKUGdLN0LkWYidHL/9QwoH28nvulpfZul367+J+u+oEmNHJOfvyqDAfI6ibtbzxPzxez6kbBGG4g4yDuHT31IIONF+P/p9pbIOorf6W5ZZgtyRkT4n4qdDTniq2ChIqTuTr/n+Hd6F0JogY/lmTFSQaYB3rOyHAcMepvSaK1ffJmp/zfyv0SDV5VOLJTxK7ZwPblLLWpqC+Thav5H+/kdDdvzk9aGKT1DtnAiPR6bnctb5/CaJNMmqwIDAQABo3UwczAKBgNVHQ4EAwQBAjAPBgNVHSMBAQAEBTADgAEBMA8GA1UdDwEB/wQFAwMHBAAwEgYDVR0TAQH/BAgwBgEB/wIBATAvBgNVHR4BAf8EJTAjoCEwH6QdMBsxCzAJBgNVBAYTAkRFMQwwCgYDVQQKDANSVUIwDQYJKoZIhvcNAQELBQADggEBAKxbvphFG87jRc/bo4mZVVM5jVwDcbR4PK59qUTAI8XP7NXLCJaUlQvsTgBv84NJXQgdcMTEQe2VCgOfxuBA48pcak4uJleXrTfbqZ5tDuYHficUTwFcwGNG4f4cYspQerJ4H9dFiA8vpk5QiIdfImruCNh85qD79/1iR1H5RftmQOW9AyOvpA5zA11zOLPlRo62oLIS9U/d6T7EmKeGq+o+ZsSCm/eaOSTjXksJu7bHzzCK8+9TdCevVE5p7Uo1S0KKMh/4F59jwFnuO1ev806EbM17DSez1NsB15Wf5gr4tCZJwIi42rEEobyZyLvUMNBs/44mjLGiFv7a9+LPlg8=
+-----END CERTIFICATE-----
diff --git a/src/tests/data/x509/name_constraint_san/leaf.pem b/src/tests/data/x509/name_constraint_san/leaf.pem
new file mode 100644
index 000000000..5f7597b9a
--- /dev/null
+++ b/src/tests/data/x509/name_constraint_san/leaf.pem
@@ -0,0 +1,3 @@
+-----BEGIN CERTIFICATE-----
+MIIEKzCCAxOgAwIBAgIBfjANBgkqhkiG9w0BAQsFADBvMQswCQYDVQQGEwJERTEMMAoGA1UECgwDUlVCMVIwUAYDVQQLDElUTFMtU2Nhbm5lciBDQ0EgQ0EgMSB2MyAoUk9PVHYzX0NBdjNfTmFtZUNvbnN0cmFpbnRzX0xFQUZfUlNBdjNfU0FOMkNyaXQpMB4XDTE5MTIxMjAwMDAwMFoXDTIwMTIxOTAwMDAwMFoweDELMAkGA1UEBhMCREUxDDAKBgNVBAoMA1JVQjFbMFkGA1UECwxSVExTLVNjYW5uZXIgQ0NBIExlYWYgQ2VydGlmaWNhdGUgKFJPT1R2M19DQXYzX05hbWVDb25zdHJhaW50c19MRUFGX1JTQXYzX1NBTjJDcml0KTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAM7lPeVQmwwIr/t3sB7+lZJwX6SDFJGN90vbfNYYen9vtCJWcyHy42nt3wJMBppcMu3POV4hrXdbji3dwkhY1qiE2Dw55znjR0W3z6MbSAxOL4x4X6AuidIHymk35eHXmeyxVG62YDtwW2sChOxIIbYfMkdmFAgbYFUxH7P8JW1Da2DucYr2VQfDTQvca7jBVOS2ywfbmHkcyOSxmBSFZfZEK6gnIZ5z95P61HAbfCpZ+O4XKHVIsmhDaW8X8ZOHPc+8tJOSF3qLkO874l7xvpzRPQJ1ae8YzgllLhkgMxTIKMx2XPqEvkRFesXkmL/zeGdQpaRW26lkN6ObOT6qUX0CAwEAAaOByDCBxTAKBgNVHQ4EAwQBAzAPBgNVHSMBAQAEBTADgAECMA8GA1UdEwEB/wQFMAMBAQAwgZQGA1UdEQEB/wSBiTCBhqSBgzCBgDELMAkGA1UEBhMCREUxEDAOBgNVBAoMB05vdCBSVUIxXzBdBgNVBAsMVlRMUy1TY2FubmVyIENDQSBMZWFmIENlcnRpZmljYXRlIFNBTiAoUk9PVHYzX0NBdjNfTmFtZUNvbnN0cmFpbnRzX0xFQUZfUlNBdjNfU0FOMkNyaXQpMA0GCSqGSIb3DQEBCwUAA4IBAQAU1SaaZ7B7WTHgZY4Rpyr/9cLH/FJeBHrvnefYreVpXl8OU0rQ7siXsQH6u2tttQYvGUsNshoV54zRTORgGYHYhmvW33QcPt2MLUJqSldeOSUVDxww0JiACo1Ora261SvcUhTvs8MBGGG5wugW3swPRk3EqXizGwtICByOZo+tQEYq45l6n6xvulQ2h7hehZwsSj4eepHyemDMpQzSv3bIoj21fJkHUihiJ41F8EsqUtZMmTi5DIewH6TEw5fHHSG16uz/wxfVwTpkw/qiMpKU0RXjJf0UsIb3YrvOBRTjc5+D6X6GKJtS+jxRnOa02wuGZzYLIt0WnfqIcryXpIdR
+-----END CERTIFICATE-----
diff --git a/src/tests/data/x509/name_constraint_san/root.pem b/src/tests/data/x509/name_constraint_san/root.pem
new file mode 100644
index 000000000..5b1a9986c
--- /dev/null
+++ b/src/tests/data/x509/name_constraint_san/root.pem
@@ -0,0 +1,20 @@
+-----BEGIN CERTIFICATE-----
+MIIDRzCCAi+gAwIBAgIBATANBgkqhkiG9w0BAQsFADBEMQswCQYDVQQGEwJERTEM
+MAoGA1UECgwDUlVCMScwJQYDVQQLDB5UTFMtU2Nhbm5lciBDQ0EgUlNBIFJPT1Qt
+Q0EgdjMwHhcNMTkxMjEyMDAwMDAwWhcNMjAxMjE5MDAwMDAwWjBEMQswCQYDVQQG
+EwJERTEMMAoGA1UECgwDUlVCMScwJQYDVQQLDB5UTFMtU2Nhbm5lciBDQ0EgUlNB
+IFJPT1QtQ0EgdjMwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDmWaMk
+zTTP966yakxiXR8MvePmYgJyz/dS1m2pZxbkgBQJy7VFE7qwJSF5EkVHnAAFohK9
+B7XtSuMRRXTrRwuBHfQrBun8pm9+4bdA62YrbG1+5+uryO3EdavhIJElvlZAI3UA
+yE+s+Vi7AG9V3oJFSR6NO3yItKkagmiX0yetXmc3efJUY7DOCnhYge9dUU12ab5Z
+wytbzTJmMOx0inlche8wm2DNXA33vDTlLoBEryqcg6JS6GFkdG7LC+LhSJOP/vbK
+P06qx93zDbjqU8eNWJABME9Vjv+MW6JRtDR35yjOyWOn4E5Sba1BiJ5wTRx0pFVw
+4mXgsKTHwHHNTasRAgMBAAGjRDBCMAoGA1UdDgQDBAEBMA8GA1UdIwEBAAQFMAOA
+AQEwDwYDVR0PAQH/BAUDAwcEADASBgNVHRMBAf8ECDAGAQH/AgEFMA0GCSqGSIb3
+DQEBCwUAA4IBAQAOF1UZQG2B0hrEahv9KlCx/MTuKBk0S/slyurj+a6KQ9TQ5ChZ
+RDD89Ypfta3tBKFWuIYvRXDCR00Cwe14hThj6eefjd2kpC/P0p8GXXT2+Q2MPEcR
+qYE+C6Obi19z7QlyviI0hx2oFAtVus6QATvavhr2j4SuhzBWhtCH3x/oFuebY7Me
+pp3XuUo57RWl947k7Up2rixrCx5fwmE5A4fbaalC4UjPG/tGUdrFoYpC1tD53q1k
+BEZRljRRRK4vu0P2ki3dI14zbwYFKc/TeZVs7vjiquXoTInTpdzJ4EIzfcCLrsIj
+aNxDdCLl1Erilh1LC1evZLt1+rs91INkEnMM
+-----END CERTIFICATE-----
diff --git a/src/tests/test_x509_path.cpp b/src/tests/test_x509_path.cpp
index a20f417dd..1bda0b55c 100644
--- a/src/tests/test_x509_path.cpp
+++ b/src/tests/test_x509_path.cpp
@@ -590,6 +590,55 @@ std::vector<Test::Result> Validate_V2Uid_in_V1_Test::run()
 
 BOTAN_REGISTER_TEST("x509_v2uid_in_v1", Validate_V2Uid_in_V1_Test);
 
+class Validate_Name_Constraint_SAN_Test final : public Test
+   {
+   public:
+      std::vector<Test::Result> run() override;
+   };
+
+std::vector<Test::Result> Validate_Name_Constraint_SAN_Test::run()
+   {
+   if(Botan::has_filesystem_impl() == false)
+      {
+      return {Test::Result::Note("Path validation",
+                                 "Skipping due to missing filesystem access")};
+      }
+
+   std::vector<Test::Result> results;
+
+   const std::string root_crt = Test::data_file("/x509/name_constraint_san/root.pem");
+   const std::string int_crt  = Test::data_file("/x509/name_constraint_san/int.pem");
+   const std::string ee_crt   = Test::data_file("/x509/name_constraint_san/leaf.pem");
+
+   auto validation_time =
+      Botan::calendar_point(2020, 1, 1, 1, 0, 0).to_std_timepoint();
+
+   Botan::X509_Certificate root(root_crt);
+   Botan::X509_Certificate intermediate(int_crt);
+   Botan::X509_Certificate ee_cert(ee_crt);
+
+   Botan::Certificate_Store_In_Memory trusted;
+   trusted.add_certificate(root);
+
+   std::vector<Botan::X509_Certificate> chain = { ee_cert, intermediate };
+
+   Botan::Path_Validation_Restrictions restrictions;
+   Botan::Path_Validation_Result validation_result =
+      Botan::x509_path_validate(chain, restrictions, trusted, "",
+                                Botan::Usage_Type::UNSPECIFIED, validation_time);
+
+   Test::Result result("Verifying certificate with alternative SAN violating name constraint");
+   result.test_eq("Path validation failed",
+                  validation_result.successful_validation(), false);
+   result.test_eq("Path validation result",
+                  validation_result.result_string(),
+                  "Certificate does not pass name constraint");
+
+   return {result};
+   }
+
+BOTAN_REGISTER_TEST("x509_name_constraint_san", Validate_Name_Constraint_SAN_Test);
+
 class BSI_Path_Validation_Tests final : public Test
 
    {