Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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)
Expand Down Expand Up @@ -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<String, X509Certificate> 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<Certificate> 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;
}
}

}
Original file line number Diff line number Diff line change
@@ -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");
}
}