From 5e8c08fb1ac4a2cdaa5d11ce1e852dab573ef86a Mon Sep 17 00:00:00 2001
From: Valery Kharseko <vharseko@3a-systems.ru>
Date: Sun, 04 Oct 2026 07:22:08 +0000
Subject: [PATCH] [#1154] Find the OpenIDM certificate by comparing names, not their string forms (#1162)
---
opendj-openidm-account-change-notification-handler/src/main/java/org/forgerock/openidm/accountchange/OpenidmAccountStatusNotificationHandler.java | 40 +++++++++++-
opendj-openidm-account-change-notification-handler/src/test/java/org/forgerock/openidm/accountchange/OpenidmAccountStatusNotificationHandlerTestCase.java | 113 +++++++++++++++++++++++++++++++++++++
opendj-openidm-account-change-notification-handler/pom.xml | 7 ++
3 files changed, 155 insertions(+), 5 deletions(-)
diff --git a/opendj-openidm-account-change-notification-handler/pom.xml b/opendj-openidm-account-change-notification-handler/pom.xml
index 9e9b926..a872b79 100644
--- a/opendj-openidm-account-change-notification-handler/pom.xml
+++ b/opendj-openidm-account-change-notification-handler/pom.xml
@@ -70,6 +70,13 @@
<groupId>org.openidentityplatform.commons.http-framework</groupId>
<artifactId>client-apache-async</artifactId>
</dependency>
+
+ <!-- Test dependencies (TestNG is inherited from the parent, BouncyCastle comes with opendj-server-legacy) -->
+ <dependency>
+ <groupId>org.openidentityplatform.commons</groupId>
+ <artifactId>build-tools</artifactId>
+ <scope>test</scope>
+ </dependency>
</dependencies>
<build><finalName>${project.groupId}.${project.artifactId}</finalName>
diff --git a/opendj-openidm-account-change-notification-handler/src/main/java/org/forgerock/openidm/accountchange/OpenidmAccountStatusNotificationHandler.java b/opendj-openidm-account-change-notification-handler/src/main/java/org/forgerock/openidm/accountchange/OpenidmAccountStatusNotificationHandler.java
index 975f69a..2ff4f32 100644
--- a/opendj-openidm-account-change-notification-handler/src/main/java/org/forgerock/openidm/accountchange/OpenidmAccountStatusNotificationHandler.java
+++ b/opendj-openidm-account-change-notification-handler/src/main/java/org/forgerock/openidm/accountchange/OpenidmAccountStatusNotificationHandler.java
@@ -337,14 +337,44 @@
OpenidmAccountStatusNotificationHandlerCfg configuration) throws ConfigException {
X509TrustManager trustMgr = (X509TrustManager) trustMgrs[0];
- String serverCertSubject = configuration.getCertificateSubjectDN().toString();
- for (X509Certificate cert : trustMgr.getAcceptedIssuers()) {
- String subjectX500Principal = cert.getSubjectX500Principal().getName(X500Principal.CANONICAL);
- if (serverCertSubject.equalsIgnoreCase(subjectX500Principal)) {
+ DN serverCertSubject = configuration.getCertificateSubjectDN();
+ X509Certificate cert = findCertificateBySubject(serverCertSubject, trustMgr.getAcceptedIssuers());
+ if (cert == null) {
+ throw new ConfigException(ERR_OPENIDM_PWSYNC_INVALID_SERVERKEYALIAS.get(serverCertSubject));
+ }
+ return cert;
+ }
+
+ /**
+ * Returns the certificate whose subject is the provided DN.
+ * <p>
+ * Names are compared, not strings: {@code DN.toString()} and the JDK's canonical form serialise the same
+ * name differently (escaped {@code =}, repeated spaces, the order of the AVAs of a multi-valued RDN,
+ * attribute types written as an OID with a BER hex string), and {@link X500Principal#equals(Object)}
+ * compares the canonical forms of both sides.
+ *
+ * @param subjectDN
+ * The subject DN of the certificate to find.
+ * @param certificates
+ * The certificates to search.
+ * @return The first certificate whose subject is {@code subjectDN}, or {@code null} if there is none,
+ * including when the JDK cannot parse {@code subjectDN} as an X.500 name.
+ */
+ static X509Certificate findCertificateBySubject(DN subjectDN, X509Certificate... certificates) {
+ X500Principal subject;
+ try {
+ subject = new X500Principal(subjectDN.toString());
+ } catch (IllegalArgumentException e) {
+ // An attribute type without a keyword known to the JDK, such as "mail": no certificate subject
+ logger.traceException(e);
+ return null;
+ }
+ for (X509Certificate cert : certificates) {
+ if (subject.equals(cert.getSubjectX500Principal())) {
return cert;
}
}
- throw new ConfigException(ERR_OPENIDM_PWSYNC_INVALID_SERVERKEYALIAS.get(serverCertSubject));
+ return null;
}
private TrustManager[] getTrustManagers(OpenidmAccountStatusNotificationHandlerCfg configuration)
diff --git a/opendj-openidm-account-change-notification-handler/src/test/java/org/forgerock/openidm/accountchange/OpenidmAccountStatusNotificationHandlerTestCase.java b/opendj-openidm-account-change-notification-handler/src/test/java/org/forgerock/openidm/accountchange/OpenidmAccountStatusNotificationHandlerTestCase.java
new file mode 100644
index 0000000..0008e13
--- /dev/null
+++ b/opendj-openidm-account-change-notification-handler/src/test/java/org/forgerock/openidm/accountchange/OpenidmAccountStatusNotificationHandlerTestCase.java
@@ -0,0 +1,113 @@
+/*
+ * The contents of this file are subject to the terms of the Common Development and
+ * Distribution License (the License). You may not use this file except in compliance with the
+ * License.
+ *
+ * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
+ * specific language governing permission and limitations under the License.
+ *
+ * When distributing Covered Software, include this CDDL Header Notice in each file and include
+ * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
+ * Header, with the fields enclosed by brackets [] replaced by your own identifying
+ * information: "Portions copyright [year] [name of copyright owner]".
+ *
+ * Copyright 2026 3A Systems, LLC.
+ */
+package org.forgerock.openidm.accountchange;
+
+import static org.forgerock.openidm.accountchange.OpenidmAccountStatusNotificationHandler.findCertificateBySubject;
+import static org.testng.Assert.assertNull;
+import static org.testng.Assert.assertSame;
+
+import java.math.BigInteger;
+import java.security.KeyPair;
+import java.security.KeyPairGenerator;
+import java.security.cert.X509Certificate;
+import java.time.Instant;
+import java.time.temporal.ChronoUnit;
+import java.util.Date;
+
+import javax.security.auth.x500.X500Principal;
+
+import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter;
+import org.bouncycastle.cert.jcajce.JcaX509v3CertificateBuilder;
+import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder;
+import org.forgerock.opendj.ldap.DN;
+import org.forgerock.testng.ForgeRockTestCase;
+import org.testng.annotations.BeforeClass;
+import org.testng.annotations.DataProvider;
+import org.testng.annotations.Test;
+
+/**
+ * Tests how the handler finds the OpenIDM certificate named by {@code certificate-subject-dn}
+ * among the certificates of its truststore (issue #1154).
+ */
+@SuppressWarnings("javadoc")
+@Test(groups = { "precommit" })
+public class OpenidmAccountStatusNotificationHandlerTestCase extends ForgeRockTestCase {
+
+ private KeyPair keyPair;
+
+ @BeforeClass
+ public void generateKeyPair() throws Exception {
+ KeyPairGenerator generator = KeyPairGenerator.getInstance("RSA");
+ generator.initialize(2048);
+ keyPair = generator.generateKeyPair();
+ }
+
+ @DataProvider
+ public Object[][] sameName() {
+ return new Object[][] {
+ // The subject of the sample configuration
+ { "CN=localhost, O=OpenIDM Self-Signed Certificate, OU=None, L=None, ST=None, C=None" },
+ // DN.toString() escapes '=' in a value, the JDK's canonical form does not
+ { "CN=idm=1,O=Example" },
+ // The JDK's canonical form collapses internal spaces
+ { "CN=idm node,O=Example" },
+ // The JDK's canonical form sorts the AVAs of a multi-valued RDN
+ { "OU=sync+CN=idm,O=Example" },
+ // The JDK's canonical form writes EMAILADDRESS as an OID with a BER hex string
+ { "EMAILADDRESS=idm@example.com,CN=idm,O=Example" },
+ // ... and DC, which it encodes as an IA5String, as a BER hex string
+ { "UID=idm,DC=example,DC=com" },
+ };
+ }
+
+ @Test(dataProvider = "sameName")
+ public void findsTheCertificateWhoseSubjectIsTheConfiguredDN(String name) throws Exception {
+ X509Certificate cert = certificate(name);
+
+ assertSame(findCertificateBySubject(DN.valueOf(name), certificate("CN=other,O=Example"), cert), cert);
+ }
+
+ @Test
+ public void findsTheCertificateWhateverTheCaseAndTheAVAOrderOfTheConfiguredDN() throws Exception {
+ X509Certificate cert = certificate("CN=idm+OU=sync,O=Example");
+
+ assertSame(findCertificateBySubject(DN.valueOf("ou=SYNC+cn=IDM,o=example"), cert), cert);
+ }
+
+ @Test
+ public void findsNoCertificateWhenNoSubjectIsTheConfiguredDN() throws Exception {
+ assertNull(findCertificateBySubject(DN.valueOf("CN=idm,O=Example"),
+ certificate("CN=idm,O=Other"), certificate("CN=idm2,O=Example")));
+ }
+
+ @Test
+ public void findsNoCertificateWhenTheJDKCannotParseTheConfiguredDN() throws Exception {
+ // X500Principal does not know the "mail" keyword: the DN cannot name a certificate subject
+ assertNull(findCertificateBySubject(DN.valueOf("mail=idm@example.com,O=Example"),
+ certificate("CN=idm,O=Example")));
+ }
+
+ /** Returns a self-signed certificate whose subject is encoded as keytool encodes it. */
+ private X509Certificate certificate(String subject) throws Exception {
+ X500Principal name = new X500Principal(subject);
+ Instant now = Instant.now();
+ JcaX509v3CertificateBuilder builder = new JcaX509v3CertificateBuilder(name, BigInteger.ONE,
+ Date.from(now.minus(1, ChronoUnit.DAYS)), Date.from(now.plus(1, ChronoUnit.DAYS)), name,
+ keyPair.getPublic());
+ return new JcaX509CertificateConverter().getCertificate(
+ builder.build(new JcaContentSignerBuilder("SHA256withRSA").build(keyPair.getPrivate())));
+ }
+}
--
Gitblit v1.10.0