CSR Generator — Swift 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 Swift implementation — the same logic the interactive tool runs, in a shareable, citable form.
// csr-generator — ASN.1 DER encoding + PKCS#10 CSR construction.
//
// Language: Swift 5.9+ (Foundation + CryptoKit + Security)
// Ported from src/lib/csr-generator.ts
// display source — part of CosmoDev's polyglot tool pages
//
// The whole certificate-request pipeline runs on-device: key pair generation
// (CryptoKit for ECDSA P-256, Security framework SecKey for RSA), DER
// assembly of the CertificationRequest structure, signature, and PEM
// wrapping. Nothing leaves the machine.
import Foundation
import CryptoKit
import Security
// MARK: - Types
enum KeyAlgorithm: String {
case rsa2048 = "RSA-2048"
case rsa4096 = "RSA-4096"
case ecdsaP256 = "ECDSA-P256"
}
struct CSROptions {
var commonName: String
var organization: String? = nil
var country: String? = nil
var state: String? = nil
var locality: String? = nil
var email: String? = nil
var keyAlgorithm: KeyAlgorithm
var subjectAltNames: [String]? = nil
}
struct CSRResult {
let csr: String
let privateKey: String
}
enum CSRError: Error, CustomStringConvertible {
case badLength
case negativeInteger
case invalidOID(String)
case notPrintable(String)
case missingCN
case badCountry
case unsupportedAlgorithm(String)
case keyGeneration(String)
var description: String {
switch self {
case .badLength: return "Length must be a non-negative integer"
case .negativeInteger: return "encodeInteger supports non-negative integers only"
case .invalidOID(let oid): return "Invalid OID: \(oid)"
case .notPrintable(let s): return "Not a PrintableString: \(s)"
case .missingCN: return "Common Name (CN) is required"
case .badCountry: return "Country must be a 2-letter ISO 3166-1 code (e.g. US, DE)"
case .unsupportedAlgorithm(let a): return "Unsupported key algorithm: \(a)"
case .keyGeneration(let why): return "Key generation failed: \(why)"
}
}
}
// MARK: - DER primitives
/// DER length: short form below 0x80, long form (0x80 | byte count) above.
func encodeLength(_ len: Int) throws -> [UInt8] {
if len < 0 { throw CSRError.badLength }
if len < 0x80 { return [UInt8(len)] }
var bytes: [UInt8] = []
var n = len
while n > 0 {
bytes.insert(UInt8(n & 0xff), at: 0)
n >>= 8
}
return [0x80 | UInt8(bytes.count)] + bytes
}
/// Wrap content bytes in a tag + DER length header.
func tlv(_ tag: UInt8, _ content: [UInt8]) throws -> [UInt8] {
let len = try encodeLength(content.count)
return [tag] + len + content
}
func concat(_ parts: [[UInt8]]) -> [UInt8] {
parts.reduce(into: []) { out, p in out += p }
}
/**
* 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.
*/
func encodeInteger(_ value: Int) throws -> [UInt8] {
if value < 0 { throw CSRError.negativeInteger }
var digits: [UInt8] = []
var n = value
while n > 0 {
digits.insert(UInt8(n & 0xff), at: 0)
n >>= 8
}
if digits.isEmpty { digits = [0] }
return try integerTLV(digits)
}
func encodeIntegerBytes(_ value: [UInt8]) throws -> [UInt8] {
var start = 0
while start < value.count - 1 && value[start] == 0 { start += 1 }
return try integerTLV(Array(value[start...]))
}
private func integerTLV(_ bytes: [UInt8]) throws -> [UInt8] {
var bytes = bytes
if bytes[0] > 0x7f { bytes = [0] + bytes }
return try tlv(0x02, bytes)
}
/// DER OBJECT IDENTIFIER from a dotted string, e.g. '1.2.840.113549.1.1.11'.
func encodeOID(_ oid: String) throws -> [UInt8] {
let parts = oid.split(separator: ".").compactMap { Int($0) }
guard oid.split(separator: ".").count >= 2, parts.count == oid.split(separator: ".").count,
parts.allSatisfy({ $0 >= 0 }) else {
throw CSRError.invalidOID(oid)
}
var content: [UInt8] = [UInt8(40 * parts[0] + parts[1])]
for i in 2..<parts.count {
var stack: [UInt8] = []
var v = parts[i]
repeat {
stack.insert(UInt8(v & 0x7f), at: 0)
v >>= 7
} while v > 0
for j in 0..<(stack.count - 1) { content.append(stack[j] | 0x80) }
content.append(stack[stack.count - 1])
}
return try tlv(0x06, content)
}
func encodeUTF8String(_ str: String) throws -> [UInt8] {
try tlv(0x0c, Array(str.utf8))
}
/// PrintableString — the required type for countryName in a Name.
func encodePrintableString(_ str: String) throws -> [UInt8] {
let allowed = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789 '()+,-./:=?"
guard str.allSatisfy({ allowed.contains($0) }) else { throw CSRError.notPrintable(str) }
return try tlv(0x13, Array(str.utf8))
}
func encodeBitString(_ data: [UInt8]) throws -> [UInt8] {
try tlv(0x03, [0] + data) // 0 unused bits in the last octet
}
func encodeOctetString(_ data: [UInt8]) throws -> [UInt8] {
try tlv(0x04, data)
}
func encodeNull() -> [UInt8] {
[0x05, 0x00]
}
func encodeSequence(_ parts: [[UInt8]]) throws -> [UInt8] {
try tlv(0x30, concat(parts))
}
func encodeSet(_ parts: [[UInt8]]) throws -> [UInt8] {
try tlv(0x31, concat(parts))
}
// MARK: - Object identifiers
let oidCountry = "2.5.4.6"
let oidState = "2.5.4.8"
let oidLocality = "2.5.4.7"
let oidOrganization = "2.5.4.10"
let oidCommonName = "2.5.4.3"
let oidEmail = "1.2.840.113549.1.9.1"
let oidExtRequest = "1.2.840.113549.1.9.14" // pkcs-9 at extensionRequest
let oidSubjectAltName = "2.5.29.17"
let oidRsaSha256 = "1.2.840.113549.1.1.11" // sha256WithRSAEncryption
let oidEcdsaSha256 = "1.2.840.10045.4.3.2" // ecdsa-with-SHA256
let oidRsaEncryption = "1.2.840.113549.1.1.1"
let oidEcPublicKey = "1.2.840.10045.2.1"
let oidPrime256v1 = "1.2.840.10045.3.1.7"
// MARK: - Validation
/// Throws on invalid input: CN required, country (when present) a 2-letter ISO
/// 3166-1 code, key algorithm one of the supported values.
func validateCSROptions(_ options: CSROptions) throws {
guard !options.commonName.trimmingCharacters(in: .whitespaces).isEmpty else {
throw CSRError.missingCN
}
if let country = options.country?.trimmingCharacters(in: .whitespaces), !country.isEmpty {
guard country.count == 2, country.allSatisfy({ $0.isLetter }) else { throw CSRError.badCountry }
}
// keyAlgorithm is a typed enum in Swift - only the three supported values
// can be constructed from the raw strings in the first place.
}
// MARK: - SAN classification
enum SanType: String {
case dns
case ip
case email
case uri
}
/// Classify a SAN entry: IPv4/IPv6 -> ip, http(s):// -> uri, contains @ -> email, else dns.
func classifySanType(_ entry: String) -> SanType {
let value = entry.trimmingCharacters(in: .whitespaces).lowercased()
if ipToBytes(value) != nil { return .ip }
if value.hasPrefix("http://") || value.hasPrefix("https://") { return .uri }
if value.contains("@") { return .email }
return .dns
}
func ipToBytes(_ ip: String) -> [UInt8]? {
// IPv4 dotted quad
let v4 = ip.split(separator: ".", omittingEmptySubsequences: false)
if v4.count == 4 {
let parts = v4.compactMap { Int($0) }
if parts.count == 4, v4.allSatisfy({ !$0.isEmpty }), parts.allSatisfy({ $0 <= 255 }) {
return parts.map(UInt8.init)
}
}
guard ip.contains(":") else { return nil }
let halves = ip.components(separatedBy: "::")
if halves.count > 2 { return nil }
var groups: [Int] = []
for half in halves {
guard !half.isEmpty else { continue }
for group in half.split(separator: ":") {
if group.contains(".") {
// IPv4-mapped tail, e.g. ::ffff:10.0.0.1
guard let tail = ipToBytes(String(group)), tail.count == 4 else { return nil }
groups.append((Int(tail[0]) << 8) | Int(tail[1]))
groups.append((Int(tail[2]) << 8) | Int(tail[3]))
} else if group.count <= 4, group.allSatisfy({ $0.isHexDigit }), let g = Int(group, radix: 16) {
groups.append(g)
} else {
return nil
}
}
}
if halves.count == 2 {
let missing = 8 - groups.count
if missing < 0 { return nil }
groups += Array(repeating: 0, count: missing)
}
guard groups.count == 8 else { return nil }
var out = [UInt8](repeating: 0, count: 16)
for (i, g) in groups.enumerated() {
out[i * 2] = UInt8(g >> 8)
out[i * 2 + 1] = UInt8(g & 0xff)
}
return out
}
/// One GeneralName: context-specific implicit tags per RFC 5280.
func encodeGeneralName(_ entry: String) throws -> [UInt8] {
let value = entry.trimmingCharacters(in: .whitespaces)
let ascii = Array(value.utf8)
switch classifySanType(value) {
case .ip:
return try tlv(0x87, ipToBytes(value.lowercased())!)
case .uri:
return try tlv(0x86, ascii)
case .email:
return try tlv(0x81, ascii)
case .dns:
return try tlv(0x82, ascii) // dNSName
}
}
// MARK: - PEM
/// PEM-wrap DER bytes: 64-character base64 lines between BEGIN/END markers.
func pemEncode(_ der: [UInt8], label: String) -> String {
let b64 = Data(der).base64EncodedString()
var lines: [String] = []
var idx = b64.startIndex
while idx < b64.endIndex {
let end = b64.index(idx, offsetBy: 64, limitedBy: b64.endIndex) ?? b64.endIndex
lines.append(String(b64[idx..<end]))
idx = end
}
return "-----BEGIN \(label)-----\n\(lines.joined(separator: "\n"))\n-----END \(label)-----\n"
}
// MARK: - Key generation + CSR assembly
/// A generated key pair, normalized to the two DER forms the CSR needs.
struct GeneratedKeyPair {
let spki: [UInt8] // SubjectPublicKeyInfo DER
let pkcs8: [UInt8] // PKCS#8 private key DER
let signSHA256: ([UInt8]) throws -> [UInt8] // signature over a SHA-256 digest payload
}
func generateKeyPair(_ algorithm: KeyAlgorithm) throws -> GeneratedKeyPair {
switch algorithm {
case .ecdsaP256:
// CryptoKit: P-256 keygen + raw r||s signatures (same as Web Crypto).
let key = P256.Signing.PrivateKey()
// SPKI = AlgorithmIdentifier(id-ecPublicKey, prime256v1) + BIT STRING(point)
let point = key.publicKey.x963Representation // 0x04 || X || Y
let spki = try encodeSequence([
try encodeSequence([try encodeOID(oidEcPublicKey), try encodeOID(oidPrime256v1)]),
try encodeBitString([UInt8](point)),
])
// PKCS#8 wraps the SEC1 ECPrivateKey produced by x963Representation.
let sec1 = [UInt8](key.x963Representation)
let pkcs8 = try encodeSequence([
try encodeInteger(0),
try encodeSequence([try encodeOID(oidEcPublicKey), try encodeOID(oidPrime256v1)]),
try encodeOctetString(sec1),
])
return GeneratedKeyPair(spki: spki, pkcs8: pkcs8) { data in
// .rawRepresentation of an ECDSASignature is the 64-byte raw r||s.
[UInt8](try key.signature(for: Data(data)).rawRepresentation)
}
case .rsa2048, .rsa4096:
// Security framework: SecKey RSA keygen (CryptoKit has no RSA).
let bits = algorithm == .rsa4096 ? 4096 : 2048
var error: Unmanaged<CFError>?
let params: [String: Any] = [
kSecAttrKeyType as String: kSecAttrKeyTypeRSA,
kSecAttrKeySizeInBits as String: bits,
]
guard let secKey = SecKeyCreateRandomKey(params as CFDictionary, &error) else {
throw CSRError.keyGeneration(String(describing: error?.takeRetainedValue()))
}
// SecKeyCreateRandomKey returns the PRIVATE key; the public one hangs
// off it. Public export is PKCS#1 (RSAPublicKey) - wrap it in SPKI.
guard let publicKey = SecKeyCopyPublicKey(secKey),
let pkcs1 = SecKeyCopyExternalRepresentation(publicKey, &error) as Data? else {
throw CSRError.keyGeneration("public key export failed")
}
let spki = try encodeSequence([
try encodeSequence([try encodeOID(oidRsaEncryption), encodeNull()]),
try encodeBitString([UInt8](pkcs1)),
])
// Private: SecKey exports RSA private keys as PKCS#8 directly.
guard let priv = SecKeyCopyExternalRepresentation(secKey, &error) as Data? else {
throw CSRError.keyGeneration("private key export failed")
}
return GeneratedKeyPair(spki: spki, pkcs8: [UInt8](priv)) { data in
guard let sig = SecKeyCreateSignature(
secKey,
.rsaSignatureMessagePKCS1v15SHA256,
Data(data) as CFData,
&error
) as Data? else {
throw CSRError.keyGeneration("RSA signing failed")
}
return [UInt8](sig)
}
}
}
/// AttributeTypeAndValue (SEQUENCE of OID + value) wrapped in its RDN SET.
func rdn(_ oid: String, _ value: [UInt8]) throws -> [UInt8] {
try encodeSet([try encodeSequence([try encodeOID(oid), value])])
}
/**
* CryptoKit/SecKey ECDSA signatures are raw r||s; PKCS#10 expects a DER
* ECDSA-Sig-Value SEQUENCE. Convert half-length integer halves to DER.
*/
func ecdsaRawToDer(_ raw: [UInt8]) throws -> [UInt8] {
let half = raw.count / 2
return try encodeSequence([try encodeIntegerBytes(Array(raw[0..<half])),
try encodeIntegerBytes(Array(raw[half...]))])
}
/**
* Generate a key pair and build a signed PKCS#10 CSR (PEM) plus the matching
* PKCS#8 private key (PEM). Runs entirely on-device.
*/
func generateCSR(_ options: CSROptions) throws -> CSRResult {
try validateCSROptions(options)
let pair = try generateKeyPair(options.keyAlgorithm)
let spki = pair.spki
let pkcs8 = pair.pkcs8
// subject: RDNSequence in the conventional C, ST, L, O, CN, email order
var rdns: [[UInt8]] = []
if let country = options.country?.trimmingCharacters(in: .whitespaces).uppercased(), !country.isEmpty {
rdns.append(try rdn(oidCountry, encodePrintableString(country)))
}
if let state = options.state?.trimmingCharacters(in: .whitespaces), !state.isEmpty {
rdns.append(try rdn(oidState, try encodeUTF8String(state)))
}
if let locality = options.locality?.trimmingCharacters(in: .whitespaces), !locality.isEmpty {
rdns.append(try rdn(oidLocality, try encodeUTF8String(locality)))
}
if let organization = options.organization?.trimmingCharacters(in: .whitespaces), !organization.isEmpty {
rdns.append(try rdn(oidOrganization, try encodeUTF8String(organization)))
}
rdns.append(try rdn(oidCommonName, try encodeUTF8String(options.commonName.trimmingCharacters(in: .whitespaces))))
if let email = options.email?.trimmingCharacters(in: .whitespaces), !email.isEmpty {
rdns.append(try rdn(oidEmail, try encodeUTF8String(email)))
}
// attributes [0] IMPLICIT SET OF — extensionRequest carrying subjectAltName
var attributes: [UInt8] = []
let sans = (options.subjectAltNames ?? [])
.map { $0.trimmingCharacters(in: .whitespaces) }
.filter { !$0.isEmpty }
if !sans.isEmpty {
let sanExtension = try encodeSequence([
try encodeOID(oidSubjectAltName),
try encodeOctetString(try encodeSequence(try sans.map(encodeGeneralName))),
])
let attribute = try encodeSequence([
try encodeOID(oidExtRequest),
try encodeSet([try encodeSequence([sanExtension])]),
])
attributes = try tlv(0xa0, attribute) // [0] IMPLICIT SET OF Attribute
}
// CertificationRequestInfo: version 0, subject, SPKI, [0] attributes
let cri = try encodeSequence([try encodeInteger(0), try encodeSequence(rdns), spki, attributes])
// signatureAlgorithm + signature over the DER CRI
let signatureAlgorithm: [UInt8]
let signature: [UInt8]
if options.keyAlgorithm == .ecdsaP256 {
signatureAlgorithm = try encodeSequence([try encodeOID(oidEcdsaSha256)])
let raw = try pair.signSHA256(cri)
signature = try encodeBitString(try ecdsaRawToDer(raw))
} else {
signatureAlgorithm = try encodeSequence([try encodeOID(oidRsaSha256), encodeNull()])
let raw = try pair.signSHA256(cri)
signature = try encodeBitString(raw)
}
return CSRResult(
csr: pemEncode(try encodeSequence([cri, signatureAlgorithm, signature]), label: "CERTIFICATE REQUEST"),
privateKey: pemEncode(pkcs8, label: "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 →