Botan 3.12.0
Crypto and TLS for C&
certstor_windows.cpp
Go to the documentation of this file.
1/*
2* Certificate Store
3* (C) 1999-2021,2026 Jack Lloyd
4* (C) 2018-2019 Patrik Fiedler, Tim Oesterreich
5* (C) 2021 René Meusel
6*
7* Botan is released under the Simplified BSD License (see license.txt)
8*/
9
10#include <botan/certstor_windows.h>
11
12#include <botan/assert.h>
13#include <botan/ber_dec.h>
14#include <botan/hash.h>
15#include <botan/mutex.h>
16#include <botan/pkix_types.h>
17#include <botan/internal/fmt.h>
18#include <botan/internal/x509_cert_cache.h>
19#include <algorithm>
20#include <array>
21#include <functional>
22#include <unordered_map>
23#include <unordered_set>
24#include <vector>
25
26#define NOMINMAX 1
27#define _WINSOCKAPI_ // stop windows.h including winsock.h
28#include <windows.h>
29
30#include <wincrypt.h>
31
32namespace Botan {
33namespace {
34
35constexpr std::array<const char*, 2> cert_store_names{"Root", "CA"};
36
37/**
38 * RAII wrapper for PCCERT_CONTEXT used as iteration state in
39 * CertFindCertificateInStore / CertEnumCertificatesInStore loops.
40 *
41 * The Windows API takes ownership of the previous context when the next one
42 * is requested: passing a non-null PCCERT_CONTEXT as the iteration state
43 * causes the API to free it and return a new one. This wrapper takes care of
44 * freeing the trailing context after the loop ends or on early return.
45 */
46class Cert_Context final {
47 public:
48 Cert_Context() : m_ctx(nullptr) {}
49
50 ~Cert_Context() {
51 if(m_ctx != nullptr) {
52 CertFreeCertificateContext(m_ctx);
53 }
54 }
55
56 Cert_Context(const Cert_Context&) = delete;
57 Cert_Context(Cert_Context&&) = delete;
58 Cert_Context& operator=(const Cert_Context&) = delete;
59 Cert_Context& operator=(Cert_Context&&) = delete;
60
61 bool assign(PCCERT_CONTEXT ctx) {
62 m_ctx = ctx;
63 return m_ctx != nullptr;
64 }
65
66 PCCERT_CONTEXT get() const { return m_ctx; }
67
68 PCCERT_CONTEXT operator->() const { return m_ctx; }
69
70 private:
71 PCCERT_CONTEXT m_ctx;
72};
73
74/**
75 * Iterate every certificate returned by a Windows API walk function across
76 * a sequence of already-open stores. The walk function is called with the
77 * current store and the previously returned context; returning nullptr ends
78 * the current store and advances to the next. The final non-null context is
79 * owned by this object and freed on destruction or on the terminating walk
80 * call that returns nullptr.
81 */
82class Cert_Enumerator final {
83 public:
84 using Next_Fn = std::function<PCCERT_CONTEXT(HCERTSTORE, PCCERT_CONTEXT)>;
85
86 Cert_Enumerator(std::span<const HCERTSTORE> stores, Next_Fn fn) : m_stores(stores), m_get_next(std::move(fn)) {}
87
88 Cert_Enumerator(const Cert_Enumerator&) = delete;
89 Cert_Enumerator(Cert_Enumerator&&) = delete;
90 Cert_Enumerator& operator=(const Cert_Enumerator&) = delete;
91 Cert_Enumerator& operator=(Cert_Enumerator&&) = delete;
92 ~Cert_Enumerator() = default;
93
94 PCCERT_CONTEXT next() {
95 while(m_store_idx < m_stores.size()) {
96 if(m_ctx.assign(m_get_next(m_stores[m_store_idx], m_ctx.get()))) {
97 return m_ctx.get();
98 }
99 // The Windows API freed the previous context when it returned
100 // nullptr, so m_ctx now holds null and we can start the next
101 // store with a null prev.
102 ++m_store_idx;
103 }
104 return nullptr;
105 }
106
107 private:
108 std::span<const HCERTSTORE> m_stores;
109 Next_Fn m_get_next;
110 size_t m_store_idx = 0;
111 Cert_Context m_ctx;
112};
113
114/**
115 * A 20-byte SHA-1 hash of a certificate's SubjectPublicKey, suitable for use
116 * as a key in an unordered associative container.
117 */
118class Pubkey_SHA1 final {
119 public:
120 static constexpr size_t LEN = 20;
121
122 explicit Pubkey_SHA1(std::span<const uint8_t> bytes) {
123 BOTAN_ARG_CHECK(bytes.size() == LEN, "invalid SHA-1 pubkey hash length");
124 std::copy(bytes.begin(), bytes.end(), m_hash.begin());
125 }
126
127 static Pubkey_SHA1 compute(HashFunction& sha1, std::span<const uint8_t> data) {
128 Pubkey_SHA1 result;
129 sha1.update(data);
130 sha1.final(result.m_hash);
131 return result;
132 }
133
134 auto operator<=>(const Pubkey_SHA1&) const = default;
135
136 size_t hash() const noexcept {
137 size_t h = 0;
138 std::memcpy(&h, m_hash.data(), sizeof(h));
139 return h;
140 }
141
142 private:
143 Pubkey_SHA1() = default;
144 std::array<uint8_t, LEN> m_hash = {};
145};
146
147struct Pubkey_SHA1_Hasher {
148 size_t operator()(const Pubkey_SHA1& h) const noexcept { return h.hash(); }
149};
150
151} // namespace
152
153/**
154 * Pimpl for Certificate_Store_Windows.
155 */
156class Certificate_Store_Windows_Impl final {
157 public:
158 // This cache size is arbitrary but probably is sufficient so that the vast
159 // majority of accesses hit the cache
160 static constexpr size_t SystemStore_CertCacheSize = 128;
161
162 Certificate_Store_Windows_Impl() : m_cert_cache(SystemStore_CertCacheSize) {
163 for(const auto* cert_store_name : cert_store_names) {
164 auto* store = CertOpenSystemStoreA(0, cert_store_name);
165 if(store == nullptr) {
166 const auto err = ::GetLastError();
167 close_stores();
168 throw System_Error(fmt("Failed to open Windows certificate store '{}'", cert_store_name), err);
169 }
170
171 CertControlStore(store, 0, CERT_STORE_CTRL_AUTO_RESYNC, nullptr);
172
173 m_stores.push_back(store);
174 }
175 }
176
177 ~Certificate_Store_Windows_Impl() { close_stores(); }
178
179 Certificate_Store_Windows_Impl(const Certificate_Store_Windows_Impl&) = delete;
180 Certificate_Store_Windows_Impl(Certificate_Store_Windows_Impl&&) = delete;
181 Certificate_Store_Windows_Impl& operator=(const Certificate_Store_Windows_Impl&) = delete;
182 Certificate_Store_Windows_Impl& operator=(Certificate_Store_Windows_Impl&&) = delete;
183
184 std::optional<X509_Certificate> find_cert(const X509_DN& subject_dn, const std::vector<uint8_t>& key_id) {
185 const lock_guard_type<mutex_type> lock(m_mutex);
186
187 const auto certs = find_cert_by_dn_and_key_id(subject_dn, key_id, true);
188 if(certs.empty()) {
189 return std::nullopt;
190 }
191 return certs.front();
192 }
193
194 std::vector<X509_Certificate> find_all_certs(const X509_DN& subject_dn, const std::vector<uint8_t>& key_id) {
195 const lock_guard_type<mutex_type> lock(m_mutex);
196
197 return find_cert_by_dn_and_key_id(subject_dn, key_id, false);
198 }
199
200 std::optional<X509_Certificate> find_cert_by_pubkey_sha1(const std::vector<uint8_t>& key_hash) {
201 if(key_hash.size() != Pubkey_SHA1::LEN) {
202 throw Invalid_Argument("Certificate_Store_Windows::find_cert_by_pubkey_sha1 invalid hash");
203 }
204
205 const Pubkey_SHA1 target(key_hash);
206
207 const lock_guard_type<mutex_type> lock(m_mutex);
208
209 if(const auto hit = m_sha1_pubkey_to_cert.find(target); hit != m_sha1_pubkey_to_cert.end()) {
210 return hit->second;
211 }
212
213 // Upper bound the cache, random eviction based on the hashes
214 while(m_sha1_pubkey_to_cert.size() >= 1024) {
215 m_sha1_pubkey_to_cert.erase(m_sha1_pubkey_to_cert.begin());
216 }
217
218 auto sha1 = HashFunction::create_or_throw("SHA-1");
219
220 Cert_Enumerator enumerator(
221 m_stores, [](HCERTSTORE store, PCCERT_CONTEXT prev) { return CertEnumCertificatesInStore(store, prev); });
222
223 while(const auto* ctx = enumerator.next()) {
224 const auto pubkey_blob = ctx->pCertInfo->SubjectPublicKeyInfo.PublicKey;
225 const auto candidate =
226 Pubkey_SHA1::compute(*sha1, {static_cast<uint8_t*>(pubkey_blob.pbData), pubkey_blob.cbData});
227
228 if(candidate == target) {
229 auto result = materialize(ctx->pbCertEncoded, ctx->cbCertEncoded);
230 m_sha1_pubkey_to_cert.emplace(target, result);
231 return result;
232 }
233 }
234
235 // insert a negative query result into the cache
236 m_sha1_pubkey_to_cert.emplace(target, std::nullopt);
237 return std::nullopt;
238 }
239
240 std::optional<X509_Certificate> find_cert_by_issuer_dn_and_serial_number(const X509_DN& issuer_dn,
241 std::span<const uint8_t> serial_number) {
242 const lock_guard_type<mutex_type> lock(m_mutex);
243
244 const std::vector<uint8_t> dn_data = issuer_dn.BER_encode();
245
246 const _CRYPTOAPI_BLOB blob{
247 .cbData = static_cast<DWORD>(dn_data.size()),
248 .pbData = const_cast<BYTE*>(dn_data.data()),
249 };
250
251 auto filter = [&](const X509_Certificate& cert) {
252 return std::ranges::equal(cert.serial_number(), serial_number);
253 };
254
255 const auto certs = search_cert_stores(blob, CERT_FIND_ISSUER_NAME, filter, true);
256 if(certs.empty()) {
257 return std::nullopt;
258 }
259 return certs.front();
260 }
261
262 bool contains(const X509_Certificate& cert) {
263 const auto cert_sha1 = cert.certificate_data_sha1();
264 const auto cert_sha256 = cert.certificate_data_sha256();
265
266 const CRYPT_HASH_BLOB sha1_blob{
267 .cbData = static_cast<DWORD>(cert_sha1.size()),
268 .pbData = const_cast<BYTE*>(cert_sha1.data()),
269 };
270
271 const lock_guard_type<mutex_type> lock(m_mutex);
272
273 Cert_Enumerator enumerator(m_stores, [&sha1_blob](HCERTSTORE store, PCCERT_CONTEXT prev) {
274 return CertFindCertificateInStore(
275 store, X509_ASN_ENCODING | PKCS_7_ASN_ENCODING, 0, CERT_FIND_SHA1_HASH, &sha1_blob, prev);
276 });
277
278 while(const auto* ctx = enumerator.next()) {
279 const auto found = materialize(ctx->pbCertEncoded, ctx->cbCertEncoded);
280 if(std::ranges::equal(found.certificate_data_sha256(), cert_sha256)) {
281 return true;
282 }
283 }
284
285 return false;
286 }
287
288 std::vector<X509_DN> all_subjects() {
289 const lock_guard_type<mutex_type> lock(m_mutex);
290
291 std::vector<X509_DN> subject_dns;
292
293 Cert_Enumerator enumerator(
294 m_stores, [](HCERTSTORE store, PCCERT_CONTEXT prev) { return CertEnumCertificatesInStore(store, prev); });
295
296 while(const auto* ctx = enumerator.next()) {
297 BER_Decoder dec(ctx->pCertInfo->Subject.pbData, ctx->pCertInfo->Subject.cbData);
298 X509_DN dn;
299 dn.decode_from(dec);
300 subject_dns.emplace_back(std::move(dn));
301 }
302
303 return subject_dns;
304 }
305
306 private:
307 void close_stores() {
308 for(auto* store : m_stores) {
309 CertCloseStore(store, 0);
310 }
311 m_stores.clear();
312 }
313
314 X509_Certificate materialize(const BYTE* der, DWORD len) { return m_cert_cache.find_or_insert({der, len}); }
315
316 // Caller must hold m_mutex.
317 std::vector<X509_Certificate> find_cert_by_dn_and_key_id(const X509_DN& subject_dn,
318 const std::vector<uint8_t>& key_id,
319 bool return_on_first_found) {
320 _CRYPTOAPI_BLOB blob{};
321 DWORD find_type = 0;
322 std::vector<uint8_t> dn_data; // has to live until search completes
323
324 // if key_id is available, prefer searching that, as it should be "more unique" than the subject DN
325 if(key_id.empty()) {
326 find_type = CERT_FIND_SUBJECT_NAME;
327 dn_data = subject_dn.DER_encode();
328 blob.cbData = static_cast<DWORD>(dn_data.size());
329 blob.pbData = reinterpret_cast<BYTE*>(dn_data.data());
330 } else {
331 find_type = CERT_FIND_KEY_IDENTIFIER;
332 blob.cbData = static_cast<DWORD>(key_id.size());
333 blob.pbData = const_cast<BYTE*>(key_id.data());
334 }
335
336 auto filter = [&](const X509_Certificate& cert) { return key_id.empty() || cert.subject_dn() == subject_dn; };
337
338 return search_cert_stores(blob, find_type, filter, return_on_first_found);
339 }
340
341 // Caller must hold m_mutex.
342 std::vector<X509_Certificate> search_cert_stores(const _CRYPTOAPI_BLOB& blob,
343 DWORD find_type,
344 const std::function<bool(const X509_Certificate&)>& filter,
345 bool return_on_first_found) {
346 std::vector<X509_Certificate> certs;
347 std::unordered_set<X509_Certificate::Tag, X509_Certificate::TagHash> seen;
348
349 Cert_Enumerator enumerator(m_stores, [&blob, find_type](HCERTSTORE store, PCCERT_CONTEXT prev) {
350 return CertFindCertificateInStore(
351 store, X509_ASN_ENCODING | PKCS_7_ASN_ENCODING, 0, find_type, &blob, prev);
352 });
353
354 while(const auto* ctx = enumerator.next()) {
355 auto cert = materialize(ctx->pbCertEncoded, ctx->cbCertEncoded);
356 if(!seen.insert(cert.tag()).second) {
357 continue;
358 }
359 if(filter(cert)) {
360 certs.push_back(std::move(cert));
361 if(return_on_first_found) {
362 break;
363 }
364 }
365 }
366
367 return certs;
368 }
369
370 mutex_type m_mutex;
371 std::vector<HCERTSTORE> m_stores;
372 std::unordered_map<Pubkey_SHA1, std::optional<X509_Certificate>, Pubkey_SHA1_Hasher> m_sha1_pubkey_to_cert;
373 X509_Certificate_Cache m_cert_cache;
374};
375
376Certificate_Store_Windows::Certificate_Store_Windows() : m_impl(std::make_shared<Certificate_Store_Windows_Impl>()) {}
377
378std::vector<X509_DN> Certificate_Store_Windows::all_subjects() const {
379 return m_impl->all_subjects();
380}
381
382std::optional<X509_Certificate> Certificate_Store_Windows::find_cert(const X509_DN& subject_dn,
383 const std::vector<uint8_t>& key_id) const {
384 return m_impl->find_cert(subject_dn, key_id);
385}
386
387std::vector<X509_Certificate> Certificate_Store_Windows::find_all_certs(const X509_DN& subject_dn,
388 const std::vector<uint8_t>& key_id) const {
389 return m_impl->find_all_certs(subject_dn, key_id);
390}
391
393 const std::vector<uint8_t>& key_hash) const {
394 return m_impl->find_cert_by_pubkey_sha1(key_hash);
395}
396
398 const std::vector<uint8_t>& subject_hash) const {
399 BOTAN_UNUSED(subject_hash);
400 throw Not_Implemented("Certificate_Store_Windows::find_cert_by_raw_subject_dn_sha256");
401}
402
404 const X509_DN& issuer_dn, std::span<const uint8_t> serial_number) const {
405 return m_impl->find_cert_by_issuer_dn_and_serial_number(issuer_dn, serial_number);
406}
407
408std::optional<X509_CRL> Certificate_Store_Windows::find_crl_for(const X509_Certificate& subject) const {
409 // TODO: this could be implemented by using the CertFindCRLInStore function
410 BOTAN_UNUSED(subject);
411 return std::nullopt;
412}
413
415 return m_impl->contains(cert);
416}
417
418} // namespace Botan
#define BOTAN_UNUSED
Definition assert.h:144
#define BOTAN_ARG_CHECK(expr, msg)
Definition assert.h:33
bool contains(const X509_Certificate &cert) 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_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::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::vector< X509_Certificate > find_all_certs(const X509_DN &subject_dn, const std::vector< uint8_t > &key_id) const override
static std::unique_ptr< HashFunction > create_or_throw(std::string_view algo_spec, std::string_view provider="")
Definition hash.cpp:308
X509_Certificate find_or_insert(std::span< const uint8_t > encoding)
noop_mutex mutex_type
Definition mutex.h:37
std::string fmt(std::string_view format, const T &... args)
Definition fmt.h:53
secure_vector< T > lock(const std::vector< T > &in)
Definition secmem.h:80
auto operator<=>(const Strong< T, Tags... > &lhs, const Strong< T, Tags... > &rhs)
lock_guard< T > lock_guard_type
Definition mutex.h:55