Originally published on Code Beneath.

<<>>
Java read X.509 certificate parsing with CertificateFactory, chain validation, DN extraction, and self-signed cert handling without external libraries.
<<>>
<<

>>

Extracting certificate details and validating certificate chains in Java requires understanding the X.509 standard and the built-in APIs available since Java 1.4. Many engineers reach for Apache Commons or Bouncy Castle before trying the standard library, but the standard APIs are sufficient for most real-world tasks: reading PEM and DER files, extracting distinguished names, validating chains, and checking self-signed certificates. This guide shows you how to java read x509 certificate parse chain example code that works in production, with no external dependencies.

Why You Need Certificate Chain Validation

When your application receives a certificate from a client (in mutual TLS, for example) or validates a server certificate, you need to ensure the chain is complete and each certificate was signed by the next one in the chain. This is not just an optional security measure: it is the foundation of trust. A self-signed certificate with an incomplete chain or an expired intermediate will cause authentication to fail. Understanding how to validate this yourself, rather than delegating it to a framework, gives you visibility into what is actually happening during the handshake.

The java.security.cert package provides CertificateFactory, X509Certificate, and CertPathValidator for this purpose. Together they let you parse certificates, extract public keys, validate chains, and inspect the distinguished names (DNs) that identify the certificate owner.

Loading and Parsing X.509 Certificates

The first step is to load a certificate from a file or stream. X.509 certificates come in two formats: DER (binary) and PEM (Base64-encoded text). Java's CertificateFactory handles both.

import java.io.*; import java.nio.file.Files; import java.nio.file.Paths; import java.security.cert.CertificateFactory; import java.security.cert.X509Certificate; import java.security.cert.Certificate; import java.util.*; public class CertificateParser { public static X509Certificate parsePemCertificate(String filePath) throws Exception { FileInputStream fis = new FileInputStream(filePath); CertificateFactory cf = CertificateFactory.getInstance("X.509"); X509Certificate cert = (X509Certificate) cf.generateCertificate(fis); fis.close(); return cert; } public static X509Certificate parseDerCertificate(String filePath) throws Exception { byte[] derBytes = Files.readAllBytes(Paths.get(filePath)); ByteArrayInputStream bais = new ByteArrayInputStream(derBytes); CertificateFactory cf = CertificateFactory.getInstance("X.509"); X509Certificate cert = (X509Certificate) cf.generateCertificate(bais); bais.close(); return cert; } public static List<X509Certificate> parseCertificateChain(String filePath) throws Exception { FileInputStream fis = new FileInputStream(filePath); CertificateFactory cf = CertificateFactory.getInstance("X.509"); Collection<? extends Certificate> certs = cf.generateCertificates(fis); fis.close(); List<X509Certificate> chain = new ArrayList<>(); for (Certificate c : certs) { chain.add((X509Certificate) c); } return chain; } public static void main(String[] args) throws Exception { String pemPath = "server.pem"; X509Certificate cert = parsePemCertificate(pemPath); System.out.println("Subject: " + cert.getSubjectDN()); System.out.println("Issuer: " + cert.getIssuerDN()); System.out.println("Serial: " + cert.getSerialNumber()); System.out.println("Not Before: " + cert.getNotBefore()); System.out.println("Not After: " + cert.getNotAfter()); } } 

The key difference: use generateCertificate() for a single certificate and generateCertificates() (plural) for a chain. The factory automatically handles PEM Base64 decoding or raw DER bytes. No Base64 decoding on your side is needed; the factory does it.

Extracting and Parsing Distinguished Names

The subject DN contains the identity of the certificate owner. It is a structured name with fields like CN (Common Name), O (Organization), C (Country), and others. The getSubjectDN() method returns a Principal, but to extract individual fields, you need to parse the X.500 format string or use an X500Principal.

import javax.security.auth.x500.X500Principal; import java.util.regex.Pattern; import java.util.regex.Matcher; public class DNParser { public static void printDN(X509Certificate cert) { X500Principal principal = cert.getSubjectX500Principal(); String dn = principal.getName(X500Principal.RFC2253); System.out.println("DN (RFC2253): " + dn); Map<String, String> dnMap = parseDN(dn); for (String key : dnMap.keySet()) { System.out.println(key + " = " + dnMap.get(key)); } } public static Map<String, String> parseDN(String dn) { Map<String, String> map = new LinkedHashMap<>(); String[] parts = dn.split(",(?=(?:[^\\\\]*\\\\\\\\)*[^\\\\]*$)"); for (String part : parts) { part = part.trim(); int eqIndex = part.indexOf('='); if (eqIndex > 0) { String key = part.substring(0, eqIndex).trim(); String value = part.substring(eqIndex + 1).trim(); map.put(key, value); } } return map; } public static String extractCommonName(X509Certificate cert) { Map<String, String> dnMap = parseDN( cert.getSubjectX500Principal().getName(X500Principal.RFC2253) ); return dnMap.get("CN"); } public static void main(String[] args) throws Exception { FileInputStream fis = new FileInputStream("server.pem"); CertificateFactory cf = CertificateFactory.getInstance("X.509"); X509Certificate cert = (X509Certificate) cf.generateCertificate(fis); fis.close(); printDN(cert); System.out.println("\nCommon Name: " + extractCommonName(cert)); } } 

The RFC2253 format uses commas to separate RDNs (Relative Distinguished Names), but commas can also appear inside escaped values. The regex split with a lookahead handles this case correctly. This parser extracts the key-value pairs into a Map so you can look up CN, O, C, or any other field.

Validating Certificate Chains

A certificate chain is valid if each certificate was signed by the next one in the chain, and the final certificate (the root) is trusted. Java provides CertPathValidator to automate this. You need to supply the chain, a trust anchor (the root certificate or a KeyStore), and validation parameters.

import java.security.KeyStore; import java.security.cert.*; import java.util.*; public class ChainValidator { public static boolean validateChain(List<X509Certificate> chain, X509Certificate rootCert) throws Exception { if (chain == null || chain.isEmpty()) { System.out.println("Chain is empty"); return false; } PKIXParameters params = new PKIXParameters( Collections.singleton(new TrustAnchor(rootCert, null)) ); params.setRevocationEnabled(false); CertPath path = CertificateFactory.getInstance("X.509") .generateCertPath(chain); CertPathValidator validator = CertPathValidator.getInstance("PKIX"); try { validator.validate(path, params); System.out.println("Chain validation succeeded"); return true; } catch (CertPathValidatorException e) { System.out.println("Chain validation failed: " + e.getMessage()); e.printStackTrace(); return false; } } public static boolean validateChainWithKeyStore(List<X509Certificate> chain, String keyStorePath, String keyStorePassword) throws Exception { KeyStore ks = KeyStore.getInstance("JKS"); ks.load(new FileInputStream(keyStorePath), keyStorePassword.toCharArray()); PKIXParameters params = new PKIXParameters(ks); params.setRevocationEnabled(false); CertPath path = CertificateFactory.getInstance("X.509") .generateCertPath(chain); CertPathValidator validator = CertPathValidator.getInstance("PKIX"); try { validator.validate(path, params); System.out.println("Chain validation with KeyStore succeeded"); return true; } catch (CertPathValidatorException e) { System.out.println("Chain validation failed: " + e.getMessage()); return false; } } public static void main(String[] args) throws Exception { List<X509Certificate> chain = new ArrayList<>(); FileInputStream fis1 = new FileInputStream("server.pem"); CertificateFactory cf = CertificateFactory.getInstance("X.509"); chain.add((X509Certificate) cf.generateCertificate(fis1)); fis1.close(); FileInputStream fis2 = new FileInputStream("intermediate.pem"); chain.add((X509Certificate) cf.generateCertificate(fis2)); fis2.close(); FileInputStream fis3 = new FileInputStream("root.pem"); X509Certificate rootCert = (X509Certificate) cf.generateCertificate(fis3); fis3.close(); validateChain(chain, rootCert); } } 

The order matters: the chain must start with the leaf (server) certificate and end with an intermediate or the root. The trust anchor is the root certificate or a KeyStore that contains trusted roots. The setRevocationEnabled(false) call disables CRL and OCSP checks, which is fine for offline validation; if you need revocation checking, you must supply a CertStore with CRL or OCSP responder URLs.

Handling Self-Signed Certificates

A self-signed certificate is one where the issuer and subject are the same, and it was signed by its own private key. These are common in testing and for internal CAs. Validating a self-signed certificate means treating it as its own trust anchor.

import java.security.PublicKey; public class SelfSignedValidator { public static boolean isSelfSigned(X509Certificate cert) { return cert.getSubjectDN().equals(cert.getIssuerDN()); } public static boolean validateSelfSigned(X509Certificate cert) { try { PublicKey publicKey = cert.getPublicKey(); cert.verify(publicKey); System.out.println("Self-signed certificate signature is valid"); return true; } catch (Exception e) { System.out.println("Self-signed certificate signature verification failed: " + e.getMessage()); return false; } } public static boolean validateSelfSignedWithExpiry(X509Certificate cert) { if (!isSelfSigned(cert)) { System.out.println("Certificate is not self-signed"); return false; } try { cert.verify(cert.getPublicKey()); System.out.println("Signature is valid"); } catch (Exception e) { System.out.println("Signature verification failed: " + e.getMessage()); return false; } try { cert.checkValidity(); System.out.println("Certificate is within validity period"); return true; } catch (CertificateExpiredException e) { System.out.println("Certificate has expired"); return false; } catch (CertificateNotYetValidException e) { System.out.println("Certificate is not yet valid"); return false; } } public static void main(String[] args) throws Exception { FileInputStream fis = new FileInputStream("selfsigned.pem"); CertificateFactory cf = CertificateFactory.getInstance("X.509"); X509Certificate cert = (X509Certificate) cf.generateCertificate(fis); fis.close(); if (isSelfSigned(cert)) { System.out.println("Certificate is self-signed"); validateSelfSignedWithExpiry(cert); } } } 

The verify() method uses the public key to verify the signature. For a self-signed cert, the public key comes from the certificate itself. The checkValidity() method checks the notBefore and notAfter dates against the current system time. Call both to fully validate a self-signed certificate.

Extracting Public Keys and Certificate Extensions

Beyond the basic DN and validity, certificates contain extensions like Subject Alternative Name (SAN), Key Usage, Extended Key Usage, and Authority Key Identifier. These extensions are crucial for modern PKI.

import java.security.PublicKey; import java.util.*; public class ExtensionExtractor { public static void extractPublicKey(X509Certificate cert) { PublicKey pk = cert.getPublicKey(); System.out.println("Algorithm: " + pk.getAlgorithm()); System.out.println("Format: " + pk.getFormat()); System.out.println("Encoded length: " + pk.getEncoded().length + " bytes"); } public static void extractExtensions(X509Certificate cert) { Set<String> criticalOids = cert.getCriticalExtensionOIDs(); Set<String> nonCriticalOids = cert.getNonCriticalExtensionOIDs(); if (criticalOids != null) { System.out.println("Critical extensions:"); for (String oid : criticalOids) { byte[] extValue = cert.getExtensionValue(oid); System.out.println(" OID: " + oid + ", length: " + extValue.length); } } if (nonCriticalOids != null) { System.out.println("Non-critical extensions:"); for (String oid : nonCriticalOids) { byte[] extValue = cert.getExtensionValue(oid); System.out.println(" OID: " + oid + ", length: " + extValue.length); } } } public static List<String> extractSubjectAlternativeNames(X509Certificate cert) { List<String> sans = new ArrayList<>(); try { Collection<List<?>> altNames = cert.getSubjectAlternativeNames(); if (altNames == null) { System.out.println("No Subject Alternative Names"); return sans; } for (List<?> altName : altNames) { Integer type = (Integer) altName.get(0); Object name = altName.get(1); if (type == 2) { sans.add("DNS: " + name.toString()); } Source:https://dev.to/bar_dror_a3d32cb94b1/reading-and-parsing-x509-certificates-in-java-certificate-chain-validation-241l