| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187 |
- #include "certs/pem_info.hpp"
- #include <algorithm>
- #include <cctype>
- #include <ctime>
- #include <memory>
- #include <openssl/bio.h>
- #include <openssl/err.h>
- #include <openssl/evp.h>
- #include <openssl/pem.h>
- #include <openssl/x509.h>
- #include <openssl/x509v3.h>
- namespace smartbotic::certs {
- using common::Error;
- using common::ErrorCode;
- using common::Result;
- namespace {
- std::string nameToString(X509_NAME* name) {
- if (!name) {
- return {};
- }
- // A BIO rather than X509_NAME_oneline: oneline mangles UTF-8 into escapes,
- // and these strings are shown to a person.
- std::unique_ptr<BIO, decltype(&BIO_free)> bio(BIO_new(BIO_s_mem()), BIO_free);
- if (!bio) {
- return {};
- }
- if (X509_NAME_print_ex(bio.get(), name, 0, XN_FLAG_RFC2253) < 0) {
- return {};
- }
- char* data = nullptr;
- const long len = BIO_get_mem_data(bio.get(), &data);
- if (len <= 0 || !data) {
- return {};
- }
- return std::string(data, static_cast<size_t>(len));
- }
- // ASN1 time to milliseconds since the epoch. Returns 0 when it cannot be read,
- // which the caller reports as "unknown" rather than as 1970.
- int64_t asn1TimeToMs(const ASN1_TIME* when) {
- if (!when) {
- return 0;
- }
- struct tm tm_value{};
- if (ASN1_TIME_to_tm(when, &tm_value) != 1) {
- return 0;
- }
- // timegm, not mktime: certificate times are UTC, and mktime would apply
- // whatever timezone the server happens to be in.
- const time_t seconds = timegm(&tm_value);
- if (seconds == static_cast<time_t>(-1)) {
- return 0;
- }
- return static_cast<int64_t>(seconds) * 1000;
- }
- std::string sha256Fingerprint(X509* cert) {
- unsigned char digest[EVP_MAX_MD_SIZE];
- unsigned int length = 0;
- if (X509_digest(cert, EVP_sha256(), digest, &length) != 1) {
- return {};
- }
- static const char* kHex = "0123456789ABCDEF";
- std::string out;
- out.reserve(length * 3);
- for (unsigned int i = 0; i < length; ++i) {
- if (i > 0) {
- out.push_back(':');
- }
- out.push_back(kHex[digest[i] >> 4]);
- out.push_back(kHex[digest[i] & 0x0F]);
- }
- return out;
- }
- std::string toLowerTrimmed(std::string value) {
- // A trailing dot names the same host - "example.com." is the fully
- // qualified spelling of "example.com" and must not miss a match.
- while (!value.empty() && value.back() == '.') {
- value.pop_back();
- }
- std::transform(value.begin(), value.end(), value.begin(),
- [](unsigned char c) { return static_cast<char>(std::tolower(c)); });
- return value;
- }
- } // namespace
- nlohmann::json CertificateInfo::toJson() const {
- return {
- {"subject", subject},
- {"issuer", issuer},
- {"notBefore", not_before_ms},
- {"notAfter", not_after_ms},
- {"fingerprintSha256", fingerprint_sha256},
- {"isCa", is_ca},
- };
- }
- Result<std::vector<CertificateInfo>> parsePem(const std::string& pem) {
- if (pem.empty()) {
- return Error(ErrorCode::InvalidArgument, "The certificate is empty");
- }
- std::unique_ptr<BIO, decltype(&BIO_free)> bio(
- BIO_new_mem_buf(pem.data(), static_cast<int>(pem.size())), BIO_free);
- if (!bio) {
- return Error(ErrorCode::Internal, "Could not read the certificate");
- }
- std::vector<CertificateInfo> out;
- while (true) {
- X509* raw = PEM_read_bio_X509(bio.get(), nullptr, nullptr, nullptr);
- if (!raw) {
- break;
- }
- std::unique_ptr<X509, decltype(&X509_free)> cert(raw, X509_free);
- CertificateInfo info;
- info.subject = nameToString(X509_get_subject_name(cert.get()));
- info.issuer = nameToString(X509_get_issuer_name(cert.get()));
- info.not_before_ms = asn1TimeToMs(X509_get0_notBefore(cert.get()));
- info.not_after_ms = asn1TimeToMs(X509_get0_notAfter(cert.get()));
- info.fingerprint_sha256 = sha256Fingerprint(cert.get());
- info.is_ca = X509_check_ca(cert.get()) > 0;
- out.push_back(std::move(info));
- }
- // Whatever PEM_read_bio_X509 stopped on is on the error stack. It is not
- // reported: stopping is how the loop ends normally, at the end of the
- // bundle. Leaving it there would poison the next OpenSSL call in this
- // thread with an error it did not cause.
- ERR_clear_error();
- if (out.empty()) {
- return Error(ErrorCode::InvalidArgument,
- "No certificate found. It has to be PEM, starting with "
- "-----BEGIN CERTIFICATE----- . A DER or PKCS#12 file has to be "
- "converted first, and a private key is not a certificate.");
- }
- return out;
- }
- bool hostMatches(const std::string& host, const std::vector<std::string>& patterns) {
- const std::string needle = toLowerTrimmed(host);
- if (needle.empty()) {
- return false;
- }
- for (const auto& raw_pattern : patterns) {
- const std::string pattern = toLowerTrimmed(raw_pattern);
- if (pattern.empty()) {
- continue;
- }
- if (pattern.rfind("*.", 0) == 0) {
- // One leading label, and only one - the same rule certificate
- // wildcards follow, so "*.fsociety.hu" covers api.fsociety.hu and
- // neither fsociety.hu nor a.b.fsociety.hu.
- const std::string suffix = pattern.substr(1); // keeps the dot
- if (needle.size() <= suffix.size()) {
- continue;
- }
- if (needle.compare(needle.size() - suffix.size(), suffix.size(), suffix) != 0) {
- continue;
- }
- const std::string label = needle.substr(0, needle.size() - suffix.size());
- if (label.empty() || label.find('.') != std::string::npos) {
- continue;
- }
- return true;
- }
- if (needle == pattern) {
- return true;
- }
- }
- return false;
- }
- } // namespace smartbotic::certs
|