Skip to content

CSR Generator — Zig 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 Zig implementation — the same logic the interactive tool runs, in a shareable, citable form.

//! csr-generator — ASN.1 DER encoding + PKCS#10 CSR construction.
//!
//! Language: Zig 0.14 (standard library only)
//! Ported from: src/lib/csr-generator.ts (the canonical TypeScript implementation).
//! display source — part of CosmoDev's polyglot tool pages.
//!
//! The whole certificate-request pipeline runs locally: key pair generation,
//! DER assembly of the CertificationRequest structure, signature, and PEM
//! wrapping. Nothing leaves the process.
//!
//! The TS reference generates RSA and EC keys through Web Crypto. Zig's
//! standard library ships ECDSA P-256 (`std.crypto.sign.ecdsa`) but not RSA
//! key generation (its `std.crypto.rsa` targets TLS verification), so this
//! port implements the ECDSA-P256 path end-to-end and reports a clear error
//! for RSA — every DER primitive, the SPKI/PKCS8 wrapping, and the RSA
//! signature slot (`sha256WithRSAEncryption`) are complete, so wiring an RSA
//! key provider in is a drop-in.

const std = @import("std");

pub const KeyAlgorithm = enum {
    rsa2048,
    rsa4096,
    ecdsa_p256,

    pub fn label(self: KeyAlgorithm) []const u8 {
        return switch (self) {
            .rsa2048 => "RSA-2048",
            .rsa4096 => "RSA-4096",
            .ecdsa_p256 => "ECDSA-P256",
        };
    }
};

pub const CSROptions = struct {
    common_name: []const u8,
    organization: ?[]const u8 = null,
    country: ?[]const u8 = null,
    state: ?[]const u8 = null,
    locality: ?[]const u8 = null,
    email: ?[]const u8 = null,
    key_algorithm: KeyAlgorithm,
    subject_alt_names: []const []const u8 = &.{},
};

pub const CSRResult = struct {
    csr: []const u8,
    private_key: []const u8,
};

pub const Error = error{
    MissingCommonName,
    InvalidCountry,
    UnsupportedAlgorithm,
    NotAPrintableString,
    InvalidOID,
    InvalidLength,
    NegativeInteger,
    RsaKeygenUnsupported,
    OutOfMemory,
};

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

const Der = std.ArrayList(u8);

/// DER length: short form below 0x80, long form (0x80 | byte count) above.
pub fn encodeLength(list: *Der, len: usize) Error!void {
    if (len < 0x80) {
        try list.append(@intCast(len));
        return;
    }
    var bytes: [8]u8 = undefined;
    var n = len;
    var count: usize = 0;
    while (n > 0) : (n >>= 8) {
        bytes[count] = @intCast(n & 0xff);
        count += 1;
    }
    try list.append(0x80 | @as(u8, @intCast(count)));
    var j = count;
    while (j > 0) {
        j -= 1;
        try list.append(bytes[j]);
    }
}

/// Wrap content bytes in a tag + DER length header.
pub fn tlv(out: *Der, tag: u8, content: []const u8) Error!void {
    try out.append(tag);
    try encodeLength(out, content.len);
    try out.appendSlice(content);
}

/// Build one TLV in its own buffer, then append it to `out`.
fn tlvAlloc(allocator: std.mem.Allocator, out: *Der, tag: u8, content: []const u8) Error!void {
    var inner = Der.init(allocator);
    defer inner.deinit();
    try tlv(&inner, tag, content);
    try out.appendSlice(inner.items);
}

/// DER INTEGER from a small non-negative number.
pub fn encodeInteger(allocator: std.mem.Allocator, out: *Der, value: u64) Error!void {
    var digits: [8]u8 = undefined;
    var n = value;
    var count: usize = 0;
    while (n > 0) : (n >>= 8) {
        digits[count] = @intCast(n & 0xff);
        count += 1;
    }
    var content: []const u8 = &[_]u8{0};
    if (count > 0) {
        std.mem.reverse(u8, digits[0..count]);
        content = digits[0..count];
    }
    try integerFromBytes(allocator, out, content);
}

/// DER INTEGER from raw big-endian bytes (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.
pub fn encodeIntegerBytes(allocator: std.mem.Allocator, out: *Der, value: []const u8) Error!void {
    try integerFromBytes(allocator, out, value);
}

fn integerFromBytes(allocator: std.mem.Allocator, out: *Der, value_in: []const u8) Error!void {
    if (value_in.len == 0) {
        try tlvAlloc(allocator, out, 0x02, &[_]u8{0});
        return;
    }
    var start: usize = 0;
    while (start < value_in.len - 1 and value_in[start] == 0) start += 1;
    const value = value_in[start..];
    var wrapped = std.ArrayList(u8).init(allocator);
    defer wrapped.deinit();
    if (value[0] > 0x7f) try wrapped.append(0); // keep the INTEGER positive
    try wrapped.appendSlice(value);
    try tlvAlloc(allocator, out, 0x02, wrapped.items);
}

/// DER OBJECT IDENTIFIER from a dotted string, e.g. "1.2.840.113549.1.1.11".
pub fn encodeOID(allocator: std.mem.Allocator, out: *Der, oid: []const u8) Error!void {
    var parts = std.mem.splitScalar(u8, oid, '.');
    const first_str = parts.next() orelse return Error.InvalidOID;
    const second_str = parts.next() orelse return Error.InvalidOID;
    const first = std.fmt.parseInt(u32, first_str, 10) catch return Error.InvalidOID;
    const second = std.fmt.parseInt(u32, second_str, 10) catch return Error.InvalidOID;

    var content = Der.init(allocator);
    defer content.deinit();
    try content.append(@intCast(40 * first + second));
    while (parts.next()) |p| {
        var v = std.fmt.parseInt(u32, p, 10) catch return Error.InvalidOID;
        var stack: [8]u8 = undefined;
        var depth: usize = 0;
        while (true) {
            stack[depth] = @intCast(v & 0x7f);
            depth += 1;
            v >>= 7;
            if (v == 0) break;
        }
        var j = depth;
        while (j > 1) {
            j -= 1;
            try content.append(stack[j] | 0x80);
        }
        try content.append(stack[0]);
    }
    try tlvAlloc(allocator, out, 0x06, content.items);
}

pub fn encodeUTF8String(allocator: std.mem.Allocator, out: *Der, str: []const u8) Error!void {
    try tlvAlloc(allocator, out, 0x0c, str);
}

/// PrintableString — the required type for countryName in a Name.
pub fn encodePrintableString(allocator: std.mem.Allocator, out: *Der, str: []const u8) Error!void {
    for (str) |ch| {
        if (!isPrintableChar(ch)) return Error.NotAPrintableString;
    }
    try tlvAlloc(allocator, out, 0x13, str);
}

fn isPrintableChar(c: u8) bool {
    return switch (c) {
        'A'...'Z', 'a'...'z', '0'...'9', ' ', '\'', '(', ')', '+', ',', '-', '.', '/', ':', '=', '?' => true,
        else => false,
    };
}

pub fn encodeBitString(allocator: std.mem.Allocator, out: *Der, data: []const u8) Error!void {
    var content = std.ArrayList(u8).init(allocator);
    defer content.deinit();
    try content.append(0); // 0 unused bits in the last octet
    try content.appendSlice(data);
    try tlvAlloc(allocator, out, 0x03, content.items);
}

pub fn encodeOctetString(allocator: std.mem.Allocator, out: *Der, data: []const u8) Error!void {
    try tlvAlloc(allocator, out, 0x04, data);
}

pub fn encodeNull(out: *Der) Error!void {
    try out.appendSlice(&[_]u8{ 0x05, 0x00 });
}

pub fn encodeSequence(allocator: std.mem.Allocator, out: *Der, parts: []const []const u8) Error!void {
    try construct(allocator, out, 0x30, parts);
}

pub fn encodeSet(allocator: std.mem.Allocator, out: *Der, parts: []const []const u8) Error!void {
    try construct(allocator, out, 0x31, parts);
}

fn construct(allocator: std.mem.Allocator, out: *Der, tag: u8, parts: []const []const u8) Error!void {
    var content = Der.init(allocator);
    defer content.deinit();
    for (parts) |p| try content.appendSlice(p);
    try tlvAlloc(allocator, out, tag, content.items);
}

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

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

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

/// Errors on invalid input: CN required, country (when present) a 2-letter ISO
/// 3166-1 code, key algorithm one of the supported values.
pub fn validateCSROptions(options: CSROptions) Error!void {
    if (std.mem.trim(u8, options.common_name, " \t").len == 0) return Error.MissingCommonName;
    if (options.country) |country| {
        const c = std.mem.trim(u8, country, " \t");
        if (c.len != 0 and c.len != 2) return Error.InvalidCountry;
        if (c.len == 2) {
            for (c) |ch| {
                if (!std.ascii.isAlphabetic(ch)) return Error.InvalidCountry;
            }
        }
    }
    switch (options.key_algorithm) {
        .rsa2048, .rsa4096, .ecdsa_p256 => {},
    }
}

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

pub const SanType = enum { dns, ip, email, uri };

/// Classify a SAN entry: IPv4/IPv6 → ip, http(s):// → uri, contains @ → email, else dns.
pub fn classifySanType(entry: []const u8) SanType {
    const value = std.mem.trim(u8, entry, " \t");
    if (ipToBytes(value) != null) return .ip;
    if (startsWithHttp(value)) return .uri;
    if (std.mem.indexOfScalar(u8, value, '@') != null) return .email;
    return .dns;
}

fn startsWithHttp(value: []const u8) bool {
    const lower_http = "http://";
    const lower_https = "https://";
    return (value.len >= lower_http.len and std.ascii.eqlIgnoreCase(value[0..lower_http.len], lower_http)) or
        (value.len >= lower_https.len and std.ascii.eqlIgnoreCase(value[0..lower_https.len], lower_https));
}

/// Parse an IPv4 or IPv6 literal into 4 or 16 bytes; null when not an IP.
pub fn ipToBytes(ip: []const u8) ?[]const u8 {
    // IPv4: d.d.d.d with every octet <= 255.
    if (std.mem.indexOfScalar(u8, ip, ':') == null) {
        var octets: [4]u16 = undefined;
        var count: usize = 0;
        var parts = std.mem.splitScalar(u8, ip, '.');
        while (parts.next()) |p| {
            if (count == 4 or p.len == 0 or p.len > 3) return null;
            for (p) |ch| {
                if (!std.ascii.isDigit(ch)) return null;
            }
            octets[count] = std.fmt.parseInt(u16, p, 10) catch return null;
            if (octets[count] > 255) return null;
            count += 1;
        }
        if (count != 4) return null;
        ipv4_bytes = .{
            @intCast(octets[0]), @intCast(octets[1]),
            @intCast(octets[2]), @intCast(octets[3]),
        };
        return ipv4_bytes[0..4];
    }
    // IPv6: groups of hex, at most one "::" elision, optional IPv4-mapped tail.
    var halves = std.mem.splitSequence(u8, ip, "::");
    var groups: [16]u16 = undefined;
    var group_count: usize = 0;
    var saw_elision = false;
    while (halves.next()) |half| {
        if (half.len == 0) {
            // The "::" elision yields one empty half at each end; a stray
            // empty half elsewhere ("1::2::") is not a valid address.
            if (saw_elision and group_count != 0) return null;
            saw_elision = true;
            continue;
        }
        var sub = std.mem.splitScalar(u8, half, ':');
        while (sub.next()) |group| {
            if (group.len == 0) return null;
            if (std.mem.indexOfScalar(u8, group, '.') != null) {
                // IPv4-mapped tail, e.g. ::ffff:10.0.0.1
                const tail = ipToBytes(group) orelse return null;
                if (tail.len != 4) return null;
                if (group_count + 2 > 8) return null;
                groups[group_count] = (@as(u16, tail[0]) << 8) | tail[1];
                groups[group_count + 1] = (@as(u16, tail[2]) << 8) | tail[3];
                group_count += 2;
                continue;
            }
            if (group.len > 4) return null;
            for (group) |ch| {
                if (!std.ascii.isHex(ch)) return null;
            }
            if (group_count == 8) return null;
            groups[group_count] = std.fmt.parseInt(u16, group, 16) catch return null;
            group_count += 1;
        }
    }
    // "::" elides one or more zero groups; without it there must be exactly 8.
    var total = group_count;
    if (saw_elision) {
        while (total < 8) : (total += 1) groups[total] = 0;
    } else if (total != 8) {
        return null;
    }
    var i: usize = 0;
    while (i < 8) : (i += 1) {
        ipv6_bytes[i * 2] = @intCast(groups[i] >> 8);
        ipv6_bytes[i * 2 + 1] = @intCast(groups[i] & 0xff);
    }
    return ipv6_bytes[0..16];
}

// Scratch outputs for ipToBytes (display snippet: single-threaded parsing).
var ipv4_bytes: [4]u8 = undefined;
var ipv6_bytes: [16]u8 = undefined;

/// One GeneralName: context-specific implicit tags per RFC 5280.
fn encodeGeneralName(allocator: std.mem.Allocator, out: *Der, entry: []const u8) Error!void {
    const value = std.mem.trim(u8, entry, " \t");
    switch (classifySanType(value)) {
        .ip => try tlvAlloc(allocator, out, 0x87, ipToBytes(value).?),
        .uri => try tlvAlloc(allocator, out, 0x86, value),
        .email => try tlvAlloc(allocator, out, 0x81, value),
        .dns => try tlvAlloc(allocator, out, 0x82, value), // dNSName
    }
}

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

const B64Encoder = std.base64.standard.Encoder;

/// PEM-wrap DER bytes: 64-character base64 lines between BEGIN/END markers.
pub fn pemEncode(allocator: std.mem.Allocator, der: []const u8, label: []const u8) Error![]u8 {
    const b64_len = B64Encoder.calcSize(der.len);
    const b64 = try allocator.alloc(u8, b64_len);
    defer allocator.free(b64);
    _ = B64Encoder.encode(b64, der);

    var out = std.ArrayList(u8).init(allocator);
    errdefer out.deinit();
    const w = out.writer();
    try w.print("-----BEGIN {s}-----\n", .{label});
    var i: usize = 0;
    while (i < b64.len) : (i += 64) {
        const end = @min(i + 64, b64.len);
        try w.print("{s}\n", .{b64[i..end]});
    }
    try w.print("-----END {s}-----\n", .{label});
    return out.toOwnedSlice();
}

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

const EcdsaP256Sha256 = std.crypto.sign.ecdsa.EcdsaP256Sha256;

/// AttributeTypeAndValue (SEQUENCE of OID + value) wrapped in its RDN SET.
fn rdn(allocator: std.mem.Allocator, out: *Der, oid: []const u8, value: []const u8) Error!void {
    var seq = Der.init(allocator);
    defer seq.deinit();
    try encodeOID(allocator, &seq, oid);
    try seq.appendSlice(value);
    try encodeSet(allocator, out, &.{seq.items});
}

/// Web Crypto ECDSA signatures are raw r||s; PKCS#10 expects a DER
/// ECDSA-Sig-Value SEQUENCE. Convert half-length integer halves to DER.
fn ecdsaRawToDer(allocator: std.mem.Allocator, raw: *const [64]u8) Error![]u8 {
    var out = Der.init(allocator);
    defer out.deinit();
    const half = 32;
    try encodeIntegerBytes(allocator, &out, raw[0..half]);
    var second = Der.init(allocator);
    defer second.deinit();
    try encodeIntegerBytes(allocator, &second, raw[half..]);
    var seq = Der.init(allocator);
    defer seq.deinit();
    try encodeSequence(allocator, &seq, &.{ out.items, second.items });
    return seq.toOwnedSlice();
}

/// SPKI for a P-256 public key:
/// SEQUENCE { SEQUENCE { OID ecPublicKey, OID prime256v1 }, BIT STRING point }.
fn ecdsaSpki(allocator: std.mem.Allocator, sec1: *const [65]u8) Error![]u8 {
    var alg = Der.init(allocator);
    defer alg.deinit();
    try encodeOID(allocator, &alg, OID_EC_PUBLIC_KEY);
    try encodeOID(allocator, &alg, OID_P256);
    var alg_seq = Der.init(allocator);
    defer alg_seq.deinit();
    try encodeSequence(allocator, &alg_seq, &.{alg.items});

    var bits = Der.init(allocator);
    defer bits.deinit();
    try encodeBitString(allocator, &bits, sec1);

    var spki = Der.init(allocator);
    defer spki.deinit();
    try encodeSequence(allocator, &spki, &.{ alg_seq.items, bits.items });
    return spki.toOwnedSlice();
}

/// PKCS8 for a P-256 private key:
/// SEQUENCE { INTEGER 0, SEQUENCE { OID ecPublicKey, OID P-256 }, OCTET STRING scalar }.
fn ecdsaPkcs8(allocator: std.mem.Allocator, scalar: *const [32]u8) Error![]u8 {
    var alg = Der.init(allocator);
    defer alg.deinit();
    try encodeOID(allocator, &alg, OID_EC_PUBLIC_KEY);
    try encodeOID(allocator, &alg, OID_P256);
    var alg_seq = Der.init(allocator);
    defer alg_seq.deinit();
    try encodeSequence(allocator, &alg_seq, &.{alg.items});

    var octet = Der.init(allocator);
    defer octet.deinit();
    try encodeOctetString(allocator, &octet, scalar);

    var version = Der.init(allocator);
    defer version.deinit();
    try encodeInteger(allocator, &version, 0);

    var pkcs8 = Der.init(allocator);
    defer pkcs8.deinit();
    try encodeSequence(allocator, &pkcs8, &.{ version.items, alg_seq.items, octet.items });
    return pkcs8.toOwnedSlice();
}

/// Generate a key pair and build a signed PKCS#10 CSR (PEM) plus the matching
/// PKCS#8 private key (PEM). Caller owns both strings.
pub fn generateCSR(allocator: std.mem.Allocator, options: CSROptions) Error!CSRResult {
    try validateCSROptions(options);
    if (options.key_algorithm != .ecdsa_p256) {
        // RSA keygen is not in Zig's standard library (see the header note).
        return Error.RsaKeygenUnsupported;
    }

    const pair = EcdsaP256Sha256.KeyPair.create() catch return Error.OutOfMemory;
    const sec1 = pair.public_key.toSec1();
    const sk = pair.secret_key.toBytes();
    const spki = try ecdsaSpki(allocator, &sec1);
    defer allocator.free(spki);
    const pkcs8 = try ecdsaPkcs8(allocator, &sk);
    defer allocator.free(pkcs8);

    // subject: RDNSequence in the conventional C, ST, L, O, CN, email order
    var rdns = std.ArrayList([]const u8).init(allocator);
    defer {
        for (rdns.items) |item| allocator.free(item);
        rdns.deinit();
    }
    if (options.country) |country_raw| {
        var upper: [64]u8 = undefined;
        const country = upperTrimUpper(&upper, country_raw);
        if (country.len > 0) {
            var entry = Der.init(allocator);
            defer entry.deinit();
            var value = Der.init(allocator);
            defer value.deinit();
            try encodePrintableString(allocator, &value, country);
            try rdn(allocator, &entry, OID_COUNTRY, value.items);
            try rdns.append(try allocator.dupe(u8, entry.items));
        }
    }
    const simple_fields = [_]struct { oid: []const u8, value: ?[]const u8 }{
        .{ .oid = OID_STATE, .value = options.state },
        .{ .oid = OID_LOCALITY, .value = options.locality },
        .{ .oid = OID_ORGANIZATION, .value = options.organization },
    };
    for (simple_fields) |field| {
        const v = field.value orelse continue;
        const t = std.mem.trim(u8, v, " \t");
        if (t.len == 0) continue;
        var entry = Der.init(allocator);
        defer entry.deinit();
        var value = Der.init(allocator);
        defer value.deinit();
        try encodeUTF8String(allocator, &value, t);
        try rdn(allocator, &entry, field.oid, value.items);
        try rdns.append(try allocator.dupe(u8, entry.items));
    }
    {
        var entry = Der.init(allocator);
        defer entry.deinit();
        var value = Der.init(allocator);
        defer value.deinit();
        try encodeUTF8String(allocator, &value, std.mem.trim(u8, options.common_name, " \t"));
        try rdn(allocator, &entry, OID_COMMON_NAME, value.items);
        try rdns.append(try allocator.dupe(u8, entry.items));
    }
    if (options.email) |email_raw| {
        const email = std.mem.trim(u8, email_raw, " \t");
        if (email.len > 0) {
            var entry = Der.init(allocator);
            defer entry.deinit();
            var value = Der.init(allocator);
            defer value.deinit();
            try encodeUTF8String(allocator, &value, email);
            try rdn(allocator, &entry, OID_EMAIL, value.items);
            try rdns.append(try allocator.dupe(u8, entry.items));
        }
    }

    // attributes [0] IMPLICIT SET OF — extensionRequest carrying subjectAltName
    var attributes = std.ArrayList(u8).init(allocator);
    defer attributes.deinit();
    var sans = std.ArrayList([]const u8).init(allocator);
    defer sans.deinit();
    for (options.subject_alt_names) |s| {
        const t = std.mem.trim(u8, s, " \t");
        if (t.len > 0) try sans.append(t);
    }
    if (sans.items.len > 0) {
        var general_names = Der.init(allocator);
        defer general_names.deinit();
        for (sans.items) |san| try encodeGeneralName(allocator, &general_names, san);
        var san_seq = Der.init(allocator);
        defer san_seq.deinit();
        try encodeSequence(allocator, &san_seq, &.{general_names.items});

        var octet = Der.init(allocator);
        defer octet.deinit();
        try encodeOctetString(allocator, &octet, san_seq.items);

        var extension = Der.init(allocator);
        defer extension.deinit();
        var oid = Der.init(allocator);
        defer oid.deinit();
        try encodeOID(allocator, &oid, OID_SUBJECT_ALT_NAME);
        try encodeSequence(allocator, &extension, &.{ oid.items, octet.items });

        var inner_set = Der.init(allocator);
        defer inner_set.deinit();
        try encodeSequence(allocator, &inner_set, &.{extension.items});

        var attribute = Der.init(allocator);
        defer attribute.deinit();
        var ext_oid = Der.init(allocator);
        defer ext_oid.deinit();
        try encodeOID(allocator, &ext_oid, OID_EXT_REQUEST);
        try encodeSet(allocator, &attribute, &.{inner_set.items});
        var attr_seq = Der.init(allocator);
        defer attr_seq.deinit();
        try encodeSequence(allocator, &attr_seq, &.{ ext_oid.items, attribute.items });

        try tlv(&attributes, 0xa0, attr_seq.items); // [0] IMPLICIT SET OF Attribute
    }

    // CertificationRequestInfo: version 0, subject, SPKI, [0] attributes
    var cri = Der.init(allocator);
    defer cri.deinit();
    var version = Der.init(allocator);
    defer version.deinit();
    try encodeInteger(allocator, &version, 0);
    var subject = Der.init(allocator);
    defer subject.deinit();
    try encodeSequence(allocator, &subject, rdns.items);
    try encodeSequence(allocator, &cri, &.{ version.items, subject.items, spki, attributes.items });

    // signatureAlgorithm + signature over the DER CRI
    var sig_alg = Der.init(allocator);
    defer sig_alg.deinit();
    try encodeOID(allocator, &sig_alg, OID_ECDSA_SHA256);
    var sig_alg_seq = Der.init(allocator);
    defer sig_alg_seq.deinit();
    try encodeSequence(allocator, &sig_alg_seq, &.{sig_alg.items});

    const signature = EcdsaP256Sha256.sign(cri.items, pair.secret_key) catch return Error.OutOfMemory;
    const raw_sig = signature.toRawBytes();
    const sig_der = try ecdsaRawToDer(allocator, &raw_sig);
    defer allocator.free(sig_der);
    var sig_bits = Der.init(allocator);
    defer sig_bits.deinit();
    try encodeBitString(allocator, &sig_bits, sig_der);

    var csr_der = Der.init(allocator);
    defer csr_der.deinit();
    try encodeSequence(allocator, &csr_der, &.{ cri.items, sig_alg_seq.items, sig_bits.items });

    return .{
        .csr = try pemEncode(allocator, csr_der.items, "CERTIFICATE REQUEST"),
        .private_key = try pemEncode(allocator, pkcs8, "PRIVATE KEY"),
    };
}

fn upperTrimUpper(buf: *[64]u8, s: []const u8) []const u8 {
    const t = std.mem.trim(u8, s, " \t");
    const n = @min(buf.len, t.len);
    for (t[0..n], 0..) |c, i| buf[i] = std.ascii.toUpper(c);
    return buf[0..n];
}

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 →