23.2 Certificate Validation and Hostname Verification
Proper certificate validation is essential for preventing man-in-the-middle attacks. This section covers validation mechanisms and best practices.
Server Certificate Validation
// Server Certificate Validation
public class ServerCertificateValidation {
// Default validation with standard trust store
public static HttpClient createSecureHTTPClient() throws Exception {
// Default behavior: Uses system trust store
// Validates certificate chain and hostname
return HttpClient.newBuilder()
.version(HttpClient.Version.HTTP_2)
.build();
}
// Validation with custom trust store
public static HttpClient createClientWithCustomTrustStore(String trustStorePath,
String trustStorePassword)
throws Exception {
// Load custom trust store
KeyStore trustStore = KeyStore.getInstance("PKCS12");
try (InputStream is = new FileInputStream(trustStorePath)) {
trustStore.load(is, trustStorePassword.toCharArray());
}
// Create trust manager factory
TrustManagerFactory tmf = TrustManagerFactory.getInstance(
TrustManagerFactory.getDefaultAlgorithm());
tmf.init(trustStore);
// Create SSL context with custom trust store
SSLContext sslContext = SSLContext.getInstance("TLSv1.3");
sslContext.init(null, tmf.getTrustManagers(), new SecureRandom());
// Create HTTP client with custom SSL context
return HttpClient.newBuilder()
.sslContext(sslContext)
.version(HttpClient.Version.HTTP_2)
.build();
}
// System trust store manipulation (advanced)
public static void printSystemTrustStore() throws Exception {
String trustStorePath = System.getProperty("javax.net.ssl.trustStore");
String trustStoreType = System.getProperty("javax.net.ssl.trustStoreType", "JKS");
System.out.println("Trust Store Path: " + trustStorePath);
System.out.println("Trust Store Type: " + trustStoreType);
if (trustStorePath == null) {
System.out.println("Using default trust store");
}
}
// Certificate chain validation
public static void validateCertificateChain(java.security.cert.X509Certificate[] chain,
TrustManager trustManager)
throws Exception {
System.out.println("=== CERTIFICATE CHAIN VALIDATION ===");
for (int i = 0; i < chain.length; i++) {
java.security.cert.X509Certificate cert = chain[i];
System.out.println("\nCertificate " + (i + 1) + ":");
System.out.println(" Subject: " + cert.getSubjectDN());
System.out.println(" Issuer: " + cert.getIssuerDN());
System.out.println(" Valid From: " + cert.getNotBefore());
System.out.println(" Valid Until: " + cert.getNotAfter());
System.out.println(" Serial: " + cert.getSerialNumber());
// Check validity date
try {
cert.checkValidity();
System.out.println(" Status: VALID");
} catch (java.security.cert.CertificateExpiredException e) {
System.out.println(" Status: EXPIRED");
} catch (java.security.cert.CertificateNotYetValidException e) {
System.out.println(" Status: NOT YET VALID");
}
}
}
}
Hostname Verification
Always verify that the certificate's domain name matches the connection target:
// Hostname Verification
public class HostnameVerification {
// Built-in hostname verifier
public static void demonstrateHostnameVerification() throws Exception {
String hostname = "api.example.com";
java.security.cert.X509Certificate certificate = getCertificate(hostname);
// Java's default hostname verifier
javax.net.ssl.HttpsURLConnection.getDefaultHostnameVerifier();
// Manual verification
verifyHostname(hostname, certificate);
}
// Manual hostname verification
public static void verifyHostname(String hostname,
java.security.cert.X509Certificate certificate)
throws Exception {
System.out.println("Verifying hostname: " + hostname);
System.out.println("Certificate CN: " + extractCN(certificate.getSubjectDN().toString()));
// Check Subject Alternative Names (SAN)
java.util.Collection<java.util.List<?>> sans = certificate.getSubjectAlternativeNames();
if (sans != null) {
System.out.println("Subject Alternative Names:");
for (java.util.List<?> san : sans) {
Integer type = (Integer) san.get(0);
String value = (String) san.get(1);
// Type 2 = DNS name
if (type == 2) {
System.out.println(" DNS: " + value);
if (matchesHostname(hostname, value)) {
System.out.println(" ✓ Hostname matches!");
return;
}
}
}
}
throw new javax.net.ssl.SSLException("Hostname verification failed");
}
// Wildcard matching
private static boolean matchesHostname(String hostname, String pattern) {
// Exact match
if (hostname.equalsIgnoreCase(pattern)) {
return true;
}
// Wildcard match: *.example.com matches api.example.com
if (pattern.startsWith("*.")) {
String domain = pattern.substring(2); // Remove *.
String[] parts = hostname.split("\\.");
if (parts.length >= 2) {
String hostDomain = String.join(".",
java.util.Arrays.copyOfRange(parts, 1, parts.length));
return hostDomain.equalsIgnoreCase(domain);
}
}
return false;
}
// Extract CN from DN
private static String extractCN(String dn) {
for (String part : dn.split(",")) {
String trimmed = part.trim();
if (trimmed.startsWith("CN=")) {
return trimmed.substring(3);
}
}
return "Unknown";
}
// Certificate retrieval helper
private static java.security.cert.X509Certificate getCertificate(String hostname)
throws Exception {
// Would retrieve actual certificate from server
return null;
}
// Hostname verifier for HTTP client
public static class CustomHostnameVerifier implements javax.net.ssl.HostnameVerifier {
@Override
public boolean verify(String hostname, javax.net.ssl.SSLSession session) {
try {
java.security.cert.X509Certificate[] certificates =
(java.security.cert.X509Certificate[])
session.getPeerCertificates();
if (certificates.length == 0) {
return false;
}
HostnameVerification.verifyHostname(hostname, certificates[0]);
return true;
} catch (Exception e) {
return false;
}
}
}
}
Certificate Pinning
Certificate pinning prevents man-in-the-middle attacks when using custom CAs:
// Certificate Pinning
public class CertificatePinning {
// Pin public key
public static class PublicKeyPinning {
private static final String PINNED_KEY_HASH =
"sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="; // Base64 encoded
// Extract and pin public key
public static String getPinnedKeyHash(java.security.cert.X509Certificate cert)
throws Exception {
java.security.PublicKey publicKey = cert.getPublicKey();
byte[] publicKeyBytes = publicKey.getEncoded();
// SHA-256 hash
java.security.MessageDigest md =
java.security.MessageDigest.getInstance("SHA-256");
byte[] hash = md.digest(publicKeyBytes);
// Base64 encode
return java.util.Base64.getEncoder().encodeToString(hash);
}
// Verify pinned key
public static boolean verifyPinnedKey(java.security.cert.X509Certificate cert,
String expectedPin) throws Exception {
String actualPin = getPinnedKeyHash(cert);
return expectedPin.equals(actualPin);
}
}
// Pin certificate
public static class CertificatePinningTrustManager implements javax.net.ssl.X509TrustManager {
private final String pinnedCertificateFingerprint;
private final TrustManager delegate;
public CertificatePinningTrustManager(String pinnedFingerprint, TrustManager delegate) {
this.pinnedCertificateFingerprint = pinnedFingerprint;
this.delegate = delegate;
}
@Override
public void checkServerTrusted(java.security.cert.X509Certificate[] chain,
String authType)
throws java.security.cert.CertificateException {
// First verify normal chain
if (delegate instanceof javax.net.ssl.X509TrustManager) {
((javax.net.ssl.X509TrustManager) delegate)
.checkServerTrusted(chain, authType);
}
// Then verify pin
if (chain.length > 0) {
byte[] certBytes = chain[0].getEncoded();
String fingerprint = computeFingerprint(certBytes);
if (!pinnedCertificateFingerprint.equals(fingerprint)) {
throw new java.security.cert.CertificateException(
"Certificate pinning failed: fingerprint mismatch");
}
}
}
@Override
public void checkClientTrusted(java.security.cert.X509Certificate[] chain,
String authType)
throws java.security.cert.CertificateException {
if (delegate instanceof javax.net.ssl.X509TrustManager) {
((javax.net.ssl.X509TrustManager) delegate)
.checkClientTrusted(chain, authType);
}
}
@Override
public java.security.cert.X509Certificate[] getAcceptedIssuers() {
if (delegate instanceof javax.net.ssl.X509TrustManager) {
return ((javax.net.ssl.X509TrustManager) delegate).getAcceptedIssuers();
}
return new java.security.cert.X509Certificate[0];
}
// Compute SHA-256 fingerprint
private String computeFingerprint(byte[] certBytes) throws Exception {
java.security.MessageDigest md = java.security.MessageDigest.getInstance("SHA-256");
byte[] hash = md.digest(certBytes);
return java.util.HexFormat.of().formatHex(hash);
}
}
// Usage example
public static HttpClient createPinnedClient(String pinnedFingerprint) throws Exception {
// Create trust manager factory
TrustManagerFactory tmf = TrustManagerFactory.getInstance(
TrustManagerFactory.getDefaultAlgorithm());
tmf.init((KeyStore) null); // Use default trust store
// Create pinning trust manager
TrustManager[] trustManagers = new TrustManager[]{
new CertificatePinningTrustManager(pinnedFingerprint, tmf.getTrustManagers()[0])
};
// Create SSL context
SSLContext sslContext = SSLContext.getInstance("TLSv1.3");
sslContext.init(null, trustManagers, new SecureRandom());
return HttpClient.newBuilder()
.sslContext(sslContext)
.build();
}
public static void printPinningStrategy() {
System.out.println("=== CERTIFICATE PINNING STRATEGIES ===");
System.out.println("\n1. PUBLIC KEY PINNING");
System.out.println(" - Hash the public key");
System.out.println(" - Pin remains valid across certificate renewals");
System.out.println(" - More resilient to CA compromise");
System.out.println("\n2. CERTIFICATE PINNING");
System.out.println(" - Hash the entire certificate");
System.out.println(" - Breaks when certificate is renewed");
System.out.println(" - More specific but less flexible");
System.out.println("\n3. BACKUP PINS");
System.out.println(" - Pin multiple keys/certificates");
System.out.println(" - One for current, one for next certificate");
System.out.println(" - Prevents lockout if pinned cert expires");
System.out.println("\nWHEN TO USE PINNING:");
System.out.println(" - Critical APIs with custom CA");
System.out.println(" - High-value connections");
System.out.println(" - Mobile applications");
System.out.println(" - Defense against CA compromise");
System.out.println("\nCAUTIONS:");
System.out.println(" - Always maintain backup pins");
System.out.println(" - Plan certificate renewal strategy");
System.out.println(" - Test pin rotation procedures");
System.out.println(" - Risk of complete lockout if misconfigured");
}
}
Custom Trust Managers
Implement custom validation logic when needed:
// Custom Trust Manager Implementation
public class CustomTrustManagers {
// Permissive trust manager (DEVELOPMENT ONLY - NEVER USE IN PRODUCTION)
public static class InsecureTrustManager implements javax.net.ssl.X509TrustManager {
@Override
public void checkServerTrusted(java.security.cert.X509Certificate[] chain,
String authType) {
// Accept all - DANGEROUS!
System.out.println("WARNING: Accepting certificate without validation!");
}
@Override
public void checkClientTrusted(java.security.cert.X509Certificate[] chain,
String authType) {
// Accept all
}
@Override
public java.security.cert.X509Certificate[] getAcceptedIssuers() {
return new java.security.cert.X509Certificate[0];
}
}
// Logging trust manager (for debugging)
public static class LoggingTrustManager implements javax.net.ssl.X509TrustManager {
private final javax.net.ssl.X509TrustManager delegate;
public LoggingTrustManager(javax.net.ssl.X509TrustManager delegate) {
this.delegate = delegate;
}
@Override
public void checkServerTrusted(java.security.cert.X509Certificate[] chain,
String authType)
throws java.security.cert.CertificateException {
System.out.println("=== SERVER CERTIFICATE VALIDATION ===");
System.out.println("Auth Type: " + authType);
System.out.println("Chain Length: " + chain.length);
for (int i = 0; i < chain.length; i++) {
java.security.cert.X509Certificate cert = chain[i];
System.out.println("\nCertificate " + (i + 1) + ":");
System.out.println(" Subject: " + cert.getSubjectDN());
System.out.println(" Issuer: " + cert.getIssuerDN());
System.out.println(" Not After: " + cert.getNotAfter());
}
// Delegate to actual validation
delegate.checkServerTrusted(chain, authType);
System.out.println("\n✓ Certificate validation successful");
}
@Override
public void checkClientTrusted(java.security.cert.X509Certificate[] chain,
String authType)
throws java.security.cert.CertificateException {
delegate.checkClientTrusted(chain, authType);
}
@Override
public java.security.cert.X509Certificate[] getAcceptedIssuers() {
return delegate.getAcceptedIssuers();
}
}
// Combined validation (chain + pinning + hostname)
public static class ComprehensiveTrustManager implements javax.net.ssl.X509TrustManager {
private final javax.net.ssl.X509TrustManager chainValidator;
private final String pinnedPublicKeyHash;
public ComprehensiveTrustManager(javax.net.ssl.X509TrustManager chainValidator,
String pinnedPublicKeyHash) {
this.chainValidator = chainValidator;
this.pinnedPublicKeyHash = pinnedPublicKeyHash;
}
@Override
public void checkServerTrusted(java.security.cert.X509Certificate[] chain,
String authType)
throws java.security.cert.CertificateException {
// 1. Validate certificate chain
chainValidator.checkServerTrusted(chain, authType);
// 2. Verify public key pin
if (chain.length > 0) {
try {
String actualPin = CertificatePinning.PublicKeyPinning
.getPinnedKeyHash(chain[0]);
if (!pinnedPublicKeyHash.equals(actualPin)) {
throw new java.security.cert.CertificateException(
"Public key pinning failed");
}
} catch (Exception e) {
throw new java.security.cert.CertificateException(
"Pinning verification error", e);
}
}
}
@Override
public void checkClientTrusted(java.security.cert.X509Certificate[] chain,
String authType)
throws java.security.cert.CertificateException {
chainValidator.checkClientTrusted(chain, authType);
}
@Override
public java.security.cert.X509Certificate[] getAcceptedIssuers() {
return chainValidator.getAcceptedIssuers();
}
}
}
Certificate Revocation Checking
// Certificate Revocation Checking
public class CertificateRevocationChecking {
public static void printRevocationMethods() {
System.out.println("=== CERTIFICATE REVOCATION CHECKING ===");
System.out.println("\n1. CRL (Certificate Revocation List)");
System.out.println(" - Signed list of revoked certificate serials");
System.out.println(" - Published by CA");
System.out.println(" - Updated periodically (daily, weekly)");
System.out.println(" - Endpoint in certificate: CRL Distribution Point");
System.out.println(" - Pros: Simple, widely supported");
System.out.println(" - Cons: Delay before revocation takes effect");
System.out.println("\n2. OCSP (Online Certificate Status Protocol)");
System.out.println(" - Real-time certificate status query");
System.out.println(" - Query individual certificate");
System.out.println(" - Responds: good, revoked, unknown");
System.out.println(" - Endpoint in certificate: Authority Information Access");
System.out.println(" - Pros: Immediate revocation");
System.out.println(" - Cons: Online query overhead");
System.out.println("\n3. OCSP STAPLING");
System.out.println(" - Server provides OCSP response with certificate");
System.out.println(" - Client verifies cached OCSP response");
System.out.println(" - Best of both: immediate + no client query");
System.out.println(" - Server refreshes OCSP response periodically");
}
// Enable OCSP in Java
public static void enableOCSPChecking() {
// Enable OCSP for certificate validation
System.setProperty("com.sun.net.ssl.checkRevocation", "true");
System.setProperty("com.sun.security.enableCRLDP", "true");
}
// Disable revocation checking (if needed for testing)
public static void disableRevocationChecking() {
System.setProperty("com.sun.net.ssl.checkRevocation", "false");
System.setProperty("com.sun.security.enableCRLDP", "false");
}
}
Best Practices for Certificate Validation
- Always validate certificates: Never disable verification.
- Verify hostname: Check domain name matches certificate SAN.
- Check certificate dates: Ensure not expired and not yet valid.
- Verify chain to root: All certificates signed by trusted CA.
- Use strong cipher suites: Only allow modern algorithms.
- Implement certificate pinning: For critical connections.
- Monitor certificate expiration: Set alerts for upcoming renewals.
- Keep trust store updated: Update root CAs regularly.
- Enable OCSP/CRL checking: For high-security applications.
- Test revocation procedures: Verify handling of revoked certificates.