From 09c2470d3e53fd883e185d81f55297c03d9e9a11 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Feb 2026 02:21:18 +0000 Subject: [PATCH 1/5] Initial plan From e350f9cf52620eeb39fb2618039bd511cded23f9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Feb 2026 02:26:48 +0000 Subject: [PATCH 2/5] Add test to verify certificate chain ordering Co-authored-by: rujche <171773178+rujche@users.noreply.github.com> --- .../utils/CertificateOrderTest.java | 118 ++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 sdk/keyvault/azure-security-keyvault-jca/src/test/java/com/azure/security/keyvault/jca/implementation/utils/CertificateOrderTest.java 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..29d358ef23b8 --- /dev/null +++ b/sdk/keyvault/azure-security-keyvault-jca/src/test/java/com/azure/security/keyvault/jca/implementation/utils/CertificateOrderTest.java @@ -0,0 +1,118 @@ +// 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.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]; + + // Print certificate information for debugging + System.out.println("PKCS12 Certificate Chain Order:"); + System.out.println("Cert 0: Subject=" + cert0.getSubjectX500Principal().getName()); + System.out.println("Cert 0: Issuer=" + cert0.getIssuerX500Principal().getName()); + System.out.println("Cert 1: Subject=" + cert1.getSubjectX500Principal().getName()); + System.out.println("Cert 1: Issuer=" + cert1.getIssuerX500Principal().getName()); + System.out.println("Cert 2: Subject=" + cert2.getSubjectX500Principal().getName()); + System.out.println("Cert 2: Issuer=" + cert2.getIssuerX500Principal().getName()); + + // Check if the first certificate is the end-entity certificate + boolean firstIsLeaf = cert0.getSubjectX500Principal().getName().contains("CN=signer"); + + if (!firstIsLeaf) { + // If the first cert is not the leaf, the order might be reversed + // We expect: signer (leaf), intermediate CA, root CA + System.out.println("WARNING: Certificate chain order may be reversed!"); + + // Check if it's reversed (root CA, intermediate CA, leaf) + assertTrue(cert2.getSubjectX500Principal().getName().contains("CN=signer"), + "If not in correct order, the last certificate should be the end-entity certificate"); + } else { + // Expected order: end-entity, intermediate, root + assertTrue(cert0.getSubjectX500Principal().getName().contains("CN=signer"), + "First certificate should be the end-entity certificate"); + assertTrue(cert1.getSubjectX500Principal().getName().contains("CN=Intermediate CA"), + "Second certificate should be the intermediate CA"); + assertTrue(cert2.getSubjectX500Principal().getName().contains("CN=Root CA"), + "Third certificate should be the root CA"); + } + } +} From abd4b84d822ff7711473aad41f327082fe8b2903 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Feb 2026 02:29:17 +0000 Subject: [PATCH 3/5] Implement certificate chain ordering fix for jarsigner compatibility Co-authored-by: rujche <171773178+rujche@users.noreply.github.com> --- .../implementation/utils/CertificateUtil.java | 108 +++++++++++++++++- .../utils/CertificateOrderTest.java | 108 +++++++++++++----- 2 files changed, 186 insertions(+), 30 deletions(-) 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..72cdd6e5b3bb 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 (current != null && 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 index 29d358ef23b8..06b590b6264a 100644 --- 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 @@ -85,34 +85,86 @@ public void testPkcs12CertificateChainOrder() throws CertificateException, IOExc X509Certificate cert1 = (X509Certificate) certs[1]; X509Certificate cert2 = (X509Certificate) certs[2]; - // Print certificate information for debugging - System.out.println("PKCS12 Certificate Chain Order:"); - System.out.println("Cert 0: Subject=" + cert0.getSubjectX500Principal().getName()); - System.out.println("Cert 0: Issuer=" + cert0.getIssuerX500Principal().getName()); - System.out.println("Cert 1: Subject=" + cert1.getSubjectX500Principal().getName()); - System.out.println("Cert 1: Issuer=" + cert1.getIssuerX500Principal().getName()); - System.out.println("Cert 2: Subject=" + cert2.getSubjectX500Principal().getName()); - System.out.println("Cert 2: Issuer=" + cert2.getIssuerX500Principal().getName()); - - // Check if the first certificate is the end-entity certificate - boolean firstIsLeaf = cert0.getSubjectX500Principal().getName().contains("CN=signer"); - - if (!firstIsLeaf) { - // If the first cert is not the leaf, the order might be reversed - // We expect: signer (leaf), intermediate CA, root CA - System.out.println("WARNING: Certificate chain order may be reversed!"); - - // Check if it's reversed (root CA, intermediate CA, leaf) - assertTrue(cert2.getSubjectX500Principal().getName().contains("CN=signer"), - "If not in correct order, the last certificate should be the end-entity certificate"); - } else { - // Expected order: end-entity, intermediate, root - assertTrue(cert0.getSubjectX500Principal().getName().contains("CN=signer"), - "First certificate should be the end-entity certificate"); - assertTrue(cert1.getSubjectX500Principal().getName().contains("CN=Intermediate CA"), - "Second certificate should be the intermediate CA"); - assertTrue(cert2.getSubjectX500Principal().getName().contains("CN=Root CA"), - "Third certificate should be the root CA"); + // 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); + assertEquals(null, 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"); } } From 20d47890af478aaba467c8bf87e7641ff769572c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Feb 2026 02:30:49 +0000 Subject: [PATCH 4/5] Address code review feedback - improve test assertions Co-authored-by: rujche <171773178+rujche@users.noreply.github.com> --- .../jca/implementation/utils/CertificateOrderTest.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 index 06b590b6264a..fe3431ad07b5 100644 --- 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 @@ -18,6 +18,7 @@ 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 { @@ -156,7 +157,7 @@ public void testOrderCertificateChainReversed() throws CertificateException, IOE public void testOrderCertificateChainEdgeCases() { // Test null array Certificate[] result = CertificateUtil.orderCertificateChain(null); - assertEquals(null, result, "Should return null for null input"); + assertNull(result, "Should return null for null input"); // Test empty array result = CertificateUtil.orderCertificateChain(new Certificate[0]); From 10558b67db933070f8d633b235d593c10a0daeef Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 11 Feb 2026 02:33:26 +0000 Subject: [PATCH 5/5] Fix SpotBugs warning - remove redundant null check Co-authored-by: rujche <171773178+rujche@users.noreply.github.com> --- .../keyvault/jca/implementation/utils/CertificateUtil.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 72cdd6e5b3bb..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 @@ -185,7 +185,7 @@ static Certificate[] orderCertificateChain(Certificate[] certificates) { List orderedChain = new ArrayList<>(); X509Certificate current = leafCert; - while (current != null && orderedChain.size() < x509Certs.length) { + while (orderedChain.size() < x509Certs.length) { orderedChain.add(current); // Find the issuer of the current certificate