pem_info.cpp 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187
  1. #include "certs/pem_info.hpp"
  2. #include <algorithm>
  3. #include <cctype>
  4. #include <ctime>
  5. #include <memory>
  6. #include <openssl/bio.h>
  7. #include <openssl/err.h>
  8. #include <openssl/evp.h>
  9. #include <openssl/pem.h>
  10. #include <openssl/x509.h>
  11. #include <openssl/x509v3.h>
  12. namespace smartbotic::certs {
  13. using common::Error;
  14. using common::ErrorCode;
  15. using common::Result;
  16. namespace {
  17. std::string nameToString(X509_NAME* name) {
  18. if (!name) {
  19. return {};
  20. }
  21. // A BIO rather than X509_NAME_oneline: oneline mangles UTF-8 into escapes,
  22. // and these strings are shown to a person.
  23. std::unique_ptr<BIO, decltype(&BIO_free)> bio(BIO_new(BIO_s_mem()), BIO_free);
  24. if (!bio) {
  25. return {};
  26. }
  27. if (X509_NAME_print_ex(bio.get(), name, 0, XN_FLAG_RFC2253) < 0) {
  28. return {};
  29. }
  30. char* data = nullptr;
  31. const long len = BIO_get_mem_data(bio.get(), &data);
  32. if (len <= 0 || !data) {
  33. return {};
  34. }
  35. return std::string(data, static_cast<size_t>(len));
  36. }
  37. // ASN1 time to milliseconds since the epoch. Returns 0 when it cannot be read,
  38. // which the caller reports as "unknown" rather than as 1970.
  39. int64_t asn1TimeToMs(const ASN1_TIME* when) {
  40. if (!when) {
  41. return 0;
  42. }
  43. struct tm tm_value{};
  44. if (ASN1_TIME_to_tm(when, &tm_value) != 1) {
  45. return 0;
  46. }
  47. // timegm, not mktime: certificate times are UTC, and mktime would apply
  48. // whatever timezone the server happens to be in.
  49. const time_t seconds = timegm(&tm_value);
  50. if (seconds == static_cast<time_t>(-1)) {
  51. return 0;
  52. }
  53. return static_cast<int64_t>(seconds) * 1000;
  54. }
  55. std::string sha256Fingerprint(X509* cert) {
  56. unsigned char digest[EVP_MAX_MD_SIZE];
  57. unsigned int length = 0;
  58. if (X509_digest(cert, EVP_sha256(), digest, &length) != 1) {
  59. return {};
  60. }
  61. static const char* kHex = "0123456789ABCDEF";
  62. std::string out;
  63. out.reserve(length * 3);
  64. for (unsigned int i = 0; i < length; ++i) {
  65. if (i > 0) {
  66. out.push_back(':');
  67. }
  68. out.push_back(kHex[digest[i] >> 4]);
  69. out.push_back(kHex[digest[i] & 0x0F]);
  70. }
  71. return out;
  72. }
  73. std::string toLowerTrimmed(std::string value) {
  74. // A trailing dot names the same host - "example.com." is the fully
  75. // qualified spelling of "example.com" and must not miss a match.
  76. while (!value.empty() && value.back() == '.') {
  77. value.pop_back();
  78. }
  79. std::transform(value.begin(), value.end(), value.begin(),
  80. [](unsigned char c) { return static_cast<char>(std::tolower(c)); });
  81. return value;
  82. }
  83. } // namespace
  84. nlohmann::json CertificateInfo::toJson() const {
  85. return {
  86. {"subject", subject},
  87. {"issuer", issuer},
  88. {"notBefore", not_before_ms},
  89. {"notAfter", not_after_ms},
  90. {"fingerprintSha256", fingerprint_sha256},
  91. {"isCa", is_ca},
  92. };
  93. }
  94. Result<std::vector<CertificateInfo>> parsePem(const std::string& pem) {
  95. if (pem.empty()) {
  96. return Error(ErrorCode::InvalidArgument, "The certificate is empty");
  97. }
  98. std::unique_ptr<BIO, decltype(&BIO_free)> bio(
  99. BIO_new_mem_buf(pem.data(), static_cast<int>(pem.size())), BIO_free);
  100. if (!bio) {
  101. return Error(ErrorCode::Internal, "Could not read the certificate");
  102. }
  103. std::vector<CertificateInfo> out;
  104. while (true) {
  105. X509* raw = PEM_read_bio_X509(bio.get(), nullptr, nullptr, nullptr);
  106. if (!raw) {
  107. break;
  108. }
  109. std::unique_ptr<X509, decltype(&X509_free)> cert(raw, X509_free);
  110. CertificateInfo info;
  111. info.subject = nameToString(X509_get_subject_name(cert.get()));
  112. info.issuer = nameToString(X509_get_issuer_name(cert.get()));
  113. info.not_before_ms = asn1TimeToMs(X509_get0_notBefore(cert.get()));
  114. info.not_after_ms = asn1TimeToMs(X509_get0_notAfter(cert.get()));
  115. info.fingerprint_sha256 = sha256Fingerprint(cert.get());
  116. info.is_ca = X509_check_ca(cert.get()) > 0;
  117. out.push_back(std::move(info));
  118. }
  119. // Whatever PEM_read_bio_X509 stopped on is on the error stack. It is not
  120. // reported: stopping is how the loop ends normally, at the end of the
  121. // bundle. Leaving it there would poison the next OpenSSL call in this
  122. // thread with an error it did not cause.
  123. ERR_clear_error();
  124. if (out.empty()) {
  125. return Error(ErrorCode::InvalidArgument,
  126. "No certificate found. It has to be PEM, starting with "
  127. "-----BEGIN CERTIFICATE----- . A DER or PKCS#12 file has to be "
  128. "converted first, and a private key is not a certificate.");
  129. }
  130. return out;
  131. }
  132. bool hostMatches(const std::string& host, const std::vector<std::string>& patterns) {
  133. const std::string needle = toLowerTrimmed(host);
  134. if (needle.empty()) {
  135. return false;
  136. }
  137. for (const auto& raw_pattern : patterns) {
  138. const std::string pattern = toLowerTrimmed(raw_pattern);
  139. if (pattern.empty()) {
  140. continue;
  141. }
  142. if (pattern.rfind("*.", 0) == 0) {
  143. // One leading label, and only one - the same rule certificate
  144. // wildcards follow, so "*.fsociety.hu" covers api.fsociety.hu and
  145. // neither fsociety.hu nor a.b.fsociety.hu.
  146. const std::string suffix = pattern.substr(1); // keeps the dot
  147. if (needle.size() <= suffix.size()) {
  148. continue;
  149. }
  150. if (needle.compare(needle.size() - suffix.size(), suffix.size(), suffix) != 0) {
  151. continue;
  152. }
  153. const std::string label = needle.substr(0, needle.size() - suffix.size());
  154. if (label.empty() || label.find('.') != std::string::npos) {
  155. continue;
  156. }
  157. return true;
  158. }
  159. if (needle == pattern) {
  160. return true;
  161. }
  162. }
  163. return false;
  164. }
  165. } // namespace smartbotic::certs