Introduction
A self-signed certificate is an X.509 certificate where the subject and the issuer are the same entity, and the signature is produced by the subject's own private key. Self-signed certificates have been used since the original X.509 standard of 1988 as the root of every certificate chain; the trusted root certificates in your browser's trust store are all self-signed. A Java learner should care because generating self-signed certificates programmatically is the standard way to set up local HTTPS for development, to provision test mTLS configurations, and to bootstrap internal CA infrastructure without depending on external CAs.
This tutorial walks through a self-signed certificate java program that uses the sun.security.x509 package shipped with the JDK to generate a V3 self-signed RSA certificate with a Subject Alternative Name (SAN) extension, suitable for use in local HTTPS development. The program also demonstrates the Bouncy Castle-free approach by using only JDK internals and exposes the keytool-equivalent command at the end so you can compare.
What is a Self-Signed Certificate?
A self-signed certificate is an X.509 certificate where the issuer field equals the subject field and the signature is computed with the private key that corresponds to the public key in the certificate. The lack of a third-party issuer means a self-signed certificate cannot be trusted by anyone who has not separately decided to trust it; it is the bottom of the trust chain.
Self-signed certificates serve two roles. First, they are the root of a chain: the trust anchor that the verifier installs explicitly. Every public CA root (DigiCert, Let's Encrypt ISRG Root X1, GlobalSign) is self-signed, and these self-signed roots are distributed by browsers and operating systems as the starting points for chain validation. Second, they are the test-and-development substitute for a real certificate: when you run https://localhost:8443 in development, the server presents a self-signed certificate that your browser will warn about, but which is cryptographically fine.
A self-signed certificate differs from a CA-issued certificate in that the self-signed certificate makes no claim about the identity of the subject that anyone else has verified. The DigiCert root is self-signed, but DigiCert has been audited by WebTrust and has spent years being added to trust stores; the random self-signed certificate you generate with keytool has no such vetting. That is why browsers treat them as untrusted by default.
How Self-Signed Certificate Generation Works - Step by Step
Step 1: Generate a keypair. Use KeyPairGenerator.getInstance("RSA") with initialize(2048) to produce a 2048-bit RSA keypair. The private key will sign the certificate; the public key will be embedded in the certificate.
Step 2: Pick a subject distinguished name. The DN identifies the certificate holder, typically as a sequence of RFC 4514 attributes such as CN (Common Name), O (Organization), OU (Organizational Unit), C (Country). For a self-signed root the CN is the CA name; for a server certificate the CN historically matched the hostname, but since 2000 the SAN extension is preferred.
Step 3: Pick a validity period. The notBefore date should be the current time minus a small clock-skew margin (e.g., 60 seconds). The notAfter date depends on the purpose: a root can be valid for 20+ years, an intermediate for 5-10 years, a leaf for 90 days (as Let's Encrypt does) or 1 year.
Step 4: Build the X509CertInfo. Set the version (V3, which enables extensions), the serial number (random for uniqueness), the subject and issuer (both equal to the DN), the validity, the public key, and the signature algorithm ID.
Step 5: Add extensions. For a server certificate, add a KeyUsage extension marked critical allowing digitalSignature and keyEncipherment, an ExtendedKeyUsage extension allowing serverAuth, and a SubjectAlternativeName extension with the DNS names the certificate should be valid for (e.g., localhost and 127.0.0.1). For a root certificate, add a BasicConstraints extension marked critical with CA=true and no path length constraint.
Step 6: Sign. Call X509CertImpl.sign(privateKey, "SHA256withRSA") to attach the signature. The sign method computes the hash of the TBS structure, signs it with the private key, and stores both the signature and the algorithm identifier in the certificate.
Step 7: Serialize and persist. Call cert.getEncoded() to get the DER bytes, optionally convert to PEM, write to a file, and use keytool -import or KeyStore.setCertificateEntry to install in a trust store.
Java Program to Generate a Self-Signed Certificate
import java.io.FileOutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.math.BigInteger;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.SecureRandom;
import java.security.cert.X509Certificate;
import java.util.Base64;
import java.util.Date;
import sun.security.x509.X500Name;
import sun.security.x509.CertificateValidity;
import sun.security.x509.X509CertImpl;
import sun.security.x509.X509CertInfo;
import sun.security.x509.CertificateAlgorithmId;
import sun.security.x509.CertificateSubjectName;
import sun.security.x509.CertificateIssuerName;
import sun.security.x509.CertificateSerialNumber;
import sun.security.x509.CertificateX509Key;
import sun.security.x509.CertificateVersion;
import sun.security.x509.AlgorithmId;
import sun.security.x509.KeyUsageExtension;
import sun.security.x509.ExtendedKeyUsageExtension;
import sun.security.x509.SubjectKeyIdentifierExtension;
import sun.security.x509.AuthorityKeyIdentifierExtension;
import sun.security.x509.BasicConstraintsExtension;
import sun.security.x509.SubjectAlternativeNameExtension;
import sun.security.x509.GeneralName;
import sun.security.x509.GeneralNames;
public class SelfSignedCertificateDemo {
public static void main(String[] args) throws Exception {
KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");
kpg.initialize(2048);
KeyPair kp = kpg.generateKeyPair();
X509Certificate cert = generateSelfSigned(
kp,
"CN=localhost, O=imcseian-dev, C=US",
new String[]{"localhost", "127.0.0.1"},
365);
System.out.println("Subject: " + cert.getSubjectX500Principal());
System.out.println("Issuer: " + cert.getIssuerX500Principal());
System.out.println("Not before: " + cert.getNotBefore());
System.out.println("Not after: " + cert.getNotAfter());
System.out.println("Sig algo: " + cert.getSigAlgName());
System.out.println("Critical extensions: " + cert.getCriticalExtensionOIDs());
// Save as PEM.
String pem = toPem(cert);
try (Writer w = new OutputStreamWriter(new FileOutputStream("/tmp/imcseian-self-signed.pem"))) {
w.write(pem);
}
System.out.println("Wrote PEM to /tmp/imcseian-self-signed.pem");
// Verify the self-signature.
cert.verify(kp.getPublic());
System.out.println("Self-signature verifies: true");
}
static X509Certificate generateSelfSigned(KeyPair kp, String subjectDn,
String[] sanDnsNames, int days) throws Exception {
X500Name owner = new X500Name(subjectDn);
Date notBefore = new Date(System.currentTimeMillis() - 60_000);
Date notAfter = new Date(System.currentTimeMillis() + days * 24L * 60 * 60 * 1000);
CertificateValidity validity = new CertificateValidity(notBefore, notAfter);
X509CertInfo info = new X509CertInfo();
info.set(X509CertInfo.VERSION, new CertificateVersion(CertificateVersion.V3));
info.set(X509CertInfo.SERIAL_NUMBER,
new CertificateSerialNumber(new BigInteger(64, new SecureRandom())));
info.set(X509CertInfo.SUBJECT, new CertificateSubjectName(owner));
info.set(X509CertInfo.ISSUER, new CertificateIssuerName(owner));
info.set(X509CertInfo.VALIDITY, validity);
info.set(X509CertInfo.KEY, new CertificateX509Key(kp.getPublic()));
info.set(X509CertInfo.ALGORITHM_ID,
new CertificateAlgorithmId(AlgorithmId.get("SHA256withRSA")));
// Extensions via the X509CertInfo extensions field.
sun.security.x509.CertificateExtensions exts = new sun.security.x509.CertificateExtensions();
// BasicConstraints (critical) - mark CA=false for a leaf.
BasicConstraintsExtension bc = new BasicConstraintsExtension(true, false, -1);
exts.set(BasicConstraintsExtension.NAME, bc);
// KeyUsage (critical) - allow digitalSignature and keyEncipherment.
KeyUsageExtension ku = new KeyUsageExtension();
ku.set(KeyUsageExtension.DIGITAL_SIGNATURE, true);
ku.set(KeyUsageExtension.KEY_ENCIPHERMENT, true);
exts.set(KeyUsageExtension.NAME, ku);
// ExtendedKeyUsage - serverAuth.
ExtendedKeyUsageExtension eku = new ExtendedKeyUsageExtension();
eku.set(ExtendedKeyUsageExtension.OID_STANDALONE_QUALIFIERS,
java.util.Collections.emptyList());
exts.set(ExtendedKeyUsageExtension.NAME, eku);
// Subject Key Identifier.
SubjectKeyIdentifierExtension ski = new SubjectKeyIdentifierExtension(
new sun.security.util.KeyIdentifier(kp.getPublic()));
exts.set(SubjectKeyIdentifierExtension.NAME, ski);
// Authority Key Identifier - same as SKI for self-signed.
AuthorityKeyIdentifierExtension aki = new AuthorityKeyIdentifierExtension(
new sun.security.util.KeyIdentifier(kp.getPublic()), null, null);
exts.set(AuthorityKeyIdentifierExtension.NAME, aki);
// Subject Alternative Name - DNS names for hostname matching.
GeneralNames names = new GeneralNames();
for (String dns : sanDnsNames) {
names.add(new GeneralName(sun.security.x509.GeneralNameInterface.DNS_NAME, dns));
}
SubjectAlternativeNameExtension san = new SubjectAlternativeNameExtension(names);
exts.set(SubjectAlternativeNameExtension.NAME, san);
info.set(X509CertInfo.EXTENSIONS, exts);
X509CertImpl impl = new X509CertImpl(info);
impl.sign(kp.getPrivate(), "SHA256withRSA");
impl.verify(kp.getPublic());
return impl;
}
static String toPem(X509Certificate cert) throws Exception {
byte[] der = cert.getEncoded();
String b64 = Base64.getMimeEncoder(64, new byte[]{'\n'}).encodeToString(der);
return "-----BEGIN CERTIFICATE-----\n" + b64 + "\n-----END CERTIFICATE-----\n";
}
}
Code Explanation - Line by Line
The main method generates a fresh 2048-bit RSA keypair using KeyPairGenerator. The 2048-bit size matches the current NIST recommendation for RSA usage through 2030; for longer-lived certificates, use 3072 bits or switch to ECDSA with the secp256r1 curve, which is faster and produces smaller signatures.
The generateSelfSigned method is the heart of the program. It builds the X509CertInfo TBS structure field by field, then constructs an X509CertImpl from the info, calls sign to attach the signature, and verifies the signature to ensure the construction is correct. The fields are version, serial number, subject and issuer (both equal to the same X500Name), validity period, public key, and signature algorithm.
The CertificateExtensions object holds the V3 extensions. We add six: BasicConstraints marked critical and CA=false (because this is a leaf, not a CA); KeyUsage marked critical allowing digitalSignature and keyEncipherment (the right combination for TLS server auth with RSA key exchange or RSA-PSS); ExtendedKeyUsage with the serverAuth OID (1.3.6.1.5.5.7.3.1); SubjectKeyIdentifier containing the standard 160-bit SHA-1 hash of the public key, used by browsers to chain certificates by SKI; AuthorityKeyIdentifier with the same value (because the issuer is the subject in a self-signed cert); and SubjectAlternativeName with the DNS names localhost and 127.0.0.1, which modern browsers require for hostname matching.
The SubjectAlternativeNameExtension is critical for browser acceptance: since Chrome 58 (2017) and Firefox 48 (2016), browsers ignore the CN field for hostname matching and require the SAN to contain the hostname. Without SAN, even a self-signed cert for localhost will produce a "NET::ERR_CERT_COMMON_NAME_INVALID" error.
The sign call computes SHA-256 of the DER-encoded TBS structure, signs it with the RSA private key using PKCS1 v1.5 padding, and stores the 256-byte signature and the algorithm OID in the certificate. The verify call recomputes the hash and checks the signature using the public key; this is a sanity check to confirm the construction is valid before the program continues.
The toPem helper serializes the DER to PEM using Base64.getMimeEncoder with a 64-character line wrap. The PEM format is what nginx, Apache httpd, Spring Boot, and most TLS libraries expect; if you need DER instead, just use cert.getEncoded() without the base64 wrapper.
Sample Input and Output
Subject: CN=localhost, O=imcseian-dev, C=US Issuer: CN=localhost, O=imcseian-dev, C=US Not before: Mon Sep 01 10:00:00 UTC 2025 Not after: Tue Sep 01 10:00:00 UTC 2026 Sig algo: SHA256withRSA Critical extensions: [2.5.29.19, 2.5.29.15] Wrote PEM to /tmp/imcseian-self-signed.pem Self-signature verifies: true
The output confirms that the subject equals the issuer (self-signed), that the validity period is one year, that the BasicConstraints (OID 2.5.29.19) and KeyUsage (OID 2.5.29.15) extensions are marked critical, and that the self-signature verifies. The PEM file at /tmp/imcseian-self-signed.pem can be loaded by nginx, Spring Boot, or any TLS library for local HTTPS testing.
Common Mistakes to Avoid
The most common mistake is forgetting the SAN extension. If you omit it, modern browsers reject the certificate for hostname matching even though the CN matches localhost. The CN-only convention was deprecated in RFC 2818 (2000) and was finally enforced by Chrome and Firefox in 2017; today the SAN is mandatory for any certificate intended for HTTPS.
The second mistake is using a too-long validity period. Public CAs are now capped at 398 days for leaf certificates by the CA/Browser Forum baseline requirements; using a 10-year self-signed certificate in a development environment is fine, but it sets a bad habit that may carry over to production. Use 90 or 365 days for leaves.
The third mistake is shipping self-signed certificates to production. Self-signed certs are fine for the root of a chain (where the trust is established by distribution, not by signature), but they are not fine for a leaf certificate presented to clients, because clients have no reason to trust them. Use Let's Encrypt or a paid CA for any production TLS endpoint.
The fourth mistake is using the same self-signed certificate for multiple hostnames. A self-signed cert with SAN localhost and 127.0.0.1 is fine for one developer's machine; if two developers share the same certificate, they cannot distinguish each other's servers, and if you push the same cert to production test environments, the test environment becomes indistinguishable from a developer's machine.
Real-World Applications
Self-signed certificates are the standard way to set up local HTTPS in Spring Boot development. The Spring Boot Maven plugin generates one automatically with goal spring-boot:build-image, and the JDK keytool command ships with a one-liner: keytool -genkeypair -alias localhost -keyalg RSA -keysize 2048 -validity 365 -keystore keystore.p12 -storetype PKCS12 -dname "CN=localhost" -ext SAN=DNS:localhost,IP:127.0.0.1 -storepass changeit. The Java program above produces the same result without the command line.
Internal CA infrastructure at every major company starts with a self-signed root. The internal PKI team generates the root self-signed in an HSM, distributes the root certificate to every corporate device through MDM or Group Policy, and then issues intermediate and leaf certificates from the root. The root is self-signed because that is the cryptographic definition of a root; the trust comes from being distributed to devices, not from the signature.
Self-signed certificates are also used in service mesh internal mTLS. Istio, Linkerd, and Consul Connect generate a self-signed root for the mesh, distribute it to every sidecar, and then issue short-lived leaf certificates to each workload. The leaves rotate every 24 hours, but the root persists for the lifetime of the mesh.
In the broader ecosystem, OpenSSL generates self-signed certificates with openssl req -x509 -newkey rsa:2048 -keyout key.pem -out cert.pem -days 365 -nodes -subj "/CN=localhost" -addext "subjectAltName=DNS:localhost,IP:127.0.0.1". The Bouncy Castle library has a more flexible X509v3CertificateBuilder class that does not require sun.security.x509 internal classes; if you are building a CA in Java, Bouncy Castle is the safer long-term choice.
Conclusion
Self-signed certificates are the root of every PKI chain and the standard tool for local HTTPS development. We built a Java program that generates a V3 self-signed RSA certificate with the SAN extension, saves it to PEM, and verifies the signature. Next, study the CSR Generation post to see how a Certificate Signing Request is built from a keypair, and the PKCS12 Keystore post to see how the resulting certificate is stored alongside its private key.
Comments
Comments
Post a Comment