Linux

Java applications + LDAPS

Certificates, chains of trust, root CA certificates, intermediate CA certificates and Java… All of this was part of my learning experience when learning how SSL/TLS works for my job. It wasn’t easy and to be perfectly honest it still isn’t 100%. However, things did get better after some time spent on the job working with these things.

Knowing how good my memory sometimes is (irony), I figured it would maybe be a good idea to do some tests in an improvided lab of my own concoction:

  • FreeIPA server installed on a Raspberry Pi 4 that I hadn’t found a use for so far (replaces the FreeIPA server I had running as a container on my homeserver)
    • Thanks to this guide for helping me out installing Fedora Server 34 ARM64 on it!
  • This guide reminding (ha!) me of how to add a certificate (in this case the root CA certificate that signed the certificate for my FreeIPA server) to the default java keystore:
keytool -importcert -file rootCA.pem -cacerts -keypass changeit -storepass changeit -noprompt -alias ipa
  • SSLPoke to perform a handshake test to the FreeIPA server
    • Thanks to this guide (mainly for the code since I previously used it at work and knew how it worked already)
  • A basic Java application found online (and slightly adapted for the LDAP connection function to fit FreeIPA) to test the LDAPS connection
/* Copyright 2011 Wes Freeman
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Properties;

import javax.naming.AuthenticationException;
import javax.naming.Context;
import javax.naming.NamingException;
import javax.naming.directory.InitialDirContext;

/**
 * Wes's TestLDAP class. For testing LDAP queries, and learning how to do LDAP
 * authentication.
 * 
 * @author wfreeman
 */
public class TestLDAP {
   /** a hash map for error messages from LDAP; set statically at bottom of file. */
   private static HashMap<String, String> errorMap;

   /**
    * Prompts for domain, user, pass, and ldapURL, attempts to connect, and
    * gives result.
    * 
    * @param args
    *           not used.
    * @throws IOException
    */
   public static void main(String args[]) throws IOException {
      // load default properties, if they exist
      Properties appProps = new Properties();
      try {
         FileInputStream propFile = new FileInputStream("TestLDAP.properties");
         appProps.load(propFile);
         propFile.close();
      } catch (IOException e) {
         System.out
               .println("no defaults file found; optional creation at end.");
      }

      BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
      String domain = "", username = "", password = "", ldapURL = "", inStr;
      
      domain = appProps.getProperty("domain");
      ldapURL = appProps.getProperty("ldapURL");

      System.out
            .println("Welcome to the TestLDAP program."
                  + "\nHere you can make sure your LDAP authentication is working properly."
                  + "\n\n  Note: Domain should be in the form of example: google.com\n"
                  + "  Note: LDAP URL should be in the form of ldap://<domain controller fqdn>:<port>/\n");
      do {
         System.out.print("enter domain [default: " + domain
               + "]: ");
         inStr = in.readLine();
         if (!inStr.equals("")) {
            domain = inStr;
         }
      } while (domain == null || domain.equals(""));

      do {
         System.out.print("enter LDAP URL [default: " + ldapURL + "]: ");
         inStr = in.readLine();
         if (!inStr.equals("")) {
            ldapURL = inStr;
         }
      } while (ldapURL == null || ldapURL.equals(""));

      do {
         System.out.print("enter username: ");
         inStr = in.readLine();
         if (!inStr.equals("")) {
            username = inStr;
         }
      } while (username == null || username.equals(""));

      do {
         System.out.print("enter password: ");
         inStr = in.readLine();
         if (!inStr.equals("")) {
            password = inStr;
         }
      } while (password == null || password.equals(""));

      if (!domain.equals(appProps.get("domain"))
            || !ldapURL.equals(appProps.get("ldapURL"))) {
         System.out
               .print("Do you want to remember this domain/ldapURL (NOT user/pass) for next time? y/n: ");
         inStr = in.readLine();
         if (inStr.equalsIgnoreCase("y")) {
            FileOutputStream outFile = new FileOutputStream(
                  "TestLDAP.properties");
            appProps.put("domain", domain);
            appProps.put("ldapURL", ldapURL);
            appProps.store(outFile, "no comment");
            outFile.close();
         }
      }
      (new TestLDAP()).testLDAP(domain, username, password, ldapURL);
      System.out.println("... ending program.");
   }

   /**
    * The actual test is here.
    */
   private void testLDAP(String domain, String username, String password,
         String ldapURL) {
      boolean isAuthenticated = false;
      Hashtable<String, String> authEnv = new Hashtable<String, String>(11);
      //String dn = username + "@" + domain; // doesn't work for me because the dn FreeIPA uses is uid=$username,cn=users,cn=accounts,dc=my-local-domain,dc=local 
      String dn = "uid=" + username + ",cn=users,cn=compat,dc=my-local-domain,dc=local";
      authEnv.put(Context.INITIAL_CONTEXT_FACTORY,
            "com.sun.jndi.ldap.LdapCtxFactory");
      authEnv.put(Context.PROVIDER_URL, ldapURL);
      authEnv.put(Context.SECURITY_PRINCIPAL, dn);
      authEnv.put(Context.SECURITY_CREDENTIALS, password);

      isAuthenticated = false;
      try {
         new InitialDirContext(authEnv);
         isAuthenticated = true;
      } catch (AuthenticationException authEx) {
         String msg = authEx.getMessage();
         // a hack to find what the error is... (see list below)
         for (String key : errorMap.keySet()) {
            if (msg.contains("data " + key)) {
               System.out.println(errorMap.get(key));
            }
         }
         System.out.println("Exception message: " + msg);
      } catch (NamingException namEx) {
         System.out.println("Something went wrong connecting to the server!");
         namEx.printStackTrace(System.out);
      }

      if (!isAuthenticated) {
         System.out.println("Not authenticated!");
      } else {
         System.out.println("Authenticated!");
      }
   }

   static {
      errorMap = new HashMap<String, String>();
      errorMap.put("525",
            "ERROR_NO_SUCH_USER (The specified account does not exist.)"
                  + "\nNOTE: Returns when username is invalid.");
      errorMap
            .put("52e",
                  "ERROR_LOGON_FAILURE (Logon failure: unknown user name or bad password.)"
                        + "\nNOTE: Returns when username is valid but password/credential is invalid."
                        + "\nWill prevent most other errors from being displayed as noted.");
      errorMap
            .put("530",
                  "ERROR_INVALID_LOGON_HOURS (Logon failure: account logon time restriction violation.)"
                        + "\nNOTE: Returns only when presented with valid username and password/credential.");
      errorMap
            .put("531",
                  "ERROR_INVALID_WORKSTATION (Logon failure: user not allowed to log on to this computer.)"
                        + "\nLDAP[userWorkstations: <multivalued list of workstation names>]"
                        + "\nNOTE: Returns only when presented with valid username and password/credential.");
      errorMap
            .put("532",
                  "ERROR_PASSWORD_EXPIRED (Logon failure: the specified account password has expired.)"
                        + "\nLDAP[userAccountControl: <bitmask=0x00800000>] - PASSWORDEXPIRED"
                        + "\nNOTE: Returns only when presented with valid username and password/credential.");
      errorMap
            .put("533",
                  "ERROR_ACCOUNT_DISABLED (Logon failure: account currently disabled.)"
                        + "\nLDAP[userAccountControl: <bitmask=0x00000002>] - ACCOUNTDISABLE"
                        + "\nNOTE: Returns only when presented with valid username and password/credential");
      errorMap
            .put("701",
                  "ERROR_ACCOUNT_EXPIRED (The user's account has expired.)"
                        + "\nLDAP[accountExpires: <value of -1, 0, or extemely large value indicates account will not expire>] - ACCOUNTEXPIRED"
                        + "\nNOTE: Returns only when presented with valid username and password/credential.");
      errorMap
            .put("773",
                  "ERROR_PASSWORD_MUST_CHANGE (The user's password must be changed before logging on the first time.)"
                        + "\nLDAP[pwdLastSet: <value of 0 indicates admin-required password change>] - MUST_CHANGE_PASSWD"
                        + "\nNOTE: Returns only when presented with valid username and password/credential.");
      errorMap
            .put("775",
                  "ERROR_ACCOUNT_LOCKED_OUT (The referenced account is currently locked out and may not be logged on to.)"
                        + "\nLDAP[userAccountControl: <bitmask=0x00000010>] - LOCKOUT"
                        + "\nNOTE: Returns even if invalid password is presented");
   }
}

This stuff isn’t easy when you’re not used to it, but it’s nice to be able to perform such tests to improve my understanding of how things work!

Basically, when you need your java application to access certificates in its default trust store, you add them there and with Linux, it seems to be that easy. If you wanted to check the chain of trust outside of using Java, you would then need to either have the web-server certificate provide the full chain or in a case like this with FreeIPA, you’d need to add the root CA certificate to the OS’ store (e.g. with Fedora like below):

cp rootCA.pem /etc/pki/ca-trust/source/anchors
update-ca-trust extract
openssl s_client -connect ipa.your-local.domain:443

Leave a Reply

Your email address will not be published. Required fields are marked *