blob: acaa07d57b96604b9b262ae8e6ac3c007024a190 [file] [log] [blame]
Adrien Béraud612b55b2023-05-29 10:42:04 -04001/*
2 * Copyright (C) 2004-2023 Savoir-faire Linux Inc.
3 *
4 * Author: Adrien Béraud <adrien.beraud@savoirfairelinux.com>
5 * Author: Vsevolod Ivanov <vsevolod.ivanov@savoirfairelinux.com>
6 *
7 * This program is free software; you can redistribute it and/or modify
8 * it under the terms of the GNU General Public License as published by
9 * the Free Software Foundation; either version 3 of the License, or
10 * (at your option) any later version.
11 *
12 * This program is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU General Public License for more details.
16 *
17 * You should have received a copy of the GNU General Public License
18 * along with this program; if not, write to the Free Software
19 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
20 */
21
22#include "certstore.h"
23#include "security_const.h"
24
25#include "fileutils.h"
26
27#include <opendht/thread_pool.h>
28#include <opendht/logger.h>
29
30#include <gnutls/ocsp.h>
31
32#include <thread>
33#include <sstream>
34#include <fmt/format.h>
35
36namespace jami {
37namespace tls {
38
39CertificateStore::CertificateStore(const std::string& path, std::shared_ptr<Logger> logger)
40 : logger_(std::move(logger))
41 , certPath_(fmt::format("{}/certificates", path))
42 , crlPath_(fmt::format("{}/crls", path))
43 , ocspPath_(fmt::format("{}/oscp", path))
44{
45 fileutils::check_dir(certPath_.c_str());
46 fileutils::check_dir(crlPath_.c_str());
47 fileutils::check_dir(ocspPath_.c_str());
48 loadLocalCertificates();
49}
50
51unsigned
52CertificateStore::loadLocalCertificates()
53{
54 std::lock_guard<std::mutex> l(lock_);
55
56 auto dir_content = fileutils::readDirectory(certPath_);
57 unsigned n = 0;
58 for (const auto& f : dir_content) {
59 try {
60 auto crt = std::make_shared<crypto::Certificate>(
61 fileutils::loadFile(certPath_ + DIR_SEPARATOR_CH + f));
62 auto id = crt->getId().toString();
63 auto longId = crt->getLongId().toString();
64 if (id != f && longId != f)
65 throw std::logic_error("Certificate id mismatch");
66 while (crt) {
67 id = crt->getId().toString();
68 longId = crt->getLongId().toString();
69 certs_.emplace(std::move(id), crt);
70 certs_.emplace(std::move(longId), crt);
71 loadRevocations(*crt);
72 crt = crt->issuer;
73 ++n;
74 }
75 } catch (const std::exception& e) {
76 if (logger_)
77 logger_->warn("Remove cert. {}", e.what());
78 remove(fmt::format("{}/{}", certPath_, f).c_str());
79 }
80 }
81 if (logger_)
82 logger_->debug("CertificateStore: loaded {} local certificates.", n);
83 return n;
84}
85
86void
87CertificateStore::loadRevocations(crypto::Certificate& crt) const
88{
89 auto dir = fmt::format("{:s}/{:s}", crlPath_, crt.getId().toString());
90 for (const auto& crl : fileutils::readDirectory(dir)) {
91 try {
92 crt.addRevocationList(std::make_shared<crypto::RevocationList>(
93 fileutils::loadFile(fmt::format("{}/{}", dir, crl))));
94 } catch (const std::exception& e) {
95 if (logger_)
96 logger_->warn("Can't load revocation list: %s", e.what());
97 }
98 }
99 auto ocsp_dir = ocspPath_ + DIR_SEPARATOR_CH + crt.getId().toString();
100 for (const auto& ocsp : fileutils::readDirectory(ocsp_dir)) {
101 try {
102 auto ocsp_filepath = fmt::format("{}/{}", ocsp_dir, ocsp);
103 if (logger_) logger_->debug("Found {:s}", ocsp_filepath);
104 auto serial = crt.getSerialNumber();
105 if (dht::toHex(serial.data(), serial.size()) != ocsp)
106 continue;
107 // Save the response
108 auto ocspBlob = fileutils::loadFile(ocsp_filepath);
109 crt.ocspResponse = std::make_shared<dht::crypto::OcspResponse>(ocspBlob.data(),
110 ocspBlob.size());
111 unsigned int status = crt.ocspResponse->getCertificateStatus();
112 if (status == GNUTLS_OCSP_CERT_GOOD) {
113 if (logger_) logger_->debug("Certificate {:s} has good OCSP status", crt.getId());
114 } else if (status == GNUTLS_OCSP_CERT_REVOKED) {
115 if (logger_) logger_->error("Certificate {:s} has revoked OCSP status", crt.getId());
116 } else if (status == GNUTLS_OCSP_CERT_UNKNOWN) {
117 if (logger_) logger_->error("Certificate {:s} has unknown OCSP status", crt.getId());
118 } else {
119 if (logger_) logger_->error("Certificate {:s} has invalid OCSP status", crt.getId());
120 }
121 } catch (const std::exception& e) {
122 if (logger_)
123 logger_->warn("Can't load OCSP revocation status: {:s}", e.what());
124 }
125 }
126}
127
128std::vector<std::string>
129CertificateStore::getPinnedCertificates() const
130{
131 std::lock_guard<std::mutex> l(lock_);
132
133 std::vector<std::string> certIds;
134 certIds.reserve(certs_.size());
135 for (const auto& crt : certs_)
136 certIds.emplace_back(crt.first);
137 return certIds;
138}
139
140std::shared_ptr<crypto::Certificate>
141CertificateStore::getCertificate(const std::string& k)
142{
143 auto getCertificate_ = [this](const std::string& k) -> std::shared_ptr<crypto::Certificate> {
144 auto cit = certs_.find(k);
145 if (cit == certs_.cend())
146 return {};
147 return cit->second;
148 };
149 std::unique_lock<std::mutex> l(lock_);
150 auto crt = getCertificate_(k);
151 // Check if certificate is complete
152 // If the certificate has been splitted, reconstruct it
153 auto top_issuer = crt;
154 while (top_issuer && top_issuer->getUID() != top_issuer->getIssuerUID()) {
155 if (top_issuer->issuer) {
156 top_issuer = top_issuer->issuer;
157 } else if (auto cert = getCertificate_(top_issuer->getIssuerUID())) {
158 top_issuer->issuer = cert;
159 top_issuer = cert;
160 } else {
161 // In this case, a certificate was not found
162 if (logger_)
163 logger_->warn("Incomplete certificate detected {:s}", k);
164 break;
165 }
166 }
167 return crt;
168}
169
170std::shared_ptr<crypto::Certificate>
171CertificateStore::getCertificateLegacy(const std::string& dataDir, const std::string& k)
172{
173 auto oldPath = fmt::format("{}/certificates/{}", dataDir, k);
174 if (fileutils::isFile(oldPath)) {
175 auto crt = std::make_shared<crypto::Certificate>(oldPath);
176 pinCertificate(crt, true);
177 return crt;
178 }
179 return {};
180}
181
182std::shared_ptr<crypto::Certificate>
183CertificateStore::findCertificateByName(const std::string& name, crypto::NameType type) const
184{
185 std::unique_lock<std::mutex> l(lock_);
186 for (auto& i : certs_) {
187 if (i.second->getName() == name)
188 return i.second;
189 if (type != crypto::NameType::UNKNOWN) {
190 for (const auto& alt : i.second->getAltNames())
191 if (alt.first == type and alt.second == name)
192 return i.second;
193 }
194 }
195 return {};
196}
197
198std::shared_ptr<crypto::Certificate>
199CertificateStore::findCertificateByUID(const std::string& uid) const
200{
201 std::unique_lock<std::mutex> l(lock_);
202 for (auto& i : certs_) {
203 if (i.second->getUID() == uid)
204 return i.second;
205 }
206 return {};
207}
208
209std::shared_ptr<crypto::Certificate>
210CertificateStore::findIssuer(const std::shared_ptr<crypto::Certificate>& crt) const
211{
212 std::shared_ptr<crypto::Certificate> ret {};
213 auto n = crt->getIssuerUID();
214 if (not n.empty()) {
215 if (crt->issuer and crt->issuer->getUID() == n)
216 ret = crt->issuer;
217 else
218 ret = findCertificateByUID(n);
219 }
220 if (not ret) {
221 n = crt->getIssuerName();
222 if (not n.empty())
223 ret = findCertificateByName(n);
224 }
225 if (not ret)
226 return ret;
227 unsigned verify_out = 0;
228 int err = gnutls_x509_crt_verify(crt->cert, &ret->cert, 1, 0, &verify_out);
229 if (err != GNUTLS_E_SUCCESS) {
230 if (logger_)
231 logger_->warn("gnutls_x509_crt_verify failed: {:s}", gnutls_strerror(err));
232 return {};
233 }
234 if (verify_out & GNUTLS_CERT_INVALID)
235 return {};
236 return ret;
237}
238
239static std::vector<crypto::Certificate>
240readCertificates(const std::string& path, const std::string& crl_path)
241{
242 std::vector<crypto::Certificate> ret;
243 if (fileutils::isDirectory(path)) {
244 auto files = fileutils::readDirectory(path);
245 for (const auto& file : files) {
246 auto certs = readCertificates(fmt::format("{}/{}", path, file), crl_path);
247 ret.insert(std::end(ret),
248 std::make_move_iterator(std::begin(certs)),
249 std::make_move_iterator(std::end(certs)));
250 }
251 } else {
252 try {
253 auto data = fileutils::loadFile(path);
254 const gnutls_datum_t dt {data.data(), (unsigned) data.size()};
255 gnutls_x509_crt_t* certs {nullptr};
256 unsigned cert_num {0};
257 gnutls_x509_crt_list_import2(&certs, &cert_num, &dt, GNUTLS_X509_FMT_PEM, 0);
258 for (unsigned i = 0; i < cert_num; i++)
259 ret.emplace_back(certs[i]);
260 gnutls_free(certs);
261 } catch (const std::exception& e) {
262 };
263 }
264 return ret;
265}
266
267void
268CertificateStore::pinCertificatePath(const std::string& path,
269 std::function<void(const std::vector<std::string>&)> cb)
270{
271 dht::ThreadPool::computation().run([&, path, cb]() {
272 auto certs = readCertificates(path, crlPath_);
273 std::vector<std::string> ids;
274 std::vector<std::weak_ptr<crypto::Certificate>> scerts;
275 ids.reserve(certs.size());
276 scerts.reserve(certs.size());
277 {
278 std::lock_guard<std::mutex> l(lock_);
279
280 for (auto& cert : certs) {
281 auto shared = std::make_shared<crypto::Certificate>(std::move(cert));
282 scerts.emplace_back(shared);
283 auto e = certs_.emplace(shared->getId().toString(), shared);
284 ids.emplace_back(e.first->first);
285 e = certs_.emplace(shared->getLongId().toString(), shared);
286 ids.emplace_back(e.first->first);
287 }
288 paths_.emplace(path, std::move(scerts));
289 }
290 if (logger_) logger_->d("CertificateStore: loaded %zu certificates from %s.", certs.size(), path.c_str());
291 if (cb)
292 cb(ids);
293 //emitSignal<libjami::ConfigurationSignal::CertificatePathPinned>(path, ids);
294 });
295}
296
297unsigned
298CertificateStore::unpinCertificatePath(const std::string& path)
299{
300 std::lock_guard<std::mutex> l(lock_);
301
302 auto certs = paths_.find(path);
303 if (certs == std::end(paths_))
304 return 0;
305 unsigned n = 0;
306 for (const auto& wcert : certs->second) {
307 if (auto cert = wcert.lock()) {
308 certs_.erase(cert->getId().toString());
309 ++n;
310 }
311 }
312 paths_.erase(certs);
313 return n;
314}
315
316std::vector<std::string>
317CertificateStore::pinCertificate(const std::vector<uint8_t>& cert, bool local) noexcept
318{
319 try {
320 return pinCertificate(crypto::Certificate(cert), local);
321 } catch (const std::exception& e) {
322 }
323 return {};
324}
325
326std::vector<std::string>
327CertificateStore::pinCertificate(crypto::Certificate&& cert, bool local)
328{
329 return pinCertificate(std::make_shared<crypto::Certificate>(std::move(cert)), local);
330}
331
332std::vector<std::string>
333CertificateStore::pinCertificate(const std::shared_ptr<crypto::Certificate>& cert, bool local)
334{
335 bool sig {false};
336 std::vector<std::string> ids {};
337 {
338 auto c = cert;
339 std::lock_guard<std::mutex> l(lock_);
340 while (c) {
341 bool inserted;
342 auto id = c->getId().toString();
343 auto longId = c->getLongId().toString();
344 decltype(certs_)::iterator it;
345 std::tie(it, inserted) = certs_.emplace(id, c);
346 if (not inserted)
347 it->second = c;
348 std::tie(it, inserted) = certs_.emplace(longId, c);
349 if (not inserted)
350 it->second = c;
351 if (local) {
352 for (const auto& crl : c->getRevocationLists())
353 pinRevocationList(id, *crl);
354 }
355 ids.emplace_back(longId);
356 ids.emplace_back(id);
357 c = c->issuer;
358 sig |= inserted;
359 }
360 if (local) {
361 if (sig)
362 fileutils::saveFile(certPath_ + DIR_SEPARATOR_CH + ids.front(), cert->getPacked());
363 }
364 }
365 //for (const auto& id : ids)
366 // emitSignal<libjami::ConfigurationSignal::CertificatePinned>(id);
367 return ids;
368}
369
370bool
371CertificateStore::unpinCertificate(const std::string& id)
372{
373 std::lock_guard<std::mutex> l(lock_);
374
375 certs_.erase(id);
376 return remove((certPath_ + DIR_SEPARATOR_CH + id).c_str()) == 0;
377}
378
379bool
380CertificateStore::setTrustedCertificate(const std::string& id, TrustStatus status)
381{
382 if (status == TrustStatus::TRUSTED) {
383 if (auto crt = getCertificate(id)) {
384 trustedCerts_.emplace_back(crt);
385 return true;
386 }
387 } else {
388 auto tc = std::find_if(trustedCerts_.begin(),
389 trustedCerts_.end(),
390 [&](const std::shared_ptr<crypto::Certificate>& crt) {
391 return crt->getId().toString() == id;
392 });
393 if (tc != trustedCerts_.end()) {
394 trustedCerts_.erase(tc);
395 return true;
396 }
397 }
398 return false;
399}
400
401std::vector<gnutls_x509_crt_t>
402CertificateStore::getTrustedCertificates() const
403{
404 std::vector<gnutls_x509_crt_t> crts;
405 crts.reserve(trustedCerts_.size());
406 for (auto& crt : trustedCerts_)
407 crts.emplace_back(crt->getCopy());
408 return crts;
409}
410
411void
412CertificateStore::pinRevocationList(const std::string& id,
413 const std::shared_ptr<dht::crypto::RevocationList>& crl)
414{
415 try {
416 if (auto c = getCertificate(id))
417 c->addRevocationList(crl);
418 pinRevocationList(id, *crl);
419 } catch (...) {
420 if (logger_)
421 logger_->warn("Can't add revocation list");
422 }
423}
424
425void
426CertificateStore::pinRevocationList(const std::string& id, const dht::crypto::RevocationList& crl)
427{
428 fileutils::check_dir((crlPath_ + DIR_SEPARATOR_CH + id).c_str());
429 fileutils::saveFile(crlPath_ + DIR_SEPARATOR_CH + id + DIR_SEPARATOR_CH
430 + dht::toHex(crl.getNumber()),
431 crl.getPacked());
432}
433
434void
435CertificateStore::pinOcspResponse(const dht::crypto::Certificate& cert)
436{
437 if (not cert.ocspResponse)
438 return;
439 try {
440 cert.ocspResponse->getCertificateStatus();
441 } catch (dht::crypto::CryptoException& e) {
442 if (logger_) logger_->error("Failed to read certificate status of OCSP response: {:s}", e.what());
443 return;
444 }
445 auto id = cert.getId().toString();
446 auto serial = cert.getSerialNumber();
447 auto serialhex = dht::toHex(serial);
448 auto dir = ocspPath_ + DIR_SEPARATOR_CH + id;
449
450 if (auto localCert = getCertificate(id)) {
451 // Update certificate in the local store if relevant
452 if (localCert.get() != &cert && serial == localCert->getSerialNumber()) {
453 if (logger_) logger_->d("Updating OCSP for certificate %s in the local store", id.c_str());
454 localCert->ocspResponse = cert.ocspResponse;
455 }
456 }
457
458 dht::ThreadPool::io().run([l=logger_,
459 path = dir + DIR_SEPARATOR_CH + serialhex,
460 dir = std::move(dir),
461 id = std::move(id),
462 serialhex = std::move(serialhex),
463 ocspResponse = cert.ocspResponse] {
464 if (l) l->d("Saving OCSP Response of device %s with serial %s", id.c_str(), serialhex.c_str());
465 std::lock_guard<std::mutex> lock(fileutils::getFileLock(path));
466 fileutils::check_dir(dir.c_str());
467 fileutils::saveFile(path, ocspResponse->pack());
468 });
469}
470
471TrustStore::PermissionStatus
472TrustStore::statusFromStr(const char* str)
473{
474 if (!std::strcmp(str, libjami::Certificate::Status::ALLOWED))
475 return PermissionStatus::ALLOWED;
476 if (!std::strcmp(str, libjami::Certificate::Status::BANNED))
477 return PermissionStatus::BANNED;
478 return PermissionStatus::UNDEFINED;
479}
480
481const char*
482TrustStore::statusToStr(TrustStore::PermissionStatus s)
483{
484 switch (s) {
485 case PermissionStatus::ALLOWED:
486 return libjami::Certificate::Status::ALLOWED;
487 case PermissionStatus::BANNED:
488 return libjami::Certificate::Status::BANNED;
489 case PermissionStatus::UNDEFINED:
490 default:
491 return libjami::Certificate::Status::UNDEFINED;
492 }
493}
494
495TrustStatus
496trustStatusFromStr(const char* str)
497{
498 if (!std::strcmp(str, libjami::Certificate::TrustStatus::TRUSTED))
499 return TrustStatus::TRUSTED;
500 return TrustStatus::UNTRUSTED;
501}
502
503const char*
504statusToStr(TrustStatus s)
505{
506 switch (s) {
507 case TrustStatus::TRUSTED:
508 return libjami::Certificate::TrustStatus::TRUSTED;
509 case TrustStatus::UNTRUSTED:
510 default:
511 return libjami::Certificate::TrustStatus::UNTRUSTED;
512 }
513}
514
515bool
516TrustStore::addRevocationList(dht::crypto::RevocationList&& crl)
517{
518 allowed_.add(crl);
519 return true;
520}
521
522bool
523TrustStore::setCertificateStatus(const std::string& cert_id,
524 const TrustStore::PermissionStatus status)
525{
526 return setCertificateStatus(nullptr, cert_id, status, false);
527}
528
529bool
530TrustStore::setCertificateStatus(const std::shared_ptr<crypto::Certificate>& cert,
531 const TrustStore::PermissionStatus status,
532 bool local)
533{
534 return setCertificateStatus(cert, cert->getId().toString(), status, local);
535}
536
537bool
538TrustStore::setCertificateStatus(std::shared_ptr<crypto::Certificate> cert,
539 const std::string& cert_id,
540 const TrustStore::PermissionStatus status,
541 bool local)
542{
543 if (cert)
544 certStore_.pinCertificate(cert, local);
545 std::lock_guard<std::recursive_mutex> lk(mutex_);
546 updateKnownCerts();
547 bool dirty {false};
548 if (status == PermissionStatus::UNDEFINED) {
549 unknownCertStatus_.erase(cert_id);
550 dirty = certStatus_.erase(cert_id);
551 } else {
552 bool allowed = (status == PermissionStatus::ALLOWED);
553 auto s = certStatus_.find(cert_id);
554 if (s == std::end(certStatus_)) {
555 // Certificate state is currently undefined
556 if (not cert)
557 cert = certStore_.getCertificate(cert_id);
558 if (cert) {
559 unknownCertStatus_.erase(cert_id);
560 auto& crt_status = certStatus_[cert_id];
561 if (not crt_status.first)
562 crt_status.first = cert;
563 crt_status.second.allowed = allowed;
564 setStoreCertStatus(*cert, allowed);
565 } else {
566 // Can't find certificate
567 unknownCertStatus_[cert_id].allowed = allowed;
568 }
569 } else {
570 // Certificate is already allowed or banned
571 if (s->second.second.allowed != allowed) {
572 s->second.second.allowed = allowed;
573 if (allowed) // Certificate is re-added after ban, rebuld needed
574 dirty = true;
575 else
576 allowed_.remove(*s->second.first, false);
577 }
578 }
579 }
580 if (dirty)
581 rebuildTrust();
582 return true;
583}
584
585TrustStore::PermissionStatus
586TrustStore::getCertificateStatus(const std::string& cert_id) const
587{
588 std::lock_guard<std::recursive_mutex> lk(mutex_);
589 auto s = certStatus_.find(cert_id);
590 if (s == std::end(certStatus_)) {
591 auto us = unknownCertStatus_.find(cert_id);
592 if (us == std::end(unknownCertStatus_))
593 return PermissionStatus::UNDEFINED;
594 return us->second.allowed ? PermissionStatus::ALLOWED : PermissionStatus::BANNED;
595 }
596 return s->second.second.allowed ? PermissionStatus::ALLOWED : PermissionStatus::BANNED;
597}
598
599std::vector<std::string>
600TrustStore::getCertificatesByStatus(TrustStore::PermissionStatus status) const
601{
602 std::lock_guard<std::recursive_mutex> lk(mutex_);
603 std::vector<std::string> ret;
604 for (const auto& i : certStatus_)
605 if (i.second.second.allowed == (status == TrustStore::PermissionStatus::ALLOWED))
606 ret.emplace_back(i.first);
607 for (const auto& i : unknownCertStatus_)
608 if (i.second.allowed == (status == TrustStore::PermissionStatus::ALLOWED))
609 ret.emplace_back(i.first);
610 return ret;
611}
612
613bool
614TrustStore::isAllowed(const crypto::Certificate& crt, bool allowPublic)
615{
616 // Match by certificate pinning
617 std::lock_guard<std::recursive_mutex> lk(mutex_);
618 bool allowed {allowPublic};
619 for (auto c = &crt; c; c = c->issuer.get()) {
620 auto status = getCertificateStatus(c->getId().toString()); // lock mutex_
621 if (status == PermissionStatus::ALLOWED)
622 allowed = true;
623 else if (status == PermissionStatus::BANNED)
624 return false;
625 }
626
627 // Match by certificate chain
628 updateKnownCerts();
629 auto ret = allowed_.verify(crt);
630 // Unknown issuer (only that) are accepted if allowPublic is true
631 if (not ret
632 and !(allowPublic and ret.result == (GNUTLS_CERT_INVALID | GNUTLS_CERT_SIGNER_NOT_FOUND))) {
633 if (certStore_.logger())
634 certStore_.logger()->warn("%s", ret.toString().c_str());
635 return false;
636 }
637
638 return allowed;
639}
640
641void
642TrustStore::updateKnownCerts()
643{
644 auto i = std::begin(unknownCertStatus_);
645 while (i != std::end(unknownCertStatus_)) {
646 if (auto crt = certStore_.getCertificate(i->first)) {
647 certStatus_.emplace(i->first, std::make_pair(crt, i->second));
648 setStoreCertStatus(*crt, i->second.allowed);
649 i = unknownCertStatus_.erase(i);
650 } else
651 ++i;
652 }
653}
654
655void
656TrustStore::setStoreCertStatus(const crypto::Certificate& crt, bool status)
657{
658 if (status)
659 allowed_.add(crt);
660 else
661 allowed_.remove(crt, false);
662}
663
664void
665TrustStore::rebuildTrust()
666{
667 allowed_ = {};
668 for (const auto& c : certStatus_)
669 setStoreCertStatus(*c.second.first, c.second.second.allowed);
670}
671
672} // namespace tls
673} // namespace jami