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 (C11, POSIX) + OpenSSL 3.x libcrypto
* Source: CosmoDev polyglot showcase port of the CSR Generator tool, ported
* from src/lib/csr-generator.ts (the canonical TypeScript
* implementation).
* License: display source — part of CosmoDev's polyglot tool pages.
*
* The TS reference runs the whole certificate-request pipeline client-side on
* Web Crypto: key pair generation, DER assembly of the CertificationRequest
* structure, the signature, and the PEM wrapping. This port is the same
* pipeline with libcrypto in Web Crypto's seat — DER is still assembled by
* hand below (encode_length / encode_integer / encode_oid / …), exactly like
* the reference.
*
* One deliberate difference: Web Crypto returns ECDSA signatures as raw r||s,
* so the TS reference converts them to the DER SEQUENCE PKCS#10 expects.
* OpenSSL's EVP_DigestSign() already emits DER-encoded ECDSA-Sig-Value, so the
* ecdsaRawToDer step has no C counterpart.
*
* Ownership: generate_csr() returns two heap PEM strings — release them with
* csr_result_free(). Functions return NULL on success or a static error
* message (the TS reference throws Error).
*
* Build: cc -std=c11 csr-generator.c -lcrypto
*/
#define _POSIX_C_SOURCE 200809L
#include <ctype.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <openssl/evp.h>
#include <openssl/obj_mac.h>
#include <openssl/rand.h>
#include <openssl/x509.h>
/* ------------------------------------------------------------------ types --- */
typedef enum { KEY_RSA_2048, KEY_RSA_4096, KEY_ECDSA_P256 } key_algorithm;
typedef struct {
const char *common_name; /* required */
const char *organization;
const char *country; /* 2-letter ISO 3166-1 when present */
const char *state;
const char *locality;
const char *email;
key_algorithm key_alg;
const char *const *subject_alt_names; /* NULL-terminated when non-NULL */
} csr_options;
typedef struct {
char *csr; /* PEM "CERTIFICATE REQUEST" */
char *private_key;/* PEM PKCS#8 "PRIVATE KEY" */
} csr_result;
/** A heap byte buffer that grows by reallocation. */
typedef struct {
uint8_t *data;
size_t len;
size_t cap;
} byte_buf;
static bool bb_reserve(byte_buf *b, size_t extra) {
if (b->len + extra <= b->cap) return true;
size_t cap = b->cap ? b->cap : 256;
while (cap < b->len + extra) cap *= 2;
uint8_t *p = realloc(b->data, cap);
if (!p) return false;
b->data = p;
b->cap = cap;
return true;
}
static bool bb_push(byte_buf *b, const uint8_t *src, size_t n) {
if (!bb_reserve(b, n)) return false;
memcpy(b->data + b->len, src, n);
b->len += n;
return true;
}
static bool bb_byte(byte_buf *b, uint8_t v) { return bb_push(b, &v, 1); }
/* -------------------------------------------------------- DER primitives --- */
/** DER length: short form below 0x80, long form (0x80 | byte count) above. */
bool encode_length(size_t len, byte_buf *out) {
if (len < 0x80) return bb_byte(out, (uint8_t)len);
uint8_t bytes[8];
size_t n = 0;
while (len > 0) {
bytes[n++] = (uint8_t)(len & 0xff);
len >>= 8;
}
if (!bb_byte(out, (uint8_t)(0x80 | n))) return false;
for (size_t i = n; i > 0; i--) {
if (!bb_byte(out, bytes[i - 1])) return false;
}
return true;
}
/** Wrap content bytes in a tag + DER length header. */
static bool tlv(uint8_t tag, const uint8_t *content, size_t content_len, byte_buf *out) {
byte_buf len = {0};
if (!encode_length(content_len, &len)) { free(len.data); return false; }
bool ok = bb_byte(out, tag) && bb_push(out, len.data, len.len) &&
bb_push(out, content, content_len);
free(len.data);
return ok;
}
/**
* DER INTEGER from raw big-endian bytes (the signature r/s halves): leading
* zeros are stripped and a 0x00 sign byte is prepended when the high bit is
* set, per DER minimal-encoding rules.
*/
bool encode_integer_bytes(const uint8_t *value, size_t value_len, byte_buf *out) {
size_t start = 0;
while (start + 1 < value_len && value[start] == 0) start++;
const uint8_t *bytes = value + start;
size_t bytes_len = value_len - start;
byte_buf content = {0};
bool ok = true;
if (bytes_len > 0 && bytes[0] > 0x7f) ok = ok && bb_byte(&content, 0);
ok = ok && bb_push(&content, bytes, bytes_len);
ok = ok && tlv(0x02, content.data, content.len, out);
free(content.data);
return ok;
}
/** DER INTEGER from a small non-negative number (version, curve points). */
bool encode_integer(unsigned long value, byte_buf *out) {
uint8_t digits[16];
size_t n = 0;
while (value > 0) {
digits[n++] = (uint8_t)(value & 0xff);
value >>= 8;
}
if (n == 0) digits[n++] = 0;
uint8_t be[16];
for (size_t i = 0; i < n; i++) be[i] = digits[n - 1 - i];
return encode_integer_bytes(be, n, out);
}
/** One base-128 OID word: high bit set on all but the last byte. */
static bool encode_base128(unsigned long v, byte_buf *content) {
uint8_t stack[8];
size_t n = 0;
do {
stack[n++] = (uint8_t)(v & 0x7f);
v >>= 7;
} while (v > 0);
for (size_t i = n; i > 1; i--) stack[i - 1] |= 0x80;
for (size_t i = n; i > 0; i--) {
if (!bb_byte(content, stack[i - 1])) return false;
}
return true;
}
/** DER OBJECT IDENTIFIER from a dotted string, e.g. "1.2.840.113549.1.1.11". */
bool encode_oid(const char *oid, byte_buf *out) {
unsigned long arcs[32];
size_t arc_count = 0;
const char *p = oid;
while (*p) {
if (!isdigit((unsigned char)*p)) return false; /* Invalid OID */
unsigned long v = 0;
while (isdigit((unsigned char)*p)) {
v = v * 10 + (unsigned)(*p - '0');
p++;
}
if (arc_count >= 32) return false;
arcs[arc_count++] = v;
if (*p == '.') p++;
else if (*p) return false;
}
if (arc_count < 2) return false;
byte_buf content = {0};
bool ok = encode_base128(40 * arcs[0] + arcs[1], &content);
for (size_t i = 2; ok && i < arc_count; i++) ok = encode_base128(arcs[i], &content);
ok = ok && tlv(0x06, content.data, content.len, out);
free(content.data);
return ok;
}
bool encode_utf8_string(const char *str, byte_buf *out) {
return tlv(0x0c, (const uint8_t *)str, strlen(str), out);
}
/** PrintableString — the required type for countryName in a Name. */
bool encode_printable_string(const char *str, byte_buf *out) {
for (const char *p = str; *p; p++) {
if (!(isalnum((unsigned char)*p) || strchr(" '()+,-./:=?", *p))) {
return false; /* Not a PrintableString */
}
}
return tlv(0x13, (const uint8_t *)str, strlen(str), out);
}
/** BIT STRING with 0 unused bits in the last octet. */
bool encode_bit_string(const uint8_t *data, size_t len, byte_buf *out) {
byte_buf content = {0};
bool ok = bb_byte(&content, 0) && bb_push(&content, data, len) &&
tlv(0x03, content.data, content.len, out);
free(content.data);
return ok;
}
bool encode_octet_string(const uint8_t *data, size_t len, byte_buf *out) {
return tlv(0x04, data, len, out);
}
bool encode_null(byte_buf *out) { return bb_byte(out, 0x05) && bb_byte(out, 0x00); }
/** Concatenates parts and wraps them in a SEQUENCE (0x30). */
bool encode_sequence(const byte_buf *parts, size_t part_count, byte_buf *out) {
byte_buf content = {0};
bool ok = true;
for (size_t i = 0; i < part_count && ok; i++) {
ok = bb_push(&content, parts[i].data, parts[i].len);
}
ok = ok && tlv(0x30, content.data, content.len, out);
free(content.data);
return ok;
}
/** Concatenates parts and wraps them in a SET (0x31). */
bool encode_set(const byte_buf *parts, size_t part_count, byte_buf *out) {
byte_buf content = {0};
bool ok = true;
for (size_t i = 0; i < part_count && ok; i++) {
ok = bb_push(&content, parts[i].data, parts[i].len);
}
ok = ok && tlv(0x31, content.data, content.len, out);
free(content.data);
return ok;
}
/* ---------------------------------------------------- object identifiers --- */
#define OID_COUNTRY "2.5.4.6"
#define OID_STATE "2.5.4.8"
#define OID_LOCALITY "2.5.4.7"
#define OID_ORGANIZATION "2.5.4.10"
#define OID_COMMON_NAME "2.5.4.3"
#define OID_EMAIL "1.2.840.113549.1.9.1"
#define OID_EXT_REQUEST "1.2.840.113549.1.9.14" /* pkcs-9 at extensionRequest */
#define OID_SUBJECT_ALT_NAME "2.5.29.17"
#define OID_RSA_SHA256 "1.2.840.113549.1.1.11" /* sha256WithRSAEncryption */
#define OID_ECDSA_SHA256 "1.2.840.10045.4.3.2" /* ecdsa-with-SHA256 */
/* -------------------------------------------------------------- validation --- */
const char *validate_csr_options(const csr_options *options) {
if (!options->common_name || !*options->common_name ||
strspn(options->common_name, " \t\r\n") == strlen(options->common_name)) {
return "Common Name (CN) is required";
}
if (options->country && *options->country) {
const char *c = options->country;
while (*c == ' ') c++;
const char *end = c + strlen(c);
while (end > c && end[-1] == ' ') end--;
if ((size_t)(end - c) != 2 || !isalpha((unsigned char)c[0]) || !isalpha((unsigned char)c[1])) {
return "Country must be a 2-letter ISO 3166-1 code (e.g. US, DE)";
}
}
switch (options->key_alg) {
case KEY_RSA_2048:
case KEY_RSA_4096:
case KEY_ECDSA_P256:
break;
default:
return "Unsupported key algorithm";
}
return NULL;
}
/* ------------------------------------------------------ SAN classification --- */
typedef enum { SAN_DNS, SAN_IP, SAN_EMAIL, SAN_URI } san_type;
/** Parse "a.b.c.d" into 4 bytes; false on any octet > 255. */
static bool parse_ipv4(const char *s, uint8_t out[4]) {
unsigned values[4] = {0};
int idx = 0;
const char *p = s;
for (;;) {
if (!isdigit((unsigned char)*p)) return false;
unsigned v = 0;
int digits = 0;
while (isdigit((unsigned char)*p) && digits < 3) {
v = v * 10 + (unsigned)(*p - '0');
p++;
digits++;
}
if (isdigit((unsigned char)*p)) return false; /* more than 3 digits */
if (v > 255) return false;
values[idx++] = v;
if (idx == 4) break;
if (*p != '.') return false;
p++;
}
if (*p != 0) return false;
for (int i = 0; i < 4; i++) out[i] = (uint8_t)values[i];
return true;
}
static bool is_hex_group(const char *s, size_t n) {
if (n == 0 || n > 4) return false;
for (size_t i = 0; i < n; i++) {
if (!isxdigit((unsigned char)s[i])) return false;
}
return true;
}
/** Full IPv6 parser (with "::" compression and IPv4-mapped tails) -> 16 bytes. */
static bool parse_ipv6(const char *s, uint8_t out[16]) {
if (!strchr(s, ':')) return false;
char halves_buf[2][64];
const char *colon2 = strstr(s, "::");
size_t halves = colon2 ? 2 : 1;
if (colon2) {
size_t left = (size_t)(colon2 - s);
if (left >= sizeof halves_buf[0]) return false;
memcpy(halves_buf[0], s, left);
halves_buf[0][left] = 0;
const char *right = colon2 + 2;
if (strlen(right) >= sizeof halves_buf[1]) return false;
strcpy(halves_buf[1], right);
if (strstr(right, "::")) return false; /* more than one "::" */
} else {
if (strlen(s) >= sizeof halves_buf[0]) return false;
strcpy(halves_buf[0], s);
if (strstr(s, "::")) return false;
}
unsigned groups[8] = {0};
size_t group_count = 0;
for (size_t h = 0; h < halves; h++) {
if (!halves_buf[h][0]) continue;
char *save = NULL;
for (char *group = strtok_r(halves_buf[h], ":", &save); group;
group = strtok_r(NULL, ":", &save)) {
if (strchr(group, '.')) { /* IPv4-mapped tail, e.g. ::ffff:10.0.0.1 */
uint8_t tail[4];
if (!parse_ipv4(group, tail) || group_count + 2 > 8) return false;
groups[group_count++] = ((unsigned)tail[0] << 8) | tail[1];
groups[group_count++] = ((unsigned)tail[2] << 8) | tail[3];
} else if (is_hex_group(group, strlen(group))) {
if (group_count >= 8) return false;
groups[group_count++] = (unsigned)strtoul(group, NULL, 16);
} else {
return false;
}
}
}
if (halves == 2) {
if (group_count > 8) return false;
for (size_t i = group_count; i < 8; i++) groups[i] = 0; /* the "missing" zeros */
}
if (group_count != 8) return false;
for (int i = 0; i < 8; i++) {
out[i * 2] = (uint8_t)(groups[i] >> 8);
out[i * 2 + 1] = (uint8_t)(groups[i] & 0xff);
}
return true;
}
static bool ip_to_bytes(const char *ip, uint8_t out[16], size_t *out_len) {
uint8_t v4[4];
if (parse_ipv4(ip, v4)) {
memcpy(out, v4, 4);
*out_len = 4;
return true;
}
if (parse_ipv6(ip, out)) {
*out_len = 16;
return true;
}
return false;
}
/** Classify a SAN entry: IPv4/IPv6 -> ip, http(s):// -> uri, has @ -> email, else dns. */
san_type classify_san_type(const char *entry) {
char lower[256];
size_t n = strlen(entry);
if (n >= sizeof lower) n = sizeof lower - 1;
for (size_t i = 0; i < n; i++) lower[i] = (char)tolower((unsigned char)entry[i]);
lower[n] = 0;
char *s = lower;
while (*s == ' ' || *s == '\t') s++;
uint8_t ip[16];
size_t ip_len;
if (ip_to_bytes(s, ip, &ip_len)) return SAN_IP;
if (strncmp(s, "http://", 7) == 0 || strncmp(s, "https://", 8) == 0) return SAN_URI;
if (strchr(s, '@')) return SAN_EMAIL;
return SAN_DNS;
}
/** One GeneralName: context-specific implicit tags per RFC 5280. */
static bool encode_general_name(const char *entry, byte_buf *out) {
char trimmed[256];
size_t n = strlen(entry);
if (n >= sizeof trimmed) n = sizeof trimmed - 1;
memcpy(trimmed, entry, n);
trimmed[n] = 0;
char *s = trimmed;
while (*s == ' ' || *s == '\t') s++;
for (char *l = s + strlen(s); l > s && (l[-1] == ' ' || l[-1] == '\t'); l--) l[-1] = 0;
for (char *p = s; *p; p++) *p = (char)tolower((unsigned char)*p);
uint8_t ip[16];
size_t ip_len;
if (ip_to_bytes(s, ip, &ip_len)) return tlv(0x87, ip, ip_len, out);
if (strncmp(s, "http://", 7) == 0 || strncmp(s, "https://", 8) == 0) {
return tlv(0x86, (const uint8_t *)s, strlen(s), out);
}
if (strchr(s, '@')) return tlv(0x81, (const uint8_t *)s, strlen(s), out);
return tlv(0x82, (const uint8_t *)s, strlen(s), out); /* dNSName */
}
/* -------------------------------------------------------------------- PEM --- */
/** PEM-wrap DER bytes: 64-character base64 lines between BEGIN/END markers. */
char *pem_encode(const uint8_t *der, size_t der_len, const char *label) {
static const char B64[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
size_t b64_len = ((der_len + 2) / 3) * 4;
char *b64 = malloc(b64_len + 1);
if (!b64) return NULL;
size_t o = 0;
for (size_t i = 0; i < der_len; i += 3) {
uint32_t chunk = (uint32_t)der[i] << 16;
if (i + 1 < der_len) chunk |= (uint32_t)der[i + 1] << 8;
if (i + 2 < der_len) chunk |= der[i + 2];
b64[o++] = B64[(chunk >> 18) & 0x3f];
b64[o++] = B64[(chunk >> 12) & 0x3f];
b64[o++] = (i + 1 < der_len) ? B64[(chunk >> 6) & 0x3f] : '=';
b64[o++] = (i + 2 < der_len) ? B64[chunk & 0x3f] : '=';
}
b64[o] = 0;
size_t lines = b64_len / 64 + 1;
char *pem = malloc(o + strlen(label) * 2 + 64 + lines * 65);
if (!pem) {
free(b64);
return NULL;
}
size_t pos = (size_t)snprintf(pem, 64, "-----BEGIN %s-----\n", label);
for (size_t i = 0; i < o; i += 64) {
size_t chunk = o - i < 64 ? o - i : 64;
memcpy(pem + pos, b64 + i, chunk);
pos += chunk;
pem[pos++] = '\n';
}
pos += (size_t)snprintf(pem + pos, 64, "-----END %s-----\n", label);
pem[pos] = 0;
free(b64);
return pem;
}
/* ---------------------------------------------------- CSR assembly (core) --- */
/** AttributeTypeAndValue (SEQUENCE of OID + value) wrapped in its RDN SET. */
static bool rdn(const char *oid, byte_buf *value, byte_buf *out) {
byte_buf oid_buf = {0}, seq = {0};
bool ok = encode_oid(oid, &oid_buf) &&
encode_sequence((byte_buf[]){oid_buf, *value}, 2, &seq) &&
encode_set((byte_buf[]){seq}, 1, out);
free(oid_buf.data);
free(seq.data);
return ok;
}
/** Assemble the DER CertificationRequest for an already-generated key pair. */
static const char *build_csr(const csr_options *options, EVP_PKEY *pkey, byte_buf *csr_der) {
/* SPKI export (the SubjectPublicKeyInfo goes into the CRI verbatim) */
uint8_t *spki = NULL;
int spki_len = i2d_PUBKEY(pkey, &spki);
if (spki_len <= 0) return "Failed to export the public key (SPKI)";
const char *err = NULL;
byte_buf rdns[7];
size_t rdn_count = 0;
byte_buf attributes = {0}, cri = {0}, version = {0}, subject = {0}, spki_part = {0};
byte_buf sig_alg = {0}, signature = {0}, oid_buf = {0}, null_part = {0};
uint8_t *sig = NULL;
/* subject: RDNSequence in the conventional C, ST, L, O, CN, email order */
char country[8] = {0};
if (options->country && *options->country) {
country[0] = (char)toupper((unsigned char)options->country[0]);
country[1] = (char)toupper((unsigned char)options->country[1]);
}
struct { const char *oid; const char *value; bool printable; } rdn_specs[6] = {
{OID_COUNTRY, country[0] ? country : NULL, true},
{OID_STATE, options->state, false},
{OID_LOCALITY, options->locality, false},
{OID_ORGANIZATION, options->organization, false},
{OID_COMMON_NAME, options->common_name, false},
{OID_EMAIL, options->email, false},
};
memset(rdns, 0, sizeof rdns);
for (size_t i = 0; i < 6 && !err; i++) {
const char *value = rdn_specs[i].value;
if (!value || !*value) continue;
/* the TS trims each value before encoding */
char trimmed[256];
size_t n = strlen(value) < 255 ? strlen(value) : 255;
memcpy(trimmed, value, n);
trimmed[n] = 0;
char *s = trimmed;
while (*s == ' ' || *s == '\t') s++;
for (char *e = s + strlen(s); e > s && (e[-1] == ' ' || e[-1] == '\t'); e--) e[-1] = 0;
if (!*s) continue;
byte_buf val = {0};
bool ok = rdn_specs[i].printable ? encode_printable_string(s, &val)
: encode_utf8_string(s, &val);
if (!ok) err = "Value is not encodable for this RDN type";
else if (!rdn(rdn_specs[i].oid, &val, &rdns[rdn_count])) err = "out of memory";
free(val.data);
rdn_count++;
}
if (!err && !encode_sequence(rdns, rdn_count, &subject)) err = "out of memory";
/* attributes [0] IMPLICIT SET OF — extensionRequest carrying subjectAltName */
byte_buf sans_seq = {0};
if (!err && options->subject_alt_names) {
byte_buf names[32];
size_t name_count = 0;
for (const char *const *san = options->subject_alt_names; *san && name_count < 32; san++) {
names[name_count] = (byte_buf){0};
const char *value = *san;
while (*value == ' ' || *value == '\t') value++;
if (!*value) continue;
if (!encode_general_name(value, &names[name_count])) {
for (size_t i = 0; i <= name_count; i++) free(names[i].data);
err = "out of memory";
break;
}
name_count++;
}
if (!err) {
byte_buf san_oid = {0}, ext_value = {0}, extension = {0}, ext_seq = {0}, attribute = {0};
bool ok = encode_oid(OID_SUBJECT_ALT_NAME, &san_oid) &&
encode_sequence(names, name_count, &sans_seq) &&
encode_octet_string(sans_seq.data, sans_seq.len, &ext_value) &&
encode_sequence((byte_buf[]){san_oid, ext_value}, 2, &extension) &&
encode_sequence((byte_buf[]){extension}, 1, &ext_seq) &&
encode_oid(OID_EXT_REQUEST, &oid_buf) &&
encode_set((byte_buf[]){ext_seq}, 1, &attribute) &&
encode_sequence((byte_buf[]){oid_buf, attribute}, 2, &attributes);
if (!ok) err = "out of memory";
free(san_oid.data);
free(ext_value.data);
free(extension.data);
free(ext_seq.data);
free(attribute.data);
}
for (size_t i = 0; i < name_count; i++) free(names[i].data);
}
/* CertificationRequestInfo: version 0, subject, SPKI, [0] attributes */
if (!err) {
/* [0] IMPLICIT wrapping of the attribute SEQUENCE */
byte_buf attr_ctx = {0};
if (attributes.len > 0 && !tlv(0xa0, attributes.data, attributes.len, &attr_ctx)) {
err = "out of memory";
}
if (!err && !encode_integer(0, &version)) err = "out of memory";
if (!err && !bb_push(&spki_part, spki, (size_t)spki_len)) err = "out of memory";
byte_buf cri_parts[4] = {version, subject, spki_part, attr_ctx};
size_t cri_part_count = attr_ctx.len ? 4 : 3;
if (!err && !encode_sequence(cri_parts, cri_part_count, &cri)) err = "out of memory";
free(attr_ctx.data);
}
/* signatureAlgorithm + signature over the DER CRI */
if (!err) {
EVP_MD_CTX *md = EVP_MD_CTX_new();
size_t sig_len = 0;
byte_buf sig_oid = {0};
bool ok = md != NULL &&
EVP_DigestSignInit(md, NULL, EVP_sha256(), NULL, pkey) == 1 &&
EVP_DigestSign(md, NULL, &sig_len, cri.data, cri.len) == 1 &&
(sig = OPENSSL_malloc(sig_len)) != NULL &&
EVP_DigestSign(md, sig, &sig_len, cri.data, cri.len) == 1;
EVP_MD_CTX_free(md);
if (!ok) {
err = options->key_alg == KEY_ECDSA_P256
? "ECDSA-P256 signing failed"
: "RSA signing failed";
} else if (options->key_alg == KEY_ECDSA_P256) {
/* OpenSSL already emits the DER ECDSA-Sig-Value SEQUENCE. */
if (!encode_oid(OID_ECDSA_SHA256, &sig_oid) ||
!encode_sequence((byte_buf[]){sig_oid}, 1, &sig_alg) ||
!encode_bit_string(sig, sig_len, &signature)) {
err = "out of memory";
}
} else {
if (!encode_oid(OID_RSA_SHA256, &sig_oid) ||
!encode_null(&null_part) ||
!encode_sequence((byte_buf[]){sig_oid, null_part}, 2, &sig_alg) ||
!encode_bit_string(sig, sig_len, &signature)) {
err = "out of memory";
}
}
free(sig_oid.data);
}
if (!err) {
byte_buf parts[3] = {cri, sig_alg, signature};
if (!encode_sequence(parts, 3, csr_der)) err = "out of memory";
}
OPENSSL_free(sig);
OPENSSL_free(spki);
free(version.data);
free(subject.data);
free(spki_part.data);
free(sans_seq.data);
free(attributes.data);
free(sig_alg.data);
free(signature.data);
free(oid_buf.data);
free(null_part.data);
free(cri.data);
for (size_t i = 0; i < rdn_count; i++) free(rdns[i].data);
return err;
}
/* -------------------------------------------------------- key generation --- */
static EVP_PKEY *generate_key_pair(key_algorithm alg) {
EVP_PKEY_CTX *ctx = NULL;
EVP_PKEY *pkey = NULL;
if (alg == KEY_ECDSA_P256) {
ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_EC, NULL);
if (!ctx || EVP_PKEY_keygen_init(ctx) <= 0 ||
EVP_PKEY_CTX_set_ec_paramgen_curve_nid(ctx, NID_X9_62_prime256v1) <= 0 ||
EVP_PKEY_CTX_set_ec_param_enc(ctx, OPENSSL_EC_NAMED_CURVE) <= 0 ||
EVP_PKEY_generate(ctx, &pkey) <= 0) {
pkey = NULL;
}
} else {
int bits = alg == KEY_RSA_4096 ? 4096 : 2048;
ctx = EVP_PKEY_CTX_new_id(EVP_PKEY_RSA, NULL);
if (!ctx || EVP_PKEY_keygen_init(ctx) <= 0 ||
EVP_PKEY_CTX_set_rsa_keygen_bits(ctx, bits) <= 0 ||
EVP_PKEY_generate(ctx, &pkey) <= 0) {
pkey = NULL;
}
}
EVP_PKEY_CTX_free(ctx);
return pkey;
}
/* ------------------------------------------------------------ public API --- */
void csr_result_free(csr_result *r) {
if (!r) return;
free(r->csr);
free(r->private_key);
r->csr = NULL;
r->private_key = NULL;
}
/**
* Generate a key pair and build a signed PKCS#10 CSR (PEM) plus the matching
* PKCS#8 private key (PEM) — the whole pipeline, mirroring generateCSR().
*/
const char *generate_csr(const csr_options *options, csr_result *result) {
memset(result, 0, sizeof *result);
const char *err = validate_csr_options(options);
if (err) return err;
EVP_PKEY *pkey = generate_key_pair(options->key_alg);
if (!pkey) return "Key generation failed";
byte_buf csr_der = {0};
err = build_csr(options, pkey, &csr_der);
if (!err) {
result->csr = pem_encode(csr_der.data, csr_der.len, "CERTIFICATE REQUEST");
PKCS8_PRIV_KEY_INFO *pkcs8 = EVP_PKEY2PKCS8(pkey);
uint8_t *pkcs8_der = NULL;
int pkcs8_len = pkcs8 ? i2d_PKCS8_PRIV_KEY_INFO(pkcs8, &pkcs8_der) : 0;
PKCS8_PRIV_KEY_INFO_free(pkcs8);
if (pkcs8_len > 0) {
result->private_key = pem_encode(pkcs8_der, (size_t)pkcs8_len, "PRIVATE KEY");
} else {
err = "Failed to export the private key (PKCS#8)";
}
OPENSSL_free(pkcs8_der);
}
free(csr_der.data);
EVP_PKEY_free(pkey);
if (err || !result->csr || !result->private_key) {
csr_result_free(result);
return err ? err : "out of memory";
}
return NULL;
}
/* ------------------------------------------------------------- demo main --- */
int main(void) {
const char *sans[] = {"example.com", "www.example.com", NULL};
csr_options options = {
.common_name = "example.com",
.organization = "CosmoLabs",
.country = "US",
.state = "California",
.locality = NULL,
.email = "admin@example.com",
.key_alg = KEY_ECDSA_P256,
.subject_alt_names = sans,
};
csr_result result;
const char *err = generate_csr(&options, &result);
if (err) {
fprintf(stderr, "error: %s\n", err);
return 1;
}
printf("%s", result.csr);
csr_result_free(&result);
return 0;
}
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 →