mirror of https://github.com/OpenIdentityPlatform/OpenDJ.git

Valery Kharseko
23 hours ago 5e8c08fb1ac4a2cdaa5d11ce1e852dab573ef86a
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>
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)
opendj-openidm-account-change-notification-handler/src/test/java/org/forgerock/openidm/accountchange/OpenidmAccountStatusNotificationHandlerTestCase.java
New file
@@ -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())));
    }
}