SSL peer failed hostname validation in Spring SAML - spring-boot

I'm trying to configure my Spring Boot project to use SAML authentication against a third party IDP. I've already achieved to make the configuration from vdenotaris work with the SSOCircle provider and now I want to switch it to the other provider.
The SAML metadata endpoint is HTTPS enabled and I've already created a keystore with both the certificate given in metadata (which is used for signing and encryption) and the one provided by the HTTP endpoint (based in this answer). Then, I save them in a cert file ($CERTIFICATE_FILE) and I use this script to generate my keystore:
keytool -delete -alias third-party -keystore $KEYSTORE_FILE -storepass $KEYSTORE_PASSWORD
keytool -import -alias third-party -file $CERTIFICATE_FILE -keystore $KEYSTORE_FILE -storepass $KEYSTORE_PASSWORD -noprompt
keytool -genkeypair -alias mycompany -keypass mycompanypass -keystore $KEYSTORE_FILE
Then, when using this keystore to retrieve the SAML metadata, I get this error:
org.opensaml.saml2.metadata.provider.MetadataProviderException: org.opensaml.saml2.metadata.provider.MetadataProviderException: Error retrieving metadata from https://third.party.provider/metadata
at org.opensaml.saml2.metadata.provider.HTTPMetadataProvider.fetchMetadata(HTTPMetadataProvider.java:274)
at org.opensaml.saml2.metadata.provider.AbstractReloadingMetadataProvider.refresh(AbstractReloadingMetadataProvider.java:267)
at org.opensaml.saml2.metadata.provider.AbstractReloadingMetadataProvider.doInitialization(AbstractReloadingMetadataProvider.java:236)
at org.opensaml.saml2.metadata.provider.AbstractMetadataProvider.initialize(AbstractMetadataProvider.java:407)
at org.springframework.security.saml.metadata.ExtendedMetadataDelegate.initialize(ExtendedMetadataDelegate.java:167)
at org.springframework.security.saml.metadata.MetadataManager.initializeProvider(MetadataManager.java:412)
at org.springframework.security.saml.metadata.MetadataManager.refreshMetadata(MetadataManager.java:238)
at org.springframework.security.saml.metadata.CachingMetadataManager.refreshMetadata(CachingMetadataManager.java:86)
at org.springframework.security.saml.metadata.MetadataManager$RefreshTask.run(MetadataManager.java:1040)
at java.util.TimerThread.mainLoop(Timer.java:555)
at java.util.TimerThread.run(Timer.java:505)
Caused by: org.opensaml.saml2.metadata.provider.MetadataProviderException: Error retrieving metadata from https://third.party.provider/metadata
at org.opensaml.saml2.metadata.provider.HTTPMetadataProvider.fetchMetadata(HTTPMetadataProvider.java:274)
at org.opensaml.saml2.metadata.provider.AbstractReloadingMetadataProvider.refresh(AbstractReloadingMetadataProvider.java:255)
... 9 common frames omitted
Caused by: javax.net.ssl.SSLPeerUnverifiedException: SSL peer failed hostname validation for name: null
at org.opensaml.ws.soap.client.http.TLSProtocolSocketFactory.verifyHostname(TLSProtocolSocketFactory.java:233)
at org.opensaml.ws.soap.client.http.TLSProtocolSocketFactory.createSocket(TLSProtocolSocketFactory.java:186)
at org.springframework.security.saml.trust.httpclient.TLSProtocolSocketFactory.createSocket(TLSProtocolSocketFactory.java:97)
at org.apache.commons.httpclient.HttpConnection.open(HttpConnection.java:707)
at org.apache.commons.httpclient.MultiThreadedHttpConnectionManager$HttpConnectionAdapter.open(MultiThreadedHttpConnectionManager.java:1361)
at org.apache.commons.httpclient.HttpMethodDirector.executeWithRetry(HttpMethodDirector.java:387)
at org.apache.commons.httpclient.HttpMethodDirector.executeMethod(HttpMethodDirector.java:171)
at org.apache.commons.httpclient.HttpClient.executeMethod(HttpClient.java:397)
at org.apache.commons.httpclient.HttpClient.executeMethod(HttpClient.java:323)
at org.opensaml.saml2.metadata.provider.HTTPMetadataProvider.fetchMetadata(HTTPMetadataProvider.java:250)
... 10 common frames omitted
These ones are the relevant parts of my configuration based in the linked project by vdenotaris:
// Setup TLS Socket Factory
#Bean
public TLSProtocolConfigurer tlsProtocolConfigurer() {
return new TLSProtocolConfigurer();
}
#Bean
public ProtocolSocketFactory socketFactory() {
return new TLSProtocolSocketFactory(keyManager(), null, "allowAll");
}
#Bean
public Protocol socketFactoryProtocol() {
return new Protocol("https", socketFactory(), 443);
}
#Bean
public MethodInvokingFactoryBean socketFactoryInitialization() {
MethodInvokingFactoryBean methodInvokingFactoryBean = new MethodInvokingFactoryBean();
methodInvokingFactoryBean.setTargetClass(Protocol.class);
methodInvokingFactoryBean.setTargetMethod("registerProtocol");
Object[] args = { "https", socketFactoryProtocol() };
methodInvokingFactoryBean.setArguments(args);
return methodInvokingFactoryBean;
}
// Central storage of cryptographic keys
#Bean
public KeyManager keyManager() {
DefaultResourceLoader loader = new DefaultResourceLoader();
Resource storeFile = loader.getResource("classpath:/saml/mySamlKeystore.jks");
String storePass = "storepass";
Map<String, String> passwords = new HashMap<String, String>();
passwords.put("mycompany", "mycompanypass");
String defaultKey = "mycompany";
return new JKSKeyManager(storeFile, storePass, passwords, defaultKey);
}
However, here I found some misconceptions. As far as I know, the TLSProtocolConfigurer creates a TLSProtocolSocketFactory itself, why is the sample project creating both beans then? According to the docs using TLSProtocolConfigurer should be enough, but how to create socketFactoryProtocol()?
I would be grateful to have some light in here.

Here is my JKS key-manager bean configuration.
#Bean
public ProtocolSocketFactory socketFactory() {
return new TLSProtocolSocketFactory(keyManager(), null, "default");
}
#Bean
public KeyManager keyManager() {
DefaultResourceLoader loader = new DefaultResourceLoader();
Resource storeFile = loader.getResource("classpath:/saml/samlKeystore.jks");
String storePass = keyStorePwd;
Map<String, String> passwords = new HashMap<String, String>();
passwords.put(keyStoreAlias, keyStorePwd);
String defaultKey = keyStoreAlias;
return new JKSKeyManager(storeFile, storePass, passwords, defaultKey);
}
Solution:
Looks like the IDP has changed their Public certificate which is not available in our local keystore (samlKeystore.jks). I manually downloaded their public certificate using OpenSSL command and imported the same using keytool utility.
Get the public certificate using OpenSSL command:
openssl s_client -showcerts -connect iam-sso.google.net:443 </dev/null 2>/dev/null|openssl x509 -outform PEM >mycertfile.pem
Import it into the Keystore:
keytool -import -alias "new-public-cert" -keystore /usr/share/tomcat8/webapps/ROOT/WEB-INF/classes/saml/samlKeystore.jks

I wasn't providing the password for the imported cert file:
#Bean
public KeyManager keyManager() {
DefaultResourceLoader loader = new DefaultResourceLoader();
Resource storeFile = loader.getResource("classpath:/saml/mySamlKeystore.jks");
String storePass = "storepass";
Map<String, String> passwords = new HashMap<String, String>();
passwords.put("mycompany", "mycompanypass");
passwords.put("third-party", "mycompanypass");
String defaultKey = "mycompany";
return new JKSKeyManager(storeFile, storePass, passwords, defaultKey);
}
With this, the application can read the certs from the keystore and trust them, without the need of installing them in the JDK cacerts.

#Bean
#Qualifier("idp-ssocircle")
public ExtendedMetadataDelegate ssoCircleExtendedMetadataProvider()
throws MetadataProviderException {
String idpSSOCircleMetadataURL = "https://idp.ssocircle.com/idp-meta.xml";
HTTPMetadataProvider httpMetadataProvider = new HTTPMetadataProvider(
this.backgroundTaskTimer, httpClient(), idpSSOCircleMetadataURL);
httpMetadataProvider.setParserPool(parserPool());
ExtendedMetadataDelegate extendedMetadataDelegate =
new ExtendedMetadataDelegate(httpMetadataProvider, extendedMetadata());
extendedMetadataDelegate.setMetadataTrustCheck(false);
extendedMetadataDelegate.setMetadataRequireSignature(false);
backgroundTaskTimer.purge();
return extendedMetadataDelegate;
}
extendedMetadataDelegate.setMetadataTrustCheck(false);

Just remove TLSProtocolConfigurer bean and set metadata trust check to false

Related

ECDSA related ciphers not working with Spring Cloud Gateway and OpenSSL

I am currently trying to make ECDSA related ciphers to work with TLS 1.2 in Spring Cloud Gateway (Spring Boot Parent 2.6.7 and Spring Cloud 2021.0.2). Here's the snippet of WebServerFactoryCustomizer
#Bean
public WebServerFactoryCustomizer<NettyReactiveWebServerFactory> customizer() {
return factory -> factory.addServerCustomizers(httpServer -> httpServer.secure(sslContextSpec -> {
try {
Ssl ssl = factory.getSsl();
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
char[] keyStorePassword = ssl.getKeyStorePassword().toCharArray();
keyStore.load(resourceLoader.getResource(ssl.getKeyStore()).getInputStream(), keyStorePassword);
KeyManagerFactory keyManagerFactory = OpenSslCachingX509KeyManagerFactory
.getInstance(KeyManagerFactory.getDefaultAlgorithm());
keyManagerFactory.init(keyStore, keyStorePassword);
Http11SslContextSpec http11SslContextSpec = Http11SslContextSpec.forServer(keyManagerFactory)
.configure(sslContextBuilder -> {
sslContextBuilder.sslProvider(SslProvider.OPENSSL);
sslContextBuilder.ciphers(Arrays.asList(ssl.getCiphers()));
sslContextBuilder.protocols(ssl.getEnabledProtocols());
sslContextBuilder.trustManager(InsecureTrustManagerFactory.INSTANCE);
sslContextBuilder.clientAuth(ClientAuth.REQUIRE);
});
sslContextSpec.sslContext(http11SslContextSpec)
.handlerConfigurator(sslHandler -> {
sslHandler.setCloseNotifyReadTimeout(18000, TimeUnit.MILLISECONDS);
sslHandler.setHandshakeTimeout(19000, TimeUnit.MILLISECONDS);
SSLParameters sslParameters = sslHandler.engine().getSSLParameters();
sslParameters.setUseCipherSuitesOrder(false);
sslHandler.engine().setSSLParameters(sslParameters);
});
} catch (UnrecoverableKeyException | IOException | CertificateException | KeyStoreException |
NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}));
}
When I try to connect using openssl s_client with ECDHE-ECDSA-AES128-GCM-SHA256 cipher the server returns an error with no shared ciphers, but I do have it in the configuration as
server.ssl.ciphers=TLS_RSA_WITH_AES_128_GCM_SHA256,\
TLS_RSA_WITH_AES_256_GCM_SHA384, \
TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,\
TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,\
TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
server.ssl.enabled-protocols=TLSv1.2
This behavior was observed when I upgraded versions from Spring Boot 2.3.3.RELEASE and Spring Cloud Hoxton.SR7. Any advice/suggestions would be of great help on fixing or correctly configuring it.

JwtAccessTokenConverter: Unable to create an RSA verifier from verifierKey

I have an authorization server Spring Boot project using Spring Cloud OAuth2. I'm using these beans for JWT:
#Bean
public JwtAccessTokenConverter accessTokenConverter() {
JwtAccessTokenConverter tokenConverter = new JwtAccessTokenConverter();
tokenConverter.setSigningKey("my-test-jwt-secret");
return tokenConverter;
}
#Bean
public JwtTokenStore tokenStore() {
return new JwtTokenStore(accessTokenConverter());
}
Login apparently is working fine, but when I run the project, I'm getting this warning:
WARN 16804 --- [main] o.s.s.o.p.t.s.JwtAccessTokenConverter : Unable to create an RSA verifier from verifierKey (ignoreable if using MAC)
How do I get rid of this warning?
A JwtAccessTokenConverter can be configured to use either a MAC key or a RSA key pair for signature generation and verification.
As the message provided in the warning states, you are probably using a MAC key and not a RSA key pair. As a consequence, it probably will not suppose a problem, but I am afraid that you cannot get rid of the warning due to the way in which the library is implemented.
As you can see in the source code of JwtAccessTokenConverter, the warning is issued when trying creating a RsaVerifier for signature verification:
SignatureVerifier verifier = new MacSigner(verifierKey);
try {
verifier = new RsaVerifier(verifierKey);
}
catch (Exception e) {
logger.warn("Unable to create an RSA verifier from verifierKey (ignoreable if using MAC)");
}
The exception is raised in the RsaVerifier constructor because it is trying parsing the verification key as a RSA public key, when probably you are using a MAC key instead:
public RsaVerifier(String key) {
this(RsaKeyHelper.parsePublicKey(key.trim()), RsaSigner.DEFAULT_ALGORITHM);
}
Here, RsaKeyHelper will unsuccessfully try parsing the provided key as neither a ssh nor pem key, because it actually is not that type of key.
The value of this verification key is assumed to be the same provided as signing key as argument of the setSigningKey method for MAC keys.
If you are actually working with RSA keys you can use the setVerifierKey or setKeyPair methods to provide the cryptographic RSA material.

Spring boot ssl how to trust all valid certificates

I have enabled ssl in my spring boot application and anytime I want to use a Rest service like googleapis or facebook I have to add the certificate in the truststore.
I use openssl to get the certificate:
openssl s_client -connect googleapis.com:443
and I import it into the truststore :
keytool.exe -import -noprompt -trustcacerts -alias googleapis.com
-file googleapis.com.cer -keystore app-server.p12 -storepass *****
The problem is that it's very inconvenient to manage, when the certificate expires I have to update the certificates in the truststore everytime.
Also even if I add the right certificate sometimes I get an error:
Caused by: javax.net.ssl.SSLHandshakeException:
sun.security.validator.ValidatorException: PKIX path building failed:
sun.security.provider.certpath.SunCertPathBuilderException: unable to
find valid certification path to requested target
I know these certificates are valid so, is there a possibility to trust them automatically without adding them in the truststore?
Here is my ssl configuration
ssl:
enabled: true
key-store: classpath:keystore/app-server.p12
key-store-password: ******
key-alias: app-server
key-store-type: PKCS12
trust-store: classpath:keystore/app-server.p12
trust-store-password: *****
trust-store-type: PKCS12
keyStorePath: config/keystore/app-server.p12
Also I had to add system properties in the main method
private static String keyStorePath;
private static String keyStorePassword;
#Value("${server.ssl.keyStorePath}")
public void setKeyStorePath(String keyStorePath) {
ClientUiApplication.keyStorePath = keyStorePath;
}
#Value("${server.ssl.key-store-password}")
public void setKeyStorePassword(String keyStorePassword) {
ClientUiApplication.keyStorePassword = keyStorePassword;
}
public static void main(String[] args) {
SpringApplication.run(ClientUiApplication.class, args);
System.setProperty("javax.net.ssl.trustStore", keyStorePath);
System.setProperty("javax.net.ssl.trustStorePassword", keyStorePassword);
}
Another important detail, I need a truststore because I use self-signed certificates generated with Keytool. Without, my services cannot communicate with each other.
You give the code:
public static void main(String[] args) {
SpringApplication.run(ClientUiApplication.class, args);
System.setProperty("javax.net.ssl.trustStore", keyStorePath);
System.setProperty("javax.net.ssl.trustStorePassword", keyStorePassword);
}
This means that you force the java trust store to be the trust store you provide. The default trust store is not in use anymore.
So yes, you have to add every needed root certificates in this trust store to not have the issue you describe.
Why do you need to have a specific trust store?
If there is no use, remove that.
If you have a specific additional certificate you have to trust, you'd better add this certificate to the default trust store (the jre/lib/security/cacerts file)
You can also Bypass SSL Certificate Checking using CloseableHttpClient
public static CloseableHttpClient getCloseableHttpClient()
{
CloseableHttpClient httpClient = null;
try {
httpClient = HttpClients.custom().
setSSLHostnameVerifier(NoopHostnameVerifier.INSTANCE).
setSSLContext(new SSLContextBuilder().loadTrustMaterial(null, new TrustStrategy()
{
public boolean isTrusted(X509Certificate[] arg0, String arg1) throws CertificateException
{
return true;
}
}).build()).build();
} catch (KeyManagementException e) {
LOGGER.error("KeyManagementException in creating http client instance", e);
} catch (NoSuchAlgorithmException e) {
LOGGER.error("NoSuchAlgorithmException in creating http client instance", e);
} catch (KeyStoreException e) {
LOGGER.error("KeyStoreException in creating http client instance", e);
}
return httpClient;
}
It will trust automatically all the certificates and saves you from SSLHandshakeException: sun.security.validator.ValidatorException
Note this method is not recommended in Production

How to use p12 client certificate with spring feign client

I have a Spring Boot application that calls a remote service.
This remote web service provided me a p12 file that should authenticate my application.
How do I configure my feign client to use the p12 certificate ?
I've tried settings these properties:
-Djavax.net.ssl.keyStore=path_to_cert.p12 -Djavax.net.ssl.keyStorePassword=xxx -Djavax.net.ssl.keyStoreType=PKCS12
But it doesn't change anything, I still get this error:
sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
I could finally manage to do it with a lot of blind trial and error.
The problem is, by default, the feign builder builds feign clients with null SSLSocketFactory:
org.springframework.cloud.openfeign.FeignClientsConfiguration#feignBuilder:
#Bean
#Scope("prototype")
#ConditionalOnMissingBean
public Feign.Builder feignBuilder(Retryer retryer) {
return Feign.builder().retryer(retryer);
}
feign.Feign.Builder:
public static class Builder {
// ...
private Client client = new Client.Default(null, null);
So, I had to define this bean in a #Configuration:
#Bean
#Profile({"prod", "docker"})
public Feign.Builder feignBuilder() {
return Feign.builder()
.retryer(Retryer.NEVER_RETRY)
.client(new Client.Default(getSSLSocketFactory(), null));
with this method: (can't remember source)
SSLSocketFactory getSSLSocketFactory() {
char[] allPassword = keyStorePassword.toCharArray();
SSLContext sslContext = null;
try {
sslContext = SSLContextBuilder
.create()
.setKeyStoreType(keyStoreType)
.loadKeyMaterial(ResourceUtils.getFile(keyStore), allPassword, allPassword)
.build();
} catch (Exception e) { /* *** */ }
return sslContext.getSocketFactory();
}
Now, it works for me, I debugged though the feign client calls and the sslSocketFactory is correctly passed to the underlying connection.
In case you wish to achieve the above effect programmatically without using keytool, you can do the following:
class CustomFeignConfiguration {
private val log = Logger.getLogger(this.javaClass.name)
#Value("\${client_p12_base64_encoded_string}")
private val clientP12: String = ""
#Value("\${client_p12_password}")
private val clientP12Pass: String = ""
#Bean
fun feignClient(): Client {
val sslSocketFactory= getSSLSocketFactory()
log.info("CUSTOM FEIGN CLIENT CALLED")
return Client.Default(sslSocketFactory, DefaultHostnameVerifier())
}
private fun getSSLSocketFactory(): SSLSocketFactory {
val decoder = java.util.Base64.getDecoder()
val p12 = decoder.decode(clientP12)
val p12File = File("clientCer.p12")
p12File.writeBytes(p12)
try {
val sslContext = SSLContexts
.custom()
.loadKeyMaterial(p12File, clientP12Pass.toCharArray(), clientP12Pass.toCharArray())
.build()
return sslContext.socketFactory
} catch (exception: Exception) {
throw RuntimeException(exception)
}
}
}
The FeignClient interface that is using the configuration has to load this specifically
#FeignClient(name = "client", configuration = [CustomFeignConfiguration::class], url = "\${url}")
interface Client {
....
....
}
The SSLContexts library can only use p12 certificates and we have to convert the certificates and keys in PEM format to the P12 format.
Create a p12 certificate from your PEM certificate and key using the following SSL command:
openssl pkcs12 -export -inkey domain.key -in domain.crt -out domain.p12
Please record the password that you enter after you run this command.
Convert this p12 certificate to a base64 string using the following command
base64 domain.p12 > domain.p12.base64
Convert this multiline string to a single line string using the following command:
tr -d "\n\r" < domain.p12.base64 > domain.p12.base64.singleline
Use the single line string from this command and the password that you recorded earlier in your application.properties.

Spring Boot Localhost https issue

I wanted to make my localhost from hypertext transfer protocol to the secured socket layer hypertext transfer protocol. So I have added a key store to my spring boot project, and configured my application.properties like this:
INFORMATION
But I get an error in chrome that:
localhost uses an unsupported protocol.
ERR__VERSION_OR_CIPHER_MISMATCH
Note: I have done no changes to any of the java files.
When I faced this issue I solved it by a bean like below. hope this helps you
#Configuration
class CustomConfiguration {
#Bean
public EmbeddedServletContainerFactory servletContainer() {
final int port = 8443;
final String keystoreFile = "/path/to/keystore"
final String keystorePass = "keystore-password"
final String keystoreType = "pkcs12"
final String keystoreProvider = "SunJSSE"
final String keystoreAlias = "tomcat"
TomcatEmbeddedServletContainerFactory factory =
new TomcatEmbeddedServletContainerFactory(this.port);
factory.addConnectorCustomizers( new TomcatConnectorCustomizer() {
void customize(Connector con) {
Http11NioProtocol proto = (Http11NioProtocol) con.getProtocolHandler();
proto.setSSLEnabled(true);
con.setScheme("https");
con.setSecure(true);
proto.setKeystoreFile(keystoreFile);
proto.setKeystorePass(keystorePass);
proto.setKeystoreType(keystoreType);
proto.setProperty("keystoreProvider", keystoreProvider);
proto.setKeyAlias(keystoreAlias);
}
});
return factory;
}
}
Also make sure you call keytool with -storetype pkcs12, not -storepass pkcs12

Resources