CSR Generator — Kotlin 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 Kotlin implementation — the same logic the interactive tool runs, in a shareable, citable form.
// CSR Generator - ASN.1 DER encoding + PKCS#10 CSR construction on the JVM's
// standard key/signature APIs.
//
// Language: Kotlin 1.9+ (JVM), standard library only.
// Ported from src/lib/csr-generator.ts — display source, part of CosmoDev's
// polyglot tool pages. Functionally equivalent to the TS reference (which
// drives Web Crypto): the whole certificate-request pipeline runs in-process
// - key pair generation, DER assembly of the CertificationRequest structure,
// signature, and PEM wrapping. Nothing leaves the machine.
//
// One deliberate divergence, noted inline: the JVM's "SHA256withECDSA"
// already emits the DER ECDSA-Sig-Value SEQUENCE that PKCS#10 expects, so
// the TS reference's raw r||s -> DER conversion is unnecessary here.
import java.math.BigInteger
import java.security.KeyPair
import java.security.KeyPairGenerator
import java.security.Signature
import java.security.spec.ECGenParameterSpec
import java.security.spec.RSAKeyGenParameterSpec
import java.util.Base64
/** The three key algorithms the tool offers. */
enum class KeyAlgorithm { RSA_2048, RSA_4096, ECDSA_P256 }
data class CSROptions(
val commonName: String,
val organization: String? = null,
val country: String? = null,
val state: String? = null,
val locality: String? = null,
val email: String? = null,
val keyAlgorithm: KeyAlgorithm,
val subjectAltNames: List<String> = emptyList(),
)
data class CSRResult(
val csr: String,
val privateKey: String,
)
// --- DER primitives ---------------------------------------------------------
/** DER length: short form below 0x80, long form (0x80 | byte count) above. */
fun encodeLength(len: Int): ByteArray {
require(len >= 0) { "Length must be a non-negative integer" }
if (len < 0x80) return byteArrayOf(len.toByte())
val bytes = mutableListOf<Byte>()
var n = len
while (n > 0) {
bytes.add(0, (n and 0xff).toByte())
n = n shr 8
}
return (byteArrayOf((0x80 or bytes.size).toByte()) + bytes.toByteArray())
}
/** Wrap content bytes in a tag + DER length header. */
private fun tlv(tag: Int, content: ByteArray): ByteArray =
byteArrayOf(tag.toByte()) + encodeLength(content.size) + content
private fun concat(vararg parts: ByteArray): ByteArray {
val out = ByteArray(parts.sumOf { it.size })
var offset = 0
for (p in parts) {
p.copyInto(out, offset)
offset += p.size
}
return out
}
/**
* DER INTEGER from a small non-negative number: leading zeros stripped and a
* 0x00 sign byte prepended when the high bit is set, per DER minimal rules.
* BigInteger.toByteArray() already emits exactly that minimal two's-complement
* form.
*/
fun encodeInteger(value: Int): ByteArray {
require(value >= 0) { "encodeInteger supports non-negative integers only" }
return tlv(0x02, BigInteger.valueOf(value.toLong()).toByteArray())
}
/**
* 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.
*/
fun encodeInteger(value: ByteArray): ByteArray {
var start = 0
while (start < value.size - 1 && value[start].toInt() == 0) start++
var bytes = value.copyOfRange(start, value.size)
if (bytes.isNotEmpty() && (bytes[0].toInt() and 0xff) > 0x7f) {
bytes = byteArrayOf(0) + bytes
}
return tlv(0x02, bytes)
}
/** DER OBJECT IDENTIFIER from a dotted string, e.g. "1.2.840.113549.1.1.11". */
fun encodeOID(oid: String): ByteArray {
val parts = oid.split('.').map { it.toIntOrNull() ?: throw IllegalArgumentException("Invalid OID: $oid") }
if (parts.size < 2 || parts.any { it < 0 }) throw IllegalArgumentException("Invalid OID: $oid")
val content = mutableListOf(40 * parts[0] + parts[1])
for (i in 2 until parts.size) {
val stack = mutableListOf<Int>()
var v = parts[i]
do {
stack.add(0, v and 0x7f)
v = v shr 7
} while (v > 0)
for (j in 0 until stack.size - 1) content.add(stack[j] or 0x80)
content.add(stack.last())
}
return tlv(0x06, content.map { it.toByte() }.toByteArray())
}
fun encodeUTF8String(str: String): ByteArray = tlv(0x0c, str.toByteArray(Charsets.UTF_8))
/** PrintableString - the required type for countryName in a Name. */
fun encodePrintableString(str: String): ByteArray {
if (!Regex("^[A-Za-z0-9 '()+,\\-./:=?]*$").matches(str)) {
throw IllegalArgumentException("Not a PrintableString: $str")
}
return tlv(0x13, str.toByteArray(Charsets.ISO_8859_1))
}
fun encodeBitString(data: ByteArray): ByteArray =
tlv(0x03, byteArrayOf(0) + data) // 0 unused bits in the last octet
fun encodeOctetString(data: ByteArray): ByteArray = tlv(0x04, data)
fun encodeNull(): ByteArray = byteArrayOf(0x05, 0x00)
fun encodeSequence(vararg parts: ByteArray): ByteArray = tlv(0x30, concat(*parts))
fun encodeSet(vararg parts: ByteArray): ByteArray = tlv(0x31, concat(*parts))
// --- Object identifiers -----------------------------------------------------
private const val OID_COUNTRY = "2.5.4.6"
private const val OID_STATE = "2.5.4.8"
private const val OID_LOCALITY = "2.5.4.7"
private const val OID_ORGANIZATION = "2.5.4.10"
private const val OID_COMMON_NAME = "2.5.4.3"
private const val OID_EMAIL = "1.2.840.113549.1.9.1"
private const val OID_EXT_REQUEST = "1.2.840.113549.1.9.14" // pkcs-9 at extensionRequest
private const val OID_SUBJECT_ALT_NAME = "2.5.29.17"
private const val OID_RSA_SHA256 = "1.2.840.113549.1.1.11" // sha256WithRSAEncryption
private const val OID_ECDSA_SHA256 = "1.2.840.10045.4.3.2" // ecdsa-with-SHA256
// --- Validation -------------------------------------------------------------
/**
* Throws on invalid input: CN required, country (when present) a 2-letter ISO
* 3166-1 code, key algorithm one of the supported values.
*/
fun validateCSROptions(options: CSROptions) {
if (options.commonName.isBlank()) {
throw IllegalArgumentException("Common Name (CN) is required")
}
val country = options.country?.trim()
if (!country.isNullOrEmpty() && !Regex("^[A-Za-z]{2}$").matches(country)) {
throw IllegalArgumentException("Country must be a 2-letter ISO 3166-1 code (e.g. US, DE)")
}
}
// --- SAN classification -----------------------------------------------------
/** How a SAN entry is encoded in the GeneralName structure. */
enum class SanType { DNS, IP, EMAIL, URI }
/** Classify a SAN entry: IPv4/IPv6 -> ip, http(s):// -> uri, contains @ -> email, else dns. */
fun classifySanType(entry: String): SanType {
val value = entry.trim().lowercase()
if (ipToBytes(value) != null) return SanType.IP
if (Regex("^https?://").containsMatchIn(value)) return SanType.URI
if (value.contains('@')) return SanType.EMAIL
return SanType.DNS
}
fun ipToBytes(ip: String): ByteArray? {
val v4 = Regex("^(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})$").find(ip)
if (v4 != null) {
val parts = v4.groupValues.drop(1).map { it.toInt() }
if (parts.any { it > 255 }) return null
return parts.map { it.toByte() }.toByteArray()
}
if (!ip.contains(':')) return null
val halves = ip.split("::")
if (halves.size > 2) return null
val groups = mutableListOf<Int>()
for (half in halves) {
if (half.isEmpty()) continue
for (group in half.split(':')) {
if (group.contains('.')) {
val tail = ipToBytes(group) // IPv4-mapped tail, e.g. ::ffff:10.0.0.1
if (tail == null || tail.size != 4) return null
groups.add(((tail[0].toInt() and 0xff) shl 8) or (tail[1].toInt() and 0xff))
groups.add(((tail[2].toInt() and 0xff) shl 8) or (tail[3].toInt() and 0xff))
} else if (Regex("^[0-9a-f]{1,4}$").matches(group)) {
groups.add(group.toInt(16))
} else {
return null
}
}
}
if (halves.size == 2) {
val missing = 8 - groups.size
if (missing < 0) return null
repeat(missing) { groups.add(0) }
}
if (groups.size != 8) return null
return ByteArray(16) { i ->
if (i % 2 == 0) (groups[i / 2] shr 8).toByte() else (groups[i / 2] and 0xff).toByte()
}
}
/** One GeneralName: context-specific implicit tags per RFC 5280. */
private fun encodeGeneralName(entry: String): ByteArray {
val value = entry.trim()
val ascii = value.toByteArray(Charsets.ISO_8859_1)
return when (classifySanType(value)) {
SanType.IP -> tlv(0x87, ipToBytes(value.lowercase())!!)
SanType.URI -> tlv(0x86, ascii)
SanType.EMAIL -> tlv(0x81, ascii)
SanType.DNS -> tlv(0x82, ascii) // dNSName
}
}
// --- PEM ----------------------------------------------------------------------
/** PEM-wrap DER bytes: 64-character base64 lines between BEGIN/END markers. */
fun pemEncode(der: ByteArray, label: String): String {
val b64 = Base64.getEncoder().encodeToString(der)
return "-----BEGIN $label-----\n${b64.chunked(64).joinToString("\n")}\n-----END $label-----\n"
}
// --- Key generation + CSR assembly -------------------------------------------
private fun generateKeyPair(algorithm: KeyAlgorithm): KeyPair {
val kpg = if (algorithm == KeyAlgorithm.ECDSA_P256) {
KeyPairGenerator.getInstance("EC").apply { initialize(ECGenParameterSpec("secp256r1")) }
} else {
val bits = if (algorithm == KeyAlgorithm.RSA_4096) 4096 else 2048
KeyPairGenerator.getInstance("RSA").apply {
initialize(RSAKeyGenParameterSpec(bits, RSAKeyGenParameterSpec.F4)) // e = 65537
}
}
return kpg.generateKeyPair()
}
/** AttributeTypeAndValue (SEQUENCE of OID + value) wrapped in its RDN SET. */
private fun rdn(oid: String, value: ByteArray): ByteArray =
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 the JVM's key and
* signature services.
*/
fun generateCSR(options: CSROptions): CSRResult {
validateCSROptions(options)
val pair = generateKeyPair(options.keyAlgorithm)
val spki = pair.public.encoded // X.509 SubjectPublicKeyInfo
val pkcs8 = pair.private.encoded // PKCS#8
// subject: RDNSequence in the conventional C, ST, L, O, CN, email order
val rdns = mutableListOf<ByteArray>()
options.country?.trim()?.takeIf { it.isNotEmpty() }?.let {
rdns.add(rdn(OID_COUNTRY, encodePrintableString(it.uppercase())))
}
options.state?.trim()?.takeIf { it.isNotEmpty() }?.let { rdns.add(rdn(OID_STATE, encodeUTF8String(it))) }
options.locality?.trim()?.takeIf { it.isNotEmpty() }?.let { rdns.add(rdn(OID_LOCALITY, encodeUTF8String(it))) }
options.organization?.trim()?.takeIf { it.isNotEmpty() }?.let { rdns.add(rdn(OID_ORGANIZATION, encodeUTF8String(it))) }
rdns.add(rdn(OID_COMMON_NAME, encodeUTF8String(options.commonName.trim())))
options.email?.trim()?.takeIf { it.isNotEmpty() }?.let { rdns.add(rdn(OID_EMAIL, encodeUTF8String(it))) }
// attributes [0] IMPLICIT SET OF — extensionRequest carrying subjectAltName
var attributes = ByteArray(0)
val sans = options.subjectAltNames.map { it.trim() }.filter { it.isNotEmpty() }
if (sans.isNotEmpty()) {
val extension = encodeSequence(
encodeOID(OID_SUBJECT_ALT_NAME),
encodeOctetString(encodeSequence(*sans.map(::encodeGeneralName).toTypedArray())),
)
val 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
val cri = encodeSequence(encodeInteger(0), encodeSequence(*rdns.toTypedArray()), spki, attributes)
// signatureAlgorithm + signature over the DER CRI. The JVM's ECDSA
// signature is already the DER ECDSA-Sig-Value SEQUENCE PKCS#10 wants
// (Web Crypto returns raw r||s, which the TS reference converts).
return if (options.keyAlgorithm == KeyAlgorithm.ECDSA_P256) {
val signatureAlgorithm = encodeSequence(encodeOID(OID_ECDSA_SHA256))
val raw = Signature.getInstance("SHA256withECDSA").apply {
initSign(pair.private)
update(cri)
}.sign()
CSRResult(
csr = pemEncode(encodeSequence(cri, signatureAlgorithm, encodeBitString(raw)), "CERTIFICATE REQUEST"),
privateKey = pemEncode(pkcs8, "PRIVATE KEY"),
)
} else {
val signatureAlgorithm = encodeSequence(encodeOID(OID_RSA_SHA256), encodeNull())
val raw = Signature.getInstance("SHA256withRSA").apply {
initSign(pair.private)
update(cri)
}.sign()
CSRResult(
csr = pemEncode(encodeSequence(cri, signatureAlgorithm, encodeBitString(raw)), "CERTIFICATE REQUEST"),
privateKey = pemEncode(pkcs8, "PRIVATE KEY"),
)
}
}
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 →