Botan 3.12.0
Crypto and TLS for C&
certstor_macos.cpp
Go to the documentation of this file.
1/*
2* Certificate Store
3* (C) 1999-2019 Jack Lloyd
4* (C) 2019-2020 René Meusel
5*
6* Botan is released under the Simplified BSD License (see license.txt)
7*/
8
9#include <botan/certstor_macos.h>
10
11#include <botan/assert.h>
12#include <botan/ber_dec.h>
13#include <botan/data_src.h>
14#include <botan/exceptn.h>
15#include <botan/pkix_types.h>
16
17#include <algorithm>
18#include <array>
19
20#define __ASSERT_MACROS_DEFINE_VERSIONS_WITHOUT_UNDERSCORES 0
21#include <CoreFoundation/CoreFoundation.h>
22#include <CoreServices/CoreServices.h>
23
24namespace Botan {
25
26namespace {
27
28/**
29 * Abstract RAII wrapper for CFTypeRef-style object handles
30 * All of those xxxRef types are eventually typedefs to void*
31 */
32template <typename T>
33class scoped_CFType {
34 public:
35 explicit scoped_CFType(T value) : m_value(value) {}
36
37 scoped_CFType(const scoped_CFType<T>& rhs) = delete;
38
39 scoped_CFType(scoped_CFType<T>&& rhs) : m_value(std::move(rhs.m_value)) { rhs.m_value = nullptr; }
40
41 ~scoped_CFType() {
42 if(m_value) {
43 CFRelease(m_value);
44 }
45 }
46
47 operator bool() const { return m_value != nullptr; }
48
49 void assign(T value) {
50 BOTAN_ASSERT(m_value == nullptr, "scoped_CFType was not set yet");
51 m_value = value;
52 }
53
54 T& get() { return m_value; }
55
56 const T& get() const { return m_value; }
57
58 private:
59 T m_value;
60};
61
62/**
63 * Apple's DN parser "normalizes" ASN1 'PrintableString' into upper-case values
64 * and strips leading, trailing as well as multiple white spaces.
65 * See: opensource.apple.com/source/Security/Security-55471/sec/Security/SecCertificate.c.auto.html
66 */
67X509_DN normalize(const X509_DN& dn) {
68 X509_DN result{};
69
70 for(const auto& [oid, str] : dn.dn_info()) {
71 if(str.tagging() == ASN1_Type::PrintableString) {
72 std::string normalized;
73 normalized.reserve(str.value().size());
74
75 for(const char c : str.value()) {
76 if(c != ' ') {
77 // PrintableString is ASCII-only; locale-independent fold to upper case
78 const char up = (c >= 'a' && c <= 'z') ? static_cast<char>(c - ('a' - 'A')) : c;
79 normalized.push_back(up);
80 } else if(!normalized.empty() && normalized.back() != ' ') {
81 // remove leading and squash multiple white spaces
82 normalized.push_back(c);
83 }
84 }
85
86 if(!normalized.empty() && normalized.back() == ' ') {
87 // remove potential remaining single trailing white space char
88 normalized.pop_back();
89 }
90
91 result.add_attribute(oid, ASN1_String(normalized, str.tagging()));
92 } else {
93 result.add_attribute(oid, str);
94 }
95 }
96 return result;
97}
98
99std::vector<uint8_t> normalizeAndSerialize(const X509_DN& dn) {
100 return normalize(dn).DER_encode();
101}
102
103std::string to_string(const CFStringRef cfstring) {
104 const char* ccstr = CFStringGetCStringPtr(cfstring, kCFStringEncodingUTF8);
105
106 if(ccstr != nullptr) {
107 return std::string(ccstr);
108 }
109
110 auto utf16_pairs = CFStringGetLength(cfstring);
111 auto max_utf8_bytes = CFStringGetMaximumSizeForEncoding(utf16_pairs, kCFStringEncodingUTF8);
112
113 std::vector<char> cstr(max_utf8_bytes, '\0');
114 auto result = CFStringGetCString(cfstring, cstr.data(), cstr.size(), kCFStringEncodingUTF8);
115
116 return (result) ? std::string(cstr.data()) : std::string();
117}
118
119std::string to_string(const OSStatus status) {
120 scoped_CFType<CFStringRef> eCFString(SecCopyErrorMessageString(status, nullptr));
121 return to_string(eCFString.get());
122}
123
124void check_success(const OSStatus status, const std::string context) {
125 if(errSecSuccess == status) {
126 return;
127 }
128
129 throw Internal_Error(std::string("failed to " + context + ": " + to_string(status)));
130}
131
132template <typename T>
133void check_notnull(const T& value, const std::string context) {
134 if(value) {
135 return;
136 }
137
138 throw Internal_Error(std::string("failed to ") + context);
139}
140
141} // namespace
142
143/**
144 * Internal class implementation (i.e. Pimpl) to keep the required platform-
145 * dependent members of Certificate_Store_MacOS contained in this compilation
146 * unit.
147 */
148class Certificate_Store_MacOS_Impl {
149 private:
150 static constexpr const char* system_roots = "/System/Library/Keychains/SystemRootCertificates.keychain";
151 static constexpr const char* system_keychain = "/Library/Keychains/System.keychain";
152
153 public:
154 /**
155 * Wraps a list of search query parameters that are later passed into
156 * Apple's certificate store API. The class provides some convenience
157 * functionality and handles the query parameter's data lifetime.
158 */
159 class Query {
160 public:
161 Query() = default;
162 ~Query() = default;
163 Query(Query&& other) = default;
164 Query& operator=(Query&& other) = default;
165
166 Query(const Query& other) = delete;
167 Query& operator=(const Query& other) = delete;
168
169 public:
170 void addParameter(CFStringRef key, CFTypeRef value) {
171 m_keys.emplace_back(key);
172 m_values.emplace_back(value);
173 }
174
175 void addParameter(CFStringRef key, std::vector<uint8_t> value) {
176 const auto& data = m_data_store.emplace_back(std::move(value));
177
178 const auto& data_ref = m_data_refs.emplace_back(
179 CFDataCreateWithBytesNoCopy(kCFAllocatorDefault, data.data(), data.size(), kCFAllocatorNull));
180 check_notnull(data_ref, "create CFDataRef of search object failed");
181
182 addParameter(key, data_ref.get());
183 }
184
185 /**
186 * Amends the user-provided search query with generic filter rules
187 * for the associated system keychains and transforms it into a
188 * representation that can be passed to the Apple keychain API.
189 */
190 scoped_CFType<CFDictionaryRef> prepare(const CFArrayRef& keychains, const SecPolicyRef& policy) {
191 addParameter(kSecClass, kSecClassCertificate);
192 addParameter(kSecReturnRef, kCFBooleanTrue);
193 addParameter(kSecMatchLimit, kSecMatchLimitAll);
194 addParameter(kSecMatchTrustedOnly, kCFBooleanTrue);
195 addParameter(kSecMatchSearchList, keychains);
196 addParameter(kSecMatchPolicy, policy);
197
198 BOTAN_ASSERT_EQUAL(m_keys.size(), m_values.size(), "valid key-value pairs");
199
200 auto query = scoped_CFType<CFDictionaryRef>(CFDictionaryCreate(kCFAllocatorDefault,
201 (const void**)m_keys.data(),
202 (const void**)m_values.data(),
203 m_keys.size(),
204 &kCFTypeDictionaryKeyCallBacks,
205 &kCFTypeDictionaryValueCallBacks));
206 check_notnull(query, "create search query");
207
208 return query;
209 }
210
211 private:
212 using Data = std::vector<std::vector<uint8_t>>;
213 using DataRefs = std::vector<scoped_CFType<CFDataRef>>;
214 using Keys = std::vector<CFStringRef>;
215 using Values = std::vector<CFTypeRef>;
216
217 Data m_data_store; //! makes sure that data parameters are kept alive
218 DataRefs m_data_refs; //! keeps track of CFDataRef objects referring into \p m_data_store
219 Keys m_keys; //! ordered list of search parameter keys
220 Values m_values; //! ordered list of search parameter values
221 };
222
223 public:
224 Certificate_Store_MacOS_Impl() :
225 m_policy(SecPolicyCreateBasicX509()),
226 m_system_roots(nullptr),
227 m_system_chain(nullptr),
228 m_keychains(nullptr) {
231 // macOS 12.0 deprecates 'Custom keychain management', though the API still works.
232 // Ideas for a replacement can be found in the discussion of GH #3122:
233 // https://github.com/randombit/botan/pull/3122
234 check_success(SecKeychainOpen(system_roots, &m_system_roots.get()), "open system root certificates");
235 check_success(SecKeychainOpen(system_keychain, &m_system_chain.get()), "open system keychain");
237 check_notnull(m_system_roots, "open system root certificate chain");
238 check_notnull(m_system_chain, "open system certificate chain");
239
240 // m_keychains is merely a convenience list view into all open keychain
241 // objects. This list is required in prepareQuery().
242 std::array<const void*, 2> keychains{{m_system_roots.get(), m_system_chain.get()}};
243
244 m_keychains.assign(
245 CFArrayCreate(kCFAllocatorDefault, keychains.data(), keychains.size(), &kCFTypeArrayCallBacks));
246 check_notnull(m_keychains, "initialize keychain array");
247 }
248
249 std::optional<X509_Certificate> findOne(Query query) const {
250 query.addParameter(kSecMatchLimit, kSecMatchLimitOne);
251
252 scoped_CFType<CFTypeRef> result(nullptr);
253 search(std::move(query), &result.get());
254
255 if(result)
256 return readCertificate(result.get());
257 else
258 return std::nullopt;
259 }
260
261 std::vector<X509_Certificate> findAll(Query query) const {
262 query.addParameter(kSecMatchLimit, kSecMatchLimitAll);
263
264 scoped_CFType<CFArrayRef> result(nullptr);
265 search(std::move(query), (CFTypeRef*)&result.get());
266
267 std::vector<X509_Certificate> output;
268
269 if(result) {
270 const auto count = CFArrayGetCount(result.get());
271 BOTAN_ASSERT(count > 0, "certificate result list contains data");
272
273 for(unsigned int i = 0; i < count; ++i) {
274 auto cert = CFArrayGetValueAtIndex(result.get(), i);
275 output.emplace_back(readCertificate(cert));
276 }
277 }
278
279 return output;
280 }
281
282 protected:
283 void search(Query query, CFTypeRef* result) const {
284 scoped_CFType<CFDictionaryRef> fullQuery(query.prepare(keychains(), policy()));
285
286 auto status = SecItemCopyMatching(fullQuery.get(), result);
287
288 if(errSecItemNotFound == status) {
289 return; // no matches
290 }
291
292 check_success(status, "look up certificate");
293 check_notnull(result, "look up certificate (invalid result value)");
294 }
295
296 /**
297 * Convert a CFTypeRef object into a X509_Certificate
298 */
299 X509_Certificate readCertificate(CFTypeRef object) const {
300 if(!object || CFGetTypeID(object) != SecCertificateGetTypeID()) {
301 throw Internal_Error("cannot convert CFTypeRef to SecCertificateRef");
302 }
303
304 auto cert = static_cast<SecCertificateRef>(const_cast<void*>(object));
305
306 scoped_CFType<CFDataRef> derData(SecCertificateCopyData(cert));
307 check_notnull(derData, "read extracted certificate");
308
309 const auto data = CFDataGetBytePtr(derData.get());
310 const auto length = CFDataGetLength(derData.get());
311
312 DataSource_Memory ds(data, length);
313 return X509_Certificate(ds);
314 }
315
316 CFArrayRef keychains() const { return m_keychains.get(); }
317
318 SecPolicyRef policy() const { return m_policy.get(); }
319
320 private:
321 scoped_CFType<SecPolicyRef> m_policy;
322 scoped_CFType<SecKeychainRef> m_system_roots;
323 scoped_CFType<SecKeychainRef> m_system_chain;
324 scoped_CFType<CFArrayRef> m_keychains;
325};
326
327//
328// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
329//
330// Implementation of Certificate_Store interface ...
331//
332// - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
333//
334
335Certificate_Store_MacOS::Certificate_Store_MacOS() : m_impl(std::make_shared<Certificate_Store_MacOS_Impl>()) {}
336
337std::vector<X509_DN> Certificate_Store_MacOS::all_subjects() const {
338 // Note: This fetches and parses all certificates in the trust store.
339 // Apple's API provides SecCertificateCopyNormalizedSubjectSequence
340 // which facilitates reading the certificate DN without parsing the
341 // entire certificate via X509_Certificate. However, this
342 // function applies the same DN "normalization" as stated above.
343 const auto certificates = m_impl->findAll({});
344
345 std::vector<X509_DN> output;
346 std::transform(certificates.cbegin(),
347 certificates.cend(),
348 std::back_inserter(output),
349 [](const std::optional<X509_Certificate> cert) { return cert->subject_dn(); });
350
351 return output;
352}
353
354std::optional<X509_Certificate> Certificate_Store_MacOS::find_cert(const X509_DN& subject_dn,
355 const std::vector<uint8_t>& key_id) const {
356 Certificate_Store_MacOS_Impl::Query query;
357 query.addParameter(kSecAttrSubject, normalizeAndSerialize(subject_dn));
358
359 if(!key_id.empty()) {
360 query.addParameter(kSecAttrSubjectKeyID, key_id);
361 }
362
363 return m_impl->findOne(std::move(query));
364}
365
366std::vector<X509_Certificate> Certificate_Store_MacOS::find_all_certs(const X509_DN& subject_dn,
367 const std::vector<uint8_t>& key_id) const {
368 Certificate_Store_MacOS_Impl::Query query;
369 query.addParameter(kSecAttrSubject, normalizeAndSerialize(subject_dn));
370
371 if(!key_id.empty()) {
372 query.addParameter(kSecAttrSubjectKeyID, key_id);
373 }
374
375 return m_impl->findAll(std::move(query));
376}
377
379 const std::vector<uint8_t>& key_hash) const {
380 if(key_hash.size() != 20) {
381 throw Invalid_Argument("Certificate_Store_MacOS::find_cert_by_pubkey_sha1 invalid hash");
382 }
383
384 Certificate_Store_MacOS_Impl::Query query;
385 query.addParameter(kSecAttrPublicKeyHash, key_hash);
386
387 return m_impl->findOne(std::move(query));
388}
389
391 const std::vector<uint8_t>& subject_hash) const {
392 BOTAN_UNUSED(subject_hash);
393 throw Not_Implemented("Certificate_Store_MacOS::find_cert_by_raw_subject_dn_sha256");
394}
395
397 const X509_DN& issuer_dn, std::span<const uint8_t> serial_number) const {
398 Certificate_Store_MacOS_Impl::Query query;
399 /*
400 Directly using kSecAttrSerialNumber can't find the certificate
401 Maybe macOS has a special encoding for the serial number
402
403 query.addParameter(kSecAttrSerialNumber, serial_number);
404 */
405 query.addParameter(kSecAttrIssuer, normalizeAndSerialize(issuer_dn));
406
407 /*
408 This is a temporary solution
409 Use only the issuer DN to find all certificates and filters the serial number, but may affect performance
410 */
411 for(const auto& cert : m_impl->findAll(std::move(query))) {
412 if(std::ranges::equal(cert.serial_number(), serial_number)) {
413 return cert;
414 }
415 }
416
417 return std::nullopt;
418}
419
420std::optional<X509_CRL> Certificate_Store_MacOS::find_crl_for(const X509_Certificate& subject) const {
421 BOTAN_UNUSED(subject);
422 return {};
423}
424
425} // namespace Botan
#define BOTAN_DIAGNOSTIC_POP
Definition api.h:122
#define BOTAN_DIAGNOSTIC_PUSH
Definition api.h:119
#define BOTAN_DIAGNOSTIC_IGNORE_DEPRECATED_DECLARATIONS
Definition api.h:120
#define BOTAN_UNUSED
Definition assert.h:144
#define BOTAN_ASSERT_EQUAL(expr1, expr2, assertion_made)
Definition assert.h:88
#define BOTAN_ASSERT(expr, assertion_made)
Definition assert.h:62
std::optional< X509_Certificate > find_cert_by_pubkey_sha1(const std::vector< uint8_t > &key_hash) const override
std::optional< X509_Certificate > find_cert(const X509_DN &subject_dn, const std::vector< uint8_t > &key_id) const override
std::vector< X509_Certificate > find_all_certs(const X509_DN &subject_dn, const std::vector< uint8_t > &key_id) const override
std::optional< X509_Certificate > find_cert_by_issuer_dn_and_serial_number(const X509_DN &issuer_dn, std::span< const uint8_t > serial_number) const override
std::vector< X509_DN > all_subjects() const override
std::optional< X509_Certificate > find_cert_by_raw_subject_dn_sha256(const std::vector< uint8_t > &subject_hash) const override
std::optional< X509_CRL > find_crl_for(const X509_Certificate &subject) const override
std::string to_string(ErrorType type)
Convert an ErrorType to string.
Definition exceptn.cpp:13