Retrieve private key from Azure Key Vault in Spring Boot app - spring-boot

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

Related

Unable to get access_token, Refresh token using client_assertion in Spring oauth2 Server

generated client assertion like below using PKCS12, still I get invalid client error. Need your help to resolve this issue. Looks like authorization server is not compatible with certificate generated RSAkey. I am badly stuck here. base branch used to create project is https://github.com/jgrandja/spring-authorization-server/tree/jwt-client-authn/samples/default-authorizationserver
InputStream fm = new FileInputStream(pathPKCS12);
KeyStore keystore = KeyStore.getInstance("PKCS12");
keystore.load(fm, pwdPKCS12.toCharArray());
Key key = keystore.getKey(keystore.aliases().nextElement(), pwdPKCS12.toCharArray());
Certificate cert = keystore.getCertificate(keystore.aliases().nextElement());
PublicKey publicKey = cert.getPublicKey();
KeyPair keyPair = new KeyPair(publicKey, (PrivateKey) key);
RSAPublicKey rsaPublicKey = (RSAPublicKey) keyPair.getPublic();
RSAPrivateKey rsaPrivateKey = (RSAPrivateKey) keyPair.getPrivate();
Instant now = Instant.now();
//The JWT signature algorithm we will be using to sign the token
//SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.RS256;
String jwt= Jwts.builder()
.setAudience("https://localhost:9000")
.setIssuedAt(Date.from(now))
.setExpiration(Date.from(now.plus(5L, ChronoUnit.DAYS)))
.setIssuer("profinch")
.setSubject("profinch")
.setId(UUID.randomUUID().toString())
.signWith(rsaPrivateKey)
.compact();
System.out.println(jwt);
error is below -
postman printscreen where error is displayed
post man request x-www-form-URL encoded is -
client_assertion_type:urn:ietf:params:oauth:client-assertion-type:jwt-bearer
client_assertion:eyJraWQiOiJKWHYwMXhuNW83eDFuTXB1SVV1Q1gyNDdPbnhMMGdMeGV2VUFnLW0xbjdvIiwidHlwIjoiSldUIiwiYWxnIjoiUlMyNTYifQ.eyJhdWQiOiJodHRwczovL2xvY2FsaG9zdDo5MDAwL29hdXRoMi90b2tlbiIsImlhdCI6MTY3NTY3NDU4NywiZXhwIjoxNjc2MTA2NTg3LCJpc3MiOiJwcm9maW5jaCIsInN1YiI6InByb2ZpbmNoIiwianRpIjoiYTFiYzdiZTktZGY3Zi00NjhkLTkwMzktNTZjOTE4NmMwNDkwIn0.LjyF122kDSTlkUzVomOpnqwpHxNEdeq73fVx4Zzcu1dEmChWpk2LB1NbDhOPTIDNpGPF4aB8RkHkZfTmbA9TFYrm20MdHYFt5fRliBILUdBuNjRok3EEmNmWnjFTyfwoufMGT-sX6orLWkNUmsOkaywGIKCgHgR-MfEpTZ_v0cNL3uSuWnDQAy6yZgm9kWt_jNZdPGouBFHGsoZxhr2XAFrwhlQVq0VDOTELX7ylDcqOR5phHxAfubx_mW79ns1Que3jO-o_mvYtXTamoOq_DiJD-xpRnAQEq36czoSHvPYVrjlLhCloBtuedzD0gODmypVc_0Dq2dPp4-y9pCiV-g
Auth-server client registration as below-
RegisteredClient registeredClient2 = RegisteredClient.withId(UUID.randomUUID().toString()) .clientId("profinch") .clientAuthenticationMethod(ClientAuthenticationMethod.PRIVATE_KEY_JWT) .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN) .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS) .redirectUri(clientModel.getRedirectUri()) .scope(OidcScopes.OPENID).scope(OidcScopes.PROFILE).scope("message.read").scope("message.write") .clientSettings(ClientSettings.builder() .requireAuthorizationConsent(false) .jwkSetUrl("http://localhost:8080/jwks") .tokenEndpointAuthenticationSigningAlgorithm(SignatureAlgorithm.RS256) .build()) .build();
Should be able to get acces_token, refresh_oken using client_assertion in postman as per printscreen shared.

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:

Changing a Spring oauth2ResourceServer JWT ex. to use a single secret instead of a Keypair throws an exception “Failed to select a JWK signing key”

I'm trying to implement a Spring Boot Rest backend with JWT-security, based on Springs new authorization server and this example: https://github.com/spring-projects/spring-security-samples/tree/main/servlet/spring-boot/java/jwt/login
It uses Asymmetric keys to sign and verify tokens which seems like an overkill, since both authentication (where the token is generated) and authorization (verified) happens on the same server. So, to simplify deployment (just pass in a single secret via an environment variable), I have been trying to rewrite it to use a single shared secret.
The example code implements two Bean-components, one to create the JwtEncoder (using a private RSA key) and one for the JWTDecoder (using the matching public key).
I have rewritten the Decoder as explained in chapter 15 in the book “Spring Security in Action” so I assume this should work, since the NimbusJwtDecoder offers a withSecretKey method.
//Will eventually come via an environment variable
static byte[] secret = "j8IoV1jF67".getBytes();
#Bean
JwtDecoder jwtDecoder() {
// return NimbusJwtDecoder.withPublicKey(this.key).build();
SecretKey theKey = new SecretKeySpec(secret, 0, secret.length, "AES");
return NimbusJwtDecoder.withSecretKey(theKey).build();
}
I have implemented the Encoder, which is coursing the problem, like so (code commented out, is the original code using the private RSA Key:
#Bean
JwtEncoder jwtEncoder() {
// JWK jwk = new RSAKey.Builder(this.key).privateKey(this.priv).build();
// JWKSource<SecurityContext> jwks = new ImmutableJWKSet<>(new JWKSet(jwk));
// return new NimbusJwtEncoder(jwks);
SecretKey originalKey = new SecretKeySpec(secret, 0, secret.length, "AES");
JWKSource<SecurityContext> immutableSecret = new ImmutableSecret<SecurityContext>(originalKey);
return new NimbusJwtEncoder(immutableSecret);
}
When I login (via the POST /token endpoint) the line that uses the encoder:
return this.encoder.encode(JwtEncoderParameters.from(claims)).getTokenValue();
Throws this exception
org.springframework.security.oauth2.jwt.JwtEncodingException: An error occurred while attempting to encode the Jwt: Failed to select a JWK signing key
at org.springframework.security.oauth2.jwt.NimbusJwtEncoder.selectJwk(NimbusJwtEncoder.java:134)
at org.springframework.security.oauth2.jwt.NimbusJwtEncoder.encode(NimbusJwtEncoder.java:108)
Any suggestions to how to implement this example with a simple shared secret, instead of asymmetric keys?

How to use Spring WebSocketClient with SSL?

I connect with my websocket client to non-SSL endpoint without any problem. But I cannot find any way how to connect to wss (SSL) endpoint. Where can I define the SSL factory etc. No object seem to have related set method.
WebSocketClient transport = new StandardWebSocketClient();
WebSocketStompClient stompClient = new WebSocketStompClient(transport);
stompClient.setMessageConverter(new MappingJackson2MessageConverter());
String url = cfg.getWebsocketEndpoint();
StompSessionHandler handler = new MySessionHandler();
WebSocketHttpHeaders headers = new WebSocketHttpHeaders();
stompClient.connect(url, handler);
I am using wss:// url and on the other side I have a server with self-signed certificate. However, this code does not throw any exception while connecting, but the session is not established.
EDIT: After enabling tracing for web.* I got a standard error, with
sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
It occurs when connecting to server with self-signed certificate. However, for RestTemplate I already updated SSLContext with this code and REST calls are fine now, but I do not know why, StandardWebSocketClient is IGNORING the SSLContext. Why?
String keystoreType = "JKS";
InputStream keystoreLocation = new FileInputStream("src/main/resources/aaa.jks");
char [] keystorePassword = "zzz".toCharArray();
char [] keyPassword = "zzz".toCharArray();
KeyStore keystore = KeyStore.getInstance(keystoreType);
keystore.load(keystoreLocation, keystorePassword);
KeyManagerFactory kmfactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
kmfactory.init(keystore, keyPassword);
InputStream truststoreLocation = new FileInputStream("src/main/resources/aaa.jks");
char [] truststorePassword = "zzz".toCharArray();
String truststoreType = "JKS";
KeyStore truststore = KeyStore.getInstance(truststoreType);
truststore.load(truststoreLocation, truststorePassword);
TrustManagerFactory tmfactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmfactory.init(truststore);
KeyManager[] keymanagers = kmfactory.getKeyManagers();
TrustManager[] trustmanagers = tmfactory.getTrustManagers();
SSLContext sslContext = SSLContext.getInstance("TLS");
sslContext.init(keymanagers, trustmanagers, new SecureRandom());
SSLContext.setDefault(sslContext);
UPDATE: Unfortunately, I did not managed to do this with custom truststore. I installed the certificate with InstallCert.java.
I think that each websocket container implementation provides ways to do this.
You have to set this configuration using the StandardWebSocketClient .setUserProperties. All those properties are internally set in the ClientEndpointConfig used by the client.
Here's an example with Tomcat as a provider:
StandardWebSocketClient wsClient = //...;
SSLContext sslContext = //...;
wsClient.setUserProperties(WsWebSocketContainer.SSL_CONTEXT_PROPERTY, sslContext);
In any case you should refer to your provider reference documentation to know which configuration keys you should use.
I've faced a similar issue, where I need to use a different ssl context instead of the default one and I could not use the Tomcat provider version solution. I've met a lot of resources and examples about this solution which does not fit my case, and for sure I landed here too on this question.
When you need to use a specific/custom SSLContext to set and you don't need the 'tomcat version', you can go for the Jetty version because it allows you to set which trust store and/or key store you want to use. In this case my Spring application was on the 'client' side and not the server one, but Jetty SslContextFactory provides the same functionalities for the 'Server' case.
The short example code below is for the client side, but it shares methods and signatures with the server side (check the jetty documentation about)
final SslContextFactory.Client factory = new SslContextFactory.Client();
factory.setSslContext(sslContext); // a different loaded java SslContext instead of the default one
//Create the web socket client with jetty client factory
final JettyWebSocketClient client =
new JettyWebSocketClient(new WebSocketClient(new HttpClient(factory)));
client.start();
final WebSocketStompClient stomp = new WebSocketStompClient(client);
There're also method for setting the trust store or key store instead a fully loaded SslContext.

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