Invalid Certification with Amazon S3 Presigned URL - spring

I am trying to download an object from an S3 bucket using a presigned url via the following configuration & code:
public void getDocumentFromPresignedUrl(final String presignedUrl, final String id) {
PresignedUrlDownload transfer = null;
try {
File file = File.createTempFile(id, ".pdf");
//errors out on this line
transfer = getTransferMgr().download(new PresignedUrlDownloadRequest(new URL(presignedUrl)), file);
transfer.waitForCompletion();
}
}
Which is configured via the following:
private ClientConfiguration getClientConfiguration() {
ClientConfiguration clientConfig = new ClientConfiguration();
clientConfig.setProtocol(Protocol.HTTPS);
return clientConfig;
}
public TransferManager getTransferMgr() {
return TransferManagerBuilder.standard().withS3Client(getS3Client()).build();
}
public AmazonS3 getS3Client() {
return AmazonS3ClientBuilder.standard().withRegion(region)
.withClientConfiguration(getClientConfiguration()).build();
}
However, the following error is thrown each time:
com.amazonaws.SdkClientException:
Unable to execute HTTP request: PKIX path building failed:
sun.security.provider.certpath.SunCertPathBuilderException:
unable to find valid certification path to requested target
WHAT I HAVE TRIED:
I tried to take the AWS cert from the presigned url location in-browser, discussed here
I tried to use the traditional RestTemplate provided by Spring, with no luck
I AM able to retrieve the object from S3 both in Postman and my browser, but not via my Spring app
How does one circumvent this sdkClientException and GET their S3 object?

The solution to this problem resides in cacerts. If you are running on a proxy (such as Zscaler), the certification will need to be added to the /cacerts file.
In the case of this question, I was adding the Zscaler cert to the WRONG JRE. Because I was using SpringToolSuite, I needed to add the cert to Spring's JRE, which in my case was:
keytool -import -alias Z_Root -keystore "C:\Program Files\sts-4.8.1.RELEASE\plugins\org.eclipse.justj.openjdk.hotspot.jre.full.win32.x86_64_1.v20201010-1246\jre\lib\security\cacerts" -storepass changeit -file "C:\Users\myUser\Downloads\MyZscalerCert.crt"
and NOT the typical %JAVA_HOME%/jre path.

Related

Get certificates from Azure KeyVault to Keystore in Kotlin/SpringBoot for outgoing requests

For some API requests I need in my backend a certificate. Currently the .p12 is versioned in the repository and loaded into the WebClient when its initialized like this:
private fun getWebClient(): WebClient {
val ks: KeyStore = KeyStore.getInstance("PKCS12")
ks.load(ClassPathResource("keystore.p12").inputStream, config.trustStorePassword.toCharArray())
val kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm())
kmf.init(ks, config.trustStorePassword.toCharArray())
val sslContext = SslContextBuilder
.forClient()
.keyManager(kmf)
.build()
val httpClient: HttpClient = HttpClient.create().secure { sslSpec -> sslSpec.sslContext(sslContext) }
return WebClient
.builder()
.baseUrl(config.BaseUrl)
.clientConnector(ReactorClientHttpConnector(httpClient))
.build()
}
I want to change it since the backend is deployed to Azure AppService. I already created a KeyVault and imported the certificate and granted access via a managed identity to the AppService.
I currently struggle to load the keystore in Spring Boot from the KeyVault. For reference I am trying to follow https://learn.microsoft.com/en-us/azure/developer/java/spring-framework/configure-spring-boot-starter-java-app-with-azure-key-vault-certificates.
It uses the azure.keyvault.uri property which is apparently deprecated, so I am using spring.cloud.azure.keyvault.certificate.endpoint.
Also documentation states:
KeyStore azureKeyVaultKeyStore = KeyStore.getInstance("AzureKeyVault");
KeyVaultLoadStoreParameter parameter = new KeyVaultLoadStoreParameter(
System.getProperty("azure.keyvault.uri"));
azureKeyVaultKeyStore.load(parameter);
SSLContext sslContext = SSLContexts.custom()
.loadTrustMaterial(azureKeyVaultKeyStore, null)
.build();
However, I am not able to resolve the class KeyVautLoadStoreParameter.
I am using spring boot 2.7.7 and
implementation("com.azure.spring:spring-cloud-azure-starter:4.5.0")
implementation("com.azure.spring:spring-cloud-azure-starter-keyvault:4.5.0")
Any help towards loading the certificates and configuring the WebClient would be greatly appreciated.
What you can do is create an app registration in azure AD and then grant access to this app registration through access policy in the azure keyvault.
Now collect the clientId , Tenant Id and client Secret from the app registration
Create the secret in certificates&secret tab and click on new registration.
Now create the application.yaml file in the following format
server:
ssl:
key-alias: TestCertificate
key-store-type: AzureKeyVault
trust-store-type: AzureKeyVault
port: 8443
azure:
keyvault:
uri: <KEYVAULT URL>
client-id: <CLIENT_ID FROM APP REGISTRATIOM>
client-secret: <CLIENT_SECRET FROM APP REGISTRATIOM>
enabled: true
tenant-id: <TENANT_ID FROM APP REGISTRATIOM>
Now you can import all the credentials from the azure key vault using the following program.
KeyStore azureKeyVaultKeyStore = KeyStore.getInstance("AzureKeyVault");
KeyVaultLoadStoreParameter parameter = new KeyVaultLoadStoreParameter(
System.getProperty("azure.keyvault.uri"),
System.getProperty("azure.keyvault.tenant-id"),
System.getProperty("azure.keyvault.client-id"),
System.getProperty("azure.keyvault.client-secret")
);
azureKeyVaultKeyStore.load(parameter);
SSLContext sslContext = SSLContexts.custom()
.loadTrustMaterial(azureKeyVaultKeyStore, null)
.build();
// Here I am cross check wether we have imported the certificate name " TestCertificate"
if(azureKeyVaultKeyStore.containsAlias("TestCertificate"))
{
System.out.println("The Certificate Exists ");
}
else {
System.out.println("Error Has Occured");
}
Output:

Achieve external api calls using Spring WebClient and .p12 cert

I have .p12 certificate inside my project placed into recources directory. All I want to do is to use this file to make external api calls. So I have read some info about how to achieve this:
private WebClient getWebClient() {
HttpClient httpClient = HttpClient.create();
httpClient.secure(spec -> {
KeyStore keyStore = KeyStore.getInstance("PKCS12");
keyStore.load(new FileInputStream(ResourceUtils.getFile(keyStorePath)), keyStorePass.toCharArray());
// Set up key manager factory to use key-store
KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
keyManagerFactory.init(keyStore, keyStorePass.toCharArray());
spec.sslContext(SslContextBuilder.forClient()
.keyManager(keyManagerFactory)
.build());
});
return WebClient
.builder()
.clientConnector(new ReactorClientHttpConnector(httpClient))
.build();
}
After the external api call I get:
unable to find valid certification path to requested target; nested exception is javax.net.ssl.SSLHandshakeException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
Thank you guys in advance for any solutions.
I propose to load p12 file via Resource loader API as an alternative approach.

Retrieve private key from Azure Key Vault in Spring Boot app

I have a .pfx certificate in Azure Key Vault. I need to retrieve the private key from this to decrypt a string value in my Spring Boot application.
I have used the azure-spring-boot-starter-keyvault-certificates library to load the certificate to java key store, this seems to be working ok.
What I don't understand is how to retrieve the private key part. Any clues to what I am doing wrong?
KeyStore azureKeyVaultKeyStore = KeyStore.getInstance("AzureKeyVault");
KeyVaultLoadStoreParameter parameter = new KeyVaultLoadStoreParameter(
System.getProperty("azure.keyvault.uri"),
System.getProperty("azure.keyvault.tenant-id"),
System.getProperty("azure.keyvault.client-id"),
System.getProperty("azure.keyvault.client-secret"));
azureKeyVaultKeyStore.load(parameter);
// returns null!
PrivateKey privateKey = (PrivateKey) azureKeyVaultKeyStore.getKey(environment.getProperty("azure.keyvault.alias"), "".toCharArray());
// decrypt value
Cipher c = Cipher.getInstance(privateKey.getAlgorithm());
c.init(Cipher.DECRYPT_MODE, privateKey);
c.update(DatatypeConverter.parseBase64Binary(cryptedMsg));
String decryptedMessage = new String(c.doFinal());
Testing with the same certificate on my machine works doing like this:
KeyStore keyStore = KeyStore.getInstance("pkcs12");
keyStore.load(new FileInputStream(filename), password.toCharArray());
PrivateKey privateKey = (PrivateKey) keyStore.getKey(alias, password.toCharArray());
The private key can be retrieved using the GetSecret() method, otherwise you only get the public part.
See this article for details (Even tough its for .NET, I hope you can figure out how to do it in Java) or see the Java samples here

Spring JAVA SSL - No name matching localhost found

I have two SSL end-points that share the same application.properties and key store file.
I want to have one end-point call the other, but getting an error No name matching localhost found
How can I adjust this to allow one microservice to call the other(s) as intended below?
I have played with the following to attempt a solution to no avail:
javax.net.ssl.HostnameVerifier()
Created a localhost certificate and added it to the keystore
#CrossOrigin(origins = "*", maxAge = 3600)
#RestController
public class submitapplicationcontroller {
#Bean
public WebClient.Builder getWebClientBuilder(){
return WebClient.builder();
}
#Autowired private WebClient.Builder webClientBuilder;
#PostMapping("/submitapplication")
public String submitapplication() {
/*** Returns Error Found Below ***/
String response = webClientBuilder.build()
.post()
.uri("https://localhost:8080/validateaddress")
.retrieve()
.bodyToMono(String.class)
.block();
return response;
}
}
javax.net.ssl.SSLHandshakeException: No name matching localhost found
at java.base/sun.security.ssl.Alert.createSSLException
Error has been observed at the following site(s):
|_ checkpoint ⇢ Request to POST https://localhost:8080/v1/validateaddress
#CrossOrigin(origins = "*", maxAge = 3600)
#RestController
public class validateaddresscontroller {
#PostMapping("/validateaddress")
public String validateaddress() {
return "response";
}
}
server.ssl.key-alias=server
server.ssl.key-password=asensitivesecret
server.ssl.key-store=classpath:server.jks
server.ssl.key-store-provider=SUN
server.ssl.key-store-type=JKS
server.ssl.key-store-password=asensitivesecret
The problem here was the way I went about creating and implementing the certificates. I had 2 separate keystores and certificates; one named "server", and one named "localhost". I added the localhost certificate to the server keystore, and applied the server keystore and certificate to the springboot application / application.properties.
What you have to do is create just one certificate and keystore dubbed "localhost" and you have to use that to apply to the application / application.properties.
What you should have after creating the localhost JKS and certificate
server.ssl.key-alias=localhost
server.ssl.key-password=asensitivesecret
server.ssl.key-store=classpath:localhost.jks
server.ssl.key-store-provider=SUN
server.ssl.key-store-type=JKS
server.ssl.key-store-password=asensitivesecret
Note: I don't believe you actually have to create a JKS named "localhost", just the certificate. I just did for testing purposes.

How to get the certificate into the X509 filter (Spring Security)?

I need to extract more information than just the CN of the certificate. Currently, I only get the standard UserDetails loadUserByUsername(String arg) where arg is the CN of the certificate. I need to get the X509Certificate object. Is it possible?
on spring security xml file :
<x509 subject-principal-regex="CN=(.*?)," user-service-ref="myUserDetailsService" />
No you can't get it that way. You need to grab it from the HttpServletRequest:
X509Certificate[] certs = (X509Certificate[])HttpServletRequest.getAttribute("javax.servlet.request.X509Certificate");
It is also worth noting that once you are authorized by the in-built X509AuthenticationFilter of Spring Security as it has accepted your certificate, then you can access the X509Certificate as
Object object = SecurityContextHolder.getContext().getAuthentication().getCredentials();
if (object instanceof X509Certificate)
{
X509Certificate x509Certificate = (X509Certificate) object;
//convert to bouncycastle if you want
X509CertificateHolder x509CertificateHolder =
new X509CertificateHolder(x509Certificate.getEncoded());
...

Resources