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.