diff --git a/sdk/keyvault/azure-security-keyvault-jca/src/main/java/com/azure/security/keyvault/jca/implementation/utils/CertificateUtil.java b/sdk/keyvault/azure-security-keyvault-jca/src/main/java/com/azure/security/keyvault/jca/implementation/utils/CertificateUtil.java index 014513520a84..a7dee67e2d07 100644 --- a/sdk/keyvault/azure-security-keyvault-jca/src/main/java/com/azure/security/keyvault/jca/implementation/utils/CertificateUtil.java +++ b/sdk/keyvault/azure-security-keyvault-jca/src/main/java/com/azure/security/keyvault/jca/implementation/utils/CertificateUtil.java @@ -23,9 +23,12 @@ import java.security.cert.Certificate; import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; +import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.Base64; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.stream.Collectors; public final class CertificateUtil { @@ -34,11 +37,16 @@ public final class CertificateUtil { public static Certificate[] loadCertificatesFromSecretBundleValue(String string) throws CertificateException, IOException, KeyStoreException, NoSuchAlgorithmException, NoSuchProviderException, PKCSException { + Certificate[] certificates; if (string.contains(BEGIN_CERTIFICATE)) { - return loadCertificatesFromSecretBundleValuePem(string); + certificates = loadCertificatesFromSecretBundleValuePem(string); } else { - return loadCertificatesFromSecretBundleValuePKCS12(string); + certificates = loadCertificatesFromSecretBundleValuePKCS12(string); } + + // Ensure certificates are in the correct order: end-entity (leaf) → intermediate(s) → root CA + // This is required for jarsigner and other Java security tools + return orderCertificateChain(certificates); } private static Certificate[] loadCertificatesFromSecretBundleValuePem(InputStream inputStream) @@ -113,4 +121,100 @@ public static String getCertificateNameFromCertificateItemId(String id) { return id.substring(id.indexOf(keyWord) + keyWord.length()); } + /** + * Orders a certificate chain to ensure it's in the correct order for jarsigner and Java security tools. + * The correct order is: end-entity (leaf) certificate, intermediate CA(s), root CA. + * + * This method identifies the end-entity certificate (the one not issuing any other certificate in the chain) + * and builds the chain from leaf to root by following the issuer relationships. + * + * @param certificates The array of certificates to order + * @return The ordered array of certificates, or the original array if ordering cannot be determined + */ + static Certificate[] orderCertificateChain(Certificate[] certificates) { + if (certificates == null || certificates.length <= 1) { + return certificates; + } + + try { + // Convert to X509Certificate for easier manipulation + X509Certificate[] x509Certs = new X509Certificate[certificates.length]; + for (int i = 0; i < certificates.length; i++) { + if (!(certificates[i] instanceof X509Certificate)) { + // If not X509, return original order + return certificates; + } + x509Certs[i] = (X509Certificate) certificates[i]; + } + + // Create a map of subject DN to certificate for quick lookup + Map subjectToCert = new HashMap<>(); + for (X509Certificate cert : x509Certs) { + subjectToCert.put(cert.getSubjectX500Principal().getName(), cert); + } + + // Find the end-entity (leaf) certificate + // It's the one that is not the issuer of any other certificate in the chain + X509Certificate leafCert = null; + for (X509Certificate cert : x509Certs) { + boolean isIssuerOfOther = false; + String certSubject = cert.getSubjectX500Principal().getName(); + + for (X509Certificate otherCert : x509Certs) { + if (cert != otherCert) { + String otherIssuer = otherCert.getIssuerX500Principal().getName(); + if (certSubject.equals(otherIssuer)) { + isIssuerOfOther = true; + break; + } + } + } + + if (!isIssuerOfOther) { + leafCert = cert; + break; + } + } + + if (leafCert == null) { + // Couldn't identify leaf certificate, return original order + return certificates; + } + + // Build the chain from leaf to root + List orderedChain = new ArrayList<>(); + X509Certificate current = leafCert; + + while (orderedChain.size() < x509Certs.length) { + orderedChain.add(current); + + // Find the issuer of the current certificate + String issuerDN = current.getIssuerX500Principal().getName(); + String currentSubjectDN = current.getSubjectX500Principal().getName(); + + // Check if this is a self-signed certificate (root CA) + if (issuerDN.equals(currentSubjectDN)) { + // Self-signed, we've reached the root + break; + } + + // Look for the issuer in the certificate chain + X509Certificate issuer = subjectToCert.get(issuerDN); + if (issuer == null || issuer == current) { + // No issuer found in chain, or circular reference + break; + } + + current = issuer; + } + + // Convert back to Certificate array + return orderedChain.toArray(new Certificate[0]); + + } catch (Exception e) { + // If any error occurs during ordering, return original order + return certificates; + } + } + } diff --git a/sdk/keyvault/azure-security-keyvault-jca/src/test/java/com/azure/security/keyvault/jca/implementation/utils/CertificateOrderTest.java b/sdk/keyvault/azure-security-keyvault-jca/src/test/java/com/azure/security/keyvault/jca/implementation/utils/CertificateOrderTest.java new file mode 100644 index 000000000000..fe3431ad07b5 --- /dev/null +++ b/sdk/keyvault/azure-security-keyvault-jca/src/test/java/com/azure/security/keyvault/jca/implementation/utils/CertificateOrderTest.java @@ -0,0 +1,171 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.security.keyvault.jca.implementation.utils; + +import org.bouncycastle.pkcs.PKCSException; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Paths; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.security.cert.X509Certificate; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class CertificateOrderTest { + + /** + * Test to verify the certificate chain order from PEM files. + * The expected order is: end-entity (leaf) cert, intermediate CA(s), root CA. + */ + @Test + public void testPemCertificateChainOrder() throws CertificateException, IOException, KeyStoreException, + NoSuchAlgorithmException, NoSuchProviderException, PKCSException { + + String pemString = new String( + Files.readAllBytes( + Paths.get("src/test/resources/certificate-util/SecretBundle.value/3-certificates-in-chain.pem")), + StandardCharsets.UTF_8); + + Certificate[] certs = CertificateUtil.loadCertificatesFromSecretBundleValue(pemString); + + assertEquals(3, certs.length, "Should have 3 certificates in chain"); + + X509Certificate cert0 = (X509Certificate) certs[0]; + X509Certificate cert1 = (X509Certificate) certs[1]; + X509Certificate cert2 = (X509Certificate) certs[2]; + + // Certificate 0 should be the end-entity (leaf) certificate with CN=signer + assertTrue(cert0.getSubjectX500Principal().getName().contains("CN=signer"), + "First certificate should be the end-entity certificate"); + + // Certificate 1 should be the intermediate CA + assertTrue(cert1.getSubjectX500Principal().getName().contains("CN=Intermediate CA"), + "Second certificate should be the intermediate CA"); + + // Certificate 2 should be the root CA + assertTrue(cert2.getSubjectX500Principal().getName().contains("CN=Root CA"), + "Third certificate should be the root CA"); + + // Verify the chain: cert0 should be issued by cert1 + assertEquals(cert0.getIssuerX500Principal(), cert1.getSubjectX500Principal(), + "End-entity cert should be issued by intermediate CA"); + + // Verify the chain: cert1 should be issued by cert2 + assertEquals(cert1.getIssuerX500Principal(), cert2.getSubjectX500Principal(), + "Intermediate CA should be issued by root CA"); + } + + /** + * Test to verify the certificate chain order from PKCS12 files. + * The expected order is: end-entity (leaf) cert, intermediate CA(s), root CA. + */ + @Test + public void testPkcs12CertificateChainOrder() throws CertificateException, IOException, KeyStoreException, + NoSuchAlgorithmException, NoSuchProviderException, PKCSException { + + String pfxString = new String( + Files.readAllBytes( + Paths.get("src/test/resources/certificate-util/SecretBundle.value/3-certificates-in-chain.pfx")), + StandardCharsets.UTF_8); + + Certificate[] certs = CertificateUtil.loadCertificatesFromSecretBundleValue(pfxString); + + assertEquals(3, certs.length, "Should have 3 certificates in chain"); + + X509Certificate cert0 = (X509Certificate) certs[0]; + X509Certificate cert1 = (X509Certificate) certs[1]; + X509Certificate cert2 = (X509Certificate) certs[2]; + + // Certificate 0 should be the end-entity (leaf) certificate + assertTrue(cert0.getSubjectX500Principal().getName().contains("CN=signer"), + "First certificate should be the end-entity certificate"); + + // Certificate 1 should be the intermediate CA + assertTrue(cert1.getSubjectX500Principal().getName().contains("CN=Intermediate CA"), + "Second certificate should be the intermediate CA"); + + // Certificate 2 should be the root CA + assertTrue(cert2.getSubjectX500Principal().getName().contains("CN=Root CA"), + "Third certificate should be the root CA"); + + // Verify the chain: cert0 should be issued by cert1 + assertEquals(cert0.getIssuerX500Principal(), cert1.getSubjectX500Principal(), + "End-entity cert should be issued by intermediate CA"); + + // Verify the chain: cert1 should be issued by cert2 + assertEquals(cert1.getIssuerX500Principal(), cert2.getSubjectX500Principal(), + "Intermediate CA should be issued by root CA"); + } + + /** + * Test to verify that the orderCertificateChain method correctly orders + * a reversed certificate chain (root CA, intermediate, leaf). + */ + @Test + public void testOrderCertificateChainReversed() throws CertificateException, IOException, KeyStoreException, + NoSuchAlgorithmException, NoSuchProviderException, PKCSException { + + String pemString = new String( + Files.readAllBytes( + Paths.get("src/test/resources/certificate-util/SecretBundle.value/3-certificates-in-chain.pem")), + StandardCharsets.UTF_8); + + Certificate[] certs = CertificateUtil.loadCertificatesFromSecretBundleValue(pemString); + + // Reverse the certificate order to simulate the issue + Certificate[] reversedCerts = new Certificate[certs.length]; + for (int i = 0; i < certs.length; i++) { + reversedCerts[i] = certs[certs.length - 1 - i]; + } + + // Now order the reversed chain + Certificate[] orderedCerts = CertificateUtil.orderCertificateChain(reversedCerts); + + assertEquals(3, orderedCerts.length, "Should have 3 certificates in chain"); + + X509Certificate cert0 = (X509Certificate) orderedCerts[0]; + X509Certificate cert1 = (X509Certificate) orderedCerts[1]; + X509Certificate cert2 = (X509Certificate) orderedCerts[2]; + + // After ordering, certificate 0 should be the end-entity (leaf) certificate + assertTrue(cert0.getSubjectX500Principal().getName().contains("CN=signer"), + "First certificate should be the end-entity certificate after ordering"); + + // Certificate 1 should be the intermediate CA + assertTrue(cert1.getSubjectX500Principal().getName().contains("CN=Intermediate CA"), + "Second certificate should be the intermediate CA after ordering"); + + // Certificate 2 should be the root CA + assertTrue(cert2.getSubjectX500Principal().getName().contains("CN=Root CA"), + "Third certificate should be the root CA after ordering"); + } + + /** + * Test to verify that orderCertificateChain handles null and empty arrays correctly. + */ + @Test + public void testOrderCertificateChainEdgeCases() { + // Test null array + Certificate[] result = CertificateUtil.orderCertificateChain(null); + assertNull(result, "Should return null for null input"); + + // Test empty array + result = CertificateUtil.orderCertificateChain(new Certificate[0]); + assertEquals(0, result.length, "Should return empty array for empty input"); + + // Test single certificate + Certificate[] singleCert = new Certificate[1]; + result = CertificateUtil.orderCertificateChain(singleCert); + assertEquals(1, result.length, "Should return single certificate unchanged"); + } +}