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 →