Skip to content

CSR Generator — C++ source

Generate a Certificate Signing Request and private key pair entirely in your browser. Download the CSR and key as PEM files.

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

// CSR Generator — ASN.1 DER encoding + PKCS#10 CSR construction on OpenSSL.
//
// Language: C++17 (standard library + OpenSSL EVP)
// Ported from src/lib/csr-generator.ts (the canonical TypeScript
// implementation, which runs on the browser's Web Crypto API).
// display source — part of CosmoDev's polyglot tool pages.
//
// The whole certificate-request pipeline runs in-process: key pair generation
// (OpenSSL), DER assembly of the CertificationRequest structure, signature,
// and PEM wrapping. Nothing leaves the process.
//
// Build: c++ -std=c++17 csr-generator.cpp -lcrypto

#include <cctype>
#include <cstdint>
#include <memory>
#include <regex>
#include <stdexcept>
#include <string>
#include <vector>

#include <openssl/evp.h>

namespace csrgen {

using Bytes = std::vector<uint8_t>;

enum class KeyAlgorithm { RSA2048, RSA4096, ECDSAP256 };

struct CSROptions {
  std::string commonName;
  std::string organization;
  std::string country;
  std::string state;
  std::string locality;
  std::string email;
  KeyAlgorithm keyAlgorithm = KeyAlgorithm::RSA2048;
  std::vector<std::string> subjectAltNames;
};

struct CSRResult {
  std::string csr;
  std::string privateKey;
};

// --- DER primitives ---------------------------------------------------------

/** DER length: short form below 0x80, long form (0x80 | byte count) above. */
Bytes encodeLength(size_t len) {
  if (len < 0x80) return {static_cast<uint8_t>(len)};
  Bytes bytes;
  size_t n = len;
  while (n > 0) {
    bytes.insert(bytes.begin(), static_cast<uint8_t>(n & 0xff));
    n >>= 8;
  }
  Bytes out{static_cast<uint8_t>(0x80 | bytes.size())};
  out.insert(out.end(), bytes.begin(), bytes.end());
  return out;
}

/** Wrap content bytes in a tag + DER length header. */
static Bytes tlv(uint8_t tag, const Bytes& content) {
  const Bytes len = encodeLength(content.size());
  Bytes out;
  out.reserve(1 + len.size() + content.size());
  out.push_back(tag);
  out.insert(out.end(), len.begin(), len.end());
  out.insert(out.end(), content.begin(), content.end());
  return out;
}

static Bytes concat(const std::vector<Bytes>& parts) {
  Bytes out;
  for (const auto& p : parts) out.insert(out.end(), p.begin(), p.end());
  return out;
}

static Bytes utf8(const std::string& s) { return Bytes(s.begin(), s.end()); }

/**
 * DER INTEGER. Accepts raw big-endian bytes: leading zeros are stripped and a
 * 0x00 sign byte is prepended when the high bit is set, per DER
 * minimal-encoding rules.
 */
Bytes encodeInteger(const Bytes& value) {
  size_t start = 0;
  while (start + 1 < value.size() && value[start] == 0) start++;
  Bytes bytes(value.begin() + start, value.end());
  if (!bytes.empty() && bytes[0] > 0x7f) bytes.insert(bytes.begin(), 0x00);
  return tlv(0x02, bytes);
}

/** DER INTEGER from a small non-negative number. */
Bytes encodeInteger(unsigned long value) {
  Bytes digits;
  unsigned long n = value;
  while (n > 0) {
    digits.insert(digits.begin(), static_cast<uint8_t>(n & 0xff));
    n >>= 8;
  }
  if (digits.empty()) digits.push_back(0x00);
  return encodeInteger(digits);
}

/** DER OBJECT IDENTIFIER from a dotted string, e.g. "1.2.840.113549.1.1.11". */
Bytes encodeOID(const std::string& oid) {
  std::vector<unsigned long> parts;
  std::string current;
  for (char c : oid + ".") {
    if (c == '.') {
      if (current.empty()) throw std::runtime_error("Invalid OID: " + oid);
      parts.push_back(std::stoul(current));
      current.clear();
    } else if (std::isdigit(static_cast<unsigned char>(c))) {
      current += c;
    } else {
      throw std::runtime_error("Invalid OID: " + oid);
    }
  }
  if (parts.size() < 2) throw std::runtime_error("Invalid OID: " + oid);
  Bytes content{static_cast<uint8_t>(40 * parts[0] + parts[1])};
  for (size_t i = 2; i < parts.size(); i++) {
    Bytes stack;
    unsigned long v = parts[i];
    do {
      stack.insert(stack.begin(), static_cast<uint8_t>(v & 0x7f));
      v /= 128;
    } while (v > 0);
    for (size_t j = 0; j + 1 < stack.size(); j++) stack[j] |= 0x80;
    content.insert(content.end(), stack.begin(), stack.end());
  }
  return tlv(0x06, content);
}

Bytes encodeUTF8String(const std::string& str) { return tlv(0x0c, utf8(str)); }

/** PrintableString — the required type for countryName in a Name. */
Bytes encodePrintableString(const std::string& str) {
  static const std::string ALLOWED = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789 '()+,-./:=?";
  for (char c : str) {
    if (ALLOWED.find(c) == std::string::npos) {
      throw std::runtime_error("Not a PrintableString: " + str);
    }
  }
  return tlv(0x13, utf8(str));
}

Bytes encodeBitString(const Bytes& data) {
  Bytes content{0x00}; // 0 unused bits in the last octet
  content.insert(content.end(), data.begin(), data.end());
  return tlv(0x03, content);
}

Bytes encodeOctetString(const Bytes& data) { return tlv(0x04, data); }

Bytes encodeNull() { return {0x05, 0x00}; }

Bytes encodeSequence(const std::vector<Bytes>& parts) { return tlv(0x30, concat(parts)); }

Bytes encodeSet(const std::vector<Bytes>& parts) { return tlv(0x31, concat(parts)); }

// --- Object identifiers -----------------------------------------------------

const char* OID_COUNTRY = "2.5.4.6";
const char* OID_STATE = "2.5.4.8";
const char* OID_LOCALITY = "2.5.4.7";
const char* OID_ORGANIZATION = "2.5.4.10";
const char* OID_COMMON_NAME = "2.5.4.3";
const char* OID_EMAIL = "1.2.840.113549.1.9.1";
const char* OID_EXT_REQUEST = "1.2.840.113549.1.9.14"; // pkcs-9 at extensionRequest
const char* OID_SUBJECT_ALT_NAME = "2.5.29.17";
const char* OID_RSA_SHA256 = "1.2.840.113549.1.1.11"; // sha256WithRSAEncryption
const char* OID_ECDSA_SHA256 = "1.2.840.10045.4.3.2"; // ecdsa-with-SHA256

// --- Validation -------------------------------------------------------------

static std::string trim(const std::string& s) {
  const size_t first = s.find_first_not_of(" \t\r\n");
  if (first == std::string::npos) return "";
  const size_t last = s.find_last_not_of(" \t\r\n");
  return s.substr(first, last - first + 1);
}

/**
 * Throws on invalid input: CN required, country (when present) a 2-letter ISO
 * 3166-1 code. The key algorithm is a closed enum, so "one of the supported
 * values" is enforced by the type system here (the TS reference validates its
 * string union at runtime).
 */
void validateCSROptions(const CSROptions& options) {
  if (trim(options.commonName).empty()) {
    throw std::runtime_error("Common Name (CN) is required");
  }
  const std::string country = trim(options.country);
  if (!country.empty()) {
    static const std::regex COUNTRY_RE("^[A-Za-z]{2}$");
    if (!std::regex_match(country, COUNTRY_RE)) {
      throw std::runtime_error("Country must be a 2-letter ISO 3166-1 code (e.g. US, DE)");
    }
  }
}

// --- SAN classification -----------------------------------------------------

enum class SanType { DNS, IP, Email, URI };

/** Parse an IPv4 or IPv6 address to bytes, or an empty vector when invalid. */
static Bytes ipToBytes(const std::string& ip) {
  static const std::regex V4_RE(R"(^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$)");
  std::smatch m;
  if (std::regex_match(ip, m, V4_RE)) {
    Bytes out;
    for (size_t i = 1; i <= 4; i++) {
      const int octet = std::stoi(m[i].str());
      if (octet > 255) return {};
      out.push_back(static_cast<uint8_t>(octet));
    }
    return out;
  }
  if (ip.find(':') == std::string::npos) return {};

  // IPv6 (may contain one "::" compression run and an IPv4-mapped tail).
  const size_t compressPos = ip.find("::");
  const bool hasCompression = compressPos != std::string::npos;
  if (hasCompression && ip.find("::", compressPos + 2) != std::string::npos) return {};
  std::vector<unsigned> groups;
  const std::vector<std::string> halves =
      hasCompression ? std::vector<std::string>{ip.substr(0, compressPos),
                                                ip.substr(compressPos + 2)}
                     : std::vector<std::string>{ip};
  static const std::regex GROUP_RE("^[0-9a-f]{1,4}$");
  for (const auto& half : halves) {
    if (half.empty()) continue;
    size_t start = 0;
    while (start <= half.size()) {
      const size_t colon = half.find(':', start);
      const std::string group =
          half.substr(start, colon == std::string::npos ? std::string::npos : colon - start);
      if (group.find('.') != std::string::npos) {
        const Bytes tail = ipToBytes(group); // IPv4-mapped tail, e.g. ::ffff:10.0.0.1
        if (tail.size() != 4) return {};
        groups.push_back((tail[0] << 8) | tail[1]);
        groups.push_back((tail[2] << 8) | tail[3]);
      } else if (std::regex_match(group, GROUP_RE)) {
        groups.push_back(static_cast<unsigned>(std::stoul(group, nullptr, 16)));
      } else {
        return {};
      }
      if (colon == std::string::npos) break;
      start = colon + 1;
    }
  }
  if (hasCompression) {
    while (groups.size() < 8) groups.push_back(0);
  }
  if (groups.size() != 8) return {};
  Bytes out(16);
  for (size_t i = 0; i < 8; i++) {
    out[i * 2] = static_cast<uint8_t>(groups[i] >> 8);
    out[i * 2 + 1] = static_cast<uint8_t>(groups[i] & 0xff);
  }
  return out;
}

static std::string toLower(const std::string& s) {
  std::string out = s;
  for (char& c : out) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
  return out;
}

static std::string toUpper(const std::string& s) {
  std::string out = s;
  for (char& c : out) c = static_cast<char>(std::toupper(static_cast<unsigned char>(c)));
  return out;
}

/** Classify a SAN entry: IPv4/IPv6 → ip, http(s):// → uri, contains @ → email, else dns. */
SanType classifySanType(const std::string& entry) {
  const std::string value = toLower(trim(entry));
  if (!ipToBytes(value).empty()) return SanType::IP;
  if (value.rfind("http://", 0) == 0 || value.rfind("https://", 0) == 0) return SanType::URI;
  if (value.find('@') != std::string::npos) return SanType::Email;
  return SanType::DNS;
}

/** One GeneralName: context-specific implicit tags per RFC 5280. */
static Bytes encodeGeneralName(const std::string& entry) {
  const std::string value = trim(entry);
  const Bytes ascii = utf8(value);
  switch (classifySanType(value)) {
    case SanType::IP:
      return tlv(0x87, ipToBytes(toLower(value)));
    case SanType::URI:
      return tlv(0x86, ascii);
    case SanType::Email:
      return tlv(0x81, ascii);
    default:
      return tlv(0x82, ascii); // dNSName
  }
}

// --- PEM ---------------------------------------------------------------------

static std::string base64Encode(const Bytes& bytes) {
  static const char* CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
  std::string out;
  out.reserve((bytes.size() + 2) / 3 * 4);
  for (size_t i = 0; i < bytes.size(); i += 3) {
    const uint8_t b0 = bytes[i];
    const uint8_t b1 = i + 1 < bytes.size() ? bytes[i + 1] : 0;
    const uint8_t b2 = i + 2 < bytes.size() ? bytes[i + 2] : 0;
    out += CHARS[b0 >> 2];
    out += CHARS[((b0 & 0x03) << 4) | (b1 >> 4)];
    out += i + 1 < bytes.size() ? CHARS[((b1 & 0x0f) << 2) | (b2 >> 6)] : '=';
    out += i + 2 < bytes.size() ? CHARS[b2 & 0x3f] : '=';
  }
  return out;
}

/** PEM-wrap DER bytes: 64-character base64 lines between BEGIN/END markers. */
std::string pemEncode(const Bytes& der, const std::string& label) {
  const std::string b64 = base64Encode(der);
  std::string out = "-----BEGIN " + label + "-----\n";
  for (size_t i = 0; i < b64.size(); i += 64) {
    out += b64.substr(i, 64);
    out += '\n';
  }
  out += "-----END " + label + "-----\n";
  return out;
}

// --- Key generation + CSR assembly ------------------------------------------

struct Deleter {
  void operator()(EVP_MD_CTX* ctx) const { EVP_MD_CTX_free(ctx); }
  void operator()(EVP_PKEY_CTX* ctx) const { EVP_PKEY_CTX_free(ctx); }
  void operator()(EVP_PKEY* key) const { EVP_PKEY_free(key); }
};
using MDContext = std::unique_ptr<EVP_MD_CTX, Deleter>;
using PKEYContext = std::unique_ptr<EVP_PKEY_CTX, Deleter>;
using PKey = std::unique_ptr<EVP_PKEY, Deleter>;

static PKey generateKeyPair(KeyAlgorithm algorithm) {
  EVP_PKEY* raw = nullptr;
  PKEYContext ctx(algorithm == KeyAlgorithm::ECDSAP256 ? EVP_PKEY_CTX_new_id(EVP_PKEY_EC, nullptr)
                                                       : EVP_PKEY_CTX_new_id(EVP_PKEY_RSA, nullptr));
  if (!ctx || EVP_PKEY_keygen_init(ctx.get()) != 1) {
    throw std::runtime_error("Key generation context setup failed.");
  }
  if (algorithm == KeyAlgorithm::ECDSAP256) {
    if (EVP_PKEY_CTX_set_ec_paramgen_curve_nid(ctx.get(), NID_X9_62_prime256v1) <= 0) {
      throw std::runtime_error("ECDSA P-256 key generation failed.");
    }
  } else if (EVP_PKEY_CTX_set_rsa_keygen_bits(
                 ctx.get(), algorithm == KeyAlgorithm::RSA4096 ? 4096 : 2048) <= 0) {
    throw std::runtime_error("RSA key generation failed.");
  }
  if (EVP_PKEY_keygen(ctx.get(), &raw) != 1) {
    throw std::runtime_error("Key generation failed.");
  }
  return PKey(raw);
}

/** Raw SPKI (SubjectPublicKeyInfo) DER of an EVP_PKEY. */
static Bytes exportSPKI(EVP_PKEY* key) {
  int len = i2d_PUBKEY(key, nullptr);
  if (len <= 0) throw std::runtime_error("Failed to export the public key.");
  Bytes out(static_cast<size_t>(len));
  uint8_t* cursor = out.data();
  if (i2d_PUBKEY(key, &cursor) <= 0) throw std::runtime_error("Failed to export the public key.");
  return out;
}

/** Raw PKCS#8 DER of an EVP_PKEY. */
static Bytes exportPKCS8(EVP_PKEY* key) {
  const PKCS8_PRIV_KEY_INFO* info = EVP_PKEY_get0_PKCS8(key);
  int len = i2d_PKCS8_PRIV_KEY_INFO(info, nullptr);
  if (len <= 0) throw std::runtime_error("Failed to export the private key.");
  Bytes out(static_cast<size_t>(len));
  uint8_t* cursor = out.data();
  if (i2d_PKCS8_PRIV_KEY_INFO(info, &cursor) <= 0) {
    throw std::runtime_error("Failed to export the private key.");
  }
  return out;
}

/** AttributeTypeAndValue (SEQUENCE of OID + value) wrapped in its RDN SET. */
static Bytes rdn(const std::string& oid, const Bytes& value) {
  return encodeSet({encodeSequence({encodeOID(oid), value})});
}

/**
 * Generate a key pair and build a signed PKCS#10 CSR (PEM) plus the matching
 * PKCS#8 private key (PEM). Runs entirely in-process via OpenSSL.
 */
CSRResult generateCSR(const CSROptions& options) {
  validateCSROptions(options);
  const PKey pair = generateKeyPair(options.keyAlgorithm);
  const Bytes spki = exportSPKI(pair.get());
  const Bytes pkcs8 = exportPKCS8(pair.get());

  // subject: RDNSequence in the conventional C, ST, L, O, CN, email order
  std::vector<Bytes> rdns;
  const std::string country = toUpper(trim(options.country));
  if (!country.empty()) rdns.push_back(rdn(OID_COUNTRY, encodePrintableString(country)));
  if (!trim(options.state).empty()) rdns.push_back(rdn(OID_STATE, encodeUTF8String(trim(options.state))));
  if (!trim(options.locality).empty()) {
    rdns.push_back(rdn(OID_LOCALITY, encodeUTF8String(trim(options.locality))));
  }
  if (!trim(options.organization).empty()) {
    rdns.push_back(rdn(OID_ORGANIZATION, encodeUTF8String(trim(options.organization))));
  }
  rdns.push_back(rdn(OID_COMMON_NAME, encodeUTF8String(trim(options.commonName))));
  if (!trim(options.email).empty()) rdns.push_back(rdn(OID_EMAIL, encodeUTF8String(trim(options.email))));

  // attributes [0] IMPLICIT SET OF — extensionRequest carrying subjectAltName
  Bytes attributes;
  std::vector<std::string> sans;
  for (const auto& s : options.subjectAltNames) {
    if (!trim(s).empty()) sans.push_back(trim(s));
  }
  if (!sans.empty()) {
    std::vector<Bytes> generalNames;
    generalNames.reserve(sans.size());
    for (const auto& san : sans) generalNames.push_back(encodeGeneralName(san));
    const Bytes extension = encodeSequence(
        {encodeOID(OID_SUBJECT_ALT_NAME), encodeOctetString(encodeSequence(generalNames))});
    const Bytes attribute =
        encodeSequence({encodeOID(OID_EXT_REQUEST), encodeSet({encodeSequence({extension})})});
    attributes = tlv(0xa0, attribute); // [0] IMPLICIT SET OF Attribute
  }

  // CertificationRequestInfo: version 0, subject, SPKI, [0] attributes
  const Bytes cri = encodeSequence(
      {encodeInteger(0UL), encodeSequence(rdns), spki, attributes});

  // signatureAlgorithm + signature over the DER CRI
  Bytes signatureAlgorithm;
  Bytes signature;
  MDContext ctx(EVP_MD_CTX_new());
  size_t len = 0;
  if (options.keyAlgorithm == KeyAlgorithm::ECDSAP256) {
    signatureAlgorithm = encodeSequence({encodeOID(OID_ECDSA_SHA256)});
    // OpenSSL emits the DER ECDSA-Sig-Value SEQUENCE directly — exactly the
    // structure PKCS#10 wants (the TS reference must convert from Web Crypto's
    // raw r||s layout; no conversion is needed here).
    if (!ctx || EVP_DigestSignInit(ctx.get(), nullptr, EVP_sha256(), nullptr, pair.get()) != 1 ||
        EVP_DigestSign(ctx.get(), nullptr, &len, cri.data(), cri.size()) != 1) {
      throw std::runtime_error("ECDSA signing failed.");
    }
    Bytes der(len);
    if (EVP_DigestSign(ctx.get(), der.data(), &len, cri.data(), cri.size()) != 1) {
      throw std::runtime_error("ECDSA signing failed.");
    }
    der.resize(len);
    signature = encodeBitString(der);
  } else {
    signatureAlgorithm = encodeSequence({encodeOID(OID_RSA_SHA256), encodeNull()});
    // RSASSA-PKCS1-v1_5 with SHA-256 (EVP_PKEY_rsa + SHA-256 digest).
    if (!ctx || EVP_DigestSignInit(ctx.get(), nullptr, EVP_sha256(), nullptr, pair.get()) != 1 ||
        EVP_DigestSign(ctx.get(), nullptr, &len, cri.data(), cri.size()) != 1) {
      throw std::runtime_error("RSA signing failed.");
    }
    Bytes raw(len);
    if (EVP_DigestSign(ctx.get(), raw.data(), &len, cri.data(), cri.size()) != 1) {
      throw std::runtime_error("RSA signing failed.");
    }
    raw.resize(len);
    signature = encodeBitString(raw);
  }

  return {pemEncode(encodeSequence({cri, signatureAlgorithm, signature}), "CERTIFICATE REQUEST"),
          pemEncode(pkcs8, "PRIVATE KEY")};
}

} // namespace csrgen

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 →