Open DevShivmohan opened 4 months ago
package com.xcelore.common.beans;
import com.xcelore.archives.mapper.ArchiveMapper; import com.xcelore.central.properties.CentralServerProperties; import com.xcelore.common.mapper.GlobalSettingMapper; import com.xcelore.common.properties.DataSourceProperties; import com.xcelore.landing.mapper.LandingMapper; import com.xcelore.metar.mapper.MetarMapper; import com.xcelore.metar.utils.MetarUtils; import com.xcelore.synop.mapper.SynopMapper; import com.xcelore.taf.mapper.TafMapper; import com.xcelore.user.mappers.UserDetailMapper; import com.xcelore.warnings.mapper.WarningMapper; import com.xcelore.weather.entity.Derived; import com.xcelore.weather.mapper.DerivedMapper; import com.xcelore.weather.mapper.WeatherMapper; import com.xcelore.weather.model.kafka.WeatherData; import com.xcelore.weather.util.StatisticalUtils; import io.netty.handler.ssl.SslContextBuilder; import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; import org.apache.hc.client5.http.impl.classic.HttpClients; import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder; import org.apache.hc.client5.http.io.HttpClientConnectionManager; import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactory; import org.apache.hc.core5.ssl.SSLContextBuilder; import org.jasypt.encryption.StringEncryptor; import org.jasypt.encryption.pbe.StandardPBEStringEncryptor; import org.mapstruct.factory.Mappers; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.jdbc.DataSourceBuilder; import org.springframework.boot.web.client.RestTemplateBuilder; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Primary; import org.springframework.core.io.ClassPathResource; import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; import org.springframework.stereotype.Component; import org.springframework.web.client.RestTemplate; import reactor.netty.http.client.HttpClient;
import javax.crypto.Cipher; import javax.crypto.NoSuchPaddingException; import javax.crypto.spec.SecretKeySpec; import javax.net.ssl.KeyManagerFactory; import javax.net.ssl.SSLContext; import javax.net.ssl.TrustManagerFactory; import javax.sql.DataSource; import java.io.IOException; import java.io.InputStream; import java.nio.charset.StandardCharsets; import java.security.InvalidKeyException; import java.security.KeyStore; import java.security.KeyStoreException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.UnrecoverableKeyException; import java.security.cert.CertificateException; import java.util.Arrays; import java.util.Base64; import java.util.List; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap;
@Configuration @EnableConfigurationProperties({DataSourceProperties.class, CentralServerProperties.class}) @Component public class Beans { private final DataSourceProperties dataSourceProperties; private final CentralServerProperties centralServerProperties;
@Value("${xcelore.airbase.software.crypto-secret}")
private String cryptoSecretKey;
public Beans(DataSourceProperties dataSourceProperties, CentralServerProperties centralServerProperties) {
this.dataSourceProperties = dataSourceProperties;
this.centralServerProperties = centralServerProperties;
}
/**
* one siteId has one weather data
*
* @return
*/
@Bean
public ConcurrentMap<Integer, WeatherData> concurrentSingleWeatherDataMap() {
return new ConcurrentHashMap<>();
}
/**
* one siteId has last three weather data
*
* @return
*/
@Bean
public ConcurrentMap<Integer, List<WeatherData>> concurrentLastThreeWeatherDataMap() {
return new ConcurrentHashMap<>();
}
/**
* All beans related to scheduler data
* @return
*/
@Bean
public SchedulerDataBean schedulerDataBean(){
return new SchedulerDataBean();
}
@Bean
public MetarMapper metarMapper() {
return Mappers.getMapper(MetarMapper.class);
}
@Bean
public SynopMapper synopMapper(){
return Mappers.getMapper(SynopMapper.class);
}
@Bean
public LandingMapper landingMapper(){ return Mappers.getMapper(LandingMapper.class);}
@Bean
public WarningMapper warningMapper(){return Mappers.getMapper(WarningMapper.class);}
@Bean
public TafMapper tafMapper() {return Mappers.getMapper(TafMapper.class);}
@Bean
public MetarUtils metarUtils(){ return new MetarUtils();
}
@Bean
public WeatherMapper weatherMapper(){
return Mappers.getMapper(WeatherMapper.class);
}
@Bean
public DerivedMapper derivedMapper() {
return Mappers.getMapper(DerivedMapper.class);
}
@Bean
public StatisticalUtils statisticalUtils() {
return new StatisticalUtils();
}
@Bean
public StringEncryptor stringEncryptor() {
StandardPBEStringEncryptor encryptor = new StandardPBEStringEncryptor();
encryptor.setAlgorithm("PBEWithMD5AndDES");
encryptor.setPassword("Xcelore.Airbase.SGS100@980$#&546312"); // same key used to encrypt
return encryptor;
}
@Bean
@Primary
public DataSource dataSource() {
return DataSourceBuilder.create()
.url(dataSourceProperties.getUrl())
.username(dataSourceProperties.getUsername())
.password(new String(Base64.getDecoder().decode(dataSourceProperties.getPassword())))
.build();
}
@Bean
public Cipher cipher() throws NoSuchAlgorithmException, NoSuchPaddingException, InvalidKeyException {
byte[] key = new String(Base64.getDecoder().decode(cryptoSecretKey)).getBytes(StandardCharsets.UTF_8);
final var sha = MessageDigest.getInstance("SHA-1");
key = sha.digest(key);
key = Arrays.copyOf(key, 16);
final var secretKey = new SecretKeySpec(key, "AES");
final var cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
return cipher;
}
@Bean
public ArchiveMapper archiveMapper() {
return Mappers.getMapper(ArchiveMapper.class);
}
@Bean
public RestTemplate restTemplate(RestTemplateBuilder builder) throws Exception {
KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
try (InputStream trustStoreStream = new ClassPathResource(centralServerProperties.getSslCertPath()).getInputStream()) {
trustStore.load(trustStoreStream, centralServerProperties.getSslCertPassword().toCharArray());
}
SSLContext sslContext = SSLContextBuilder.create()
.loadTrustMaterial(trustStore, (chain, authType) -> true) // Trust all certificates
.build();
SSLConnectionSocketFactory sslConFactory = new SSLConnectionSocketFactory(sslContext);
HttpClientConnectionManager cm = PoolingHttpClientConnectionManagerBuilder.create()
.setSSLSocketFactory(sslConFactory)
.build();
CloseableHttpClient httpClient = HttpClients.custom().setConnectionManager(cm).build();
HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory(httpClient);
return builder
.requestFactory(() -> factory)
.build();
}
/**
*
* Web Reactor client config with SSL cert
* @return
*/
@Bean
public HttpClient httpClient() throws KeyStoreException, IOException, NoSuchAlgorithmException, UnrecoverableKeyException {
KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
try (InputStream trustStoreStream = new ClassPathResource(centralServerProperties.getSslCertPath()).getInputStream()) {
trustStore.load(trustStoreStream, centralServerProperties.getSslCertPassword().toCharArray());
} catch (CertificateException e) {
throw new RuntimeException(e);
}
KeyManagerFactory keyManagerFactory =
KeyManagerFactory.getInstance("SunX509");
keyManagerFactory.init(trustStore, centralServerProperties.getSslCertPassword().toCharArray());
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
trustManagerFactory.init(trustStore);
final var sslContextBuilder = SslContextBuilder.forClient()
.trustManager(trustManagerFactory)
.keyManager(keyManagerFactory)
.build();
return HttpClient.create().secure(sslSpec -> sslSpec.sslContext(sslContextBuilder));
}
@Bean
public UserDetailMapper userMapper() {
return Mappers.getMapper(UserDetailMapper.class);
}
@Bean
public GlobalSettingMapper globalSettingMapper() {
return Mappers.getMapper(GlobalSettingMapper.class);
}
@Bean
public Derived derived() {
return new Derived();
}
@Bean
public ApplicationDataBean applicationDataBean(){
return new ApplicationDataBean();
}
}
package com.xcelore.central.event;
import com.xcelore.central.dto.metar.MetarReportResponseDto; import com.xcelore.central.impl.AirbaseCentralCryptoBuilder; import com.xcelore.central.properties.CentralServerProperties; import com.xcelore.common.beans.ApplicationDataBean; import com.xcelore.common.constants.Constants; import com.xcelore.common.services.GlobalSettingService; import com.xcelore.weather.live.WebSocketMessageBroker; import jakarta.annotation.PostConstruct; import lombok.AllArgsConstructor; import lombok.extern.log4j.Log4j2; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.http.client.reactive.ReactorClientHttpConnector; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Component; import reactor.netty.http.client.HttpClient; import reactor.util.retry.Retry;
import java.time.Duration; import java.time.temporal.ChronoUnit; import java.util.Objects;
import static com.xcelore.common.constants.Constants.SSE_DIVERSIONARY_METAR_REPORT;
@Component @Log4j2 @EnableConfigurationProperties(CentralServerProperties.class) @AllArgsConstructor public class SSECentralServerConsumer { private final CentralServerProperties centralServerProperties; private final AirbaseCentralCryptoBuilder airbaseCentralCryptoBuilder; private final ApplicationDataBean applicationDataBean; private final WebSocketMessageBroker webSocketMessageBroker; private final GlobalSettingService globalSettingService; private final HttpClient httpClient;
@PostConstruct
public void consumeEvents() {
final var disposable = applicationDataBean
.getDiversionaryMetarReportWebClientBuilder()
.clientConnector(new ReactorClientHttpConnector(httpClient))
.baseUrl(centralServerProperties.getBaseURL())
.build()
.get()
.uri(SSE_DIVERSIONARY_METAR_REPORT)
.accept(MediaType.TEXT_EVENT_STREAM)
.header(HttpHeaders.AUTHORIZATION, Constants.BEARER_SPACE + airbaseCentralCryptoBuilder.getAirbaseDataModelAsEncryptedJsonString())
.retrieve()
.bodyToFlux(MetarReportResponseDto.class)
.doOnSubscribe(subscription -> log.info("Subscribe event diversionary"))
.retryWhen(Retry.backoff(15000, Duration.of(10, ChronoUnit.SECONDS))
.doBeforeRetry(retrySignal -> log.warn("Retrying connection attempt {}", retrySignal.totalRetries())))
.subscribe(
this::broadCastIfNeeded,
error -> {
log.error("Error occurred {}", error.getMessage());
if (applicationDataBean.getDiversionaryMetarReportSSEDisposable() != null && !applicationDataBean.getDiversionaryMetarReportSSEDisposable().isDisposed()) {
applicationDataBean.getDiversionaryMetarReportSSEDisposable().dispose();
}
consumeEvents();
},
() -> log.info("Subscription connection for diversionary event completed")
);
applicationDataBean.setDiversionaryMetarReportSSEDisposable(disposable);
}
@Async
public void broadCastIfNeeded(final MetarReportResponseDto metarReportResponseDto) {
log.info("Diversionary report received from station code {}", metarReportResponseDto.getStationCode());
final var globalSetting = globalSettingService.fetchGlobalSetting();
if (Objects.isNull(globalSetting)) {
return;
}
if (Objects.equals(metarReportResponseDto.getStationCode(), globalSetting.getDiversionaryOneStationCode())) {
webSocketMessageBroker.broadCastDiversionaryOneReport(metarReportResponseDto);
}
if (Objects.equals(metarReportResponseDto.getStationCode(), globalSetting.getDiversionaryTwoStationCode())) {
webSocketMessageBroker.broadCastDiversionaryTwoReport(metarReportResponseDto);
}
}
}
create san.cnf file and use the below config
[ req ]
default_bits = 2048
prompt = no
default_md = sha256
distinguished_name = dn
req_extensions = req_ext
[ dn ]
CN = 18.216.85.166
[ req_ext ]
subjectAltName = @alt_names
[ alt_names ]
IP.1 = 18.216.85.166
Generate a Private Key:
openssl genpkey -algorithm RSA -out server.key -aes256
Generate the CSR using the Configuration File:
openssl req -new -key server.key -out server.csr -config san.cnf
Generate a Self-Signed Certificate with SAN:
openssl x509 -req -in server.csr -signkey server.key -out server.crt -days 365 -extensions req_ext -extfile san.cnf
Understanding Certificates and Keys Before we dive into generating certificates, let’s clarify some terminology:
CA (Certificate Authority) - An entity that issues digital certificates. Certificate - A digital form of identification, like a passport, for your application. Private Key - A secret key that is used in conjunction with a public certificate to encrypt and decrypt data. CSR (Certificate Signing Request) - A request sent from an applicant to a CA to obtain a digital identity certificate. Truststore - A repository that holds trusted certificates (usually CA certificates). Keystore - A repository that holds certificates along with their private keys. Generating Certificates and Keys Here’s a simplified process to generate all the necessary files for mTLS:
Step 1: Generate the CA Certificate
Create the CA’s Private Key:
openssl genrsa -out ca.key 2048
openssl req -x509 -new -nodes -key ca.key -sha256 -days 365 -out ca.pem
This creates a self-signed CA certificate valid for 365 days.
Step 2: Generate Server and Client Certificates
Generate Private Keys:
For the server: openssl genrsa -out server.key 2048
For the client: openssl genrsa -out client.key 2048
For the server: openssl req -new -key server.key -out server.csr
For the client: openssl req -new -key client.key -out client.csr
For the server: openssl x509 -req -in server.csr -CA ca.pem -CAkey ca.key -CAcreateserial -out server.crt -days 365
For the client: openssl x509 -req -in client.csr -CA ca.pem -CAkey ca.key -CAcreateserial -out client.crt -days 365
Step 3: Create PKCS#12 Keystores
Convert Certificates and Keys to PKCS#12 Format:
For the server: openssl pkcs12 -export -out server.p12 -name "server" -inkey server.key -in server.crt -certfile ca.pem
For the client: openssl pkcs12 -export -out client.p12 -name "client" -inkey client.key -in client.crt -certfile ca.pem
Step 4: Create the Truststore
Import the CA Certificate into a PKCS#12 Truststore:
keytool -import -file ca.pem -alias "ca" -keystore truststore.p12 -storetype PKCS12
Why Trust a CA Instead of Individual Client Certificates in mTLS?
In mutual TLS configurations, servers typically trust a Certificate Authority (CA) rather than individual client certificates. This approach significantly enhances scalability, as the server can authenticate any client with a CA-signed certificate, rather than needing to keep an updated list of every client’s certificate. It simplifies management since the truststore doesn’t require updates with each client certificate renewal. Moreover, security remains robust if the CA’s issuance process is strict and its private key is secure, as the server can rely on the CA’s verification of clients. While there may be scenarios where trusting individual certificates is necessary, such as in a one-client setup, using a CA is the standard practice for systems handling multiple clients.
Configuring Spring Boot for mTLS With all the necessary files generated, you can configure your Spring Boot applications to use mTLS.
Server application.properties:
server.port=8443
server.ssl.key-store=classpath:cert/server.p12
server.ssl.key-store-password=[server_keystore_password]
server.ssl.key-store-type=PKCS12
server.ssl.client-auth=NEED
server.ssl.trust-store=classpath:cert/truststore.p12
server.ssl.trust-store-password=password
server.ssl.trust-store-type=PKCS12
Client application.properties:
client.ssl.key-store=classpath:cert/client.p12
client.ssl.key-store-password=[client_keystore_password]
client.ssl.trust-store=classpath:cert/truststore.p12
client.ssl.trust-store-password=password
Client-Side RestTemplate Configuration for mTLS On the client side, the RestTemplate bean needs to be configured to support mTLS with the necessary SSL context. Below is the configuration class that sets up the RestTemplate:
@Configuration
public class RestClientConfig {
// Load keystore and truststore locations and passwords
@Value("${client.ssl.trust-store}")
private Resource trustStore;
@Value("${client.ssl.key-store}")
private Resource keyStore;
@Value("${client.ssl.trust-store-password}")
private String trustStorePassword;
@Value("${client.ssl.key-store-password}")
private String keyStorePassword;
@Bean
public RestTemplate restTemplate() throws Exception {
// Set up SSL context with truststore and keystore
SSLContext sslContext = new SSLContextBuilder()
.loadKeyMaterial(
keyStore.getURL(),
keyStorePassword.toCharArray(),
keyStorePassword.toCharArray()
)
.loadTrustMaterial(
trustStore.getURL(),
trustStorePassword.toCharArray()
)
.build();
// Configure the SSLConnectionSocketFactory to use NoopHostnameVerifier
SSLConnectionSocketFactory sslConFactory = new SSLConnectionSocketFactory(sslContext, new NoopHostnameVerifier());
// Use a connection manager with the SSL socket factory
HttpClientConnectionManager cm = PoolingHttpClientConnectionManagerBuilder.create()
.setSSLSocketFactory(sslConFactory)
.build();
// Build the CloseableHttpClient and set the connection manager
CloseableHttpClient httpClient = HttpClients.custom()
.setConnectionManager(cm)
.build();
// Set the HttpClient as the request factory for the RestTemplate
ClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory(httpClient);
return new RestTemplate(requestFactory);
}
}
NoopHostnameVerifier is used for hostname verification which accepts any valid SSL session in this example for simplicity. For production, you would use a stricter hostname verifier.
Verification of Secure Communication Once you’ve set up your server and client with mutual TLS, it’s time to verify that the secure connection is in place and functioning correctly. Here’s how to do it:
Start the Server Launch the server application first. It will begin listening for incoming connections on the configured port (typically 8443 for HTTPS).
With the server running, initiate the client application. The client is configured to reach out to the server’s /connect endpoint.
If the mTLS handshake is successful, the client will receive and display a message from the server’s controller — “Successfully connected!”. This confirms that the API call was successful and assures you that the trusted connection was established using mTLS.
The absence of any SSL handshake errors or SSLPeerUnverifiedException exceptions is a good indicator that the certificates and keystores are correctly set up and recognized on both ends. Congratulations, you've secured your applications with mutual TLS!
@Bean
public HttpClient httpClient1() throws KeyStoreException, IOException, NoSuchAlgorithmException, UnrecoverableKeyException {
KeyStore trustStore = KeyStore.getInstance(KeyStore.getDefaultType());
try (InputStream trustStoreStream = centralServerProperties.getTrustStore().getInputStream()) {
trustStore.load(trustStoreStream, centralServerProperties.getTrustStorePassword().toCharArray());
} catch (CertificateException e) {
throw new RuntimeException(e);
}
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
try (InputStream keyStoreStream = centralServerProperties.getKeyStore().getInputStream()) {
keyStore.load(keyStoreStream, centralServerProperties.getKeyStorePassword().toCharArray());
} catch (CertificateException e) {
throw new RuntimeException(e);
}
// Initialize KeyManagerFactory with PKCS12 Keystore
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
keyManagerFactory.init(keyStore, centralServerProperties.getKeyStorePassword().toCharArray());
// Initialize TrustManagerFactory with PKCS12 Truststore
TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
trustManagerFactory.init(trustStore);
// Create SslContext using the KeyManagerFactory and TrustManagerFactory
SslContextBuilder sslContextBuilder = SslContextBuilder.forClient()
.keyManager(keyManagerFactory)
.trustManager(trustManagerFactory);
return HttpClient.create().secure(sslSpec -> sslSpec.sslContext(sslContextBuilder));
}
Server side generate Open SSL and configure it in Spring application
Generate a private key
openssl genpkey -algorithm RSA -out server.key
Generate a self-signed certificate
openssl req -new -x509 -key server.key -out server.crt -days 365
Create a PKCS12 keystore
openssl pkcs12 -export -in server.crt -inkey server.key -out keystore.p12 -name tomcat
Configure Spring Boot to Use SSL
Client-Side Configuration
Trust the Server's Certificate Import the server's certificate into a trust store
keytool -import -alias server-cert -file server.crt -keystore truststore.jks -storepass changeit
Spring boot application create bean of RestTemplate with SSL certificate
Use maven dependency below
Configure RestTemplate on SSL certificate