Introduction
Certificate management is crucial for secure communication in Java applications. It involves handling digital certificates, key stores, trust stores, and establishing secure TLS/SSL connections. Proper certificate management ensures authentication, encryption, and integrity in distributed systems.
Key Concepts
Digital Certificates
- X.509 Certificates - Standard format for public key certificates
- Public Key Infrastructure (PKI) - Framework for managing digital certificates
- Certificate Authorities (CA) - Entities that issue digital certificates
- KeyStore - Database for private keys and certificates
- TrustStore - Database for trusted CA certificates
Java Security Components
- KeyStore API - Management of cryptographic keys and certificates
- SSLContext - Factory for creating SSL sockets and engines
- TrustManager - Validation of certificate chains
- KeyManager - Management of client certificates
Setup and Dependencies
Maven Dependencies
<properties>
<bouncycastle.version>1.70</bouncycastle.version>
</properties>
<dependencies>
<!-- Bouncy Castle Crypto API -->
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk18on</artifactId>
<version>${bouncycastle.version}</version>
</dependency>
<!-- For PEM file handling -->
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk18on</artifactId>
<version>${bouncycastle.version}</version>
</dependency>
<!-- Apache Commons IO for file operations -->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.11.0</version>
</dependency>
</dependencies>
Certificate Management Core Classes
1. Certificate Utility Class
package com.example.security.cert;
import org.bouncycastle.asn1.x500.X500Name;
import org.bouncycastle.cert.X509CertificateHolder;
import org.bouncycastle.cert.X509v3CertificateBuilder;
import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.openssl.PEMParser;
import org.bouncycastle.openssl.jcajce.JcaPEMWriter;
import org.bouncycastle.operator.ContentSigner;
import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
import org.bouncycastle.pkcs.PKCS10CertificationRequest;
import org.bouncycastle.pkcs.PKCS10CertificationRequestBuilder;
import org.bouncycastle.pkcs.jcajce.JcaPKCS10CertificationRequestBuilder;
import java.io.*;
import java.math.BigInteger;
import java.security.*;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.Date;
public class CertificateUtils {
static {
Security.addProvider(new BouncyCastleProvider());
}
private CertificateUtils() {
// Utility class
}
public static KeyPair generateKeyPair(String algorithm, int keySize)
throws NoSuchAlgorithmException, NoSuchProviderException {
KeyPairGenerator keyGen = KeyPairGenerator.getInstance(algorithm, "BC");
keyGen.initialize(keySize);
return keyGen.generateKeyPair();
}
public static X509Certificate generateSelfSignedCertificate(
KeyPair keyPair, String dn, Date startDate, Date endDate)
throws Exception {
X500Name subject = new X500Name(dn);
BigInteger serial = BigInteger.valueOf(System.currentTimeMillis());
X509v3CertificateBuilder certBuilder = new JcaX509v3CertificateBuilder(
subject, serial, startDate, endDate, subject, keyPair.getPublic());
ContentSigner signer = new JcaContentSignerBuilder("SHA256WithRSAEncryption")
.setProvider("BC")
.build(keyPair.getPrivate());
X509CertificateHolder certHolder = certBuilder.build(signer);
return new JcaX509CertificateConverter()
.setProvider("BC")
.getCertificate(certHolder);
}
public static PKCS10CertificationRequest generateCertificateSigningRequest(
KeyPair keyPair, String subjectDN) throws Exception {
X500Name subject = new X500Name(subjectDN);
PKCS10CertificationRequestBuilder requestBuilder =
new JcaPKCS10CertificationRequestBuilder(subject, keyPair.getPublic());
ContentSigner signer = new JcaContentSignerBuilder("SHA256WithRSAEncryption")
.setProvider("BC")
.build(keyPair.getPrivate());
return requestBuilder.build(signer);
}
public static void saveCertificateToPEM(X509Certificate certificate, String filePath)
throws IOException {
try (JcaPEMWriter writer = new JcaPEMWriter(new FileWriter(filePath))) {
writer.writeObject(certificate);
}
}
public static X509Certificate loadCertificateFromPEM(String filePath)
throws IOException, CertificateException {
try (PEMParser parser = new PEMParser(new FileReader(filePath))) {
Object object = parser.readObject();
if (object instanceof X509Certificate) {
return (X509Certificate) object;
} else if (object instanceof X509CertificateHolder) {
return new JcaX509CertificateConverter()
.setProvider("BC")
.getCertificate((X509CertificateHolder) object);
}
throw new CertificateException("Invalid certificate format");
}
}
public static void savePrivateKeyToPEM(PrivateKey privateKey, String filePath)
throws IOException {
try (JcaPEMWriter writer = new JcaPEMWriter(new FileWriter(filePath))) {
writer.writeObject(privateKey);
}
}
public static boolean verifyCertificate(X509Certificate certificate, PublicKey publicKey) {
try {
certificate.verify(publicKey);
return true;
} catch (Exception e) {
return false;
}
}
}
2. KeyStore Manager
package com.example.security.keystore;
import java.io.*;
import java.security.*;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.Enumeration;
public class KeyStoreManager {
private final KeyStore keyStore;
private final String keyStorePath;
private final char[] keyStorePassword;
public KeyStoreManager(String keyStorePath, char[] keyStorePassword, String type)
throws KeyStoreException, IOException, CertificateException, NoSuchAlgorithmException {
this.keyStorePath = keyStorePath;
this.keyStorePassword = keyStorePassword;
this.keyStore = KeyStore.getInstance(type);
File keyStoreFile = new File(keyStorePath);
if (keyStoreFile.exists()) {
try (FileInputStream fis = new FileInputStream(keyStoreFile)) {
keyStore.load(fis, keyStorePassword);
}
} else {
// Initialize empty keystore
keyStore.load(null, keyStorePassword);
}
}
public void addPrivateKeyEntry(String alias, PrivateKey privateKey, char[] keyPassword,
Certificate[] certificateChain)
throws KeyStoreException {
keyStore.setKeyEntry(alias, privateKey, keyPassword, certificateChain);
}
public void addCertificateEntry(String alias, Certificate certificate)
throws KeyStoreException {
keyStore.setCertificateEntry(alias, certificate);
}
public Certificate getCertificate(String alias) throws KeyStoreException {
return keyStore.getCertificate(alias);
}
public PrivateKey getPrivateKey(String alias, char[] keyPassword)
throws KeyStoreException, NoSuchAlgorithmException, UnrecoverableKeyException {
return (PrivateKey) keyStore.getKey(alias, keyPassword);
}
public Certificate[] getCertificateChain(String alias) throws KeyStoreException {
return keyStore.getCertificateChain(alias);
}
public boolean containsAlias(String alias) throws KeyStoreException {
return keyStore.containsAlias(alias);
}
public void deleteEntry(String alias) throws KeyStoreException {
keyStore.deleteEntry(alias);
}
public void save() throws IOException, CertificateException, KeyStoreException, NoSuchAlgorithmException {
try (FileOutputStream fos = new FileOutputStream(keyStorePath)) {
keyStore.store(fos, keyStorePassword);
}
}
public Enumeration<String> listAliases() throws KeyStoreException {
return keyStore.aliases();
}
public void listContents() throws KeyStoreException {
Enumeration<String> aliases = keyStore.aliases();
System.out.println("KeyStore Contents:");
while (aliases.hasMoreElements()) {
String alias = aliases.nextElement();
System.out.println("Alias: " + alias);
if (keyStore.isKeyEntry(alias)) {
System.out.println(" Type: Private Key Entry");
} else if (keyStore.isCertificateEntry(alias)) {
System.out.println(" Type: Certificate Entry");
}
}
}
}
3. SSL Context Builder
package com.example.security.ssl;
import javax.net.ssl.*;
import java.io.FileInputStream;
import java.security.*;
import java.security.cert.CertificateException;
public class SSLContextBuilder {
private KeyManager[] keyManagers;
private TrustManager[] trustManagers;
private SecureRandom secureRandom;
public SSLContextBuilder withKeyStore(String keyStorePath, char[] keyStorePassword,
String keyAlias, char[] keyPassword)
throws Exception {
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(
KeyManagerFactory.getDefaultAlgorithm());
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
try (FileInputStream fis = new FileInputStream(keyStorePath)) {
keyStore.load(fis, keyStorePassword);
}
keyManagerFactory.init(keyStore, keyPassword != null ? keyPassword : keyStorePassword);
this.keyManagers = keyManagerFactory.getKeyManagers();
return this;
}
public SSLContextBuilder withTrustStore(String trustStorePath, char[] trustStorePassword)
throws Exception {
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(
TrustManagerFactory.getDefaultAlgorithm());
KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
try (FileInputStream fis = new FileInputStream(trustStorePath)) {
trustStore.load(fis, trustStorePassword);
}
trustManagerFactory.init(trustStore);
this.trustManagers = trustManagerFactory.getTrustManagers();
return this;
}
public SSLContextBuilder withCustomTrustManager(TrustManager trustManager) {
this.trustManagers = new TrustManager[]{trustManager};
return this;
}
public SSLContextBuilder withSecureRandom(SecureRandom secureRandom) {
this.secureRandom = secureRandom;
return this;
}
public SSLContext build() throws NoSuchAlgorithmException, KeyManagementException {
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(keyManagers, trustManagers, secureRandom);
return sslContext;
}
// Convenience method for creating SSLContext with default settings
public static SSLContext createDefaultSSLContext() throws Exception {
return SSLContext.getDefault();
}
}
4. Custom Trust Manager
package com.example.security.ssl;
import javax.net.ssl.X509TrustManager;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
public class CustomTrustManager implements X509TrustManager {
private final X509TrustManager defaultTrustManager;
private final Set<String> allowedCertificates;
public CustomTrustManager(X509TrustManager defaultTrustManager) {
this.defaultTrustManager = defaultTrustManager;
this.allowedCertificates = new HashSet<>();
}
public void addAllowedCertificate(String certificateThumbprint) {
allowedCertificates.add(certificateThumbprint.toLowerCase());
}
private String getThumbprint(X509Certificate certificate) {
try {
MessageDigest md = MessageDigest.getInstance("SHA-256");
byte[] der = certificate.getEncoded();
md.update(der);
byte[] digest = md.digest();
return bytesToHex(digest);
} catch (Exception e) {
throw new RuntimeException("Failed to calculate certificate thumbprint", e);
}
}
private String bytesToHex(byte[] bytes) {
StringBuilder result = new StringBuilder();
for (byte b : bytes) {
result.append(String.format("%02x", b));
}
return result.toString();
}
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType)
throws CertificateException {
// Additional client certificate validation
if (chain != null && chain.length > 0) {
String thumbprint = getThumbprint(chain[0]);
if (!allowedCertificates.contains(thumbprint)) {
throw new CertificateException("Client certificate not in allowed list");
}
}
defaultTrustManager.checkClientTrusted(chain, authType);
}
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType)
throws CertificateException {
// Additional server certificate validation
if (chain != null && chain.length > 0) {
String thumbprint = getThumbprint(chain[0]);
if (!allowedCertificates.contains(thumbprint)) {
throw new CertificateException("Server certificate not in allowed list");
}
}
defaultTrustManager.checkServerTrusted(chain, authType);
}
@Override
public X509Certificate[] getAcceptedIssuers() {
return defaultTrustManager.getAcceptedIssuers();
}
public static CustomTrustManager wrapDefaultTrustManager() throws Exception {
TrustManagerFactory tmf = TrustManagerFactory.getInstance(
TrustManagerFactory.getDefaultAlgorithm());
tmf.init((KeyStore) null);
for (TrustManager tm : tmf.getTrustManagers()) {
if (tm instanceof X509TrustManager) {
return new CustomTrustManager((X509TrustManager) tm);
}
}
throw new IllegalStateException("No X509TrustManager found");
}
}
Certificate Service Implementation
1. Certificate Service
package com.example.security.service;
import com.example.security.cert.CertificateUtils;
import com.example.security.keystore.KeyStoreManager;
import org.springframework.stereotype.Service;
import java.io.File;
import java.security.KeyPair;
import java.security.KeyStore;
import java.security.cert.X509Certificate;
import java.util.Date;
@Service
public class CertificateService {
private static final String KEYSTORE_TYPE = "PKCS12";
private static final String KEY_ALGORITHM = "RSA";
private static final int KEY_SIZE = 2048;
public void createSelfSignedCertificate(String keyStorePath, String keyStorePassword,
String alias, String keyPassword,
String subjectDN, int validityDays) throws Exception {
// Create keystore directory if it doesn't exist
File keyStoreFile = new File(keyStorePath);
keyStoreFile.getParentFile().mkdirs();
KeyStoreManager keyStoreManager = new KeyStoreManager(
keyStorePath, keyStorePassword.toCharArray(), KEYSTORE_TYPE);
// Generate key pair
KeyPair keyPair = CertificateUtils.generateKeyPair(KEY_ALGORITHM, KEY_SIZE);
// Calculate validity dates
Date startDate = new Date();
Date endDate = new Date(startDate.getTime() + validityDays * 24L * 60 * 60 * 1000);
// Generate self-signed certificate
X509Certificate certificate = CertificateUtils.generateSelfSignedCertificate(
keyPair, subjectDN, startDate, endDate);
// Add to keystore
keyStoreManager.addPrivateKeyEntry(
alias,
keyPair.getPrivate(),
keyPassword.toCharArray(),
new java.security.cert.Certificate[]{certificate}
);
keyStoreManager.save();
}
public void importCertificate(String keyStorePath, String keyStorePassword,
String alias, String certificatePath) throws Exception {
KeyStoreManager keyStoreManager = new KeyStoreManager(
keyStorePath, keyStorePassword.toCharArray(), KEYSTORE_TYPE);
X509Certificate certificate = CertificateUtils.loadCertificateFromPEM(certificatePath);
keyStoreManager.addCertificateEntry(alias, certificate);
keyStoreManager.save();
}
public void exportCertificate(String keyStorePath, String keyStorePassword,
String alias, String exportPath) throws Exception {
KeyStoreManager keyStoreManager = new KeyStoreManager(
keyStorePath, keyStorePassword.toCharArray(), KEYSTORE_TYPE);
java.security.cert.Certificate certificate = keyStoreManager.getCertificate(alias);
if (certificate instanceof X509Certificate) {
CertificateUtils.saveCertificateToPEM((X509Certificate) certificate, exportPath);
} else {
throw new IllegalArgumentException("Certificate is not an X509 certificate");
}
}
public void listCertificates(String keyStorePath, String keyStorePassword)
throws Exception {
KeyStoreManager keyStoreManager = new KeyStoreManager(
keyStorePath, keyStorePassword.toCharArray(), KEYSTORE_TYPE);
keyStoreManager.listContents();
}
}
2. SSL Configuration Service
package com.example.security.service;
import com.example.security.ssl.SSLContextBuilder;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import javax.net.ssl.SSLContext;
import java.io.File;
@Service
public class SSLConfigurationService {
@Value("${app.ssl.keystore.path:keystore.p12}")
private String keyStorePath;
@Value("${app.ssl.keystore.password:changeit}")
private String keyStorePassword;
@Value("${app.ssl.truststore.path:truststore.p12}")
private String trustStorePath;
@Value("${app.ssl.truststore.password:changeit}")
private String trustStorePassword;
@Value("${app.ssl.key.alias:server}")
private String keyAlias;
@Value("${app.ssl.key.password:changeit}")
private String keyPassword;
public SSLContext createServerSSLContext() throws Exception {
validateKeyStoreExists(keyStorePath);
return new SSLContextBuilder()
.withKeyStore(keyStorePath, keyStorePassword.toCharArray(),
keyAlias, keyPassword.toCharArray())
.withTrustStore(trustStorePath, trustStorePassword.toCharArray())
.build();
}
public SSLContext createClientSSLContext() throws Exception {
validateKeyStoreExists(trustStorePath);
return new SSLContextBuilder()
.withTrustStore(trustStorePath, trustStorePassword.toCharArray())
.build();
}
public SSLContext createMutualSSLContext() throws Exception {
validateKeyStoreExists(keyStorePath);
validateKeyStoreExists(trustStorePath);
return new SSLContextBuilder()
.withKeyStore(keyStorePath, keyStorePassword.toCharArray(),
keyAlias, keyPassword.toCharArray())
.withTrustStore(trustStorePath, trustStorePassword.toCharArray())
.build();
}
private void validateKeyStoreExists(String path) {
File keyStoreFile = new File(path);
if (!keyStoreFile.exists()) {
throw new IllegalStateException("Keystore file not found: " + path);
}
}
}
Spring Boot Configuration
1. SSL Properties Configuration
# application.yml server: port: 8443 ssl: key-store: classpath:keystore.p12 key-store-password: changeit key-store-type: PKCS12 key-alias: server key-password: changeit trust-store: classpath:truststore.p12 trust-store-password: changeit trust-store-type: PKCS12 client-auth: need # for mutual TLS app: ssl: keystore: path: keystore.p12 password: changeit truststore: path: truststore.p12 password: changeit key: alias: server password: changeit logging: level: com.example.security: DEBUG
2. REST Client with SSL
package com.example.client;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.http.client.SimpleClientHttpRequestFactory;
import org.springframework.stereotype.Component;
import org.springframework.web.client.RestTemplate;
import javax.net.ssl.SSLContext;
import java.net.HttpURLConnection;
@Component
public class SecureRestClient {
private final RestTemplate restTemplate;
@Autowired
public SecureRestClient(SSLContext sslContext) {
this.restTemplate = createSecureRestTemplate(sslContext);
}
private RestTemplate createSecureRestTemplate(SSLContext sslContext) {
SimpleClientHttpRequestFactory requestFactory = new SimpleClientHttpRequestFactory() {
@Override
protected void prepareConnection(HttpURLConnection connection, String httpMethod) {
super.prepareConnection(connection, httpMethod);
if (connection instanceof javax.net.ssl.HttpsURLConnection) {
javax.net.ssl.HttpsURLConnection httpsConnection =
(javax.net.ssl.HttpsURLConnection) connection;
httpsConnection.setSSLSocketFactory(sslContext.getSocketFactory());
// Optional: Custom hostname verifier
httpsConnection.setHostnameVerifier((hostname, session) -> true);
}
}
};
return new RestTemplate(requestFactory);
}
public <T> T get(String url, Class<T> responseType) {
return restTemplate.getForObject(url, responseType);
}
public <T> ResponseEntity<T> getWithResponse(String url, Class<T> responseType) {
return restTemplate.getForEntity(url, responseType);
}
public <T> T post(String url, Object request, Class<T> responseType) {
return restTemplate.postForObject(url, request, responseType);
}
}
3. WebClient Configuration for SSL
package com.example.client;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslContextBuilder;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.Resource;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.netty.http.client.HttpClient;
import javax.net.ssl.TrustManagerFactory;
import java.io.InputStream;
import java.security.KeyStore;
@Configuration
public class WebClientConfig {
@Value("${app.ssl.truststore.path}")
private Resource trustStoreResource;
@Value("${app.ssl.truststore.password}")
private String trustStorePassword;
@Bean
public WebClient secureWebClient() throws Exception {
SslContext sslContext = createSSLContext();
HttpClient httpClient = HttpClient.create()
.secure(spec -> spec.sslContext(sslContext));
return WebClient.builder()
.clientConnector(new ReactorClientHttpConnector(httpClient))
.build();
}
private SslContext createSSLContext() throws Exception {
KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
try (InputStream is = trustStoreResource.getInputStream()) {
trustStore.load(is, trustStorePassword.toCharArray());
}
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(
TrustManagerFactory.getDefaultAlgorithm());
trustManagerFactory.init(trustStore);
return SslContextBuilder.forClient()
.trustManager(trustManagerFactory)
.build();
}
}
Certificate Monitoring and Validation
1. Certificate Validator
package com.example.security.validation;
import java.security.cert.X509Certificate;
import java.util.Date;
public class CertificateValidator {
public static ValidationResult validateCertificate(X509Certificate certificate) {
ValidationResult result = new ValidationResult();
try {
// Check validity period
Date now = new Date();
certificate.checkValidity(now);
result.setValid(true);
// Additional validations
if (certificate.getSubjectX500Principal() == null) {
result.addError("Certificate has no subject");
}
if (certificate.getIssuerX500Principal() == null) {
result.addError("Certificate has no issuer");
}
// Check key usage
boolean[] keyUsage = certificate.getKeyUsage();
if (keyUsage != null) {
// Validate based on intended use
}
} catch (Exception e) {
result.setValid(false);
result.addError("Certificate validation failed: " + e.getMessage());
}
return result;
}
public static class ValidationResult {
private boolean valid;
private StringBuilder errors = new StringBuilder();
public boolean isValid() { return valid; }
public void setValid(boolean valid) { this.valid = valid; }
public void addError(String error) {
if (errors.length() > 0) {
errors.append("; ");
}
errors.append(error);
}
public String getErrors() {
return errors.toString();
}
@Override
public String toString() {
return "ValidationResult{valid=" + valid +
", errors='" + errors.toString() + "'}";
}
}
}
2. Certificate Monitor
package com.example.security.monitoring;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.security.KeyStore;
import java.security.cert.X509Certificate;
import java.util.Date;
import java.util.Enumeration;
@Component
public class CertificateMonitor {
private static final Logger logger = LoggerFactory.getLogger(CertificateMonitor.class);
private final CertificateService certificateService;
public CertificateMonitor(CertificateService certificateService) {
this.certificateService = certificateService;
}
@Scheduled(cron = "0 0 6 * * ?") // Run daily at 6 AM
public void checkCertificateExpiry() {
try {
// This would typically check multiple keystores
checkKeystoreExpiry("keystore.p12", "changeit");
checkKeystoreExpiry("truststore.p12", "changeit");
} catch (Exception e) {
logger.error("Failed to check certificate expiry", e);
}
}
private void checkKeystoreExpiry(String keystorePath, String password) throws Exception {
KeyStoreManager keyStoreManager = new KeyStoreManager(
keystorePath, password.toCharArray(), "PKCS12");
Enumeration<String> aliases = keyStoreManager.listAliases();
while (aliases.hasMoreElements()) {
String alias = aliases.nextElement();
java.security.cert.Certificate cert = keyStoreManager.getCertificate(alias);
if (cert instanceof X509Certificate) {
checkCertificateExpiry((X509Certificate) cert, alias, keystorePath);
}
}
}
private void checkCertificateExpiry(X509Certificate cert, String alias, String keystorePath) {
Date expiryDate = cert.getNotAfter();
Date now = new Date();
long daysUntilExpiry = (expiryDate.getTime() - now.getTime()) / (1000 * 60 * 60 * 24);
if (daysUntilExpiry <= 30) {
logger.warn("Certificate '{}' in keystore '{}' expires in {} days",
alias, keystorePath, daysUntilExpiry);
} else if (daysUntilExpiry <= 7) {
logger.error("Certificate '{}' in keystore '{}' expires in {} days - RENEWAL REQUIRED",
alias, keystorePath, daysUntilExpiry);
}
}
}
Testing Certificate Management
1. Certificate Service Test
package com.example.security;
import com.example.security.service.CertificateService;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import java.io.File;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest
class CertificateServiceTest {
@Autowired
private CertificateService certificateService;
private static final String TEST_KEYSTORE = "test-keystore.p12";
private static final String KEYSTORE_PASSWORD = "testpass";
private static final String KEY_PASSWORD = "keypass";
private static final String ALIAS = "test-cert";
@BeforeEach
void setUp() {
// Clean up any existing test files
new File(TEST_KEYSTORE).delete();
}
@AfterEach
void tearDown() {
// Clean up test files
new File(TEST_KEYSTORE).delete();
}
@Test
void testCreateSelfSignedCertificate() throws Exception {
// When
certificateService.createSelfSignedCertificate(
TEST_KEYSTORE, KEYSTORE_PASSWORD, ALIAS, KEY_PASSWORD,
"CN=Test Certificate, O=Test Org", 365);
// Then
File keyStoreFile = new File(TEST_KEYSTORE);
assertTrue(keyStoreFile.exists());
// Verify certificate can be listed
assertDoesNotThrow(() ->
certificateService.listCertificates(TEST_KEYSTORE, KEYSTORE_PASSWORD));
}
@Test
void testExportAndImportCertificate() throws Exception {
// Given
certificateService.createSelfSignedCertificate(
TEST_KEYSTORE, KEYSTORE_PASSWORD, ALIAS, KEY_PASSWORD,
"CN=Test Certificate, O=Test Org", 365);
String exportPath = "test-cert.pem";
// When
certificateService.exportCertificate(
TEST_KEYSTORE, KEYSTORE_PASSWORD, ALIAS, exportPath);
// Then
File exportedFile = new File(exportPath);
assertTrue(exportedFile.exists());
// Clean up
exportedFile.delete();
}
}
2. SSL Context Test
package com.example.security;
import com.example.security.ssl.SSLContextBuilder;
import org.junit.jupiter.api.Test;
import javax.net.ssl.SSLContext;
import static org.junit.jupiter.api.Assertions.*;
class SSLContextBuilderTest {
@Test
void testCreateDefaultSSLContext() throws Exception {
// When
SSLContext sslContext = SSLContextBuilder.createDefaultSSLContext();
// Then
assertNotNull(sslContext);
assertEquals("TLS", sslContext.getProtocol());
}
@Test
void testBuildSSLContext() throws Exception {
// Given - This would require actual keystore files
// For testing, we'd use test resources
// When & Then
assertDoesNotThrow(() -> {
SSLContext sslContext = new SSLContextBuilder().build();
// Basic validation
assertNotNull(sslContext);
});
}
}
Best Practices
1. Security Configuration
package com.example.security.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(authz -> authz
.requestMatchers("/public/**").permitAll()
.anyRequest().authenticated()
)
.requiresChannel(channel -> channel
.anyRequest().requiresSecure() // Force HTTPS
);
return http.build();
}
}
2. Certificate Rotation Strategy
package com.example.security.rotation;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
@Component
public class CertificateRotationManager {
@Scheduled(cron = "0 0 2 1 * ?") // Run on 1st day of month at 2 AM
public void rotateCertificates() {
try {
LocalDateTime now = LocalDateTime.now();
logger.info("Starting certificate rotation at {}", now);
// Implementation would:
// 1. Generate new certificates
// 2. Deploy to load balancers/servers
// 3. Verify new certificates work
// 4. Remove old certificates
} catch (Exception e) {
logger.error("Certificate rotation failed", e);
}
}
}
Conclusion
Effective certificate management in Java involves:
Key Components
- KeyStore Management - Secure storage of private keys and certificates
- TrustStore Management - Management of trusted CA certificates
- SSL Context Configuration - Proper setup of secure connections
- Certificate Validation - Regular checks for validity and security
Best Practices
- Secure Storage - Protect keystore files and passwords
- Regular Rotation - Implement certificate expiration monitoring
- Proper Validation - Custom trust managers for additional security
- Testing - Comprehensive testing of SSL configurations
- Monitoring - Continuous monitoring of certificate health
Security Considerations
- Use strong passwords for keystores and private keys
- Implement proper access controls for certificate files
- Regularly update and patch security libraries
- Monitor for certificate expiration and vulnerabilities
- Consider using hardware security modules (HSM) for production
Proper certificate management is essential for maintaining secure communications in Java applications, ensuring authentication, confidentiality, and integrity in distributed systems.