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 (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 →