Skip to content

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 →