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.
// C# 12 / .NET 8 — ported from src/lib/csr-generator.ts (the canonical
// TypeScript implementation). Display source for CosmoDev's polyglot pages.
//
// The whole certificate-request pipeline runs locally: key pair generation
// (RSA / ECDsa via System.Security.Cryptography), DER assembly of the
// CertificationRequest structure, signature, and PEM wrapping.
using System.Security.Cryptography;
using System.Text;
/// <summary>The supported key algorithms.</summary>
public enum CsrKeyAlgorithm
{
Rsa2048,
Rsa4096,
EcdsaP256,
}
/// <summary>CSR generation options.</summary>
public sealed record CsrOptions(
string CommonName,
CsrKeyAlgorithm KeyAlgorithm,
string? Organization = null,
string? Country = null,
string? State = null,
string? Locality = null,
string? Email = null,
string[]? SubjectAltNames = null);
/// <summary>A generated CSR plus its matching private key, both PEM.</summary>
/// <param name="Csr">The signed PKCS#10 CSR, PEM-wrapped.</param>
/// <param name="PrivateKey">The PKCS#8 private key, PEM-wrapped.</param>
public sealed record CsrResult(string Csr, string PrivateKey);
public static class CsrGenerator
{
// --- DER primitives ---------------------------------------------------------
/// <summary>DER length: short form below 0x80, long form (0x80 | byte count) above.</summary>
public static byte[] EncodeLength(int len)
{
if (len < 0) throw new ArgumentOutOfRangeException(nameof(len));
if (len < 0x80) return [(byte)len];
var bytes = new List<byte>();
var n = len;
while (n > 0)
{
bytes.Insert(0, (byte)(n & 0xff));
n /= 256;
}
return [(byte)(0x80 | bytes.Count), .. bytes];
}
/// <summary>Wrap content bytes in a tag + DER length header.</summary>
private static byte[] Tlv(byte tag, byte[] content)
{
var len = EncodeLength(content.Length);
var output = new byte[1 + len.Length + content.Length];
output[0] = tag;
len.CopyTo(output, 1);
content.CopyTo(output, 1 + len.Length);
return output;
}
private static byte[] Concat(params byte[][] parts)
{
var output = new byte[parts.Sum(p => p.Length)];
var offset = 0;
foreach (var p in parts)
{
p.CopyTo(output, offset);
offset += p.Length;
}
return output;
}
/// <summary>
/// DER INTEGER. Accepts a small non-negative number, or 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.
/// </summary>
public static byte[] EncodeInteger(int value)
{
if (value < 0) throw new ArgumentOutOfRangeException(nameof(value));
var digits = new List<byte>();
var n = value;
while (n > 0)
{
digits.Insert(0, (byte)(n & 0xff));
n /= 256;
}
var bytes = digits.Count > 0 ? digits.ToArray() : [0];
return Tlv(0x02, bytes[0] > 0x7f ? [0, .. bytes] : bytes);
}
public static byte[] EncodeInteger(ReadOnlySpan<byte> value)
{
var start = 0;
while (start < value.Length - 1 && value[start] == 0) start++;
var bytes = value[start..].ToArray();
return Tlv(0x02, bytes.Length > 0 && bytes[0] > 0x7f ? [0, .. bytes] : bytes);
}
/// <summary>DER OBJECT IDENTIFIER from a dotted string, e.g. "1.2.840.113549.1.1.11".</summary>
public static byte[] EncodeOid(string oid)
{
var parts = oid.Split('.').Select(long.Parse).ToArray();
if (parts.Length < 2 || parts.Any(p => p < 0))
{
throw new FormatException($"Invalid OID: {oid}");
}
var content = new List<byte> { (byte)(40 * parts[0] + parts[1]) };
for (var i = 2; i < parts.Length; i++)
{
var stack = new List<byte>();
var v = parts[i];
do
{
stack.Insert(0, (byte)(v & 0x7f));
v /= 128;
} while (v > 0);
for (var j = 0; j < stack.Count - 1; j++) content.Add((byte)(stack[j] | 0x80));
content.Add(stack[^1]);
}
return Tlv(0x06, [.. content]);
}
public static byte[] EncodeUtf8String(string str) =>
Tlv(0x0c, Encoding.UTF8.GetBytes(str));
/// <summary>PrintableString — the required type for countryName in a Name.</summary>
public static byte[] EncodePrintableString(string str)
{
if (str.Any(ch => !char.IsAsciiLetterOrDigit(ch) && " '()+,-./:=?".IndexOf(ch) < 0))
{
throw new FormatException($"Not a PrintableString: {str}");
}
return Tlv(0x13, [.. str.Select(ch => (byte)ch)]);
}
public static byte[] EncodeBitString(byte[] data) =>
Tlv(0x03, [0, .. data]); // 0 unused bits in the last octet
public static byte[] EncodeOctetString(byte[] data) => Tlv(0x04, data);
public static byte[] EncodeNull() => [0x05, 0x00];
public static byte[] EncodeSequence(params byte[][] parts) =>
Tlv(0x30, Concat(parts));
public static byte[] EncodeSet(params byte[][] parts) =>
Tlv(0x31, Concat(parts));
// --- Object identifiers -----------------------------------------------------
private const string OidCountry = "2.5.4.6";
private const string OidState = "2.5.4.8";
private const string OidLocality = "2.5.4.7";
private const string OidOrganization = "2.5.4.10";
private const string OidCommonName = "2.5.4.3";
private const string OidEmail = "1.2.840.113549.1.9.1";
private const string OidExtRequest = "1.2.840.113549.1.9.14"; // pkcs-9 at extensionRequest
private const string OidSubjectAltName = "2.5.29.17";
private const string OidRsaSha256 = "1.2.840.113549.1.1.11"; // sha256WithRSAEncryption
private const string OidEcdsaSha256 = "1.2.840.10045.4.3.2"; // ecdsa-with-SHA256
// --- Validation -------------------------------------------------------------
/// <summary>
/// Throws on invalid input: CN required, country (when present) a 2-letter
/// ISO 3166-1 code, key algorithm one of the supported values.
/// </summary>
public static void ValidateOptions(CsrOptions options)
{
if (string.IsNullOrWhiteSpace(options.CommonName))
{
throw new ArgumentException("Common Name (CN) is required.");
}
var country = options.Country?.Trim();
if (country is { Length: 2 } && country.All(char.IsLetter)) return;
if (!string.IsNullOrEmpty(country))
{
throw new ArgumentException("Country must be a 2-letter ISO 3166-1 code (e.g. US, DE).");
}
}
// --- SAN classification -----------------------------------------------------
/// <summary>The four SAN entry kinds.</summary>
public enum SanType { Dns, Ip, Email, Uri }
/// <summary>Classify a SAN entry: IPv4/IPv6 → ip, http(s):// → uri, contains @ → email, else dns.</summary>
public static SanType ClassifySanType(string entry)
{
var value = entry.Trim().ToLowerInvariant();
if (IpToBytes(value) is not null) return SanType.Ip;
if (value.StartsWith("http://", StringComparison.Ordinal) ||
value.StartsWith("https://", StringComparison.Ordinal)) return SanType.Uri;
if (value.Contains('@')) return SanType.Email;
return SanType.Dns;
}
/// <summary>Parse an IPv4 or IPv6 literal to 4/16 bytes; null when not an IP literal.</summary>
private static byte[]? IpToBytes(string ip)
{
var v4 = System.Text.RegularExpressions.Regex.Match(
ip, @"^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$");
if (v4.Success)
{
var parts = v4.Groups.Cast<Group>().Skip(1).Select(g => int.Parse(g.Value)).ToArray();
if (parts.Any(n => n > 255)) return null;
return [.. parts.Select(n => (byte)n)];
}
if (!ip.Contains(':')) return null;
var halves = ip.Split("::");
if (halves.Length > 3) return null;
var groups = new List<ushort>();
foreach (var half in halves)
{
if (half.Length == 0) continue;
foreach (var group in half.Split(':'))
{
if (group.Contains('.'))
{
var tail = IpToBytes(group); // IPv4-mapped tail, e.g. ::ffff:10.0.0.1
if (tail is null || tail.Length != 4) return null;
groups.Add((ushort)((tail[0] << 8) | tail[1]));
groups.Add((ushort)((tail[2] << 8) | tail[3]));
}
else if (group.Length is >= 1 and <= 4 && group.All(Uri.IsHexDigit))
{
groups.Add(Convert.ToUInt16(group, 16));
}
else
{
return null;
}
}
}
if (halves.Length == 3) // one "::" present
{
var missing = 8 - groups.Count;
if (missing < 0) return null;
groups.AddRange(new ushort[missing]);
}
if (groups.Count != 8) return null;
var output = new byte[16];
for (var i = 0; i < 8; i++)
{
output[i * 2] = (byte)(groups[i] >> 8);
output[i * 2 + 1] = (byte)(groups[i] & 0xff);
}
return output;
}
/// <summary>One GeneralName: context-specific implicit tags per RFC 5280.</summary>
private static byte[] EncodeGeneralName(string entry)
{
var value = entry.Trim();
var ascii = Encoding.ASCII.GetBytes(value);
return ClassifySanType(value) switch
{
SanType.Ip => Tlv(0x87, IpToBytes(value.ToLowerInvariant())!),
SanType.Uri => Tlv(0x86, ascii),
SanType.Email => Tlv(0x81, ascii),
_ => Tlv(0x82, ascii), // dNSName
};
}
// --- PEM ---------------------------------------------------------------------
/// <summary>PEM-wrap DER bytes: 64-character base64 lines between BEGIN/END markers.</summary>
public static string PemEncode(byte[] der, string label)
{
var base64 = Convert.ToBase64String(der);
var lines = Enumerable.Range(0, (base64.Length + 63) / 64)
.Select(i => base64.Substring(i * 64, Math.Min(64, base64.Length - i * 64)));
return $"-----BEGIN {label}-----\n{string.Join('\n', lines)}\n-----END {label}-----\n";
}
// --- Key generation + CSR assembly -------------------------------------------
/// <summary>AttributeTypeAndValue (SEQUENCE of OID + value) wrapped in its RDN SET.</summary>
private static byte[] Rdn(string oid, byte[] value) =>
EncodeSet(EncodeSequence(EncodeOid(oid), value));
/// <summary>
/// Generate a key pair and build a signed PKCS#10 CSR (PEM) plus the
/// matching PKCS#8 private key (PEM). .NET's ECDsa.SignData already
/// returns the DER ECDSA-Sig-Value SEQUENCE the TS reference converts
/// raw r||s into, so no conversion is needed here.
/// </summary>
public static CsrResult Generate(CsrOptions options)
{
ValidateOptions(options);
byte[] spki;
byte[] cri;
byte[] signature;
byte[] signatureAlgorithm;
string privateKeyPem;
if (options.KeyAlgorithm == CsrKeyAlgorithm.EcdsaP256)
{
using var ecdsa = ECDsa.Create(ECCurve.NamedCurves.nistP256);
spki = ecdsa.ExportSubjectPublicKeyInfo();
privateKeyPem = ecdsa.ExportPkcs8PrivateKeyPem().Replace("\r\n", "\n");
cri = BuildCri(options, spki, out _);
signatureAlgorithm = EncodeSequence(EncodeOid(OidEcdsaSha256));
var der = ecdsa.SignData(cri, HashAlgorithmName.SHA256); // already DER r,s SEQUENCE
signature = EncodeBitString(der);
}
else
{
using var rsa = RSA.Create(options.KeyAlgorithm == CsrKeyAlgorithm.Rsa4096 ? 4096 : 2048);
spki = rsa.ExportSubjectPublicKeyInfo();
privateKeyPem = rsa.ExportPkcs8PrivateKeyPem().Replace("\r\n", "\n");
cri = BuildCri(options, spki, out _);
signatureAlgorithm = EncodeSequence(EncodeOid(OidRsaSha256), EncodeNull());
var raw = rsa.SignData(cri, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
signature = EncodeBitString(raw);
}
return new CsrResult(
PemEncode(EncodeSequence(cri, signatureAlgorithm, signature), "CERTIFICATE REQUEST"),
privateKeyPem);
}
/// <summary>Assemble the CertificationRequestInfo: version 0, subject, SPKI, [0] attributes.</summary>
private static byte[] BuildCri(CsrOptions options, byte[] spki, out byte[] attributes)
{
// subject: RDNSequence in the conventional C, ST, L, O, CN, email order
var rdns = new List<byte[]>();
var country = options.Country?.Trim().ToUpperInvariant();
if (!string.IsNullOrEmpty(country)) rdns.Add(Rdn(OidCountry, EncodePrintableString(country)));
if (!string.IsNullOrWhiteSpace(options.State)) rdns.Add(Rdn(OidState, EncodeUtf8String(options.State!.Trim())));
if (!string.IsNullOrWhiteSpace(options.Locality)) rdns.Add(Rdn(OidLocality, EncodeUtf8String(options.Locality!.Trim())));
if (!string.IsNullOrWhiteSpace(options.Organization)) rdns.Add(Rdn(OidOrganization, EncodeUtf8String(options.Organization!.Trim())));
rdns.Add(Rdn(OidCommonName, EncodeUtf8String(options.CommonName.Trim())));
if (!string.IsNullOrWhiteSpace(options.Email)) rdns.Add(Rdn(OidEmail, EncodeUtf8String(options.Email!.Trim())));
// attributes [0] IMPLICIT SET OF — extensionRequest carrying subjectAltName
attributes = [];
var sans = (options.SubjectAltNames ?? []).Select(s => s.Trim()).Where(s => s.Length > 0).ToArray();
if (sans.Length > 0)
{
var extension = EncodeSequence(
EncodeOid(OidSubjectAltName),
EncodeOctetString(EncodeSequence(sans.Select(EncodeGeneralName).ToArray())));
var attribute = EncodeSequence(EncodeOid(OidExtRequest), EncodeSet(EncodeSequence(extension)));
attributes = Tlv(0xa0, attribute); // [0] IMPLICIT SET OF Attribute
}
return EncodeSequence(EncodeInteger(0), EncodeSequence([.. rdns]), spki, attributes);
}
}
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 →