djangofan
djangofan

Reputation: 29669

Having trouble downloading a certificate from Azure KeyVault as a .PFX using the Java SDK

Having trouble downloading a certificate from Azure KeyVault as a .PFX using the Java SDK.

After the download, the cert is not openable like the original HEX-encoded .PFX file is , before I uploaded it to the Certificates blade in Azure KeyVault. NOTE: I tried both a PFX file with a password and one without a password.

After download, the resulting HEX file 373 lines and 14,919 chars and not openable. Before upload the .pfx hex file is 374 lines and 14,947 characters.

Anyone know what I am doing wrong? After download, with either the original password OR a blank password, I get this error: test.pfx is not a KeyStore of any of the following recognized types: JCE, JCEKS, PKCS#12, UBER or BCFKS (I use Keystore Explorer for testing)

public static void main(String[] args) throws Exception {

    System.setProperty("AZURE_CLIENT_ID", "...");
    System.setProperty("AZURE_CLIENT_SECRET", "...");
    System.setProperty("AZURE_TENANT_ID", "..."");
    final DefaultAzureCredential azureCredential = new DefaultAzureCredentialBuilder()
            .build();
    CertificateClient certificateClient = new CertificateClientBuilder()
            .vaultUrl("https://djangofan-kv-test.vault.azure.net")
            .credential(azureCredential)
            .buildClient();

    String certificateName = "test";

    KeyVaultCertificateWithPolicy certificate = certificateClient.getCertificate(certificateName);
    CertificateKeyType keyType = certificate.getPolicy().getKeyType();
    System.out.println("\nKey type: " + keyType);

    String secretIdAndUrl = certificate.getKeyId();
    System.out.println("\nSecret ID: " + secretIdAndUrl);

    String secretValue = secretClient.getSecret(certificateName).getValue();
    System.out.println("\nSecret value:\n" + secretValue);

    Base64.Decoder decoder = Base64.getDecoder();
    byte[] decodedBytes = decoder.decode(secretValue);
    System.out.println("\nSecret value decoded:\n" + Arrays.toString(decodedBytes));

    String hexData = bytesToHex(decodedBytes);
    System.out.println("\nSecret value HEX dump:\n" + hexData);
    try (BufferedWriter writer = new BufferedWriter(new FileWriter("test.pfx", StandardCharsets.UTF_8))) {
        writer.write(hexData);
    }
    System.exit(0);
}

NOTE: Manually downloading the .pfx cert from the KeyVault GUI works fine.

I also tried it this way, with SecretsClient:

SecretClient secretClient = new SecretClientBuilder()
        .vaultUrl("https://djangofan-kv-test.vault.azure.net")
        .credential(azureCredential)
        .buildClient();
String secretName = "secretnopass";
KeyVaultSecret secret = secretClient.getSecret(secretName);
String secretValue = secret.getValue();
System.out.println("\nSecret value:\n" + secretValue);
String hexData = bytesToHex(secretValue.getBytes(StandardCharsets.UTF_8));
System.out.println("\nSecret value HEX dump:\n" + hexData);

Maven dependencies:

<dependency>
  <groupId>com.azure</groupId>
  <artifactId>azure-security-keyvault-certificates</artifactId>
  <version>4.3.0</version>
</dependency>
<dependency>
  <groupId>com.azure</groupId>
  <artifactId>azure-security-keyvault-secrets</artifactId>
  <version>4.3.0</version>
</dependency>
<dependency>
  <groupId>com.azure</groupId>
  <artifactId>azure-identity</artifactId>
  <version>1.3.5</version>
</dependency>

NOTE: I'll try this alternative method but I am unsure why direct download to keyVault wouldn't work: https://youtu.be/26WGUCQ_LAI?t=618

Upvotes: 0

Views: 885

Answers (2)

djangofan
djangofan

Reputation: 29669

Here is the solution, although it is not ideal, since I means I have to download the keystore parts and then re-assemble them in code to produce a valid keystore.

NOTE: Unfortunately, i could find no way to just do a single download of keystore to my function. Maybe this is because the function has to "do things in memory" and it has no file storage. When I tried, all I could get is the keystore WITHOUT the private key.

For this to work, you need to upload the 3 public certs + 1 private key into the Secrets blade of your KeyVault instance.

You pass a KeyStore object, with pass-by-reference, into this method to re-constuct the keystore:

private void addNonprodKeysToKeystore(KeyStore keyStore, String nonprodCertificateAlias, SecretClient secretClient) throws Exception {
    // get cert chain root cert
    KeyVaultSecret rootCertSecret = secretClient.getSecret(DIGICERT_ROOT_AZURE_PEM);
    String rootCertPEM = rootCertSecret.getValue();
    // get cert chain intermediate cert
    KeyVaultSecret intermediateCertSecret = secretClient.getSecret(DIGICERT_INTERMEDIATE_AZURE_PEM);
    String intermediateCertPEM = intermediateCertSecret.getValue();
    // get cert chain head cert
    KeyVaultSecret headCertSecret = secretClient.getSecret(HEAD_CERT_AZURE_PEM);
    String headCertPEM = headCertSecret.getValue();
    // build cert chain for keystore
    Certificate[] certificateChain = buildCertificateChain(headCertPEM, intermediateCertPEM, rootCertPEM);

    KeyVaultSecret privateKeySecret = secretClient.getSecret(PRIVATE_KEY_AZURE_PEM);
    String encodedPrivateKey = privateKeySecret.getValue();
    byte[] decodedPrivateKey = Base64.getDecoder().decode(encodedPrivateKey);
    String decodedPkString = new String(decodedPrivateKey, StandardCharsets.UTF_8);
    String strippedPkPEM = stripPEMHeadersFromPrivateKey(decodedPkString);
    byte[] decodedPkBody = Base64.getDecoder().decode(strippedPkPEM);
    KeyFactory keyFactory = KeyFactory.getInstance("RSA");
    PKCS8EncodedKeySpec privateKeySpec = new PKCS8EncodedKeySpec(decodedPkBody);
    PrivateKey privateKey = keyFactory.generatePrivate(privateKeySpec);
    keyStore.setKeyEntry(nonprodCertificateAlias, privateKey, this.resolveKeystorePassword(), certificateChain);
}

And the additional utility methods:

public Certificate[] buildCertificateChain(String headCertPEM, String intermediateCertPEM, String rootCertPEM) throws Exception {
        CertificateFactory certFactory = CertificateFactory.getInstance("X.509");

        String strippedHeadCertPEM = stripPEMHeadersFromCertificate(headCertPEM);
        byte[] decodedHeadCertificate = Base64.getDecoder().decode(strippedHeadCertPEM);
        X509Certificate headCert = (X509Certificate) certFactory.generateCertificate(new ByteArrayInputStream(decodedHeadCertificate));

        String strippedIntermediateCertPEM = stripPEMHeadersFromCertificate(intermediateCertPEM);
        byte[] decodedIntermediateCertificate = Base64.getDecoder().decode(strippedIntermediateCertPEM);
        X509Certificate intermediateCert = (X509Certificate) certFactory.generateCertificate(new ByteArrayInputStream(decodedIntermediateCertificate));

        String strippedRootCertPEM = stripPEMHeadersFromCertificate(rootCertPEM);
        byte[] decodedRootCertificate = Base64.getDecoder().decode(strippedRootCertPEM);
        X509Certificate rootCert = (X509Certificate) certFactory.generateCertificate(new ByteArrayInputStream(decodedRootCertificate));

        log.info("Returning newly generated cert chain.");
        return new Certificate[]{headCert, intermediateCert, rootCert};
    }

    public String stripPEMHeadersFromPrivateKey(String pemEncodedPrivateKey) {
        if (pemEncodedPrivateKey == null || pemEncodedPrivateKey.isEmpty()) {
            throw new IllegalArgumentException("The provided PEM string is null or empty.");
        }
        return pemEncodedPrivateKey.replace("-----BEGIN PRIVATE KEY-----", "")
                .replace("-----END PRIVATE KEY-----", "")
                .replaceAll("\\s+", ""); // Removes all whitespaces, newlines, tabs, etc.
    }

    public String stripPEMHeadersFromCertificate(String pemEncodedCertificate) {
        if (pemEncodedCertificate == null || pemEncodedCertificate.isEmpty()) {
            throw new IllegalArgumentException("The provided PEM string is null or empty.");
        }

        return pemEncodedCertificate.replace("-----BEGIN CERTIFICATE-----", "")
                .replace("-----END CERTIFICATE-----", "")
                .replaceAll("\\s+", ""); // Removes all whitespaces, newlines, tabs, etc.
    }

Upvotes: 0

Heath
Heath

Reputation: 3292

PFX - also known as PKCS #12 - is a binary encoding. You shouldn't be encoding the byte[] to a hex string to write out to a file. Even if the original certificate import were PEM, you wouldn't do any decoding - just save the file, which is tagged base64-encoded data already.

See the .NET SDK implementation I wrote. You're on the right track getting the secret; though, you should use the secret ID attached to the certificate: that it's the same ID is merely an implementation detail that could change. Just base64 decode it only if the content-type is "application/x-pkcs12".

Upvotes: 0

Related Questions