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 →