Skip to content

Certificate Decoder — C++ source

Paste a PEM X.509 certificate and see its subject, issuer, SAN, validity, key usage, fingerprints, and chain details in a human-readable format.

This is the C++ implementation — the same logic the interactive tool runs, in a shareable, citable form.

// cert-decoder — Pure ASN.1 DER parser + X.509 certificate decoder.
//
// Language: C++ (C++17, standard library only)
// Ported from src/lib/cert-decoder.ts (the canonical TypeScript implementation).
// display source — part of CosmoDev's polyglot tool pages.
//
// Like the TS reference, this port needs no X.509 library: DER is
// deterministic and is parsed sequentially. A small recursive-descent TLV
// reader walks the certificate, and the same OID tables surface human-readable
// subject, key, signature and extension facts. Dates are read into
// std::chrono::sys_seconds (UTC) so expiry checks are plain comparisons.

#include <chrono>
#include <cmath>
#include <cstdint>
#include <map>
#include <optional>
#include <regex>
#include <stdexcept>
#include <string>
#include <vector>

namespace certdecoder {

using Bytes = std::vector<uint8_t>;

// ── OID name map ───────────────────────────────────────────────────────────

const std::map<std::string, std::string>& oidNames() {
  static const std::map<std::string, std::string> NAMES = {
      {"1.2.840.113549.1.1.1", "RSA"},
      {"1.2.840.113549.1.1.5", "SHA-1 with RSA"},
      {"1.2.840.113549.1.1.11", "SHA-256 with RSA"},
      {"1.2.840.113549.1.1.12", "SHA-384 with RSA"},
      {"1.2.840.113549.1.1.13", "SHA-512 with RSA"},
      {"1.2.840.113549.1.1.14", "SHA-224 with RSA"},
      {"1.2.840.10045.2.1", "ECDSA"},
      {"1.2.840.10045.4.3.2", "ECDSA with SHA-256"},
      {"1.2.840.10045.4.3.3", "ECDSA with SHA-384"},
      {"1.2.840.10045.4.3.4", "ECDSA with SHA-512"},
      {"1.3.14.3.2.29", "SHA-1 with RSA (OIW)"},
      {"2.5.4.3", "CN"},
      {"2.5.4.6", "C"},
      {"2.5.4.7", "L"},
      {"2.5.4.8", "ST"},
      {"2.5.4.10", "O"},
      {"2.5.4.11", "OU"},
      {"2.5.29.14", "Subject Key Identifier"},
      {"2.5.29.15", "Key Usage"},
      {"2.5.29.17", "Subject Alternative Name"},
      {"2.5.29.19", "Basic Constraints"},
      {"2.5.29.35", "Authority Key Identifier"},
      {"2.5.29.37", "Extended Key Usage"},
      {"1.3.6.1.5.5.7.1.1", "Authority Information Access"},
      {"1.3.6.1.5.5.7.3.1", "serverAuth"},
      {"1.3.6.1.5.5.7.3.2", "clientAuth"},
      {"1.3.6.1.5.5.7.3.3", "codeSigning"},
      {"1.3.6.1.5.5.7.3.4", "emailProtection"},
      {"1.3.6.1.5.5.7.3.8", "timeStamping"},
      {"1.2.840.113549.1.9.14", "Extension Request"},
      {"1.2.840.113549.1.9.1", "emailAddress"},
      {"1.3.6.1.4.1.11129.2.1.17", "CT Precertificate SCTs"},
      {"1.3.6.1.5.5.7.1.3", "CRL Distribution Points"},
      {"1.3.6.1.4.1.311.21.7", "Microsoft Certificate Template"},
  };
  return NAMES;
}

/** EC named-curve OIDs. */
const std::map<std::string, std::string>& ecCurveNames() {
  static const std::map<std::string, std::string> CURVES = {
      {"1.2.840.10045.3.1.7", "P-256"},
      {"1.3.132.0.34", "P-384"},
      {"1.3.132.0.35", "P-512"},
      {"1.3.132.0.10", "secp256k1"},
  };
  return CURVES;
}

/** Key Usage bit names, MSB-first inside each octet (RFC 5280 §4.2.1.3). */
const std::vector<std::string>& keyUsageBits() {
  static const std::vector<std::string> BITS = {
      "digitalSignature", "nonRepudiation", "keyEncipherment", "dataEncipherment",
      "keyAgreement",      "keyCertSign",    "cRLSign",         "encipherOnly",
      "decipherOnly",
  };
  return BITS;
}

// ── ASN.1 DER types ────────────────────────────────────────────────────────

enum class TagClass { Universal, Context, Application, Private };

struct ASN1Node {
  TagClass tagClass = TagClass::Universal;
  bool constructed = false;
  uint32_t tagNumber = 0;
  Bytes rawValue; // value bytes (decoded content)
  size_t offset = 0; // offset into the original buffer
  std::optional<std::vector<ASN1Node>> children;
};

// ── DER parsing ────────────────────────────────────────────────────────────

/** Sequential TLV reader over one DER buffer. */
class DERParser {
public:
  explicit DERParser(Bytes buf) : buf_(std::move(buf)) {}

  bool exhausted() const { return pos_ >= buf_.size(); }

  /** Read the next TLV node. */
  ASN1Node readNode() {
    const size_t offset = pos_;
    const uint8_t byte0 = readByte();

    const auto tagClass = (byte0 & 0xc0) == 0x00   ? TagClass::Universal
                          : (byte0 & 0xc0) == 0x40 ? TagClass::Application
                          : (byte0 & 0xc0) == 0x80 ? TagClass::Context
                                                    : TagClass::Private;
    const bool constructed = (byte0 & 0x20) != 0;
    uint32_t tagNumber = byte0 & 0x1f;

    // Long-form tag (tag number >= 31)
    if (tagNumber == 0x1f) {
      tagNumber = 0;
      uint8_t b;
      do {
        b = readByte();
        tagNumber = (tagNumber << 7) | (b & 0x7f);
      } while (b & 0x80);
    }

    const size_t length = readLength();
    if (pos_ + length > buf_.size()) {
      throw std::runtime_error("Truncated DER: need " + std::to_string(length) +
                               " bytes at offset " + std::to_string(pos_) + ", but only " +
                               std::to_string(buf_.size() - pos_) + " remain");
    }

    ASN1Node node;
    node.tagClass = tagClass;
    node.constructed = constructed;
    node.tagNumber = tagNumber;
    node.offset = offset;
    node.rawValue.assign(buf_.begin() + pos_, buf_.begin() + pos_ + length);
    pos_ += length;

    // Parse children for constructed types
    if (constructed && !node.rawValue.empty()) {
      DERParser child(node.rawValue);
      std::vector<ASN1Node> children;
      while (!child.exhausted()) children.push_back(child.readNode());
      node.children = std::move(children);
    }
    return node;
  }

private:
  uint8_t readByte() {
    if (pos_ >= buf_.size()) throw std::runtime_error("Unexpected end of DER data");
    return buf_[pos_++];
  }

  size_t readLength() {
    const uint8_t first = readByte();
    if (first < 0x80) return first;

    const uint8_t numBytes = first & 0x7f;
    if (numBytes == 0) throw std::runtime_error("Indefinite length is not supported in DER");
    if (numBytes > 4) throw std::runtime_error("Length too large: " + std::to_string(numBytes) + " bytes");

    size_t len = 0;
    for (uint8_t i = 0; i < numBytes; i++) len = (len << 8) | readByte();
    return len;
  }

  Bytes buf_;
  size_t pos_ = 0;
};

// ── OID decoding ───────────────────────────────────────────────────────────

/** Decode OID content bytes to dotted form ("2.5.4.3"). */
std::string decodeOID(const Bytes& bytes) {
  if (bytes.empty()) throw std::runtime_error("Empty OID");
  std::vector<std::string> parts;
  parts.push_back(std::to_string(bytes[0] / 40));
  parts.push_back(std::to_string(bytes[0] % 40));

  uint64_t value = 0;
  for (size_t i = 1; i < bytes.size(); i++) {
    const uint8_t b = bytes[i];
    value = (value << 7) | (b & 0x7f);
    if ((b & 0x80) == 0) {
      parts.push_back(std::to_string(value));
      value = 0;
    }
  }
  std::string out;
  for (size_t i = 0; i < parts.size(); i++) {
    if (i > 0) out += '.';
    out += parts[i];
  }
  return out;
}

/** Human-readable OID name, or the dotted form when unknown. */
std::string oidName(const std::string& oid) {
  const auto& names = oidNames();
  const auto it = names.find(oid);
  return it != names.end() ? it->second : oid;
}

// ── RDN (Relative Distinguished Name) helpers ──────────────────────────────

struct RDNAttribute {
  std::string type; // short name like "CN", "O", ...
  std::string value;
};

/** Decode node content as text; falls back to hex when not valid UTF-8. */
std::string readString(const ASN1Node& node) {
  const uint32_t tag = node.tagNumber;
  if (tag == 12 || tag == 19 || tag == 22 || tag == 30 || tag == 36) {
    // UTF8String(12), PrintableString(19), IA5String(22), BMPString(30), ... —
    // all decode as text (BMPString is UTF-16; shown raw here like the TS ref).
    return std::string(node.rawValue.begin(), node.rawValue.end());
  }
  // Fallback: try UTF-8 (reject continuation bytes without a lead byte).
  bool validUtf8 = true;
  for (size_t i = 0; i < node.rawValue.size() && validUtf8; i++) {
    const uint8_t b = node.rawValue[i];
    if (b < 0x80) continue;
    const size_t extra = b >= 0xf0 ? 3 : b >= 0xe0 ? 2 : b >= 0xc0 ? 1 : 99;
    if (extra == 99 || i + extra >= node.rawValue.size()) {
      validUtf8 = false;
      break;
    }
    i += extra;
  }
  if (validUtf8) return std::string(node.rawValue.begin(), node.rawValue.end());
  // Last resort: hex
  static const char* HEX = "0123456789abcdef";
  std::string out;
  for (size_t i = 0; i < node.rawValue.size(); i++) {
    if (i > 0) out += ':';
    out += HEX[node.rawValue[i] >> 4];
    out += HEX[node.rawValue[i] & 0x0f];
  }
  return out;
}

/** Name (RDNSequence) → ordered attribute list. */
std::vector<RDNAttribute> parseRDN(const ASN1Node& node) {
  std::vector<RDNAttribute> attrs;
  if (!node.children) return attrs;

  for (const auto& rdnSet : *node.children) {
    if (!rdnSet.children) continue;
    for (const auto& attrSeq : *rdnSet.children) {
      if (!attrSeq.children || attrSeq.children->size() < 2) continue;
      const std::string oid = decodeOID(attrSeq.children->at(0).rawValue);
      attrs.push_back({oidName(oid), readString(attrSeq.children->at(1))});
    }
  }
  return attrs;
}

/** "CN=example.com, O=CosmoLabs" — the conventional one-line DN form. */
std::string formatDN(const std::vector<RDNAttribute>& attrs) {
  std::string out;
  for (size_t i = 0; i < attrs.size(); i++) {
    if (i > 0) out += ", ";
    out += attrs[i].type + "=" + attrs[i].value;
  }
  return out;
}

// ── Time helpers ───────────────────────────────────────────────────────────

using TimePoint = std::chrono::sys_seconds;

/** Parse a 2-digit year / month / ... field triple into a UTC time point. */
static TimePoint utcTime(int year, int month, int day, int hour, int minute, int second) {
  return std::chrono::sys_days(std::chrono::year(year) / month / day) +
         std::chrono::hours(hour) + std::chrono::minutes(minute) + std::chrono::seconds(second);
}

/** UTCTime (tag 23) / GeneralizedTime (tag 24) → UTC time point. */
TimePoint parseTime(const ASN1Node& node) {
  const std::string str = readString(node);
  static const std::regex UTCTIME_RE(R"(^(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})Z$)");
  static const std::regex GENTIME_RE(R"(^(\d{4})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})Z$)");

  std::smatch m;
  if (node.tagNumber == 23 && std::regex_match(str, m, UTCTIME_RE)) {
    int year = std::stoi(m[1]);
    year += year >= 50 ? 1900 : 2000;
    return utcTime(year, std::stoi(m[2]), std::stoi(m[3]), std::stoi(m[4]), std::stoi(m[5]),
                   std::stoi(m[6]));
  }
  if (node.tagNumber == 24 && std::regex_match(str, m, GENTIME_RE)) {
    return utcTime(std::stoi(m[1]), std::stoi(m[2]), std::stoi(m[3]), std::stoi(m[4]),
                   std::stoi(m[5]), std::stoi(m[6]));
  }
  throw std::runtime_error("Unknown or invalid time value: " + str);
}

// ── Extension parsing ──────────────────────────────────────────────────────

struct Extensions {
  std::optional<bool> basicConstraintsCA;
  std::optional<std::vector<std::string>> keyUsage;
  std::optional<std::vector<std::string>> extKeyUsage;
  std::vector<std::string> sanDNS;
  std::vector<std::string> sanIP;
  std::vector<std::string> sanEmail;
};

/** Key Usage BIT STRING → set flag names (honours the unused-bits count). */
std::vector<std::string> parseBitString(const ASN1Node& node) {
  if (node.rawValue.size() < 2) return {};
  const uint8_t unusedBits = node.rawValue[0];
  const uint8_t* octets = node.rawValue.data() + 1;
  const size_t octetCount = node.rawValue.size() - 1;
  std::vector<std::string> flags;

  const auto& bits = keyUsageBits();
  for (size_t i = 0; i < bits.size(); i++) {
    const size_t octetIdx = i / 8;
    const int bitIdx = 7 - static_cast<int>(i % 8);
    if (octetIdx < octetCount && (octets[octetIdx] & (1 << bitIdx)) != 0) {
      flags.push_back(bits[i]);
    }
  }
  if (unusedBits > 0 && !flags.empty()) {
    const size_t totalBits = octetCount * 8 - unusedBits;
    while (flags.size() > totalBits) flags.pop_back();
  }
  return flags;
}

/** Walk an extensions SEQUENCE's children and pull the facts we display. */
Extensions parseExtensions(const std::vector<ASN1Node>& nodes) {
  Extensions ext;

  for (const auto& extNode : nodes) {
    if (!extNode.children || extNode.children->size() < 2) continue;
    const std::string oid = decodeOID(extNode.children->at(0).rawValue);

    // Skip the critical BOOLEAN when present
    size_t valueIdx = 1;
    if (extNode.children->size() >= 3 && extNode.children->at(1).tagNumber == 1 &&
        extNode.children->at(1).tagClass == TagClass::Universal) {
      valueIdx = 2;
    }

    const Bytes& octetContent = extNode.children->at(valueIdx).rawValue;
    if (octetContent.empty()) continue;

    DERParser inner(octetContent);
    if (inner.exhausted()) continue;
    const ASN1Node content = inner.readNode();

    if (oid == "2.5.29.19") { // Basic Constraints
      ext.basicConstraintsCA = false;
      if (content.children && !content.children->empty()) {
        const auto& boolVal = content.children->front();
        if (boolVal.tagNumber == 1 && boolVal.rawValue.size() == 1 && boolVal.rawValue[0] == 0xff) {
          ext.basicConstraintsCA = true;
        }
      }
    } else if (oid == "2.5.29.15") { // Key Usage — BIT STRING
      ext.keyUsage = parseBitString(content);
    } else if (oid == "2.5.29.37") { // Extended Key Usage
      ext.extKeyUsage = std::vector<std::string>{};
      if (content.children) {
        for (const auto& child : *content.children) {
          ext.extKeyUsage->push_back(oidName(decodeOID(child.rawValue)));
        }
      }
    } else if (oid == "2.5.29.17") { // Subject Alternative Name
      if (content.children) {
        for (const auto& child : *content.children) {
          if (child.tagClass != TagClass::Context) continue;
          if (child.tagNumber == 2) { // dNSName
            ext.sanDNS.push_back(readString(child));
          } else if (child.tagNumber == 7) { // iPAddress
            const Bytes& ip = child.rawValue;
            if (ip.size() == 4) {
              ext.sanIP.push_back(std::to_string(ip[0]) + "." + std::to_string(ip[1]) + "." +
                                  std::to_string(ip[2]) + "." + std::to_string(ip[3]));
            } else if (ip.size() == 16) { // IPv6 — standard hex groups
              static const char* HEX = "0123456789abcdef";
              std::string joined;
              for (size_t i = 0; i < 16; i += 2) {
                const unsigned group = (ip[i] << 8) | ip[i + 1];
                if (i > 0) joined += ':';
                if (group >> 8 != 0) joined += HEX[group >> 8];
                joined += HEX[group & 0x0f];
              }
              ext.sanIP.push_back(joined);
            }
          } else if (child.tagNumber == 1) { // rfc822Name
            ext.sanEmail.push_back(readString(child));
          }
        }
      }
    }
  }
  return ext;
}

// ── Public types ───────────────────────────────────────────────────────────

struct CertificateInfo {
  std::vector<RDNAttribute> subject; // parsed subject RDN attributes
  std::string subjectDN; // formatted subject DN string
  std::vector<RDNAttribute> issuer; // parsed issuer RDN attributes
  std::string issuerDN; // formatted issuer DN string
  TimePoint notBefore{}; // not-before, UTC
  TimePoint notAfter{}; // not-after, UTC
  std::string serialNumber; // serial number as colon-separated hex
  std::string signatureAlgorithm; // human-readable name
  std::string keyAlgorithm; // public key algorithm name
  size_t keySize = 0; // public key size in bits
  std::optional<bool> basicConstraintsCA; // Basic Constraints CA flag
  std::vector<std::string> keyUsage; // Key Usage flags
  std::vector<std::string> extKeyUsage; // Extended Key Usage purposes
  std::vector<std::string> sanDNS; // SAN DNS entries
  std::vector<std::string> sanIP; // SAN IP entries
  std::vector<std::string> sanEmail; // SAN email entries
  unsigned version = 0; // 0=v1, 1=v2, 2=v3
  Bytes rawDER; // raw DER bytes (for fingerprinting)
};

// ── PEM handling ───────────────────────────────────────────────────────────

const std::regex& pemRegex() {
  static const std::regex RE(
      R"(-----BEGIN\s+CERTIFICATE-----\s*\r?\n([\s\S]*?)\r?\n-----END\s+CERTIFICATE-----)");
  return RE;
}

/** Standard Base64 (PEM body) → bytes. Throws on non-alphabet input. */
Bytes base64Decode(const std::string& b64) {
  static const std::string CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
  auto valueOf = [&](char c) -> int {
    const size_t v = CHARS.find(c);
    return v == std::string::npos ? -1 : static_cast<int>(v);
  };
  Bytes out;
  uint32_t buffer = 0;
  int bits = 0;
  for (char c : b64) {
    if (c == '=' || c == '\n' || c == '\r' || c == ' ' || c == '\t') continue;
    const int v = valueOf(c);
    if (v < 0) throw std::runtime_error("Invalid Base64 character in PEM body");
    buffer = (buffer << 6) | static_cast<uint32_t>(v);
    bits += 6;
    if (bits >= 8) {
      bits -= 8;
      out.push_back(static_cast<uint8_t>((buffer >> bits) & 0xff));
    }
  }
  return out;
}

/** Extract every PEM CERTIFICATE block's Base64 body from `pem`. */
std::vector<std::string> pemBlocks(const std::string& pem) {
  std::vector<std::string> blocks;
  auto begin = std::sregex_iterator(pem.begin(), pem.end(), pemRegex());
  auto end = std::sregex_iterator();
  for (auto it = begin; it != end; ++it) blocks.push_back((*it)[1].str());
  return blocks;
}

Bytes pemToDER(const std::string& body) { return base64Decode(body); }

// ── Main decoder ───────────────────────────────────────────────────────────

/** Decode the first PEM certificate in `pem` into a CertificateInfo. */
CertificateInfo decodeCertificate(const std::string& pem) {
  if (pem.empty() || pem.find_first_not_of(" \t\r\n") == std::string::npos) {
    throw std::runtime_error("Empty input — paste a PEM certificate");
  }

  const std::vector<std::string> blocks = pemBlocks(pem);
  if (blocks.empty()) {
    throw std::runtime_error("No PEM certificate block found — expected -----BEGIN CERTIFICATE-----");
  }

  const Bytes der = pemToDER(blocks.front());

  // Parse outer SEQUENCE
  DERParser parser(der);
  const ASN1Node certSeq = parser.readNode();
  if (!certSeq.children || certSeq.children->size() < 3) {
    throw std::runtime_error(
        "Invalid certificate structure: expected TBSCertificate, signatureAlgorithm, signatureValue");
  }

  const ASN1Node& tbs = certSeq.children->at(0);
  const ASN1Node& sigAlgNode = certSeq.children->at(1);
  const std::string sigAlgOID =
      sigAlgNode.children ? decodeOID(sigAlgNode.children->front().rawValue) : "";

  // Parse TBSCertificate
  if (!tbs.children || tbs.children->size() < 7) {
    throw std::runtime_error("Invalid TBSCertificate structure");
  }

  const std::vector<ASN1Node>& tbsChildren = *tbs.children;
  size_t idx = 0;

  // Version (explicit context [0])
  unsigned version = 0; // default v1
  if (tbsChildren[idx].tagClass == TagClass::Context && tbsChildren[idx].tagNumber == 0) {
    if (tbsChildren[idx].children && !tbsChildren[idx].children->empty()) {
      const ASN1Node& versionNode = tbsChildren[idx].children->front();
      if (versionNode.rawValue.size() == 1) version = versionNode.rawValue[0];
    }
    idx++;
  }

  // Serial Number
  const ASN1Node& serialNode = tbsChildren[idx++];

  idx++; // skip the inner signature algorithm

  // Issuer
  const std::vector<RDNAttribute> issuer = parseRDN(tbsChildren[idx++]);

  // Validity
  TimePoint notBefore = TimePoint{};
  TimePoint notAfter = TimePoint{};
  const ASN1Node& validityNode = tbsChildren[idx++];
  if (validityNode.children && validityNode.children->size() >= 2) {
    notBefore = parseTime(validityNode.children->at(0));
    notAfter = parseTime(validityNode.children->at(1));
  }

  // Subject
  const std::vector<RDNAttribute> subject = parseRDN(tbsChildren[idx++]);

  // SubjectPublicKeyInfo
  const ASN1Node& spkiNode = tbsChildren[idx++];
  std::string keyAlgorithm = "unknown";
  size_t keySize = 0;

  if (spkiNode.children && spkiNode.children->size() >= 2) {
    const ASN1Node& algSeq = spkiNode.children->at(0);
    const Bytes& keyBits = spkiNode.children->at(1).rawValue;

    if (algSeq.children && !algSeq.children->empty()) {
      const std::string algOID = decodeOID(algSeq.children->front().rawValue);
      keyAlgorithm = oidName(algOID);

      if (algOID == "1.2.840.113549.1.1.1") {
        // RSA: the BIT STRING holds 1 unused-bits byte, then a DER SEQUENCE
        // of { modulus INTEGER, exponent INTEGER }.
        if (keyBits.size() > 1) {
          Bytes inner(keyBits.begin() + 1, keyBits.end());
          DERParser rsaParser(std::move(inner));
          if (!rsaParser.exhausted()) {
            const ASN1Node rsaSeq = rsaParser.readNode();
            if (rsaSeq.children && !rsaSeq.children->empty()) {
              const Bytes& modBytes = rsaSeq.children->front().rawValue;
              // First byte may be 0x00 padding for positive sign
              const size_t effectiveLen =
                  (modBytes[0] == 0x00 ? modBytes.size() - 1 : modBytes.size()) * 8;
              keySize = effectiveLen;
            }
          }
        }
      } else if (algOID == "1.2.840.10045.2.1") {
        // ECDSA: name the curve from the parameter OID; key size = curve bits.
        if (algSeq.children->size() >= 2) {
          const std::string curveOID = decodeOID(algSeq.children->at(1).rawValue);
          const auto& curves = ecCurveNames();
          const auto it = curves.find(curveOID);
          if (it != curves.end()) keyAlgorithm += " (" + it->second + ")";
        }
        if (keyBits.size() > 1) {
          keySize = (keyBits.size() - 1 - 1) * 8; // minus unused-bits byte and 0x04 prefix
        }
      } else {
        keySize = keyBits.size() > 1 ? (keyBits.size() - 1) * 8 : 0;
      }
    }
  }

  // Extensions — look for context [3] after subjectPublicKeyInfo
  Extensions extensions;
  while (idx < tbsChildren.size()) {
    const ASN1Node& child = tbsChildren[idx];
    if (child.tagClass == TagClass::Context && child.tagNumber == 3 && child.children) {
      // context [3] wraps a single outer SEQUENCE of Extension entries
      const ASN1Node& outerSeq = child.children->front();
      if (outerSeq.children) extensions = parseExtensions(*outerSeq.children);
    }
    idx++;
  }

  // Serial number as colon-separated uppercase hex
  std::string serialHex;
  if (!serialNode.rawValue.empty()) {
    const uint8_t* serialBytes = serialNode.rawValue.data();
    size_t serialLen = serialNode.rawValue.size();
    if (serialBytes[0] == 0x00 && serialLen > 1) { // strip pure sign padding
      serialBytes++;
      serialLen--;
    }
    static const char* HEX = "0123456789ABCDEF";
    for (size_t i = 0; i < serialLen; i++) {
      if (i > 0) serialHex += ':';
      serialHex += HEX[serialBytes[i] >> 4];
      serialHex += HEX[serialBytes[i] & 0x0f];
    }
  }

  CertificateInfo info;
  info.subject = subject;
  info.subjectDN = formatDN(subject);
  info.issuer = issuer;
  info.issuerDN = formatDN(issuer);
  info.notBefore = notBefore;
  info.notAfter = notAfter;
  info.serialNumber = serialHex;
  info.signatureAlgorithm = oidName(sigAlgOID);
  info.keyAlgorithm = keyAlgorithm;
  info.keySize = keySize;
  info.basicConstraintsCA = extensions.basicConstraintsCA;
  info.keyUsage = extensions.keyUsage.value_or(std::vector<std::string>{});
  info.extKeyUsage = extensions.extKeyUsage.value_or(std::vector<std::string>{});
  info.sanDNS = std::move(extensions.sanDNS);
  info.sanIP = std::move(extensions.sanIP);
  info.sanEmail = std::move(extensions.sanEmail);
  info.version = version;
  info.rawDER = der;
  return info;
}

/** True when the certificate's not-after moment is in the past. */
bool isExpired(const CertificateInfo& cert) {
  return cert.notAfter < std::chrono::system_clock::now();
}

/** Whole days from now until expiry, rounded toward +∞ (negative when expired). */
long daysUntilExpiry(const CertificateInfo& cert) {
  using namespace std::chrono;
  const auto diff = cert.notAfter - floor<seconds>(system_clock::now());
  const long double days = duration_cast<duration<long double, std::ratio<86400>>>(diff).count();
  return static_cast<long>(std::ceil(days));
}

/** Extract all PEM certificate blocks and return info for each. */
std::vector<CertificateInfo> decodeCertificateChain(const std::string& pem) {
  if (pem.empty() || pem.find_first_not_of(" \t\r\n") == std::string::npos) {
    throw std::runtime_error("Empty input — paste PEM certificate(s)");
  }
  const std::vector<std::string> blocks = pemBlocks(pem);
  if (blocks.empty()) {
    throw std::runtime_error("No PEM certificate block found — expected -----BEGIN CERTIFICATE-----");
  }
  std::vector<CertificateInfo> chain;
  chain.reserve(blocks.size());
  for (const auto& block : blocks) {
    chain.push_back(decodeCertificate(
        "-----BEGIN CERTIFICATE-----\n" + block + "\n-----END CERTIFICATE-----"));
  }
  return chain;
}

} // namespace certdecoder

Also available in 8 other languages

Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →