Repository: randombit/botan
Pull request #1885 (2019-04-08 08:53:48)

add a system certificate store for linux
This PR adds a certificate store for linux. That `Certificate_Store_Linux` expects a file name for a file of concatenated PEMs as its input. By default, this is set to the path used by Ubuntu. The default can be changed during compile-time by redefining `BOTAN_LINUX_CERTSTORE_DEFAULT_FILE`, using the `--linux-certstore-default-file` build option.

This PR is based on #1878 and includes that commit.

randombit: I'll review this soon but one thing that strikes me from a quick glance. Right now it looks like this code is only compiled/built for exactly Linux, but it does not make use of any Linux specific APIs and I would think roughly the same certstore design is also used on all other Unix systems since it the same layout that OpenSSL uses. And it could even be quite usable on Windows/Mac/non-POSIX, eg for an application that wants to use their own certificate store instead of the system impl. Given that I'm not sure exactly what a good name would be. `Unix_Certificate_Store`? But it should work on non-Unix too? `Flatfile_Certificate_Store`?

We should remember to add a note about the new configure option to `doc/manual/building.rst` so distributors (or at least ones that read the documentation) know about the new option.

alnkpa: I'll implement the `Flatfile_Certificate_Store` without a default file path and let `Certificate_Store_Linux` inherit from it with a configurable default path.
alnkpa: Also:

- [ ] fix documentation
codecov-io: # [Codecov](https://codecov.io/gh/randombit/botan/pull/1885?src=pr&el=h1) Report
> Merging [#1885](https://codecov.io/gh/randombit/botan/pull/1885?src=pr&el=desc) into [master](https://codecov.io/gh/randombit/botan/commit/6bfa5117b77aa760263d306901a7e3eff9c1ba2e?src=pr&el=desc) will **decrease** coverage by `1.32%`.
> The diff coverage is `89.63%`.

[![Impacted file tree graph](https://codecov.io/gh/randombit/botan/pull/1885/graphs/tree.svg?width=650&token=VR6iYmSPH8&height=150&src=pr)](https://codecov.io/gh/randombit/botan/pull/1885?src=pr&el=tree)

```diff
@@            Coverage Diff            @@
##           master   #1885      +/-   ##
=========================================
- Coverage   91.43%   90.1%   -1.33%     
=========================================
  Files         526     529       +3     
  Lines       55975   56176     +201     
  Branches     5839    5843       +4     
=========================================
- Hits        51179   50616     -563     
- Misses       4796    5560     +764
```


| [Impacted Files](https://codecov.io/gh/randombit/botan/pull/1885?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [src/lib/pubkey/pem/pem.cpp](https://codecov.io/gh/randombit/botan/pull/1885/diff?src=pr&el=tree#diff-c3JjL2xpYi9wdWJrZXkvcGVtL3BlbS5jcHA=) | `100% <100%> (ø)` | :arrow_up: |
| [src/tests/test\_pem.cpp](https://codecov.io/gh/randombit/botan/pull/1885/diff?src=pr&el=tree#diff-c3JjL3Rlc3RzL3Rlc3RfcGVtLmNwcA==) | `100% <100%> (ø)` | :arrow_up: |
| [src/tests/test\_certstor\_utils.h](https://codecov.io/gh/randombit/botan/pull/1885/diff?src=pr&el=tree#diff-c3JjL3Rlc3RzL3Rlc3RfY2VydHN0b3JfdXRpbHMuaA==) | `100% <100%> (ø)` | |
| [.../lib/x509/certstor\_system\_linux/certstor\_linux.cpp](https://codecov.io/gh/randombit/botan/pull/1885/diff?src=pr&el=tree#diff-c3JjL2xpYi94NTA5L2NlcnRzdG9yX3N5c3RlbV9saW51eC9jZXJ0c3Rvcl9saW51eC5jcHA=) | `87.5% <87.5%> (ø)` | |
| [src/tests/test\_certstor\_linux.cpp](https://codecov.io/gh/randombit/botan/pull/1885/diff?src=pr&el=tree#diff-c3JjL3Rlc3RzL3Rlc3RfY2VydHN0b3JfbGludXguY3Bw) | `87.78% <87.78%> (ø)` | |
| [src/lib/stream/chacha/chacha\_avx2/chacha\_avx2.cpp](https://codecov.io/gh/randombit/botan/pull/1885/diff?src=pr&el=tree#diff-c3JjL2xpYi9zdHJlYW0vY2hhY2hhL2NoYWNoYV9hdngyL2NoYWNoYV9hdngyLmNwcA==) | `0% <0%> (-100%)` | :arrow_down: |
| [src/lib/hash/sha2\_32/sha2\_32\_bmi2/sha2\_32\_bmi2.cpp](https://codecov.io/gh/randombit/botan/pull/1885/diff?src=pr&el=tree#diff-c3JjL2xpYi9oYXNoL3NoYTJfMzIvc2hhMl8zMl9ibWkyL3NoYTJfMzJfYm1pMi5jcHA=) | `0% <0%> (-100%)` | :arrow_down: |
| [...fish\_512/threefish\_512\_avx2/threefish\_512\_avx2.cpp](https://codecov.io/gh/randombit/botan/pull/1885/diff?src=pr&el=tree#diff-c3JjL2xpYi9ibG9jay90aHJlZWZpc2hfNTEyL3RocmVlZmlzaF81MTJfYXZ4Mi90aHJlZWZpc2hfNTEyX2F2eDIuY3Bw) | `0% <0%> (-100%)` | :arrow_down: |
| [...rc/lib/block/serpent/serpent\_avx2/serpent\_avx2.cpp](https://codecov.io/gh/randombit/botan/pull/1885/diff?src=pr&el=tree#diff-c3JjL2xpYi9ibG9jay9zZXJwZW50L3NlcnBlbnRfYXZ4Mi9zZXJwZW50X2F2eDIuY3Bw) | `0% <0%> (-100%)` | :arrow_down: |
| [src/lib/hash/sha2\_64/sha2\_64\_bmi2/sha2\_64\_bmi2.cpp](https://codecov.io/gh/randombit/botan/pull/1885/diff?src=pr&el=tree#diff-c3JjL2xpYi9oYXNoL3NoYTJfNjQvc2hhMl82NF9ibWkyL3NoYTJfNjRfYm1pMi5jcHA=) | `0% <0%> (-100%)` | :arrow_down: |
| ... and [31 more](https://codecov.io/gh/randombit/botan/pull/1885/diff?src=pr&el=tree-more) | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/randombit/botan/pull/1885?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/randombit/botan/pull/1885?src=pr&el=footer). Last update [6bfa511...ae5a6c3](https://codecov.io/gh/randombit/botan/pull/1885?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

randombit: Modulo the test issue raised by @reneme  this looks good to me.

Patch:
diff --git a/src/lib/x509/certstor_flatfile/certstor_flatfile.cpp b/src/lib/x509/certstor_flatfile/certstor_flatfile.cpp
new file mode 100644
index 000000000..cf0ca2d90
--- /dev/null
+++ b/src/lib/x509/certstor_flatfile/certstor_flatfile.cpp
@@ -0,0 +1,150 @@
+/*
+* Certificate Store
+* (C) 1999-2019 Jack Lloyd
+* (C) 2019      Patrick Schmidt
+*
+* Botan is released under the Simplified BSD License (see license.txt)
+*/
+
+#include <botan/build.h>
+
+#include <botan/certstor_flatfile.h>
+#include <botan/data_src.h>
+#include <botan/pem.h>
+
+namespace Botan {
+namespace {
+std::vector<std::vector<uint8_t>> decode_all_certificates(DataSource& source)
+   {
+   std::vector<std::vector<uint8_t>> pems;
+
+   while(!source.end_of_data())
+      {
+      std::string label;
+      std::vector<uint8_t> cert;
+      try
+         {
+         cert = unlock(PEM_Code::decode(source, label));
+
+         if(label == "CERTIFICATE" || label == "X509 CERTIFICATE" || label == "TRUSTED CERTIFICATE")
+            {
+            pems.push_back(cert);
+            }
+         }
+      catch(const Decoding_Error&) {}
+      }
+
+   return pems;
+   }
+}
+
+Flatfile_Certificate_Store::Flatfile_Certificate_Store(const std::string& file)
+   {
+   if(file.empty())
+      {
+      throw Invalid_Argument("Flatfile_Certificate_Store::Flatfile_Certificate_Store invalid file path");
+      }
+
+   DataSource_Stream file_stream(file);
+
+   for(const std::vector<uint8_t> der : decode_all_certificates(file_stream))
+      {
+      std::shared_ptr<const X509_Certificate> cert = std::make_shared<const X509_Certificate>(der.data(), der.size());
+
+      if(!cert->is_self_signed() || !cert->is_CA_cert())
+         {
+         throw Invalid_Argument("Flatfile_Certificate_Store::Flatfile_Certificate_Store certificate is not self-signed CA");
+         }
+
+      m_all_subjects.push_back(cert->subject_dn());
+      m_dn_to_cert.emplace(cert->subject_dn(), cert);
+      m_pubkey_sha1_to_cert.emplace(cert->subject_public_key_bitstring_sha1(), cert);
+      m_subject_dn_sha256_to_cert.emplace(cert->raw_subject_dn_sha256(), cert);
+      }
+
+   if(m_all_subjects.empty())
+      {
+      throw Invalid_Argument("Flatfile_Certificate_Store::Flatfile_Certificate_Store cert file is empty");
+      }
+   }
+
+std::vector<X509_DN> Flatfile_Certificate_Store::all_subjects() const
+   {
+   return m_all_subjects;
+   }
+
+std::shared_ptr<const X509_Certificate>
+Flatfile_Certificate_Store::find_cert(const X509_DN& subject_dn,
+                                      const std::vector<uint8_t>& key_id) const
+   {
+   std::vector<std::shared_ptr<const X509_Certificate>> found_certs = find_all_certs(subject_dn, key_id);
+
+   return !found_certs.empty() ? found_certs.front() : nullptr;
+   }
+
+std::vector<std::shared_ptr<const X509_Certificate>> Flatfile_Certificate_Store::find_all_certs(
+         const X509_DN& subject_dn,
+         const std::vector<uint8_t>& key_id) const
+   {
+   const auto found_range = m_dn_to_cert.equal_range(subject_dn);
+
+   if(found_range.first == m_dn_to_cert.end())
+      {
+      return {};
+      }
+
+   std::vector<std::shared_ptr<const X509_Certificate>> found_certs;
+
+   for(auto i = found_range.first; i != found_range.second; ++i)
+      {
+      const std::shared_ptr<const X509_Certificate> cert = i->second;
+
+      if(key_id.empty() || key_id == cert->subject_key_id())
+         {
+         found_certs.push_back(cert);
+         }
+      }
+
+   return found_certs;
+   }
+
+std::shared_ptr<const X509_Certificate>
+Flatfile_Certificate_Store::find_cert_by_pubkey_sha1(const std::vector<uint8_t>& key_hash) const
+   {
+   if(key_hash.size() != 20)
+      {
+      throw Invalid_Argument("Flatfile_Certificate_Store::find_cert_by_pubkey_sha1 invalid hash");
+      }
+
+   auto found_cert = m_pubkey_sha1_to_cert.find(key_hash);
+
+   if(found_cert != m_pubkey_sha1_to_cert.end())
+      {
+      return found_cert->second;
+      }
+
+   return nullptr;
+   }
+
+std::shared_ptr<const X509_Certificate>
+Flatfile_Certificate_Store::find_cert_by_raw_subject_dn_sha256(const std::vector<uint8_t>& subject_hash) const
+   {
+   if(subject_hash.size() != 32)
+      { throw Invalid_Argument("Flatfile_Certificate_Store::find_cert_by_raw_subject_dn_sha256 invalid hash"); }
+
+   auto found_cert = m_subject_dn_sha256_to_cert.find(subject_hash);
+
+   if(found_cert != m_subject_dn_sha256_to_cert.end())
+      {
+      return found_cert->second;
+      }
+
+   return nullptr;
+   }
+
+std::shared_ptr<const X509_CRL> Flatfile_Certificate_Store::find_crl_for(const X509_Certificate& subject) const
+   {
+   BOTAN_UNUSED(subject);
+   return {};
+   }
+}
diff --git a/src/lib/x509/certstor_flatfile/certstor_flatfile.h b/src/lib/x509/certstor_flatfile/certstor_flatfile.h
new file mode 100644
index 000000000..611bd3895
--- /dev/null
+++ b/src/lib/x509/certstor_flatfile/certstor_flatfile.h
@@ -0,0 +1,81 @@
+/*
+* Certificate Store
+* (C) 1999-2019 Jack Lloyd
+* (C) 2019      Patrick Schmidt
+*
+* Botan is released under the Simplified BSD License (see license.txt)
+*/
+
+#ifndef BOTAN_CERT_STORE_FLATFILE_H_
+#define BOTAN_CERT_STORE_FLATFILE_H_
+
+#include <botan/certstor.h>
+
+#include <vector>
+#include <memory>
+#include <map>
+
+namespace Botan {
+/**
+* Certificate Store that is backed by a file of PEMs of trusted CAs.
+*/
+class BOTAN_PUBLIC_API(2, 11) Flatfile_Certificate_Store final : public Certificate_Store
+   {
+   public:
+      /**
+      * Construct a new Certificate_Store given a file path to a file including
+      * PEMs of trusted self-signed CAs.
+      */
+      Flatfile_Certificate_Store(const std::string& file);
+
+      Flatfile_Certificate_Store(const Flatfile_Certificate_Store&) = default;
+      Flatfile_Certificate_Store(Flatfile_Certificate_Store&&) = default;
+      Flatfile_Certificate_Store& operator=(const Flatfile_Certificate_Store&) = default;
+      Flatfile_Certificate_Store& operator=(Flatfile_Certificate_Store&&) = default;
+
+      /**
+      * @return DNs for all certificates managed by the store
+      */
+      std::vector<X509_DN> all_subjects() const override;
+
+      /**
+      * Find a certificate by Subject DN and (optionally) key identifier
+      * @return the first certificate that matches
+      */
+      std::shared_ptr<const X509_Certificate> find_cert(
+         const X509_DN& subject_dn,
+         const std::vector<uint8_t>& key_id) const override;
+
+      /**
+      * Find all certificates with a given Subject DN.
+      * Subject DN and even the key identifier might not be unique.
+      */
+      std::vector<std::shared_ptr<const X509_Certificate>> find_all_certs(
+               const X509_DN& subject_dn, const std::vector<uint8_t>& key_id) const override;
+
+      /**
+      * Find a certificate by searching for one with a matching SHA-1 hash of
+      * public key.
+      * @return a matching certificate or nullptr otherwise
+      */
+      std::shared_ptr<const X509_Certificate>
+      find_cert_by_pubkey_sha1(const std::vector<uint8_t>& key_hash) const override;
+
+      std::shared_ptr<const X509_Certificate>
+      find_cert_by_raw_subject_dn_sha256(const std::vector<uint8_t>& subject_hash) const override;
+
+      /**
+       * Fetching CRLs is not supported by this certificate store. This will
+       * always return an empty list.
+       */
+      std::shared_ptr<const X509_CRL> find_crl_for(const X509_Certificate& subject) const override;
+
+   private:
+      std::vector<X509_DN> m_all_subjects;
+      std::multimap<X509_DN, std::shared_ptr<const X509_Certificate>> m_dn_to_cert;
+      std::map<std::vector<uint8_t>, std::shared_ptr<const X509_Certificate>> m_pubkey_sha1_to_cert;
+      std::map<std::vector<uint8_t>, std::shared_ptr<const X509_Certificate>> m_subject_dn_sha256_to_cert;
+   };
+}
+
+#endif
diff --git a/src/lib/x509/certstor_flatfile/info.txt b/src/lib/x509/certstor_flatfile/info.txt
new file mode 100644
index 000000000..902997be7
--- /dev/null
+++ b/src/lib/x509/certstor_flatfile/info.txt
@@ -0,0 +1,12 @@
+<defines>
+CERTSTOR_FLATFILE -> 20190410
+</defines>
+
+<os_features>
+filesystem
+</os_features>
+
+<header:public>
+certstor_flatfile.h
+</header:public>
+


Test patch:
diff --git a/src/tests/data/x509/misc/certstor/ca_bundle_containing_non_ca.pem b/src/tests/data/x509/misc/certstor/ca_bundle_containing_non_ca.pem
new file mode 100644
index 000000000..e6b723261
--- /dev/null
+++ b/src/tests/data/x509/misc/certstor/ca_bundle_containing_non_ca.pem
@@ -0,0 +1,71 @@
+
+> DST Root CA X3
+
+-----BEGIN CERTIFICATE-----
+MIIDSjCCAjKgAwIBAgIQRK+wgNajJ7qJMDmGLvhAazANBgkqhkiG9w0BAQUFADA/
+MSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMT
+DkRTVCBSb290IENBIFgzMB4XDTAwMDkzMDIxMTIxOVoXDTIxMDkzMDE0MDExNVow
+PzEkMCIGA1UEChMbRGlnaXRhbCBTaWduYXR1cmUgVHJ1c3QgQ28uMRcwFQYDVQQD
+Ew5EU1QgUm9vdCBDQSBYMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB
+AN+v6ZdQCINXtMxiZfaQguzH0yxrMMpb7NnDfcdAwRgUi+DoM3ZJKuM/IUmTrE4O
+rz5Iy2Xu/NMhD2XSKtkyj4zl93ewEnu1lcCJo6m67XMuegwGMoOifooUMM0RoOEq
+OLl5CjH9UL2AZd+3UWODyOKIYepLYYHsUmu5ouJLGiifSKOeDNoJjj4XLh7dIN9b
+xiqKqy69cK3FCxolkHRyxXtqqzTWMIn/5WgTe1QLyNau7Fqckh49ZLOMxt+/yUFw
+7BZy1SbsOFU5Q9D8/RhcQPGX69Wam40dutolucbY38EVAjqr2m7xPi71XAicPNaD
+aeQQmxkqtilX4+U9m5/wAl0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNV
+HQ8BAf8EBAMCAQYwHQYDVR0OBBYEFMSnsaR7LHH62+FLkHX/xBVghYkQMA0GCSqG
+SIb3DQEBBQUAA4IBAQCjGiybFwBcqR7uKGY3Or+Dxz9LwwmglSBd49lZRNI+DT69
+ikugdB/OEIKcdBodfpga3csTS7MgROSR6cz8faXbauX+5v3gTt23ADq1cEmv8uXr
+AvHRAosZy5Q6XkjEGB5YGV8eAlrwDPGxrancWYaLbumR9YbK+rlmM6pZW87ipxZz
+R8srzJmwN0jP41ZL9c8PDHIyh8bwRLtTcm1D9SZImlJnt1ir/md2cXjbDaJWFBM5
+JDGFoqgCWjBH4d1QB7wCCZAA62RjYJsWvIjJEubSfZGL+T0yjWW06XyxV3bqxbYo
+Ob8VZRzI9neWagqNdwvYkQsEjgfbKbYK7p2CNTUQ
+-----END CERTIFICATE-----
+
+> rmeusel-eduroam@cern.ch
+> Note: this is NOT a CA
+
+-----BEGIN CERTIFICATE-----
+MIIHyjCCBbKgAwIBAgIKIx3D0gAAAAAbxDANBgkqhkiG9w0BAQsFADBRMRIwEAYK
+CZImiZPyLGQBGRYCY2gxFDASBgoJkiaJk/IsZAEZFgRjZXJuMSUwIwYDVQQDExxD
+RVJOIENlcnRpZmljYXRpb24gQXV0aG9yaXR5MB4XDTE1MDQzMDA4MzQwMloXDTIw
+MDQyODA4MzQwMlowIjEgMB4GA1UEAwwXcm1ldXNlbC1lZHVyb2FtQGNlcm4uY2gw
+ggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCqPQ4XLqlywbOnHYNztq2Y
+kVh1krXncE1IlLozJS40gBk/wnya+MKfRT6vQF5a9FWSh9P33oLd7JSBasEOtqpG
+rNL8f2BvoRlNOFoFOxJb3lTmS3lCW8ve051B0Icayt5sQDwdVkoDTqpzBhvDAGF+
+uVmY1BB+Hu9TFK/e3h/eHCx1iDL7ldXjz72H2lu/+nEuozai2Pha0GpWtoUDzZMP
+yuhT0tGrXeCHG732/VXZJdEZS/bDIuHvOXDZwcpQIy6yVgpAwGiXRK0sbpUE1ix7
+hEj/vSa3DT2iIJMIjHp5vZrE9QnfJl4LmXgonu43RXu0R/pnP1vFRmEdo04gEi+p
+AgMBAAGjggPRMIIDzTA9BgkrBgEEAYI3FQcEMDAuBiYrBgEEAYI3FQiDvdAJgu2N
+DYbtiyuB3vU3hYDQYh6CpsJKhqHpAAIBZAIBFTAdBgNVHQ4EFgQUKE5KOCt6b1mx
+Q+YgJhbZKVEoQeIwHwYDVR0jBBgwFoAUHZAZ6sjOzDyNFLFM2ewU4mFAA2EwggEq
+BgNVHR8EggEhMIIBHTCCARmgggEVoIIBEYZHaHR0cDovL2NhZmlsZXMuY2Vybi5j
+aC9jYWZpbGVzL2NybC9DRVJOJTIwQ2VydGlmaWNhdGlvbiUyMEF1dGhvcml0eS5j
+cmyGgcVsZGFwOi8vL0NOPUNFUk4lMjBDZXJ0aWZpY2F0aW9uJTIwQXV0aG9yaXR5
+LENOPUNFUk5QS0kwNyxDTj1DRFAsQ049UHVibGljJTIwS2V5JTIwU2VydmljZXMs
+Q049U2VydmljZXMsQ049Q29uZmlndXJhdGlvbixEQz1jZXJuLERDPWNoP2NlcnRp
+ZmljYXRlUmV2b2NhdGlvbkxpc3Q/YmFzZT9vYmplY3RDbGFzcz1jUkxEaXN0cmli
+dXRpb25Qb2ludDCCAVQGCCsGAQUFBwEBBIIBRjCCAUIwXAYIKwYBBQUHMAKGUGh0
+dHA6Ly9jYWZpbGVzLmNlcm4uY2gvY2FmaWxlcy9jZXJ0aWZpY2F0ZXMvQ0VSTiUy
+MENlcnRpZmljYXRpb24lMjBBdXRob3JpdHkuY3J0MIG7BggrBgEFBQcwAoaBrmxk
+YXA6Ly8vQ049Q0VSTiUyMENlcnRpZmljYXRpb24lMjBBdXRob3JpdHksQ049QUlB
+LENOPVB1YmxpYyUyMEtleSUyMFNlcnZpY2VzLENOPVNlcnZpY2VzLENOPUNvbmZp
+Z3VyYXRpb24sREM9Y2VybixEQz1jaD9jQUNlcnRpZmljYXRlP2Jhc2U/b2JqZWN0
+Q2xhc3M9Y2VydGlmaWNhdGlvbkF1dGhvcml0eTAkBggrBgEFBQcwAYYYaHR0cDov
+L29jc3AuY2Vybi5jaC9vY3NwMA4GA1UdDwEB/wQEAwIGwDATBgNVHSUEDDAKBggr
+BgEFBQcDAjBkBgNVHSAEXTBbMFkGCisGAQQBYAoEAQEwSzBJBggrBgEFBQcCARY9
+aHR0cDovL2NhZmlsZXMuY2Vybi5jaC9jYWZpbGVzL2NwLWNwcy9jZXJuLWdyaWQt
+Y2EtY3AtY3BzLnBkZjAbBgkrBgEEAYI3FQoEDjAMMAoGCCsGAQUFBwMCMB4GA1Ud
+EQQXMBWBE3JlbmUubWV1c2VsQGNlcm4uY2gwDQYJKoZIhvcNAQELBQADggIBABzw
+A5GXuiLGOlIQcdmQM0VKAO0OYbqc+O/QzQgn6h5sH7X4FJCYkJE8OMuxIo59xIke
+2fKK3RVMgF7u7yeKeh2uVoPA+SN/Hwo4md0lIOiZYs78i6fsNW9d36/fPjh3dLRr
+1G8JitHeoBZyt66fnLjrO+/Jsrea5iYAXpLB4auQRUZ6Mr4oCKsm35ty7bQNA/Sj
+am2eOUMZ76PjS7ApMe3wJUxAgRAEmncL9uKyPRV4Yjr8g6a0pdKnHNRISquSwOBF
+QjJFmoD7jooLg3lYF14DMHoHYZq9CMpGZQRlhLl3eOz2aH4v/UZJaoPjxQ/8IwIJ
+mlHun8atlnUCjVpUURSO21YUxjYqwbwYxw4xGzEPOQI8k0ujBnVAPbwUdfhf3oeY
+sKtNvWcPEK2wC3Hx5lkFNFw3p4CLaGKH5r3Da6bWoOtepRznxKuND6rBEyEfCFaG
++slSSdFjHbFxgTffweHoZq3JBd1A0hrutVgak4XA5TbbOsW1gugjfjUxJ2xbYAYl
+iA9xWRvuG2eXqhl5v2S/ZKI6moeocdg0gAz6nbvYu6qa2Cc4neLfXLqNV+8jqwAP
+lzXku1UKY0kRPiVWqcO7jRj/UoEv0y9Fh5o4fgp/vRXLIxrvc2qbc/nRoEjaV+vt
+m42lVMMeStmDqX/fM4XIzvP05Sl/wt/Z1B3ViXtl
+-----END CERTIFICATE-----
diff --git a/src/tests/data/x509/misc/certstor/valid_ca_bundle.pem b/src/tests/data/x509/misc/certstor/valid_ca_bundle.pem
new file mode 100644
index 000000000..d1024179c
--- /dev/null
+++ b/src/tests/data/x509/misc/certstor/valid_ca_bundle.pem
@@ -0,0 +1,79 @@
+
+> D-TRUST Root Class 3 CA 2 EV 2009
+
+-----BEGIN CERTIFICATE-----
+MIIEQzCCAyugAwIBAgIDCYP0MA0GCSqGSIb3DQEBCwUAMFAxCzAJBgNVBAYTAkRF
+MRUwEwYDVQQKDAxELVRydXN0IEdtYkgxKjAoBgNVBAMMIUQtVFJVU1QgUm9vdCBD
+bGFzcyAzIENBIDIgRVYgMjAwOTAeFw0wOTExMDUwODUwNDZaFw0yOTExMDUwODUw
+NDZaMFAxCzAJBgNVBAYTAkRFMRUwEwYDVQQKDAxELVRydXN0IEdtYkgxKjAoBgNV
+BAMMIUQtVFJVU1QgUm9vdCBDbGFzcyAzIENBIDIgRVYgMjAwOTCCASIwDQYJKoZI
+hvcNAQEBBQADggEPADCCAQoCggEBAJnxhDRwui+3MKCOvXwEz75ivJn9gpfSegpn
+ljgJ9hBOlSJzmY3aFS3nBfwZcyK3jpgAvDw9rKFs+9Z5JUut8Mxk2og+KbgPCdM0
+3TP1YtHhzRnp7hhPTFiu4h7WDFsVWtg6uMQYZB7jM7K1iXdODL/ZlGsTl28So/6Z
+qQTMFexgaDbtCHu39b+T7WYxg4zGcTSHThfqr4uRjRxWQa4iN1438h3Z0S0NL2lR
+p75mpoo6Kr3HGrHhFPC+Oh25z1uxav60sUYgovseO3Dvk5h9jHOW8sXvhXCtKSb8
+HgQ+HKDYD8tSg2J87otTlZCpV6LqYQXY+U3EJ/pure3511H3a6UCAwEAAaOCASQw
+ggEgMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFNOUikxiEyoZLsyvcop9Ntea
+HNxnMA4GA1UdDwEB/wQEAwIBBjCB3QYDVR0fBIHVMIHSMIGHoIGEoIGBhn9sZGFw
+Oi8vZGlyZWN0b3J5LmQtdHJ1c3QubmV0L0NOPUQtVFJVU1QlMjBSb290JTIwQ2xh
+c3MlMjAzJTIwQ0ElMjAyJTIwRVYlMjAyMDA5LE89RC1UcnVzdCUyMEdtYkgsQz1E
+RT9jZXJ0aWZpY2F0ZXJldm9jYXRpb25saXN0MEagRKBChkBodHRwOi8vd3d3LmQt
+dHJ1c3QubmV0L2NybC9kLXRydXN0X3Jvb3RfY2xhc3NfM19jYV8yX2V2XzIwMDku
+Y3JsMA0GCSqGSIb3DQEBCwUAA4IBAQA07XtaPKSUiO8aEXUHL7P+PPoeUSbrh/Yp
+3uDx1MYkCenBz1UbtDDZzhr+BlGmFaQt77JLvyAoJUnRpjZ3NOhk31KxEcdzes05
+nsKtjHEh8lprr988TlWvsoRlFIm5d8sqMb7Po23Pb0iUMkZv53GMoKaEGTcH8gNF
+CSuGdXzfX2lXANtu2KZyIktQ1HWYVt+3GP9DQ1CuekR78HlR10M9p9OB0/DJT7na
+xpeG0ILD5EJt/rDiZE4OJudANCa1CInXCGNjOCd1HjPqbqjdn5lPdE2BiYBL3ZqX
+KVwvvoFBuYz/6n1gBp7N1z3TLqMVvKjmJuVvw9y4AyHqnxbxLFS1
+-----END CERTIFICATE-----
+
+> DST Root CA X3
+
+-----BEGIN CERTIFICATE-----
+MIIDSjCCAjKgAwIBAgIQRK+wgNajJ7qJMDmGLvhAazANBgkqhkiG9w0BAQUFADA/
+MSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMT
+DkRTVCBSb290IENBIFgzMB4XDTAwMDkzMDIxMTIxOVoXDTIxMDkzMDE0MDExNVow
+PzEkMCIGA1UEChMbRGlnaXRhbCBTaWduYXR1cmUgVHJ1c3QgQ28uMRcwFQYDVQQD
+Ew5EU1QgUm9vdCBDQSBYMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB
+AN+v6ZdQCINXtMxiZfaQguzH0yxrMMpb7NnDfcdAwRgUi+DoM3ZJKuM/IUmTrE4O
+rz5Iy2Xu/NMhD2XSKtkyj4zl93ewEnu1lcCJo6m67XMuegwGMoOifooUMM0RoOEq
+OLl5CjH9UL2AZd+3UWODyOKIYepLYYHsUmu5ouJLGiifSKOeDNoJjj4XLh7dIN9b
+xiqKqy69cK3FCxolkHRyxXtqqzTWMIn/5WgTe1QLyNau7Fqckh49ZLOMxt+/yUFw
+7BZy1SbsOFU5Q9D8/RhcQPGX69Wam40dutolucbY38EVAjqr2m7xPi71XAicPNaD
+aeQQmxkqtilX4+U9m5/wAl0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNV
+HQ8BAf8EBAMCAQYwHQYDVR0OBBYEFMSnsaR7LHH62+FLkHX/xBVghYkQMA0GCSqG
+SIb3DQEBBQUAA4IBAQCjGiybFwBcqR7uKGY3Or+Dxz9LwwmglSBd49lZRNI+DT69
+ikugdB/OEIKcdBodfpga3csTS7MgROSR6cz8faXbauX+5v3gTt23ADq1cEmv8uXr
+AvHRAosZy5Q6XkjEGB5YGV8eAlrwDPGxrancWYaLbumR9YbK+rlmM6pZW87ipxZz
+R8srzJmwN0jP41ZL9c8PDHIyh8bwRLtTcm1D9SZImlJnt1ir/md2cXjbDaJWFBM5
+JDGFoqgCWjBH4d1QB7wCCZAA62RjYJsWvIjJEubSfZGL+T0yjWW06XyxV3bqxbYo
+Ob8VZRzI9neWagqNdwvYkQsEjgfbKbYK7p2CNTUQ
+-----END CERTIFICATE-----
+
+> VeriSign Class 3 Public Primary Certification Authority - G3
+> Note: this is a v1 style X.509 certificate
+
+-----BEGIN CERTIFICATE-----
+MIIEGjCCAwICEQCbfgZJoz5iudXukEhxKe9XMA0GCSqGSIb3DQEBBQUAMIHKMQsw
+CQYDVQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZl
+cmlTaWduIFRydXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWdu
+LCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxRTBDBgNVBAMTPFZlcmlT
+aWduIENsYXNzIDMgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3Jp
+dHkgLSBHMzAeFw05OTEwMDEwMDAwMDBaFw0zNjA3MTYyMzU5NTlaMIHKMQswCQYD
+VQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZlcmlT
+aWduIFRydXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWduLCBJ
+bmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxRTBDBgNVBAMTPFZlcmlTaWdu
+IENsYXNzIDMgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkg
+LSBHMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMu6nFL8eB8aHm8b
+N3O9+MlrlBIwT/A2R/XQkQr1F8ilYcEWQE37imGQ5XYgwREGfassbqb1EUGO+i2t
+KmFZpGcmTNDovFJbcCAEWNF6yaRpvIMXZK0Fi7zQWM6NjPXr8EJJC52XJ2cybuGu
+kxUccLwgTS8Y3pKI6GyFVxEa6X7jJhFUokWWVYPKMIno3Nij7SqAP395ZVc+FSBm
+CC+Vk7+qRy+oRpfwEuL+wgorUeZ25rdGt+INpsyow0xZVYnm6FNcHOqd8GIWC6fJ
+Xwzw3sJ2zq/3avL6QaaiMxTJ5Xpj055iN9WFZZ4O5lMkdBteHRJTW8cs54NJOxWu
+imi5V5cCAwEAATANBgkqhkiG9w0BAQUFAAOCAQEAERSWwauSCPc/L8my/uRan2Te
+2yFPhpk0djZX3dAVL8WtfxUfN2JzPtTnX84XA9s1+ivbrmAJXx5fj267Cz3qWhMe
+DGBvtcC1IyIuBwvLqXTLR7sdwdela8wv0kL9Sd2nic9TutoAWii/gt/4uhMdUIaC
+/Y4wjylGsB49Ndo4YhYYSq3mtlFs3q9i6wHQHiT+eo8SGhJouPtmmRQURVyu565p
+F4ErWjfJXir0xuKhXFSbplQAz/DxwceYMBo7Nhbbo27q/a2ywtrvAkcTisDxszGt
+TxzhT5yvDwyd93gN2PQ1VoDat20Xj50egWTh/sVFuq1ruQp6Tk9LhO5L8X3dEQ==
+-----END CERTIFICATE-----
diff --git a/src/tests/test_certstor_flatfile.cpp b/src/tests/test_certstor_flatfile.cpp
new file mode 100644
index 000000000..2c24128c8
--- /dev/null
+++ b/src/tests/test_certstor_flatfile.cpp
@@ -0,0 +1,278 @@
+/*
+* (C) 1999-2019 Jack Lloyd
+* (C) 2019      René Meusel
+*
+* Botan is released under the Simplified BSD License (see license.txt)
+*/
+
+#include "tests.h"
+
+#if defined(BOTAN_HAS_CERTSTOR_FLATFILE)
+
+#include "test_certstor_utils.h"
+#include <botan/certstor_flatfile.h>
+#include <botan/ber_dec.h>
+#include <botan/der_enc.h>
+#include <botan/hex.h>
+
+namespace Botan_Tests {
+
+namespace {
+
+std::string get_valid_ca_bundle_path()
+   {
+   return Test::data_file("x509/misc/certstor/valid_ca_bundle.pem");
+   }
+
+std::string get_ca_bundle_containing_user_cert()
+   {
+   return Test::data_file("x509/misc/certstor/ca_bundle_containing_non_ca.pem");
+   }
+
+Test::Result open_certificate_store()
+   {
+   Test::Result result("Flatfile Certificate Store - Open Store");
+
+   try
+      {
+      result.start_timer();
+      Botan::Flatfile_Certificate_Store unused(get_valid_ca_bundle_path());
+      result.end_timer();
+      result.test_gt("found some certificates", unused.all_subjects().size(), 0);
+      }
+   catch(std::exception& e)
+      {
+      result.test_failure(e.what());
+      }
+
+   result.test_success();
+
+   return result;
+   }
+
+Test::Result find_certificate_by_pubkey_sha1()
+   {
+   Test::Result result("Flatfile Certificate Store - Find Certificate by SHA1(pubkey)");
+
+   try
+      {
+      result.start_timer();
+      Botan::Flatfile_Certificate_Store certstore(get_valid_ca_bundle_path());
+      auto cert = certstore.find_cert_by_pubkey_sha1(get_key_id());
+      result.end_timer();
+
+      if(result.test_not_null("found certificate", cert.get()))
+         {
+         auto cns = cert->subject_dn().get_attribute("CN");
+         result.test_int_eq("exactly one CN", cns.size(), 1);
+         result.test_eq("CN", cns.front(), "DST Root CA X3");
+         }
+      }
+   catch(std::exception& e)
+      {
+      result.test_failure(e.what());
+      }
+
+   result.test_throws("on invalid SHA1 hash data", [&]
+      {
+      Botan::Flatfile_Certificate_Store certstore(get_valid_ca_bundle_path());
+      certstore.find_cert_by_pubkey_sha1({});
+      });
+
+   return result;
+   }
+
+Test::Result find_cert_by_subject_dn()
+   {
+   Test::Result result("Flatfile Certificate Store - Find Certificate by subject DN");
+
+   try
+      {
+      auto dn = get_dn();
+
+      result.start_timer();
+      Botan::Flatfile_Certificate_Store certstore(get_valid_ca_bundle_path());
+      auto cert = certstore.find_cert(dn, std::vector<uint8_t>());
+      result.end_timer();
+
+      if(result.test_not_null("found certificate", cert.get()))
+         {
+         auto cns = cert->subject_dn().get_attribute("CN");
+         result.test_int_eq("exactly one CN", cns.size(), 1);
+         result.test_eq("CN", cns.front(), "DST Root CA X3");
+         }
+      }
+   catch(std::exception& e)
+      {
+      result.test_failure(e.what());
+      }
+
+   return result;
+   }
+
+Test::Result find_cert_by_subject_dn_and_key_id()
+   {
+   Test::Result result("Flatfile Certificate Store - Find Certificate by subject DN and key ID");
+
+   try
+      {
+      auto dn = get_dn();
+
+      result.start_timer();
+      Botan::Flatfile_Certificate_Store certstore(get_valid_ca_bundle_path());
+      auto cert = certstore.find_cert(dn, get_key_id());
+      result.end_timer();
+
+      if(result.test_not_null("found certificate", cert.get()))
+         {
+         auto cns = cert->subject_dn().get_attribute("CN");
+         result.test_int_eq("exactly one CN", cns.size(), 1);
+         result.test_eq("CN", cns.front(), "DST Root CA X3");
+         }
+      }
+   catch(std::exception& e)
+      {
+      result.test_failure(e.what());
+      }
+
+   return result;
+   }
+
+Test::Result find_certs_by_subject_dn_and_key_id()
+   {
+   Test::Result result("Flatfile Certificate Store - Find Certificates by subject DN and key ID");
+
+   try
+      {
+      auto dn = get_dn();
+
+      result.start_timer();
+      Botan::Flatfile_Certificate_Store certstore(get_valid_ca_bundle_path());
+      auto certs = certstore.find_all_certs(dn, get_key_id());
+      result.end_timer();
+
+      if(result.confirm("result not empty", !certs.empty()) &&
+            result.test_eq("exactly one certificate", certs.size(), 1))
+         {
+         auto cns = certs.front()->subject_dn().get_attribute("CN");
+         result.test_int_eq("exactly one CN", cns.size(), 1);
+         result.test_eq("CN", cns.front(), "DST Root CA X3");
+         }
+      }
+   catch(std::exception& e)
+      {
+      result.test_failure(e.what());
+      }
+
+   return result;
+   }
+
+Test::Result find_all_subjects()
+   {
+   Test::Result result("Flatfile Certificate Store - Find all Certificate Subjects");
+
+   try
+      {
+      result.start_timer();
+      Botan::Flatfile_Certificate_Store certstore(get_valid_ca_bundle_path());
+      auto subjects = certstore.all_subjects();
+      result.end_timer();
+
+      if(result.confirm("result not empty", !subjects.empty()))
+         {
+         auto dn = get_dn();
+         auto needle = std::find_if(subjects.cbegin(),
+                                    subjects.cend(),
+                                    [=](const Botan::X509_DN &subject)
+            {
+            return subject == dn;
+            });
+
+         if(result.confirm("found expected certificate", needle != subjects.end()))
+            {
+            result.confirm("expected certificate", *needle == dn);
+            }
+         }
+      }
+   catch(std::exception& e)
+      {
+      result.test_failure(e.what());
+      }
+
+   return result;
+   }
+
+Test::Result no_certificate_matches()
+   {
+   Test::Result result("Flatfile Certificate Store - can deal with no matches (regression test)");
+
+   try
+      {
+      auto dn  = get_unknown_dn();
+      auto kid = get_unknown_key_id();
+
+      result.start_timer();
+      Botan::Flatfile_Certificate_Store certstore(get_valid_ca_bundle_path());
+
+      auto certs = certstore.find_all_certs(dn, kid);
+      auto cert = certstore.find_cert(dn, kid);
+      auto pubk_cert = certstore.find_cert_by_pubkey_sha1(kid);
+      result.end_timer();
+
+      result.confirm("find_all_certs did not find the dummy", certs.empty());
+      result.confirm("find_cert did not find the dummy", !cert);
+      result.confirm("find_cert_by_pubkey_sha1 did not find the dummy", !pubk_cert);
+      }
+   catch(std::exception& e)
+      {
+      result.test_failure(e.what());
+      }
+
+   return result;
+   }
+
+Test::Result certstore_contains_user_certificate()
+   {
+   Test::Result result("Flatfile Certificate Store - rejects bundles with non-CA certs");
+
+   try
+      {
+      result.start_timer();
+      Botan::Flatfile_Certificate_Store certstore(get_ca_bundle_containing_user_cert());
+      result.test_failure("CA bundle with non-CA certs should be rejected");
+      }
+   catch(Botan::Invalid_Argument&)
+      {
+      result.test_success();
+      }
+
+   return result;
+   }
+
+class Certstor_Flatfile_Tests final : public Test
+   {
+   public:
+      std::vector<Test::Result> run() override
+         {
+         std::vector<Test::Result> results;
+
+         results.push_back(open_certificate_store());
+         results.push_back(find_certificate_by_pubkey_sha1());
+         results.push_back(find_cert_by_subject_dn());
+         results.push_back(find_cert_by_subject_dn_and_key_id());
+         results.push_back(find_certs_by_subject_dn_and_key_id());
+         results.push_back(find_all_subjects());
+         results.push_back(no_certificate_matches());
+         results.push_back(certstore_contains_user_certificate());
+
+         return results;
+         }
+   };
+
+BOTAN_REGISTER_TEST("certstor_flatfile", Certstor_Flatfile_Tests);
+
+}
+
+}
+
+#endif
diff --git a/src/tests/test_certstor_macos.cpp b/src/tests/test_certstor_macos.cpp
index 243a150da..a21fd2a32 100644
--- a/src/tests/test_certstor_macos.cpp
+++ b/src/tests/test_certstor_macos.cpp
@@ -9,6 +9,7 @@
 
 #if defined(BOTAN_HAS_CERTSTOR_MACOS)
 
+#include "test_certstor_utils.h"
 #include <botan/certstor_macos.h>
 #include <botan/ber_dec.h>
 #include <botan/der_enc.h>
@@ -18,59 +19,6 @@ namespace Botan_Tests {
 
 namespace {
 
-Botan::X509_DN read_dn(const std::string hex)
-   {
-   Botan::X509_DN dn;
-   Botan::BER_Decoder decoder(Botan::hex_decode(hex));
-   dn.decode_from(decoder);
-   return dn;
-   }
-
-Botan::X509_DN get_dn()
-   {
-   // Public key fingerprint of "DST Root CA X3"
-   // This certificate is in the standard "System Roots" of any macOS setup,
-   // serves as the trust root of botan.randombit.net and expires on
-   // Thursday, 30. September 2021 at 16:01:15 Central European Summer Time
-   return read_dn("303f31243022060355040a131b4469676974616c205369676e6174757265"
-                  "20547275737420436f2e311730150603550403130e44535420526f6f7420"
-                  "4341205833");
-   }
-
-std::vector<uint8_t> get_key_id()
-   {
-   // this is the same as the public key SHA1
-   return Botan::hex_decode("c4a7b1a47b2c71fadbe14b9075ffc41560858910");
-   }
-
-Botan::X509_DN get_unknown_dn()
-   {
-   // thats a D-Trust "Test Certificate". It should be fairly likely that
-   // _nobody_ will _ever_ have that in their system keychain
-   // CN: D-TRUST Limited Basic Test PU CA 1-4 2016
-   return read_dn("305b310b300906035504061302444531153013060355040a0c0c442d5472"
-                  "75737420476d62483135303306035504030c2c442d5452555354204c696d"
-                  "6974656420426173696320526f6f74205465737420505520434120312032"
-                  "303135");
-   }
-
-Botan::X509_DN get_skewed_dn()
-   {
-   // This DN contains ASN.1 PrintableString fields that are not 'normalized'
-   // according to Apple's idea of a normalized PrintableString field:
-   //   (1) It has leading and trailing white space
-   //   (2) It contains multiple spaces between 'words'
-   return read_dn("304b312a3028060355040a132120204469676974616c2020205369676e61"
-                  "7475726520547275737420436f2e2020311d301b06035504031314202044"
-                  "5354202020526f6f742043412058332020");
-   }
-
-std::vector<uint8_t> get_unknown_key_id()
-   {
-   // this is the same as the public key SHA1
-   return Botan::hex_decode("785c0b67b536eeacbb2b27cf9123301abe7ab09a");
-   }
-
 Test::Result open_certificate_store()
    {
    Test::Result result("macOS Certificate Store - Open Keychain");
diff --git a/src/tests/test_certstor_utils.cpp b/src/tests/test_certstor_utils.cpp
new file mode 100644
index 000000000..9a0a141d9
--- /dev/null
+++ b/src/tests/test_certstor_utils.cpp
@@ -0,0 +1,71 @@
+/*
+* (C) 1999-2019 Jack Lloyd
+* (C) 2019      René Meusel
+*
+* Botan is released under the Simplified BSD License (see license.txt)
+*/
+
+#include "test_certstor_utils.h"
+
+#if defined(BOTAN_HAS_X509_CERTIFICATES)
+
+#include <botan/ber_dec.h>
+#include <botan/hex.h>
+
+namespace Botan_Tests {
+
+Botan::X509_DN read_dn(const std::string hex)
+   {
+   Botan::X509_DN dn;
+   Botan::BER_Decoder decoder(Botan::hex_decode(hex));
+   dn.decode_from(decoder);
+   return dn;
+   }
+
+Botan::X509_DN get_dn()
+   {
+   // Public key fingerprint of "DST Root CA X3"
+   // This certificate is in the standard "System Roots" of any macOS setup,
+   // serves as the trust root of botan.randombit.net and expires on
+   // Thursday, 30. September 2021 at 16:01:15 Central European Summer Time
+   return read_dn("303f31243022060355040a131b4469676974616c205369676e6174757265"
+                  "20547275737420436f2e311730150603550403130e44535420526f6f7420"
+                  "4341205833");
+   }
+
+std::vector<uint8_t> get_key_id()
+   {
+   // this is the same as the public key SHA1
+   return Botan::hex_decode("c4a7b1a47b2c71fadbe14b9075ffc41560858910");
+   }
+
+Botan::X509_DN get_unknown_dn()
+   {
+   // thats a D-Trust "Test Certificate". It should be fairly likely that
+   // _nobody_ will _ever_ have that in their system keychain
+   // CN: D-TRUST Limited Basic Test PU CA 1-4 2016
+   return read_dn("305b310b300906035504061302444531153013060355040a0c0c442d5472"
+                  "75737420476d62483135303306035504030c2c442d5452555354204c696d"
+                  "6974656420426173696320526f6f74205465737420505520434120312032"
+                  "303135");
+   }
+
+Botan::X509_DN get_skewed_dn()
+   {
+   // This DN contains ASN.1 PrintableString fields that are not 'normalized'
+   // according to Apple's idea of a normalized PrintableString field:
+   //   (1) It has leading and trailing white space
+   //   (2) It contains multiple spaces between 'words'
+   return read_dn("304b312a3028060355040a132120204469676974616c2020205369676e61"
+                  "7475726520547275737420436f2e2020311d301b06035504031314202044"
+                  "5354202020526f6f742043412058332020");
+   }
+
+std::vector<uint8_t> get_unknown_key_id()
+   {
+   // this is the same as the public key SHA1
+   return Botan::hex_decode("785c0b67b536eeacbb2b27cf9123301abe7ab09a");
+   }
+}
+
+#endif
diff --git a/src/tests/test_certstor_utils.h b/src/tests/test_certstor_utils.h
new file mode 100644
index 000000000..f86e87596
--- /dev/null
+++ b/src/tests/test_certstor_utils.h
@@ -0,0 +1,37 @@
+/*
+* (C) 1999-2019 Jack Lloyd
+* (C) 2019      René Meusel
+*
+* Botan is released under the Simplified BSD License (see license.txt)
+*/
+
+#ifndef BOTAN_TEST_CERT_STORE_UTILS_H_
+#define BOTAN_TEST_CERT_STORE_UTILS_H_
+
+#include <string>
+#include <vector>
+
+#include <botan/build.h>
+
+#if defined(BOTAN_HAS_X509_CERTIFICATES)
+
+#include <botan/x509_dn.h>
+
+namespace Botan_Tests {
+
+Botan::X509_DN read_dn(const std::string hex);
+
+Botan::X509_DN get_dn();
+
+std::vector<uint8_t> get_key_id();
+
+Botan::X509_DN get_unknown_dn();
+
+Botan::X509_DN get_skewed_dn();
+
+std::vector<uint8_t> get_unknown_key_id();
+}
+
+#endif
+
+#endif