Skip to content

CSR Generator — Java 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 Java 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 JDK's
// crypto providers.
//
// Language: Java (17+, standard library only)
// Ported from src/lib/csr-generator.ts
// display source — part of CosmoDev's polyglot tool pages.
//
// The whole certificate-request pipeline runs locally: key pair generation
// (java.security), DER assembly of the CertificationRequest structure,
// signature, and PEM wrapping. Nothing leaves the machine.
//
// One platform difference from the TS reference: Web Crypto's ECDSA sign()
// returns a raw r||s pair that must be hand-converted to a DER
// ECDSA-Sig-Value, while JCA's "SHA256withECDSA" already emits DER — so the
// raw-to-DER helper is not needed here.

import java.nio.charset.StandardCharsets;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.Signature;
import java.security.spec.ECGenParameterSpec;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import java.util.regex.Pattern;

public final class CsrGenerator {

    public record CSROptions(
        String commonName, String organization, String country, String state,
        String locality, String email, String keyAlgorithm, List<String> subjectAltNames) {
    }

    public record CSRResult(String csr, String privateKey) {
    }

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

    /** DER length: short form below 0x80, long form (0x80 | byte count) above. */
    public static byte[] encodeLength(int len) {
        if (len < 0) throw new IllegalArgumentException("Length must be a non-negative integer");
        if (len < 0x80) return new byte[] {(byte) len};
        List<Byte> bytes = new ArrayList<>();
        int n = len;
        while (n > 0) {
            bytes.add(0, (byte) (n & 0xff));
            n >>>= 8;
        }
        byte[] out = new byte[1 + bytes.size()];
        out[0] = (byte) (0x80 | bytes.size());
        for (int i = 0; i < bytes.size(); i++) out[i + 1] = bytes.get(i);
        return out;
    }

    /** Wrap content bytes in a tag + DER length header. */
    private static byte[] tlv(int tag, byte[] content) {
        byte[] len = encodeLength(content.length);
        byte[] out = new byte[1 + len.length + content.length];
        out[0] = (byte) tag;
        System.arraycopy(len, 0, out, 1, len.length);
        System.arraycopy(content, 0, out, 1 + len.length, content.length);
        return out;
    }

    private static byte[] concat(byte[]... parts) {
        int total = 0;
        for (byte[] p : parts) total += p.length;
        byte[] out = new byte[total];
        int offset = 0;
        for (byte[] p : parts) {
            System.arraycopy(p, 0, out, offset, p.length);
            offset += p.length;
        }
        return out;
    }

    /**
     * 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.
     */
    public static byte[] encodeInteger(byte[] value) {
        int start = 0;
        while (start < value.length - 1 && value[start] == 0) start++;
        byte[] bytes = java.util.Arrays.copyOfRange(value, start, value.length);
        if ((bytes[0] & 0xff) > 0x7f) bytes = concat(new byte[] {0}, bytes);
        return tlv(0x02, bytes);
    }

    /** DER INTEGER from a small non-negative number. */
    public static byte[] encodeInteger(int value) {
        if (value < 0) {
            throw new IllegalArgumentException("encodeInteger supports non-negative integers only");
        }
        List<Byte> digits = new ArrayList<>();
        int n = value;
        while (n > 0) {
            digits.add(0, (byte) (n & 0xff));
            n >>>= 8;
        }
        byte[] bytes = new byte[digits.isEmpty() ? 1 : digits.size()];
        if (digits.isEmpty()) bytes[0] = 0;
        for (int i = 0; i < digits.size(); i++) bytes[i] = digits.get(i);
        return encodeInteger(bytes);
    }

    /** DER OBJECT IDENTIFIER from a dotted string, e.g. '1.2.840.113549.1.1.11'. */
    public static byte[] encodeOID(String oid) {
        String[] parts = oid.split("\\.");
        if (parts.length < 2) throw new IllegalArgumentException("Invalid OID: " + oid);
        int[] numbers = new int[parts.length];
        for (int i = 0; i < parts.length; i++) {
            try {
                numbers[i] = Integer.parseInt(parts[i]);
            } catch (NumberFormatException e) {
                throw new IllegalArgumentException("Invalid OID: " + oid);
            }
            if (numbers[i] < 0) throw new IllegalArgumentException("Invalid OID: " + oid);
        }
        List<Integer> content = new ArrayList<>();
        content.add(40 * numbers[0] + numbers[1]);
        for (int i = 2; i < numbers.length; i++) {
            List<Integer> stack = new ArrayList<>();
            int v = numbers[i];
            do {
                stack.add(0, v & 0x7f);
                v >>>= 7;
            } while (v > 0);
            for (int j = 0; j < stack.size() - 1; j++) content.add(stack.get(j) | 0x80);
            content.add(stack.get(stack.size() - 1));
        }
        byte[] bytes = new byte[content.size()];
        for (int i = 0; i < content.size(); i++) bytes[i] = content.get(i).byteValue();
        return tlv(0x06, bytes);
    }

    public static byte[] encodeUTF8String(String str) {
        return tlv(0x0c, str.getBytes(StandardCharsets.UTF_8));
    }

    private static final Pattern PRINTABLE = Pattern.compile("^[A-Za-z0-9 '()+,\\-./:=?]*$");

    /** PrintableString — the required type for countryName in a Name. */
    public static byte[] encodePrintableString(String str) {
        if (!PRINTABLE.matcher(str).matches()) {
            throw new IllegalArgumentException("Not a PrintableString: " + str);
        }
        byte[] bytes = new byte[str.length()];
        for (int i = 0; i < str.length(); i++) bytes[i] = (byte) str.charAt(i);
        return tlv(0x13, bytes);
    }

    public static byte[] encodeBitString(byte[] data) {
        return tlv(0x03, concat(new byte[] {0}, data)); // 0 unused bits in the last octet
    }

    public static byte[] encodeOctetString(byte[] data) {
        return tlv(0x04, data);
    }

    public static byte[] encodeNull() {
        return new byte[] {0x05, 0x00};
    }

    public static byte[] encodeSequence(byte[]... parts) {
        return tlv(0x30, concat(parts));
    }

    public static byte[] encodeSet(byte[]... parts) {
        return tlv(0x31, concat(parts));
    }

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

    private static final String OID_COUNTRY = "2.5.4.6";
    private static final String OID_STATE = "2.5.4.8";
    private static final String OID_LOCALITY = "2.5.4.7";
    private static final String OID_ORGANIZATION = "2.5.4.10";
    private static final String OID_COMMON_NAME = "2.5.4.3";
    private static final String OID_EMAIL = "1.2.840.113549.1.9.1";
    private static final String OID_EXT_REQUEST = "1.2.840.113549.1.9.14"; // pkcs-9 at extensionRequest
    private static final String OID_SUBJECT_ALT_NAME = "2.5.29.17";
    private static final String OID_RSA_SHA256 = "1.2.840.113549.1.1.11"; // sha256WithRSAEncryption
    private static final String 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.
     */
    public static void validateCSROptions(CSROptions options) {
        if (options.commonName() == null || options.commonName().trim().isEmpty()) {
            throw new IllegalArgumentException("Common Name (CN) is required");
        }
        String country = options.country() == null ? "" : options.country().trim();
        if (!country.isEmpty() && !country.matches("^[A-Za-z]{2}$")) {
            throw new IllegalArgumentException("Country must be a 2-letter ISO 3166-1 code (e.g. US, DE)");
        }
        List<String> allowed = List.of("RSA-2048", "RSA-4096", "ECDSA-P256");
        if (!allowed.contains(options.keyAlgorithm())) {
            throw new IllegalArgumentException("Unsupported key algorithm: " + options.keyAlgorithm());
        }
    }

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

    public enum SanType { DNS, IP, EMAIL, URI }

    /** Classify a SAN entry: IPv4/IPv6 → ip, http(s):// → uri, contains @ → email, else dns. */
    public static SanType classifySanType(String entry) {
        String value = entry.trim().toLowerCase();
        if (ipToBytes(value) != null) return SanType.IP;
        if (value.matches("^https?://.*")) return SanType.URI;
        if (value.contains("@")) return SanType.EMAIL;
        return SanType.DNS;
    }

    private static byte[] ipToBytes(String ip) {
        var v4 = Pattern.compile("^(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})\\.(\\d{1,3})$").matcher(ip);
        if (v4.matches()) {
            byte[] out = new byte[4];
            for (int i = 0; i < 4; i++) {
                int n = Integer.parseInt(v4.group(i + 1));
                if (n > 255) return null;
                out[i] = (byte) n;
            }
            return out;
        }
        if (!ip.contains(":")) return null;
        String[] halves = ip.split("::", -1);
        if (halves.length > 2) return null;
        List<Integer> groups = new ArrayList<>();
        for (String half : halves) {
            if (half.isEmpty()) continue;
            for (String group : half.split(":", -1)) {
                if (group.contains(".")) {
                    byte[] tail = ipToBytes(group); // IPv4-mapped tail, e.g. ::ffff:10.0.0.1
                    if (tail == null || tail.length != 4) return null;
                    groups.add(((tail[0] & 0xff) << 8) | (tail[1] & 0xff));
                    groups.add(((tail[2] & 0xff) << 8) | (tail[3] & 0xff));
                } else if (group.matches("^[0-9a-f]{1,4}$")) {
                    groups.add(Integer.parseInt(group, 16));
                } else {
                    return null;
                }
            }
        }
        if (halves.length == 2) {
            int missing = 8 - groups.size();
            if (missing < 0) return null;
            for (int i = 0; i < missing; i++) groups.add(0);
        }
        if (groups.size() != 8) return null;
        byte[] out = new byte[16];
        for (int i = 0; i < 8; i++) {
            out[i * 2] = (byte) (groups.get(i) >> 8);
            out[i * 2 + 1] = (byte) (groups.get(i) & 0xff);
        }
        return out;
    }

    /** One GeneralName: context-specific implicit tags per RFC 5280. */
    private static byte[] encodeGeneralName(String entry) {
        String value = entry.trim();
        byte[] ascii = new byte[value.length()];
        for (int i = 0; i < value.length(); i++) ascii[i] = (byte) value.charAt(i);
        return switch (classifySanType(value)) {
            case IP -> tlv(0x87, ipToBytes(value.toLowerCase()));
            case URI -> tlv(0x86, ascii);
            case EMAIL -> tlv(0x81, ascii);
            default -> tlv(0x82, ascii); // dNSName
        };
    }

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

    /** PEM-wrap DER bytes: 64-character base64 lines between BEGIN/END markers. */
    public static String pemEncode(byte[] der, String label) {
        String base64 = Base64.getEncoder().encodeToString(der);
        StringBuilder sb = new StringBuilder("-----BEGIN ").append(label).append("-----\n");
        for (int i = 0; i < base64.length(); i += 64) {
            sb.append(base64, i, Math.min(i + 64, base64.length())).append('\n');
        }
        sb.append("-----END ").append(label).append("-----\n");
        return sb.toString();
    }

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

    private static KeyPair generateKeyPair(String algorithm) throws Exception {
        if ("ECDSA-P256".equals(algorithm)) {
            KeyPairGenerator kpg = KeyPairGenerator.getInstance("EC");
            kpg.initialize(new ECGenParameterSpec("secp256r1"));
            return kpg.generateKeyPair();
        }
        int modulusLength = "RSA-4096".equals(algorithm) ? 4096 : 2048;
        KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");
        kpg.initialize(modulusLength);
        return kpg.generateKeyPair();
    }

    /** AttributeTypeAndValue (SEQUENCE of OID + value) wrapped in its RDN SET. */
    private static byte[] rdn(String oid, byte[] value) {
        return 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 locally via the JDK's
     * crypto providers.
     */
    public static CSRResult generateCSR(CSROptions options) throws Exception {
        validateCSROptions(options);
        KeyPair pair = generateKeyPair(options.keyAlgorithm());
        byte[] spki = pair.getPublic().getEncoded();   // X.509 SubjectPublicKeyInfo
        byte[] pkcs8 = pair.getPrivate().getEncoded(); // PKCS#8

        // subject: RDNSequence in the conventional C, ST, L, O, CN, email order
        List<byte[]> rdns = new ArrayList<>();
        String country = options.country() == null ? "" : options.country().trim().toUpperCase();
        if (!country.isEmpty()) rdns.add(rdn(OID_COUNTRY, encodePrintableString(country)));
        if (options.state() != null && !options.state().trim().isEmpty()) {
            rdns.add(rdn(OID_STATE, encodeUTF8String(options.state().trim())));
        }
        if (options.locality() != null && !options.locality().trim().isEmpty()) {
            rdns.add(rdn(OID_LOCALITY, encodeUTF8String(options.locality().trim())));
        }
        if (options.organization() != null && !options.organization().trim().isEmpty()) {
            rdns.add(rdn(OID_ORGANIZATION, encodeUTF8String(options.organization().trim())));
        }
        rdns.add(rdn(OID_COMMON_NAME, encodeUTF8String(options.commonName().trim())));
        if (options.email() != null && !options.email().trim().isEmpty()) {
            rdns.add(rdn(OID_EMAIL, encodeUTF8String(options.email().trim())));
        }

        // attributes [0] IMPLICIT SET OF — extensionRequest carrying subjectAltName
        byte[] attributes = new byte[0];
        List<String> sans = new ArrayList<>();
        for (String s : options.subjectAltNames() == null ? List.<String>of() : options.subjectAltNames()) {
            if (!s.trim().isEmpty()) sans.add(s.trim());
        }
        if (!sans.isEmpty()) {
            byte[] extension = encodeSequence(
                encodeOID(OID_SUBJECT_ALT_NAME),
                encodeOctetString(encodeSequence(sans.stream().map(CsrGenerator::encodeGeneralName)
                    .toArray(byte[][]::new))));
            byte[] 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
        byte[] cri = encodeSequence(encodeInteger(0),
            encodeSequence(rdns.toArray(new byte[0][])), spki, attributes);

        // signatureAlgorithm + signature over the DER CRI
        byte[] signatureAlgorithm;
        byte[] signature;
        if ("ECDSA-P256".equals(options.keyAlgorithm())) {
            signatureAlgorithm = encodeSequence(encodeOID(OID_ECDSA_SHA256));
            // JCA ECDSA signatures are already the DER ECDSA-Sig-Value SEQUENCE.
            Signature signer = Signature.getInstance("SHA256withECDSA");
            signer.initSign(pair.getPrivate());
            signer.update(cri);
            signature = encodeBitString(signer.sign());
        } else {
            signatureAlgorithm = encodeSequence(encodeOID(OID_RSA_SHA256), encodeNull());
            Signature signer = Signature.getInstance("SHA256withRSA");
            signer.initSign(pair.getPrivate());
            signer.update(cri);
            signature = encodeBitString(signer.sign());
        }

        return new CSRResult(
            pemEncode(encodeSequence(cri, signatureAlgorithm, signature), "CERTIFICATE REQUEST"),
            pemEncode(pkcs8, "PRIVATE KEY"));
    }

    private CsrGenerator() {
    }
}

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 →