6

I'm trying to fetch a certificate and its private key from Azure Key Vault then call a remote server and do client certificate authentication.

The first part works well (fetching from Key Vault), however i'm completely stuck at importing the public and private material into KeyStore.

I've tried

keyStore.load(publicKey, null);
keyStore.load(new ByteArrayInputStream(privateKey.getBytes()),
    "thePassphrase".toCharArray());

but this leads to

java.io.IOException: DER input, Integer tag error
        at java.base/sun.security.util.DerInputStream.getInteger(DerInputStream.java:192)
        at java.base/sun.security.pkcs12.PKCS12KeyStore.engineLoad(PKCS12KeyStore.java:1995)
        at java.base/sun.security.util.KeyStoreDelegator.engineLoad(KeyStoreDelegator.java:222)
        at java.base/java.security.KeyStore.load(KeyStore.java:1479)

Here's the full thing minus what i don't know how to implement -

DefaultAzureCredential credential = new DefaultAzureCredentialBuilder().build();

SecretClient secretClient = new SecretClientBuilder()
    .vaultUrl("https://<myvault>.vault.azure.net")
    .credential(credential)
    .buildClient();

CertificateClient certClient = new CertificateClientBuilder()
    .vaultUrl("https://<myvault>.vault.azure.net")
    .credential(credential)
    .buildClient();

// Get the public part of the cert
KeyVaultCertificateWithPolicy certificate = certClient.getCertificate("Joes-Crab-Shack");
byte[] publicKey = certificate.getCer();

// Get the private key
KeyVaultSecret secret = secretClient.getSecret(
    certificate.getName(),
    certificate.getProperties().getVersion());
String privateKey = secret.getValue();

// ***************
// How do i get the cert and its private key into KeyStore?
KeyStore keyStore = KeyStore.getInstance("PKCS12");
// I've also tried "JKS" but that leads to
//    java.io.IOException: Invalid keystore format
keyStore.load(...)
// ***************

// Do client certificate authentication
SSLContext sslContext = SSLContexts.custom().loadKeyMaterial(keyStore, null).build();

CloseableHttpClient httpClient = HttpClients.custom().setSSLContext(sslContext).build();
response = httpClient.execute(new HttpGet("https://remote.that.asks.for.client.cert/"));

InputStream inputStream = response.getEntity().getContent();

body = IOUtils.readInputStreamToString(inputStream, Charsets.UTF_8);

How do i get the certificate and its private key into KeyStore so i can then use it in my HTTP client?

evilSnobu
  • 24,582
  • 8
  • 41
  • 71
  • `keyStore.load()` requires a PKCS#12 file, but you are providing a privateKey, which is usually in pcks1 or pkcs8 (java needs pcks8). Your private key doesn't seem to be in pkcs8 either because you converted it from a string, and pkcs8 is binary (DER encoding). `String privateKey = secret.getValue ();` I have looked at the AzureKeyVault API for java and it is not clear what `secret.getValue ()` returns. Maybe you have to look for an example, or if you prefer, paste an example in the question and take a look at the content – pedrofb Jul 07 '20 at 06:03
  • `secret.getValue()` returns a string that starts with "MII..", so base64. If i base64 decode that i get a bunch of binary. Is that DER? – evilSnobu Jul 07 '20 at 08:44
  • Yes, it is base64. To get binary DER just decode it. What is not clear is if it is a private key or a pcks12. If `KeyStore.load()` still doesn't work, they try to load the private key directly. https://stackoverflow.com/a/19387517/6371459 – pedrofb Jul 07 '20 at 09:20
  • Alright, but `SSLContexts.custom().loadKeyMaterial(...` expects a KeyStore object, so i guess i've gone nowhere. – evilSnobu Jul 07 '20 at 09:30
  • 1
    You can add programmatically an entry to your `KeyStore` using `PrivateKey` and `Certificate`. Use `KeyStore.load(null,null)` and `keystore.setKeyEntry()`. Alternatively you can download the keying material and create a pkcs12 file using openssl or KeystoreExplorer tool – pedrofb Jul 07 '20 at 11:57
  • Thanks. I think i'm very close, however now i'm getting a `java.security.InvalidKeyException: IOException : version mismatch: (supported: 00, parsed: 03` for the private key which i'm not sure what it means. My key is RSA2048 generated by `openssl req -x509 -days 365 -newkey rsa:2048 -keyout key.pem -out cert.pem` – evilSnobu Jul 07 '20 at 18:30
  • Openssl generates private keys in pkcs#1 format, but java only supports pkcs#8. You probably need to convert the key to pkcs8 format using openssl or bouncycastle. See https://stackoverflow.com/a/7473874/6371459 or https://stackoverflow.com/a/41953072/6371459 – pedrofb Jul 07 '20 at 19:28
  • @pedrofb: that's mostly wrong for OpenSSL since 1.0.0 in 2010. `genrsa rsa` still write PKCS1, but `genpkey pkey pkcs12 (import)` and (as here) `req -newkey` all write PKCS8 for PEM (though not DER, weirdly). But _both_ PKCS1-private _and_ PKCS8-clear have version=00, or 01 for rfc5958 which isn't really PKCS8 and anyway neither OpenSSL nor JCE supports; only PKCS12 has version=03, suggesting this was (base64'ed) PKCS12 after all, but then I don't know why KeyStore type PKCS12 had trouble reading it, without looking in detail at data that shouldn't be made public. – dave_thompson_085 Jul 13 '20 at 01:34
  • Snobu: can you reproduce the problem with a _throwaway_ key that you don't need to keep secure and can safely post for us to look at? And specify Java version(s)? – dave_thompson_085 Jul 13 '20 at 01:35
  • Here it is: https://gist.github.com/snobu/ebf9258717691ac93784398a9b54922b (as `.pem`). This is the exact private key that makes the code throw version mismatch `supported: 00, parsed: 03`. – evilSnobu Jul 13 '20 at 16:48
  • @dave_thompson_085, the documentation doesn't make any reference to the type of key it generates, but I've tested it and you're right – pedrofb Jul 15 '20 at 09:28
  • @evilSnobu, your key is pkcs8 so you should have no problem loading it as a `PrivateKey` object (not using `KeyStore`) as I indicated above – pedrofb Jul 15 '20 at 09:29
  • @evilSnobu Did you figure out what secret.getValue() returns? I have same issue. My testing shows it is not any of certificate/public/private key https://stackoverflow.com/questions/75577031/what-is- it-that-i-get-in-java-from-azure-keyvault-certificate – Abe Feb 28 '23 at 05:23
  • Sorry, no, it was so long ago i don't even remember why i needed it :) You could revive this issue here - https://github.com/Azure/azure-sdk-for-java/issues/6681 - hopefully it gets more traction this time around. – evilSnobu Mar 03 '23 at 12:36

3 Answers3

4

It is a bit late, but I want to place a resolution that I have practiced whilst retrieving a certificate from Azure Keyvault then putting it into the java Keystore.

The dependencies that I utilized are the following.

com.azure:azure-security-keyvault-certificates:jar:4.1.3
com.azure:azure-identity:jar:1.0.4
com.azure:azure-security-keyvault-secrets:jar:4.1.1

Then, the code block is below.

public class RestTemplateProvider {

    @Value("${azure.keyvault.service.cert-alias}")
    private String alias;
    @Value("${azure.keyvault.tenant-id}")
    private String tenantId;
    @Value("${azure.keyvault.client-key}")
    private String clientSecret;
    @Value("${azure.keyvault.client-id}")
    private String clientId;
    @Value("${azure.keyvault.uri}")
    private String vaultUri;

    public RestTemplate provideRestTemplate() {
        char[] emptyPass = {};
        CloseIoStream closeableIoStream = CloseIoStream.newInstance();

        try {
            // Azure KeyVault Credentials
            ClientSecretCredential credential = new ClientSecretCredentialBuilder()
                .tenantId(tenantId)
                .clientId(clientId)
                .clientSecret(clientSecret)
                .build();

            CertificateClient certificateClient = new CertificateClientBuilder()
                .vaultUrl(vaultUri)
                .credential(credential)
                .buildClient();

            SecretClient secretClient = new SecretClientBuilder()
                .vaultUrl(vaultUri)
                .credential(credential)
                .buildClient();
            // Azure KeyVault Credentials

            // Retrieving certificate
            KeyVaultCertificateWithPolicy certificateWithPolicy = certificateClient.getCertificate(alias);
            KeyVaultSecret secret = secretClient.getSecret(alias, certificateWithPolicy.getProperties().getVersion());

            byte[] rawCertificate = certificateWithPolicy.getCer();
            CertificateFactory cf = CertificateFactory.getInstance("X.509");
            ByteArrayInputStream certificateStream = new ByteArrayInputStream(rawCertificate);
            Certificate certificate = cf.generateCertificate(certificateStream);

            close(certificateStream);
            // Retrieving certificate

            // Retrieving private key
            String base64PrivateKey = secret.getValue();
            byte[] rawPrivateKey = Base64.getDecoder().decode(base64PrivateKey);

            KeyStore rsaKeyGenerator = KeyStore.getInstance(KeyStore.getDefaultType());
            ByteArrayInputStream keyStream = new ByteArrayInputStream(rawPrivateKey);
            rsaKeyGenerator.load(keyStream, null);

            close(keyStream);

            Key rsaPrivateKey = rsaKeyGenerator.getKey(rsaKeyGenerator.aliases().nextElement(), emptyPass);
            // Retrieving private key

            // Importing certificate and private key into the KeyStore
            KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
            keyStore.load(null);
            keyStore.setKeyEntry(alias, rsaPrivateKey, emptyPass, new Certificate[] {certificate});
            // Importing certificate and private key into the KeyStore

            SSLConnectionSocketFactory socketFactory = new SSLConnectionSocketFactory(
                new SSLContextBuilder()
                    .loadTrustMaterial(null, new TrustAllStrategy())
                    .loadKeyMaterial(keyStore, emptyPass)
                    .build(),
                NoopHostnameVerifier.INSTANCE);

            // It is just a sample, except for the sslsocketfactory please use the configuration which is right for you.
            CloseableHttpClient httpClient = HttpClients.custom()
                                                        .setSSLSocketFactory(socketFactory)
                                                        .setConnectionTimeToLive(1000, TimeUnit.MILLISECONDS)
                                                        .setKeepAliveStrategy((httpResponse, httpContext) -> 10 * 1000)
                                                        .build();

            ClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory(httpClient);
            RestTemplate restTemplate = new RestTemplate(requestFactory);

            return restTemplate;
        } catch (IOException | CertificateException
            | NoSuchAlgorithmException | UnrecoverableKeyException
            | KeyStoreException | KeyManagementException e) {
            log.error("Error!!!")
        }

        return null;
    }

    private void close(Closeable c) {
        if (c != null) {
            try {
                c.close();
            } catch (IOException e) {
                // log
            }
        }
    }
}

I hope someone looking for a solution around using Azure Keyvault certificates in java Keystores can benefit from it.

nurisezgin
  • 1,530
  • 12
  • 19
  • Can I have complete code. I am trying same thing to access certificate from Key vault in java. Also How are you able to access certificate by it's name as there can be many certificates under **vaultUri** ? – Duega Dec 20 '21 at 11:58
  • Hi Duega, certificateClient.getCertificate(alias); the alias you have given here would be the name of the certificate. I'II update the code tomorrow morning. – nurisezgin Dec 20 '21 at 14:34
  • Thanks for the code and the information you have provided. I am trying with `ClientSecretCredential credential = new ClientSecretCredentialBuilder() .tenantId(tenantId) .clientId(clientId) .clientSecret(clientSecret) .build(); ` With all value correct and I am getting an exception (RPCException) [Link For Stack trace](https://ctxt.io/2/AABgRhtBFg) – Duega Dec 22 '21 at 11:45
  • 1
    It seems you have a connection issue, checking dependency versions might be good, top of the my answer you can find dependencies with versions which are being used. Additionally, there might be overwrite issue 'mvn dependency:tree' shows dependencies and versions that could help you. – nurisezgin Dec 23 '21 at 08:57
  • Hi Nurisezgin, Yes, You are right. I was getting error from some dependency mismatch. Thank you so much for your help. Happy new year. :) – Duega Dec 31 '21 at 17:11
1

It sounds to me that the import is a "one time job" and do not need to get solved programmatically. I recommend (same as @pedrofb) that you use the Keystore Explorer for this job - it worked perfectly in my testcase:

Here are my steps to do the import, all files are available in my GitHub-Repo (https://github.com/java-crypto/Stackoverflow/tree/master/Load_certificate_and_private_key_into_Java_KeyStore):

  1. create the private key + certificate file with open ssl:

openssl req -x509 -days 365 -newkey rsa:2048 -keyout key.pem -out cert.pem

This will create the 2 files key.pem (encrypted private key) and cert.pem (certificate with public key). I used some sample data and the password for key.pem is 123456.

  1. download the Keystore Explorer from https://keystore-explorer.org/downloads.html

  2. run explorer and create a new KeyStore

create new keystore

  1. select KeyStore type = PKCS12

select Key Store type

  1. Import KeyPair with "Tools" - "Import Key Pair"

import Key Pair

  1. Select Key Pair Type as PKCS #8

select Key Pair type

  1. Set the password (123456) and choose the key- and cert-file and press "Import"

set the password

  1. Choose the alias for the key (default is the given email in the certificate

choose an alias

  1. Set a Key Pair Entry password: kpe123456

set Key Pair Entry password

  1. Message: Key Pair Import successful

Message Key Pair Import successful

save your new keystore

  1. save your new keystore with "File" - "Save as"

save your new Key Store

  1. Set a password for the Key Store: ks123456

Set a password for the Key Store

  1. choose path + filename: keystore.p12

  2. ready - your private and certificate are imported in the new created Key Store keystore.p12

Michael Fehr
  • 5,827
  • 2
  • 19
  • 40
  • 2
    Apologies, i failed to mention that i do not want to have a static .pem or .pfx or .jks laying around (that already works , e.g. loading a PFX bundle in KeyStore), i simply want pub + private key material to be fetched from Azure Key Vault through secure mechanisms (Managed Identity in this case). This would also make the code highly portable (clouds/PaaS platforms/OSes) and not dependent on manual steps, it also gives me managed cert renewal without me having to worry about that cert anymore. I do appreciate your writeup. – evilSnobu Jul 12 '20 at 15:59
0

When searching via Google and "azure key vault get private key" I found a GitHub-issue that describes in detail how to retrieve a private key from Azure Key Vault:

https://github.com/Azure/azure-sdk-for-js/issues/7647

Scrolling down to answer https://github.com/Azure/azure-sdk-for-js/issues/7647#issuecomment-594935307 there is this statement from one of the Dev's:

How to obtain the private key

Knowing that the private key is stored in a KeyVault Secret, with the public certificate included, we can retrieve it by using the KeyVault-Secrets client, as follows:

// Using the same credential object we used before,
// and the same keyVaultUrl,
// let's create a SecretClient
const secretClient = new SecretClient(keyVaultUrl, credential);

// Assuming you've already created a KeyVault certificate,
// and that certificateName contains the name of your certificate
const certificateSecret = await secretClient.getSecret(certificateName);

// Here we can find both the private key and the public certificate, in PKCS 12 format:
const PKCS12Certificate = certificateSecret.value!;

// You can write this into a file:
fs.writeFileSync("myCertificate.p12", PKCS12Certificate);

Note that, by default, the contentType of the certificates is PKCS 12. Though specifying the content type of your certificate will make it trivial to get it's secret key in PEM format, let's explore how to retrieve a PEM secret key from a PKCS 12 certificate first.

Using openssl, you can retrieve the public certificate in PEM format by using the following command:
openssl pkcs12 -in myCertificate.p12 -out myCertificate.crt.pem -clcerts -nokeys
You can also use openssl to retrieve the private key, as follows:
openssl pkcs12 -in myCertificate.p12 -out myCertificate.key.pem -nocerts -nodes

In short: You do not need to "rebuild" a PKCS12-keystore from separate "files" (private key-PEM and certificate-PEM) as you get the PKCS12/P12-keystore as shown in the example.

Michael Fehr
  • 5,827
  • 2
  • 19
  • 40
  • I don't want to upset the developers of Key Vault but that's the wrong way to store a certificate since you can't take advantage of automatic renewal which is the real added value here. Since it's stored as a p12/pfx "blob", Key Vault doesn't know it's a cert, it's simply storing it as an array of bytes. I do appreciate your answer and there's nothing terribly wrong with this approach, however we should be able to fetch a certificate as a *certificate* from Key Vault with its associated private key (which alone should be the Secret type here), do you agree? – evilSnobu Jul 14 '20 at 09:08
  • Reading the documentation of Key Vault makes me more than one time shaking my head. B.t.w. your demo-pemfile (provided in a comment above is an UNENCRYPTED private key. – Michael Fehr Jul 14 '20 at 09:23