Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
a309f35
feat(config): make outbound TLS hostname checks configurable
mbuckton Aug 7, 2026
f85807f
fix(config): resolve TLS and DTLS security profiles correctly
mbuckton Aug 7, 2026
c43f5de
fix(config): make key-store updates null safe
mbuckton Aug 7, 2026
ea5379d
fix(config): apply nested TLS settings during updates
mbuckton Aug 7, 2026
6ccff26
fix(config): load and update nested DTLS settings
mbuckton Aug 7, 2026
0e3ebc4
fix(network): retain configured DTLS client authentication
mbuckton Aug 7, 2026
6e04810
fix(network): expose authenticated DTLS peer identity
mbuckton Aug 7, 2026
bd58392
fix(network): expose optional TLS client identity
mbuckton Aug 7, 2026
c9f32f4
fix(network): verify outbound TLS server hostnames
mbuckton Aug 7, 2026
454e7ec
fix(config): apply endpoint updates to stored state
mbuckton Aug 7, 2026
57ee042
docs(config): enable TLS hostname verification by default
mbuckton Aug 7, 2026
eb4ae20
test(config): cover TLS and DTLS profile updates
mbuckton Aug 7, 2026
6e40853
test(network): cover outbound TLS hostname verification
mbuckton Aug 7, 2026
f586088
refactor(network): isolate DTLS engine parameters
mbuckton Aug 7, 2026
f875f67
test(network): preserve DTLS client certificate policy
mbuckton Aug 7, 2026
87e31fd
test(config): apply endpoint updates to stored state
mbuckton Aug 7, 2026
6c52d6d
fix(config): preserve generic TLS protocol negotiation
mbuckton Aug 7, 2026
c8044ae
fix(config): allow DTLS context values in SSL DTO
mbuckton Aug 7, 2026
620900e
test(config): use supported DTLS context
mbuckton Aug 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ private boolean updateEndPointServerConfig(EndPointServerConfigDTO endPointServe
}
if(name.equals(endPointServerConfig.getName())
&& endPointServerConfigDTO instanceof EndPointServerConfig) {
return ((EndPointServerConfig) endPointServerConfig).update(endPointServerConfigDTO);
return ((EndPointServerConfig) endPointServerConfigDTO).update(endPointServerConfig);
}
}
return false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import io.mapsmessaging.configuration.ConfigurationProperties;
import io.mapsmessaging.dto.rest.config.BaseConfigDTO;
import io.mapsmessaging.dto.rest.config.network.KeyStoreConfigDTO;
import java.util.Objects;
import lombok.EqualsAndHashCode;
import lombok.ToString;

Expand All @@ -45,31 +46,31 @@ public boolean update(BaseConfigDTO config) {
boolean hasChanged = false;
if (config instanceof KeyStoreConfigDTO) {
KeyStoreConfigDTO newConfig = (KeyStoreConfigDTO) config;
if (!this.alias.equals(newConfig.getAlias())) {
if (!Objects.equals(this.alias, newConfig.getAlias())) {
this.alias = newConfig.getAlias();
hasChanged = true;
}
if (!this.type.equals(newConfig.getType())) {
if (!Objects.equals(this.type, newConfig.getType())) {
this.type = newConfig.getType();
hasChanged = true;
}
if (!this.providerName.equals(newConfig.getProviderName())) {
if (!Objects.equals(this.providerName, newConfig.getProviderName())) {
this.providerName = newConfig.getProviderName();
hasChanged = true;
}
if (!this.managerFactory.equals(newConfig.getManagerFactory())) {
if (!Objects.equals(this.managerFactory, newConfig.getManagerFactory())) {
this.managerFactory = newConfig.getManagerFactory();
hasChanged = true;
}
if (!this.path.equals(newConfig.getPath())) {
if (!Objects.equals(this.path, newConfig.getPath())) {
this.path = newConfig.getPath();
hasChanged = true;
}
if (!this.passphrase.equals(newConfig.getPassphrase())) {
if (!Objects.equals(this.passphrase, newConfig.getPassphrase())) {
this.passphrase = newConfig.getPassphrase();
hasChanged = true;
}
if (!this.provider.equals(newConfig.getProvider())) {
if (!Objects.equals(this.provider, newConfig.getProvider())) {
this.provider = newConfig.getProvider();
hasChanged = true;
}
Expand Down
100 changes: 60 additions & 40 deletions src/main/java/io/mapsmessaging/config/network/SslConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,77 +23,97 @@
import io.mapsmessaging.configuration.ConfigurationProperties;
import io.mapsmessaging.dto.rest.config.BaseConfigDTO;
import io.mapsmessaging.dto.rest.config.network.SslConfigDTO;
import java.util.Objects;
import lombok.EqualsAndHashCode;
import lombok.ToString;

@EqualsAndHashCode(callSuper = true)
@ToString
public class SslConfig extends SslConfigDTO implements Config {
public class SslConfig extends SslConfigDTO implements Config {

public SslConfig(ConfigurationProperties config) {
ConfigurationProperties securityProps = locateConfig(config);
this.context = securityProps.getProperty("context", "tls");
this.clientCertificateRequired = config.getBooleanProperty("clientCertificateRequired", false);
this.clientCertificateWanted = config.getBooleanProperty("clientCertificateWanted", false);
this.crlUrl = config.getProperty("crlUrl", null);
this.crlInterval = config.getLongProperty("crlInterval", 0);
this(config, "tls");
}

public SslConfig(ConfigurationProperties config, String transport) {
ConfigurationProperties securityProps = locateConfig(config, transport);
if (securityProps == null) {
throw new IllegalArgumentException("Missing security." + transport + " configuration");
}

String defaultContext = "dtls".equalsIgnoreCase(transport) ? "DTLSv1.2" : "TLS";
this.context = securityProps.getProperty("context", defaultContext);
this.clientCertificateRequired = securityProps.getBooleanProperty("clientCertificateRequired", false);
this.clientCertificateWanted = securityProps.getBooleanProperty("clientCertificateWanted", false);
this.hostnameVerificationEnabled = securityProps.getBooleanProperty("hostnameVerificationEnabled", true);
this.crlUrl = securityProps.getProperty("crlUrl", null);
this.crlInterval = securityProps.getLongProperty("crlInterval", 3600000L);
this.keyStore = new KeyStoreConfig((ConfigurationProperties) securityProps.get("keyStore"));
this.trustStore = new KeyStoreConfig((ConfigurationProperties) securityProps.get("trustStore"));
}

private ConfigurationProperties locateConfig(ConfigurationProperties config) {
if (config.containsKey("clientCertificateRequired")) {
private ConfigurationProperties locateConfig(ConfigurationProperties config, String transport) {
if (config.containsKey("keyStore") || config.containsKey("trustStore") || config.containsKey("clientCertificateRequired")) {
return config;
}
ConfigurationProperties security = (ConfigurationProperties) config.get("security");
if (security != null) {
security = (ConfigurationProperties) security.get("tls");
}

ConfigurationProperties endPoint = (ConfigurationProperties) config.get("endPoint");
if (endPoint != null) {
return locateConfig(endPoint);
ConfigurationProperties endPointSecurity = locateConfig(endPoint, transport);
if (endPointSecurity != null) {
return endPointSecurity;
}
}

return security;
ConfigurationProperties security = (ConfigurationProperties) config.get("security");
return security == null ? null : (ConfigurationProperties) security.get(transport);
}

public boolean update(BaseConfigDTO config) {
boolean hasChanged = false;
if (config instanceof SslConfigDTO) {
SslConfigDTO newConfig = (SslConfigDTO) config;
if (!(config instanceof SslConfigDTO newConfig)) {
return false;
}

if (this.clientCertificateRequired != newConfig.isClientCertificateRequired()) {
this.clientCertificateRequired = newConfig.isClientCertificateRequired();
hasChanged = true;
}
if (this.clientCertificateWanted != newConfig.isClientCertificateWanted()) {
this.clientCertificateWanted = newConfig.isClientCertificateWanted();
hasChanged = true;
}
if (!this.crlUrl.equals(newConfig.getCrlUrl())) {
this.crlUrl = newConfig.getCrlUrl();
hasChanged = true;
}
if (this.crlInterval != newConfig.getCrlInterval()) {
this.crlInterval = newConfig.getCrlInterval();
hasChanged = true;
}
if (((KeyStoreConfig)this.keyStore).update(newConfig.getKeyStore())) {
hasChanged = true;
}
if (((KeyStoreConfig)this.trustStore).update(newConfig.getTrustStore())) {
hasChanged = true;
}
boolean hasChanged = false;
if (this.clientCertificateRequired != newConfig.isClientCertificateRequired()) {
this.clientCertificateRequired = newConfig.isClientCertificateRequired();
hasChanged = true;
}
if (this.clientCertificateWanted != newConfig.isClientCertificateWanted()) {
this.clientCertificateWanted = newConfig.isClientCertificateWanted();
hasChanged = true;
}
if (this.hostnameVerificationEnabled != newConfig.isHostnameVerificationEnabled()) {
this.hostnameVerificationEnabled = newConfig.isHostnameVerificationEnabled();
hasChanged = true;
}
if (!Objects.equals(this.context, newConfig.getContext())) {
this.context = newConfig.getContext();
hasChanged = true;
}
if (!Objects.equals(this.crlUrl, newConfig.getCrlUrl())) {
this.crlUrl = newConfig.getCrlUrl();
hasChanged = true;
}
if (this.crlInterval != newConfig.getCrlInterval()) {
this.crlInterval = newConfig.getCrlInterval();
hasChanged = true;
}
if (newConfig.getKeyStore() != null && ((KeyStoreConfig) this.keyStore).update(newConfig.getKeyStore())) {
hasChanged = true;
}
if (newConfig.getTrustStore() != null && ((KeyStoreConfig) this.trustStore).update(newConfig.getTrustStore())) {
hasChanged = true;
}
return hasChanged;
}

public ConfigurationProperties toConfigurationProperties() {
ConfigurationProperties config = new ConfigurationProperties();
config.put("context", this.context);
config.put("clientCertificateRequired", this.clientCertificateRequired);
config.put("clientCertificateWanted", this.clientCertificateWanted);
config.put("hostnameVerificationEnabled", this.hostnameVerificationEnabled);
config.put("crlUrl", this.crlUrl);
config.put("crlInterval", this.crlInterval);
config.put("keyStore", ((KeyStoreConfig) keyStore).toConfigurationProperties());
Expand Down
18 changes: 10 additions & 8 deletions src/main/java/io/mapsmessaging/config/network/impl/DtlsConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -38,19 +38,20 @@ public class DtlsConfig extends DtlsConfigDTO implements Config {
public DtlsConfig(ConfigurationProperties config) {
setType("dtls");
NetworkConfigFactory.unpack(config, this);
sslConfig = new SslConfig(config);
sslConfig = new SslConfig(config, "dtls");
if(!sslConfig.getContext().toLowerCase().startsWith("dtls")){
sslConfig.setContext("DTLSv1.2");
}
}

public boolean update(BaseConfigDTO update) {
boolean hasChanged = false;
if (update instanceof DtlsConfigDTO) {
hasChanged = NetworkConfigFactory.update(this, (DtlsConfigDTO) update);
if(((SslConfig)sslConfig).update(update)){
hasChanged = true;
}
if (!(update instanceof DtlsConfigDTO newConfig)) {
return false;
}

boolean hasChanged = NetworkConfigFactory.update(this, newConfig);
if (newConfig.getSslConfig() != null && ((SslConfig) sslConfig).update(newConfig.getSslConfig())) {
hasChanged = true;
}
return hasChanged;
}
Expand All @@ -62,4 +63,5 @@ public ConfigurationProperties toConfigurationProperties() {
security.put("dtls", ((SslConfig)sslConfig).toConfigurationProperties());
config.put("security", security);
return config;
}}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,6 @@
import io.mapsmessaging.config.network.SslConfig;
import io.mapsmessaging.configuration.ConfigurationProperties;
import io.mapsmessaging.dto.rest.config.BaseConfigDTO;
import io.mapsmessaging.dto.rest.config.network.impl.TcpConfigDTO;
import io.mapsmessaging.dto.rest.config.network.impl.TlsConfigDTO;
import lombok.Data;
import lombok.EqualsAndHashCode;
Expand All @@ -38,21 +37,21 @@ public class TlsConfig extends TlsConfigDTO implements Config {

public TlsConfig(ConfigurationProperties config) {
NetworkConfigFactory.unpack(config, this);
sslConfig = new SslConfig(config);
sslConfig = new SslConfig(config, "tls");
if(sslConfig.getContext() == null || sslConfig.getContext().isEmpty()) {
sslConfig.setContext("TLSv1.3");
}
}

public boolean update(BaseConfigDTO update) {
boolean hasChanged = false;
if (update instanceof TcpConfigDTO) {
hasChanged = NetworkConfigFactory.update(this, (TcpConfigDTO) update);
if(((SslConfig)sslConfig).update(update)){
hasChanged = true;
}
if (!(update instanceof TlsConfigDTO newConfig)) {
return false;
}

boolean hasChanged = NetworkConfigFactory.update(this, newConfig);
if (newConfig.getSslConfig() != null && ((SslConfig) sslConfig).update(newConfig.getSslConfig())) {
hasChanged = true;
}
return hasChanged;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,15 @@ public class SslConfigDTO extends BaseConfigDTO {
)
protected boolean clientCertificateWanted = false;

@Schema(
description = "Whether outbound TLS connections verify the server certificate hostname.",
example = "true",
defaultValue = "true",
requiredMode = Schema.RequiredMode.REQUIRED,
nullable = false
)
protected boolean hostnameVerificationEnabled = true;

@Schema(
description = "URL for the Certificate Revocation List (CRL). " +
"If not set, CRL checking is disabled.",
Expand All @@ -72,10 +81,10 @@ public class SslConfigDTO extends BaseConfigDTO {
protected long crlInterval = 3600000L;

@Schema(
description = "SSL context identifier or protocol profile to use (for example: TLS, TLSv1.2, TLSv1.3).",
description = "SSL context identifier or protocol profile to use (for example: TLS, TLSv1.3, DTLS, DTLSv1.2).",
example = "TLS",
defaultValue = "TLS",
pattern = "^TLS(?:v1\\.(?:2|3))?$",
pattern = "^(?:TLS(?:v1\\.(?:2|3))?|DTLS(?:v1\\.(?:0|2))?)$",
requiredMode = Schema.RequiredMode.REQUIRED,
nullable = false
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@

import io.mapsmessaging.logging.Logger;
import io.mapsmessaging.logging.LoggerFactory;
import io.mapsmessaging.logging.ServerLogMessages;
import io.mapsmessaging.network.admin.EndPointJMX;
import io.mapsmessaging.network.admin.EndPointManagerJMX;
import io.mapsmessaging.network.io.*;
Expand All @@ -33,7 +34,10 @@
import java.net.SocketAddress;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.SelectionKey;
import java.security.Principal;
import java.util.concurrent.FutureTask;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLPeerUnverifiedException;

public class DTLSEndPoint extends EndPoint implements StateChangeListener, Timeoutable {

Expand Down Expand Up @@ -128,6 +132,24 @@ public void handshakeComplete() {
}
}

@Override
public Principal getEndPointPrincipal() {
SSLEngine sslEngine = stateEngine.getSslEngine();
if (sslEngine.getNeedClientAuth() || sslEngine.getWantClientAuth()) {
try {
return sslEngine.getSession().getPeerPrincipal();
} catch (SSLPeerUnverifiedException e) {
logger.log(ServerLogMessages.SSL_ENGINE_CLIENT_AUTH);
}
}
return null;
}

@Override
public boolean isSSL() {
return true;
}

@Override
public boolean isUDP() {
return true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,12 +90,7 @@ public boolean processPacket(@NonNull @NotNull Packet packet) throws IOException
if (state == null) {
StateEngine stateEngine;
SSLEngine sslEngine = SslHelper.createSSLEngine(sslContext, ((Config)((DtlsConfig)udpEndPoint.getConfig().getEndPointConfig()).getSslConfig()).toConfigurationProperties());
SSLParameters paras = sslEngine.getSSLParameters();
int mtu = 8192;
paras.setMaximumPacketSize(mtu);
paras.setEnableRetransmissions(true);
paras.setNeedClientAuth(false);
sslEngine.setSSLParameters(paras);
configureEngine(sslEngine);
stateEngine = new StateEngine(packet.getFromAddress(), sslEngine, this);
endPoint = new DTLSEndPoint(this, uniqueId.incrementAndGet(), packet.getFromAddress(), server, stateEngine, managerMBean);
sessionMapping.addState(packet.getFromAddress(), new UDPSessionState<>(endPoint));
Expand All @@ -112,6 +107,13 @@ public boolean processPacket(@NonNull @NotNull Packet packet) throws IOException
return true;
}

static void configureEngine(SSLEngine sslEngine) {
SSLParameters sslParameters = sslEngine.getSSLParameters();
sslParameters.setMaximumPacketSize(8192);
sslParameters.setEnableRetransmissions(true);
sslEngine.setSSLParameters(sslParameters);
}

public void close(SocketAddress clientId) {
UDPSessionState<DTLSEndPoint> state = sessionMapping.getState(clientId);
if (state != null && state.getContext() != null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,7 @@ private SSLEngineResult handleSSLEngineResult(SSLEngineResult result) throws IOE

@Override
public Principal getEndPointPrincipal() {
if (sslEngine.getNeedClientAuth()) {
if (sslEngine.getNeedClientAuth() || sslEngine.getWantClientAuth()) {
try {
return sslEngine.getSession().getPeerPrincipal();
} catch (SSLPeerUnverifiedException e) {
Expand Down
Loading
Loading