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 →