diff options
author | René Meusel <[email protected]> | 2019-02-08 14:22:30 +0100 |
---|---|---|
committer | René Meusel <[email protected]> | 2019-02-18 15:54:18 +0100 |
commit | 9c9fd8754bf348a8b6c39395747ea87de6e23259 (patch) | |
tree | c64b8c284704021b212ee4e50982a03a1d804893 | |
parent | c9357f4a2c4e69fb388b2132ffd6431b468d7988 (diff) |
add an adaptor for certificate keychain access on macOS
FIX: build on non-macOS platforms
FIX: move constructor of scoped_CFType
implement Certificate_Store_MacOS::all_subjects()
Refactor: factor out X509 policy creation
FIX: cosmetics
FIX: code cosmetics
* Refactor: use a unique_ptr rather than free()
* Refactor: use a unique_ptr rather than free()
* use std::shared_ptr<> for Pimpl
* double-check opening of cert stores
* add some documentation
* copyright notice dates
shush clang compiler warning -Wcast-qual
See here for details: https://bugs.webkit.org/show_bug.cgi?id=177893
shush -Wmissing-braces on Travis CI (hopefully)
FIX: crash when no certificate matches
FIX: 'normalize' DNs according to Apple's needs
FIX: take early review comments into account
FIX: compiler warning regarding variable shadowing
add timers to the cerstore tests
FIX: catch invalid SHA-1 parameter
FIX: handle ambiguous match in .find_cert_by_pubkey_sha1() according to the super-class's documentation
FIX: API documentation
FIX: cosmetics
make Certificate_Store_MacOS::find_crl_for() return {}
FIX: low-hanging review comments
FIX: more review comments
FIX: compiler warnings
-rw-r--r-- | src/build-data/os/darwin.txt | 2 | ||||
-rw-r--r-- | src/lib/x509/certstor_system_macos/certstor_macos.cpp | 472 | ||||
-rw-r--r-- | src/lib/x509/certstor_system_macos/certstor_macos.h | 81 | ||||
-rw-r--r-- | src/lib/x509/certstor_system_macos/info.txt | 16 | ||||
-rw-r--r-- | src/tests/test_certstor_macos.cpp | 331 |
5 files changed, 902 insertions, 0 deletions
diff --git a/src/build-data/os/darwin.txt b/src/build-data/os/darwin.txt index 3b0147c22..b54dd99f1 100644 --- a/src/build-data/os/darwin.txt +++ b/src/build-data/os/darwin.txt @@ -21,6 +21,8 @@ commoncrypto sockets threads filesystem + +apple_keychain </target_features> <aliases> diff --git a/src/lib/x509/certstor_system_macos/certstor_macos.cpp b/src/lib/x509/certstor_system_macos/certstor_macos.cpp new file mode 100644 index 000000000..3a3652ae1 --- /dev/null +++ b/src/lib/x509/certstor_system_macos/certstor_macos.cpp @@ -0,0 +1,472 @@ +/* +* Certificate Store +* (C) 1999-2019 Jack Lloyd +* (C) 2019 René Meusel +* +* Botan is released under the Simplified BSD License (see license.txt) +*/ + +#include <botan/build.h> + +#include <algorithm> +#include <array> + +#include <CoreFoundation/CoreFoundation.h> +#include <CoreServices/CoreServices.h> + +#include <botan/assert.h> +#include <botan/ber_dec.h> +#include <botan/certstor_macos.h> +#include <botan/data_src.h> +#include <botan/der_enc.h> +#include <botan/exceptn.h> +#include <botan/x509_dn.h> + +namespace Botan { + +namespace { + +/** + * Abstract RAII wrapper for CFTypeRef-style object handles + * All of those xxxRef types are eventually typedefs to void* + */ +template<typename T> +class scoped_CFType + { + public: + explicit scoped_CFType(T value) + : m_value(value) + { + } + + scoped_CFType(const scoped_CFType<T>& rhs) = delete; + scoped_CFType(scoped_CFType<T>&& rhs) : + m_value(std::move(rhs.m_value)) + { + rhs.m_value = nullptr; + } + + ~scoped_CFType() + { + if(m_value) + { + CFRelease(m_value); + } + } + + operator bool() const { return m_value != nullptr; } + + void assign(T value) + { + BOTAN_ASSERT(m_value == nullptr, "scoped_CFType was not set yet"); + m_value = value; + } + + T& get() { return m_value; } + const T& get() const { return m_value; } + + private: + T m_value; + }; + +/** + * Apple's DN parser "normalizes" ASN1 'PrintableString' into upper-case values + * and strips leading, trailing as well as multiple white spaces. + * See: opensource.apple.com/source/Security/Security-55471/sec/Security/SecCertificate.c.auto.html + */ +X509_DN normalize(const X509_DN& dn) + { + X509_DN result; + + for(const auto& rdn : dn.dn_info()) + { + // TODO: C++14 - use std::get<ASN1_String>(), resp. std::get<OID>() + const auto oid = rdn.first; + auto str = rdn.second; + + if(str.tagging() == ASN1_Tag::PRINTABLE_STRING) + { + std::string normalized; + normalized.reserve(str.value().size()); + for(const char c : str.value()) + { + if(c != ' ') + { + // store all 'normal' characters as upper case + normalized.push_back(::toupper(c)); + } + else if(!normalized.empty() && normalized.back() != ' ') + { + // remove leading and squash multiple white spaces + normalized.push_back(c); + } + } + + if(normalized.back() == ' ') + { + // remove potential remaining single trailing white space char + normalized.erase(normalized.end() - 1); + } + + str = ASN1_String(normalized, str.tagging()); + } + + result.add_attribute(oid, str); + } + + return result; + } + +std::string to_string(const CFStringRef cfstring) + { + const char* ccstr = CFStringGetCStringPtr(cfstring, kCFStringEncodingUTF8); + + if(ccstr != nullptr) + { + return std::string(ccstr); + } + + auto utf16_pairs = CFStringGetLength(cfstring); + auto max_utf8_bytes = CFStringGetMaximumSizeForEncoding(utf16_pairs, kCFStringEncodingUTF8); + + std::vector<char> cstr(max_utf8_bytes, '\0'); + auto result = CFStringGetCString(cfstring, + cstr.data(), cstr.size(), + kCFStringEncodingUTF8); + + return (result) ? std::string(cstr.data()) : std::string(); + } + +std::string to_string(const OSStatus status) + { + scoped_CFType<CFStringRef> eCFString( + SecCopyErrorMessageString(status, nullptr)); + return to_string(eCFString.get()); + } + +void check_success(const OSStatus status, const std::string context) + { + if(errSecSuccess == status) + { + return; + } + + throw Internal_Error( + std::string("failed to " + context + ": " + to_string(status))); + } + +template <typename T> +void check_notnull(const scoped_CFType<T>& value, const std::string context) + { + if(value) + { + return; + } + + throw Internal_Error(std::string("failed to ") + context); + } + +SecCertificateRef to_SecCertificateRef(CFTypeRef object) + { + if(!object || CFGetTypeID(object) != SecCertificateGetTypeID()) + { + throw Internal_Error("cannot convert CFTypeRef to SecCertificateRef"); + } + + return static_cast<SecCertificateRef>(const_cast<void*>(object)); + } + +/** + * Create a CFDataRef view over some provided std::vector<uint8_t. The data is + * not copied but the resulting CFDataRef uses the std::vector's buffer as data + * store. Note that the CFDataRef still needs to be manually freed, hence the + * scoped_CFType wrapper. + */ +scoped_CFType<CFDataRef> createCFDataView(const std::vector<uint8_t>& data) + { + return scoped_CFType<CFDataRef>( + CFDataCreateWithBytesNoCopy(kCFAllocatorDefault, + data.data(), + data.size(), + kCFAllocatorNull)); + } + +/** + * Convert a SecCertificateRef object into a Botan::X509_Certificate + */ +std::shared_ptr<const X509_Certificate> readCertificate(SecCertificateRef cert) + { + scoped_CFType<CFDataRef> derData(SecCertificateCopyData(cert)); + check_notnull(derData, "read extracted certificate"); + + // TODO: factor this out into a createDataSourceView() as soon as this class + // gets a move-constructor + const auto data = CFDataGetBytePtr(derData.get()); + const auto length = CFDataGetLength(derData.get()); + + DataSource_Memory ds(data, length); + return std::make_shared<Botan::X509_Certificate>(ds); + } + +} + +/** + * Internal class implementation (i.e. Pimpl) to keep the required platform- + * dependent members of Certificate_Store_MacOS contained in this compilation + * unit. + */ +class Certificate_Store_MacOS_Impl + { + private: + static constexpr const char* system_roots = + "/System/Library/Keychains/SystemRootCertificates.keychain"; + static constexpr const char* system_keychain = + "/Library/Keychains/System.keychain"; + + public: + using Query = std::vector<std::pair<CFStringRef, CFTypeRef>>; + + public: + Certificate_Store_MacOS_Impl() : + m_policy(SecPolicyCreateBasicX509()), + m_system_roots(nullptr), + m_system_chain(nullptr), + m_keychains(nullptr) + { + check_success(SecKeychainOpen(system_roots, &m_system_roots.get()), + "open system root certificates"); + check_success(SecKeychainOpen(system_keychain, &m_system_chain.get()), + "open system keychain"); + check_notnull(m_system_roots, "open system root certificate chain"); + check_notnull(m_system_chain, "open system certificate chain"); + + // m_keychains is merely a convenience list view into all open keychain + // objects. This list is required in prepareQuery(). + std::array<const void*, 2> keychains{{ + m_system_roots.get(), + m_system_chain.get() + }}; + + m_keychains.assign( + CFArrayCreate(kCFAllocatorDefault, + keychains.data(), + keychains.size(), + &kCFTypeArrayCallBacks)); + check_notnull(m_keychains, "initialize keychain array"); + } + + CFArrayRef keychains() const { return m_keychains.get(); } + SecPolicyRef policy() const { return m_policy.get(); } + + /** + * Searches certificates in all opened system keychains. Takes an optional + * \p query that defines filter attributes to be searched for. That query + * is amended by generic attributes for "certificate filtering". + * + * \param query a list of key-value pairs used for filtering + * \returns an array with the resulting certificates or nullptr if + * no matching certificate was found + */ + scoped_CFType<CFArrayRef> search(Query query = Query()) const + { + scoped_CFType<CFDictionaryRef> fullQuery( + prepareQuery(std::move(query))); + check_notnull(fullQuery, "create search query"); + + scoped_CFType<CFArrayRef> result(nullptr); + auto status = SecItemCopyMatching(fullQuery.get(), + (CFTypeRef*)&result.get()); + if(errSecItemNotFound == status) + { + return scoped_CFType<CFArrayRef>(nullptr); // no matches + } + + check_success(status, "look up certificate"); + check_notnull(result, "look up certificate (invalid result value)"); + + return result; + } + + protected: + /** + * Amends the user-provided search query with generic filter rules for + * the associated system keychains. + */ + scoped_CFType<CFDictionaryRef> prepareQuery(Query pairs) const + { + std::vector<CFStringRef> keys({kSecClass, + kSecReturnRef, + kSecMatchLimit, + kSecMatchTrustedOnly, + kSecMatchSearchList, + kSecMatchPolicy}); + std::vector<CFTypeRef> values({kSecClassCertificate, + kCFBooleanTrue, + kSecMatchLimitAll, + kCFBooleanTrue, + keychains(), + policy()}); + keys.reserve(pairs.size() + keys.size()); + values.reserve(pairs.size() + values.size()); + + for(const auto& pair : pairs) + { + keys.push_back(pair.first); + values.push_back(pair.second); + } + + BOTAN_ASSERT_EQUAL(keys.size(), values.size(), "valid key-value pairs"); + + return scoped_CFType<CFDictionaryRef>(CFDictionaryCreate( + kCFAllocatorDefault, (const void**)keys.data(), + (const void**)values.data(), keys.size(), + &kCFTypeDictionaryKeyCallBacks, &kCFTypeDictionaryValueCallBacks)); + } + + private: + scoped_CFType<SecPolicyRef> m_policy; + scoped_CFType<SecKeychainRef> m_system_roots; + scoped_CFType<SecKeychainRef> m_system_chain; + scoped_CFType<CFArrayRef> m_keychains; + }; + + +Certificate_Store_MacOS::Certificate_Store_MacOS() : + m_impl(std::make_shared<Certificate_Store_MacOS_Impl>()) + { + } + +std::vector<X509_DN> Certificate_Store_MacOS::all_subjects() const + { + scoped_CFType<CFArrayRef> result(m_impl->search()); + + if(!result) + { + return {}; // not a single certificate found in the keychain + } + + const auto count = CFArrayGetCount(result.get()); + BOTAN_ASSERT(count > 0, "subject result list contains data"); + + std::vector<X509_DN> output; + output.reserve(count); + for(unsigned int i = 0; i < count; ++i) + { + // Note: Apple's API provides SecCertificateCopyNormalizedSubjectSequence + // which would have saved us from reading a Botan::X509_Certificate, + // however, this function applies the same DN "normalization" as + // stated above. + auto cfCert = to_SecCertificateRef(CFArrayGetValueAtIndex(result.get(), i)); + auto cert = readCertificate(cfCert); + output.emplace_back(cert->subject_dn()); + } + + return output; + } + +std::shared_ptr<const X509_Certificate> +Certificate_Store_MacOS::find_cert(const X509_DN& subject_dn, + const std::vector<uint8_t>& key_id) const + { + const auto certs = find_all_certs(subject_dn, key_id); + + if(certs.empty()) + { + return nullptr; // certificate not found + } + + if(certs.size() != 1) + { + throw Lookup_Error("ambiguous certificate result"); + } + + return certs.front(); + } + +std::vector<std::shared_ptr<const X509_Certificate>> Certificate_Store_MacOS::find_all_certs( + const X509_DN& subject_dn, + const std::vector<uint8_t>& key_id) const + { + std::vector<uint8_t> dn_data; + DER_Encoder encoder(dn_data); + normalize(subject_dn).encode_into(encoder); + + scoped_CFType<CFDataRef> dn_cfdata(createCFDataView(dn_data)); + check_notnull(dn_cfdata, "create DN search object"); + + Certificate_Store_MacOS_Impl::Query query_params( + { + {kSecAttrSubject, dn_cfdata.get()} + }); + + scoped_CFType<CFDataRef> keyid_cfdata(createCFDataView(key_id)); + check_notnull(keyid_cfdata, "create key ID search object"); + if(!key_id.empty()) + { + query_params.push_back({kSecAttrSubjectKeyID, keyid_cfdata.get()}); + } + + scoped_CFType<CFArrayRef> result(m_impl->search(std::move(query_params))); + + if(!result) + { + return {}; // no certificates found + } + + const auto count = CFArrayGetCount(result.get()); + BOTAN_ASSERT(count > 0, "certificate result list contains data"); + + std::vector<std::shared_ptr<const X509_Certificate>> output; + output.reserve(count); + for(unsigned int i = 0; i < count; ++i) + { + auto cfCert = to_SecCertificateRef(CFArrayGetValueAtIndex(result.get(), i)); + output.emplace_back(readCertificate(cfCert)); + } + + return output; + } + +std::shared_ptr<const X509_Certificate> +Certificate_Store_MacOS::find_cert_by_pubkey_sha1(const std::vector<uint8_t>& key_hash) const + { + if(key_hash.size() != 20) + { + throw Invalid_Argument("Certificate_Store_MacOS::find_cert_by_pubkey_sha1 invalid hash"); + } + + scoped_CFType<CFDataRef> key_hash_cfdata(createCFDataView(key_hash)); + check_notnull(key_hash_cfdata, "create key hash search object"); + + scoped_CFType<CFArrayRef> result(m_impl->search( + { + {kSecAttrPublicKeyHash, key_hash_cfdata.get()}, + })); + + if(!result) + { + return nullptr; // no certificate found + } + + const auto count = CFArrayGetCount(result.get()); + BOTAN_ASSERT(count > 0, "certificate result list contains an object"); + + // `count` might be greater than 1, but we'll just select the first match + auto cfCert = to_SecCertificateRef(CFArrayGetValueAtIndex(result.get(), 0)); + return readCertificate(cfCert); + } + +std::shared_ptr<const X509_Certificate> +Certificate_Store_MacOS::find_cert_by_raw_subject_dn_sha256(const std::vector<uint8_t>& subject_hash) const + { + BOTAN_UNUSED(subject_hash); + throw Not_Implemented("Certificate_Store_MacOS::find_cert_by_raw_subject_dn_sha256"); + } + +std::shared_ptr<const X509_CRL> Certificate_Store_MacOS::find_crl_for(const X509_Certificate& subject) const + { + BOTAN_UNUSED(subject); + return {}; + } + +} diff --git a/src/lib/x509/certstor_system_macos/certstor_macos.h b/src/lib/x509/certstor_system_macos/certstor_macos.h new file mode 100644 index 000000000..e7416e631 --- /dev/null +++ b/src/lib/x509/certstor_system_macos/certstor_macos.h @@ -0,0 +1,81 @@ +/* +* Certificate Store +* (C) 1999-2019 Jack Lloyd +* (C) 2019 René Meusel +* +* Botan is released under the Simplified BSD License (see license.txt) +*/ + +#ifndef BOTAN_CERT_STORE_SYSTEM_MACOS_H_ +#define BOTAN_CERT_STORE_SYSTEM_MACOS_H_ + +#include <memory> + +#include <botan/certstor.h> + +namespace Botan { + +class Certificate_Store_MacOS_Impl; + +/** +* Certificate Store that is backed by the system trust store on macOS. This +* opens a handle to the macOS keychain and serves certificate queries directly +* from there. +*/ +class BOTAN_PUBLIC_API(2, 10) Certificate_Store_MacOS final : public Certificate_Store + { + public: + Certificate_Store_MacOS(); + + Certificate_Store_MacOS(const Certificate_Store_MacOS&) = default; + Certificate_Store_MacOS(Certificate_Store_MacOS&&) = default; + Certificate_Store_MacOS& operator=(const Certificate_Store_MacOS&) = default; + Certificate_Store_MacOS& operator=(Certificate_Store_MacOS&&) = 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; + + /** + * @throws Botan::Not_Implemented + */ + 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 the keychain on macOS. This will + * always return an empty list. + */ + std::shared_ptr<const X509_CRL> find_crl_for(const X509_Certificate& subject) const override; + + private: + std::shared_ptr<Certificate_Store_MacOS_Impl> m_impl; + }; + +} + +#endif
\ No newline at end of file diff --git a/src/lib/x509/certstor_system_macos/info.txt b/src/lib/x509/certstor_system_macos/info.txt new file mode 100644 index 000000000..46cc432e5 --- /dev/null +++ b/src/lib/x509/certstor_system_macos/info.txt @@ -0,0 +1,16 @@ +<defines> +CERTSTOR_MACOS -> 20190207 +</defines> + +<os_features> +apple_keychain +</os_features> + +<header:public> +certstor_macos.h +</header:public> + +<frameworks> +darwin -> CoreFoundation +darwin -> Security +</frameworks>
\ No newline at end of file diff --git a/src/tests/test_certstor_macos.cpp b/src/tests/test_certstor_macos.cpp new file mode 100644 index 000000000..243a150da --- /dev/null +++ b/src/tests/test_certstor_macos.cpp @@ -0,0 +1,331 @@ +/* +* (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_MACOS) + +#include <botan/certstor_macos.h> +#include <botan/ber_dec.h> +#include <botan/der_enc.h> +#include <botan/hex.h> + +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"); + + try + { + result.start_timer(); + Botan::Certificate_Store_MacOS unused; + result.end_timer(); + } + catch(std::exception& e) + { + result.test_failure(e.what()); + } + + result.test_success(); + + return result; + } + +Test::Result find_certificate_by_pubkey_sha1() + { + Test::Result result("macOS Certificate Store - Find Certificate by SHA1(pubkey)"); + + try + { + result.start_timer(); + Botan::Certificate_Store_MacOS certstore; + 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_is_eq("exactly one CN", cns.size(), 1ul); + 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::Certificate_Store_MacOS certstore; + certstore.find_cert_by_pubkey_sha1({}); + }); + + return result; + } + +Test::Result find_cert_by_subject_dn() + { + Test::Result result("macOS Certificate Store - Find Certificate by subject DN"); + + try + { + auto dn = get_dn(); + + result.start_timer(); + Botan::Certificate_Store_MacOS certstore; + 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_is_eq("exactly one CN", cns.size(), 1ul); + 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("macOS Certificate Store - Find Certificate by subject DN and key ID"); + + try + { + auto dn = get_dn(); + + result.start_timer(); + Botan::Certificate_Store_MacOS certstore; + 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_is_eq("exactly one CN", cns.size(), 1ul); + 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("macOS Certificate Store - Find Certificates by subject DN and key ID"); + + try + { + auto dn = get_dn(); + + result.start_timer(); + Botan::Certificate_Store_MacOS certstore; + 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_is_eq("exactly one CN", cns.size(), 1ul); + 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("macOS Certificate Store - Find all Certificate Subjects"); + + try + { + result.start_timer(); + Botan::Certificate_Store_MacOS certstore; + 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("macOS 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::Certificate_Store_MacOS certstore; + + 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 certificate_matching_with_dn_normalization() + { + Test::Result result("macOS Certificate Store - normalization of X.509 DN (regression test)"); + + try + { + auto dn = get_skewed_dn(); + + result.start_timer(); + Botan::Certificate_Store_MacOS certstore; + + auto certs = certstore.find_all_certs(dn, std::vector<uint8_t>()); + auto cert = certstore.find_cert(dn, std::vector<uint8_t>()); + result.end_timer(); + + if(result.confirm("find_all_certs did find the skewed DN", !certs.empty()) && + result.confirm("find_cert did find the skewed DN", cert != nullptr)) + { + result.test_eq("it is the correct cert", certs.front()->subject_dn().get_first_attribute("CN"), "DST Root CA X3"); + result.test_eq("it is the correct cert", cert->subject_dn().get_first_attribute("CN"), "DST Root CA X3"); + } + } + catch(std::exception& e) + { + result.test_failure(e.what()); + } + + return result; + } + +class Certstor_macOS_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(certificate_matching_with_dn_normalization()); + + return results; + } + }; + +BOTAN_REGISTER_TEST("certstor_macos", Certstor_macOS_Tests); + +} + +} + +#endif |