CLOVER🍀

That was when it all began.

JavaからLDAP/LDAPS接続(自己署名証明書の検証スルーコード付き)

最近、ちょっとLDAP…LDAPSでの接続とかをやってみたので、メモとして。

LDAPおよびLDAPSでの接続を、Javaから簡単な例で書いてみます。

こちらを参考に。

JNDI/LDAPサービス・プロバイダ

環境

JavaMaven

$ java -version
openjdk version "1.8.0_171"
OpenJDK Runtime Environment (build 1.8.0_171-8u171-b11-0ubuntu0.16.04.1-b11)
OpenJDK 64-Bit Server VM (build 25.171-b11, mixed mode)

$ mvn -v
Apache Maven 3.5.3 (3383c37e1f9e9b3bc3df5050c29c8aff9f295297; 2018-02-25T04:49:05+09:00)
Maven home: /usr/local/maven3/current
Java version: 1.8.0_171, vendor: Oracle Corporation
Java home: /usr/lib/jvm/java-8-openjdk-amd64/jre
Default locale: ja_JP, platform encoding: UTF-8
OS name: "linux", version: "4.4.0-104-generic", arch: "amd64", family: "unix"

LDAPサーバーについては、OpenLDAPのDockerイメージを使いました。

osixia/openldap / Ducker Hub

osixia/openldap / GitHub

Dockerイメージの起動コマンドは、こちらで。

$ docker run -it --rm --name openldap --env LDAP_ADMIN_PASSWORD="admin-password" --env LDAP_DOMAIN=test.example.com --env LDAP_TLS_VERIFY_CLIENT=try osixia/openldap:1.2.0

ドメインは「test.example.com」にして、adminのパスワードは「admin-password」へ、あとTLSの設定を。

TLSの設定は、Javaからの接続の時にちょっとハマったので。

TLS: can't accept: No certificate was found.. #105

準備

Maven依存関係は、こちら。

        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-api</artifactId>
            <version>5.2.0</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.assertj</groupId>
            <artifactId>assertj-core</artifactId>
            <version>3.10.0</version>
            <scope>test</scope>
        </dependency>

LDAPサーバーには、エントリをひとつ追加。

test.ldif

dn: uid=user001,dc=test,dc=example,dc=com
objectClass: inetOrgPerson
objectClass: posixAccount
uid: user001
cn: テスト 太郎
sn: テスト
uidNumber: 10001
gidNumber: 10001
homeDirectory: /home/user001
userPassword: {SSHA}K1h08ZgBJQIInrqH1eerLG/I4jO2H9fh
description: My Test Account

追加。

$ ldapadd -f test.ldif -D "cn=admin,dc=test,dc=example,dc=com" -w admin-password
adding new entry "uid=user001,dc=test,dc=example,dc=com"

確認(Dockerコンテナ内で実行)。

## LDAP
$ ldapsearch -x -H ldap://localhost -b uid=user001,dc=test,dc=example,dc=com -D "cn=admin,dc=test,dc=example,dc=com" -w admin-password
# extended LDIF
#
# LDAPv3
# base <uid=user001,dc=test,dc=example,dc=com> with scope subtree
# filter: (objectclass=*)
# requesting: ALL
#

# user001, test.example.com
dn: uid=user001,dc=test,dc=example,dc=com
objectClass: inetOrgPerson
objectClass: posixAccount
uid: user001
cn:: 44OG44K544OIIOWkqumDjg==
sn:: 44OG44K544OI
uidNumber: 10001
gidNumber: 10001
homeDirectory: /home/user001
userPassword:: e1NIQX1hUnloRlJkSXJudFNzY2d1cXF6R0Q0MUIyNEk9
description: My Test Account

# search result
search: 2
result: 0 Success

# numResponses: 2
# numEntries: 1


## LDAPS
$ ldapsearch -x -H ldap://localhost -b uid=user001,dc=test,dc=example,dc=com -D "cn=admin,dc=test,dc=example,dc=com" -w admin-password
# extended LDIF
#
# LDAPv3
# base <uid=user001,dc=test,dc=example,dc=com> with scope subtree
# filter: (objectclass=*)
# requesting: ALL
#

# user001, test.example.com
dn: uid=user001,dc=test,dc=example,dc=com
objectClass: inetOrgPerson
objectClass: posixAccount
uid: user001
cn:: 44OG44K544OIIOWkqumDjg==
sn:: 44OG44K544OI
uidNumber: 10001
gidNumber: 10001
homeDirectory: /home/user001
userPassword:: e1NIQX1hUnloRlJkSXJudFNzY2d1cXF6R0Q0MUIyNEk9
description: My Test Account

# search result
search: 2
result: 0 Success

# numResponses: 2
# numEntries: 1

テストコードの雛形

実行は、テストコードで行っていきます。雛形は、こんな感じで。
src/test/java/org/liittlewings/ldap/example/SimpleLdapTest.java

package org.liittlewings.ldap.example;

import java.util.Hashtable;
import javax.naming.CommunicationException;
import javax.naming.Context;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.DirContext;
import javax.naming.directory.InitialDirContext;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;
import javax.net.ssl.SSLHandshakeException;

import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

public class SimpleLdapTest {
    // ここに、テストを書く!!
}

LDAP接続

LDAPへの接続コードは、こちらも合わせて参考に。

JavaでLDAP認証をやってみる - 眩しいサインを見ただろう

JavaでActiveDirectory検索を行う(AD認証) | 株式会社アースリンク

LDAP接続用コード | 寺田 佳央 - Yoshio Terada

adminと、追加ユーザーそれぞれで接続できれば、OKとしましょう。

で、作ったのはこんなコード。

    @Test
    public void ldapGettingStarted() throws NamingException {
        String url = "ldap://172.17.0.2:389";

        Hashtable<String, String> env = new Hashtable<>();
        env.put(Context.PROVIDER_URL, url);
        env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
        env.put(Context.SECURITY_AUTHENTICATION, "simple");
        env.put(Context.SECURITY_PRINCIPAL, "cn=admin,dc=test,dc=example,dc=com");
        env.put(Context.SECURITY_CREDENTIALS, "admin-password");

        DirContext ctx = new InitialDirContext(env);

        try {
            SearchControls searchControls = new SearchControls();
            searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE);
            NamingEnumeration<SearchResult> searchResult =
                    ctx.search("dc=test,dc=example,dc=com", "uid=user001", searchControls);

            assertThat(searchResult.hasMoreElements()).isTrue();
            assertThat(searchResult.nextElement().getAttributes().get("uid").get()).isEqualTo("user001");

            Hashtable<String, String> uenv = new Hashtable<>(env);
            uenv.put(Context.SECURITY_PRINCIPAL, "uid=user001,dc=test,dc=example,dc=com");
            uenv.put(Context.SECURITY_CREDENTIALS, "user-password");

            DirContext uctx = new InitialDirContext(uenv);

            // ok

            uctx.close();
        } finally {
            ctx.close();
        }
    }

はい。

LDAPS(自己署名証明書

続いて、LDAPS。ですが、証明書が正規のものではないため、SSLハンドシェイクで失敗します。

    @Test
    public void ldapsGettingStarted() throws NamingException {
        String url = "ldaps://172.17.0.2:636";

        Hashtable<String, String> env = new Hashtable<>();
        env.put(Context.PROVIDER_URL, url);
        env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
        env.put(Context.SECURITY_AUTHENTICATION, "simple");
        env.put(Context.SECURITY_PRINCIPAL, "cn=admin,dc=test,dc=example,dc=com");
        env.put(Context.SECURITY_CREDENTIALS, "admin-password");

        assertThatThrownBy(() -> new InitialDirContext(env))
                .isInstanceOf(CommunicationException.class)
                .hasMessage("simple bind failed: 172.17.0.2:636")
                .hasCauseInstanceOf(SSLHandshakeException.class);
        // javax.net.ssl.SSLHandshakeException:
        // sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
    }

さてどうしましょう、ということで、証明書をkeystoreに追加してもいいのですが、ここはコードで解決してみましょう。

java.naming.ldap.factory.socket」で、SocketFactoryを指定できるようなので、カスタムなSSLSocketFactoryを作成してこちらを使用するようにしてみます。

src/main/java/org/littlewings/ldap/example/LooseSSLSocketFactory.java

package org.littlewings.ldap.example;

import java.io.IOException;
import java.net.InetAddress;
import java.net.Socket;
import java.net.UnknownHostException;
import java.security.SecureRandom;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import javax.net.SocketFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.X509TrustManager;

public class LooseSSLSocketFactory extends SSLSocketFactory {
    SSLSocketFactory delegate;

    public LooseSSLSocketFactory() {
        try {
            SSLContext sslContext = SSLContext.getInstance("SSL");
            sslContext.init(null,
                    new X509TrustManager[]{
                            new X509TrustManager() {
                                @Override
                                public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
                                }

                                @Override
                                public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
                                }

                                @Override
                                public X509Certificate[] getAcceptedIssuers() {
                                    return null;
                                }
                            }
                    },
                    new SecureRandom());

            delegate = sslContext.getSocketFactory();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    @Override
    public String[] getDefaultCipherSuites() {
        return delegate.getDefaultCipherSuites();
    }

    @Override
    public String[] getSupportedCipherSuites() {
        return delegate.getSupportedCipherSuites();
    }

    @Override
    public Socket createSocket(Socket socket, String s, int i, boolean b) throws IOException {
        return delegate.createSocket(socket, s, i, b);
    }

    @Override
    public Socket createSocket(String s, int i) throws IOException, UnknownHostException {
        return delegate.createSocket(s, i);
    }

    @Override
    public Socket createSocket(String s, int i, InetAddress inetAddress, int i1) throws IOException, UnknownHostException {
        return delegate.createSocket(s, i, inetAddress, i1);
    }

    @Override
    public Socket createSocket(InetAddress inetAddress, int i) throws IOException {
        return delegate.createSocket(inetAddress, i);
    }

    @Override
    public Socket createSocket(InetAddress inetAddress, int i, InetAddress inetAddress1, int i1) throws IOException {
        return delegate.createSocket(inetAddress, i, inetAddress1, i1);
    }

    public static SocketFactory getDefault() {
        return new LooseSSLSocketFactory();
    }
}

このSSLSocketFactoryを使うように、修正したコード。

    @Test
    public void ldapGettingStartedWithCustomSSLSocketFactory() throws NamingException {
        String url = "ldaps://172.17.0.2:636";

        Hashtable<String, String> env = new Hashtable<>();
        env.put(Context.PROVIDER_URL, url);
        env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
        env.put(Context.SECURITY_AUTHENTICATION, "simple");
        env.put(Context.SECURITY_PRINCIPAL, "cn=admin,dc=test,dc=example,dc=com");
        env.put(Context.SECURITY_CREDENTIALS, "admin-password");
        env.put("java.naming.ldap.factory.socket", "org.littlewings.ldap.example.LooseSSLSocketFactory");

        DirContext ctx = new InitialDirContext(env);

        try {
            SearchControls searchControls = new SearchControls();
            searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE);
            NamingEnumeration<SearchResult> searchResult =
                    ctx.search("dc=test,dc=example,dc=com", "uid=user001", searchControls);

            assertThat(searchResult.hasMoreElements()).isTrue();
            SearchResult searchResult1 = searchResult.nextElement();
            assertThat(searchResult1.getAttributes().get("uid").get()).isEqualTo("user001");

            Hashtable<String, String> uenv = new Hashtable<>(env);
            uenv.put(Context.SECURITY_PRINCIPAL, "uid=user001,dc=test,dc=example,dc=com");
            uenv.put(Context.SECURITY_CREDENTIALS, "user-password");

            DirContext uctx = new InitialDirContext(uenv);

            // ok

            uctx.close();
        } finally {
            ctx.close();
        }
    }

追加したのは、この部分ですね。

        env.put("java.naming.ldap.factory.socket", "org.littlewings.ldap.example.LooseSSLSocketFactory");

LDAPに慣れていないのでだいぶてこずりましたが、とりあえずはこんなところで。