Browse Source

trustmanager improvements and improved handling of self-signed certificates

Jason Rivard 5 years ago
parent
commit
db7c2ee968
18 changed files with 574 additions and 241 deletions
  1. 5 0
      server/pom.xml
  2. 1 1
      server/src/main/java/password/pwm/config/function/AbstractUriCertImportFunction.java
  3. 6 4
      server/src/main/java/password/pwm/http/servlet/ClientApiServlet.java
  4. 1 1
      server/src/main/java/password/pwm/http/servlet/configeditor/ConfigEditorServlet.java
  5. 2 2
      server/src/main/java/password/pwm/ldap/LdapOperationsHelper.java
  6. 5 3
      server/src/main/java/password/pwm/svc/email/EmailServerUtil.java
  7. 2 2
      server/src/main/java/password/pwm/svc/event/SyslogAuditService.java
  8. 6 3
      server/src/main/java/password/pwm/svc/httpclient/HttpTrustManagerHelper.java
  9. 4 3
      server/src/main/java/password/pwm/svc/httpclient/PwmHttpClient.java
  10. 2 1
      server/src/main/java/password/pwm/util/cli/commands/LdapSchemaExtendCommand.java
  11. 119 0
      server/src/main/java/password/pwm/util/secure/CertificateReadingTrustManager.java
  12. 5 6
      server/src/main/java/password/pwm/util/secure/HttpsServerCertificateManager.java
  13. 74 0
      server/src/main/java/password/pwm/util/secure/PromiscuousTrustManager.java
  14. 201 0
      server/src/main/java/password/pwm/util/secure/PwmTrustManager.java
  15. 44 0
      server/src/main/java/password/pwm/util/secure/TrustManagerSettings.java
  16. 49 215
      server/src/main/java/password/pwm/util/secure/X509Utils.java
  17. 29 0
      server/src/test/java/password/pwm/util/secure/PromiscuousTrustManagerTest.java
  18. 19 0
      server/src/test/resources/password/pwm/util/secure/PromiscuousTrustManagerTest.properties

+ 5 - 0
server/pom.xml

@@ -195,6 +195,11 @@
             <artifactId>ldapchai</artifactId>
             <version>0.7.5</version>
         </dependency>
+        <dependency>
+            <groupId>org.apache.directory.api</groupId>
+            <artifactId>api-all</artifactId>
+            <version>2.0.0</version>
+        </dependency>
         <dependency>
             <groupId>commons-net</groupId>
             <artifactId>commons-net</artifactId>

+ 1 - 1
server/src/main/java/password/pwm/config/function/AbstractUriCertImportFunction.java

@@ -60,7 +60,7 @@ abstract class AbstractUriCertImportFunction implements SettingUIFunction
             final URI uri = URI.create( urlString );
             if ( "https".equalsIgnoreCase( uri.getScheme() ) )
             {
-                certs = X509Utils.readRemoteHttpCertificates( pwmRequest.getPwmApplication(), pwmRequest.getLabel(), uri, new Configuration( modifier.newStoredConfiguration() ) );
+                certs = X509Utils.readRemoteHttpCertificates( pwmRequest.getPwmApplication(), pwmRequest.getLabel(), uri );
             }
             else
             {

+ 6 - 4
server/src/main/java/password/pwm/http/servlet/ClientApiServlet.java

@@ -479,10 +479,12 @@ public class ClientApiServlet extends ControlledPwmServlet
     private void precheckPublicHealthAndStats( final PwmRequest pwmRequest )
             throws PwmUnrecoverableException
     {
-        if (
-                pwmRequest.getPwmApplication().getApplicationMode() != PwmApplicationMode.RUNNING
-                        && pwmRequest.getPwmApplication().getApplicationMode() != PwmApplicationMode.CONFIGURATION
-        )
+        if ( pwmRequest.getPwmApplication().getApplicationMode() == PwmApplicationMode.CONFIGURATION )
+        {
+            return;
+        }
+
+        if ( pwmRequest.getPwmApplication().getApplicationMode() != PwmApplicationMode.RUNNING )
         {
             final ErrorInformation errorInformation = new ErrorInformation( PwmError.ERROR_SERVICE_NOT_AVAILABLE );
             throw new PwmUnrecoverableException( errorInformation );

+ 1 - 1
server/src/main/java/password/pwm/http/servlet/configeditor/ConfigEditorServlet.java

@@ -752,7 +752,6 @@ public class ConfigEditorServlet extends ControlledPwmServlet
         final ConfigManagerBean configManagerBean = getBean( pwmRequest );
         final StoredConfigurationModifier modifier = StoredConfigurationModifier.newModifier( configManagerBean.getStoredConfiguration() );
 
-
         final String key = pwmRequest.readParameterAsString( "key" );
         final PwmSetting setting = PwmSetting.forKey( key );
         final int maxFileSize = Integer.parseInt( pwmRequest.getConfig().readAppProperty( AppProperty.CONFIG_MAX_JDBC_JAR_SIZE ) );
@@ -788,6 +787,7 @@ public class ConfigEditorServlet extends ControlledPwmServlet
                         alias
                 );
 
+                configManagerBean.setStoredConfiguration( modifier.newStoredConfiguration() );
                 pwmRequest.outputJsonResult( RestResultBean.forSuccessMessage( pwmRequest, Message.Success_Unknown ) );
                 return ProcessStatus.Halt;
             }

+ 2 - 2
server/src/main/java/password/pwm/ldap/LdapOperationsHelper.java

@@ -64,7 +64,7 @@ import password.pwm.util.java.StringUtil;
 import password.pwm.util.java.TimeDuration;
 import password.pwm.util.logging.PwmLogger;
 import password.pwm.util.macro.MacroMachine;
-import password.pwm.util.secure.X509Utils;
+import password.pwm.util.secure.PwmTrustManager;
 
 import javax.net.ssl.X509TrustManager;
 import java.io.ByteArrayInputStream;
@@ -735,7 +735,7 @@ public class LdapOperationsHelper
         final List<X509Certificate> ldapServerCerts = ldapProfile.readSettingAsCertificate( PwmSetting.LDAP_SERVER_CERTS );
         if ( ldapServerCerts != null && ldapServerCerts.size() > 0 )
         {
-            final X509TrustManager tm = new X509Utils.CertMatchingTrustManager( config, ldapServerCerts );
+            final X509TrustManager tm = PwmTrustManager.createPwmTrustManager( config, ldapServerCerts );
             configBuilder.setTrustManager( new X509TrustManager[]
                     {
                             tm,

+ 5 - 3
server/src/main/java/password/pwm/svc/email/EmailServerUtil.java

@@ -38,6 +38,8 @@ import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.StringUtil;
 import password.pwm.util.logging.PwmLogger;
 import password.pwm.util.macro.MacroMachine;
+import password.pwm.util.secure.PwmTrustManager;
+import password.pwm.util.secure.CertificateReadingTrustManager;
 import password.pwm.util.secure.X509Utils;
 
 import javax.mail.Message;
@@ -132,7 +134,7 @@ public class EmailServerUtil
         {
             return X509Utils.getDefaultJavaTrustManager( configuration );
         }
-        final TrustManager certMatchingTrustManager = new X509Utils.CertMatchingTrustManager( configuration, configuredCerts );
+        final TrustManager certMatchingTrustManager = PwmTrustManager.createPwmTrustManager( configuration, configuredCerts );
         return new TrustManager[]
                 {
                         certMatchingTrustManager,
@@ -386,8 +388,8 @@ public class EmailServerUtil
             throws PwmUnrecoverableException
     {
         final EmailServerProfile emailServerProfile = configuration.getEmailServerProfiles().get( profile );
-        final X509Utils.CertReaderTrustManager certReaderTm = new X509Utils.CertReaderTrustManager(
-                new X509Utils.PromiscuousTrustManager(),
+        final CertificateReadingTrustManager certReaderTm = CertificateReadingTrustManager.newCertReaderTrustManager(
+                configuration,
                 X509Utils.ReadCertificateFlag.ReadOnlyRootCA );
         final TrustManager[] trustManagers =  new TrustManager[]
                 {

+ 2 - 2
server/src/main/java/password/pwm/svc/event/SyslogAuditService.java

@@ -58,7 +58,7 @@ import password.pwm.util.localdb.LocalDBException;
 import password.pwm.util.localdb.LocalDBStoredQueue;
 import password.pwm.util.localdb.WorkQueueProcessor;
 import password.pwm.util.logging.PwmLogger;
-import password.pwm.util.secure.X509Utils;
+import password.pwm.util.secure.PwmTrustManager;
 
 import javax.net.SocketFactory;
 import javax.net.ssl.SSLContext;
@@ -365,7 +365,7 @@ public class SyslogAuditService
                     final SSLContext sc = SSLContext.getInstance( "SSL" );
                     sc.init( null, new X509TrustManager[]
                                     {
-                                            new X509Utils.CertMatchingTrustManager( configuration, certificates ),
+                                            PwmTrustManager.createPwmTrustManager( configuration, certificates ),
                                     },
                             new java.security.SecureRandom() );
                     return sc.getSocketFactory();

+ 6 - 3
server/src/main/java/password/pwm/svc/httpclient/HttpTrustManagerHelper.java

@@ -27,6 +27,9 @@ import password.pwm.config.Configuration;
 import password.pwm.error.PwmError;
 import password.pwm.error.PwmUnrecoverableException;
 import password.pwm.util.java.JavaHelper;
+import password.pwm.util.secure.PwmTrustManager;
+import password.pwm.util.secure.CertificateReadingTrustManager;
+import password.pwm.util.secure.PromiscuousTrustManager;
 import password.pwm.util.secure.PwmHashAlgorithm;
 import password.pwm.util.secure.X509Utils;
 
@@ -84,20 +87,20 @@ class HttpTrustManagerHelper
             case promiscuous:
                 return new TrustManager[]
                         {
-                                new X509Utils.PromiscuousTrustManager( ),
+                                PromiscuousTrustManager.createPromiscuousTrustManager( ),
                         };
 
             case promiscuousCertReader:
                 return new TrustManager[]
                         {
-                                new X509Utils.CertReaderTrustManager( new X509Utils.PromiscuousTrustManager( ) ),
+                                CertificateReadingTrustManager.newCertReaderTrustManager( configuration ),
                         };
 
             case configuredCertificates:
             {
                 return new TrustManager[]
                         {
-                                new X509Utils.CertMatchingTrustManager( configuration, pwmHttpClientConfiguration.getCertificates() ),
+                                PwmTrustManager.createPwmTrustManager( configuration, pwmHttpClientConfiguration.getCertificates() ),
                         };
             }
 

+ 4 - 3
server/src/main/java/password/pwm/svc/httpclient/PwmHttpClient.java

@@ -74,7 +74,7 @@ import password.pwm.util.java.StringUtil;
 import password.pwm.util.java.TimeDuration;
 import password.pwm.util.logging.PwmLogLevel;
 import password.pwm.util.logging.PwmLogger;
-import password.pwm.util.secure.X509Utils;
+import password.pwm.util.secure.CertificateReadingTrustManager;
 
 import javax.net.ssl.SSLContext;
 import javax.net.ssl.TrustManager;
@@ -562,15 +562,16 @@ public class PwmHttpClient implements AutoCloseable
     }
 
     public List<X509Certificate> readServerCertificates()
+            throws PwmUnrecoverableException
     {
         final List<X509Certificate> returnList = new ArrayList<>(  );
         if ( trustManagers != null )
         {
             for ( final TrustManager trustManager : trustManagers )
             {
-                if ( trustManager instanceof X509Utils.CertReaderTrustManager )
+                if ( trustManager instanceof CertificateReadingTrustManager )
                 {
-                    returnList.addAll( ( (X509Utils.CertReaderTrustManager) trustManager ).getCertificates() );
+                    returnList.addAll( ( ( CertificateReadingTrustManager ) trustManager ).getCertificates() );
                 }
             }
         }

+ 2 - 1
server/src/main/java/password/pwm/util/cli/commands/LdapSchemaExtendCommand.java

@@ -30,6 +30,7 @@ import password.pwm.ldap.schema.SchemaOperationResult;
 import password.pwm.util.cli.CliParameters;
 import password.pwm.util.java.JavaHelper;
 import password.pwm.util.java.JsonUtil;
+import password.pwm.util.secure.PwmTrustManager;
 import password.pwm.util.secure.X509Utils;
 
 import javax.net.ssl.X509TrustManager;
@@ -74,7 +75,7 @@ public class LdapSchemaExtendCommand extends AbstractCliCommand
                 out( "canceled" );
                 return;
             }
-            trustManager = new X509Utils.CertMatchingTrustManager( cliEnvironment.getConfig(), certificates );
+            trustManager = PwmTrustManager.createPwmTrustManager( cliEnvironment.getConfig(), certificates );
         }
         else
         {

+ 119 - 0
server/src/main/java/password/pwm/util/secure/CertificateReadingTrustManager.java

@@ -0,0 +1,119 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2019 The PWM Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package password.pwm.util.secure;
+
+import password.pwm.config.Configuration;
+import password.pwm.error.PwmError;
+import password.pwm.error.PwmUnrecoverableException;
+import password.pwm.util.java.JavaHelper;
+import password.pwm.util.java.JsonUtil;
+import password.pwm.util.logging.PwmLogger;
+
+import javax.net.ssl.X509TrustManager;
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.Optional;
+
+public class CertificateReadingTrustManager implements X509TrustManager
+{
+    private static final PwmLogger LOGGER = PwmLogger.forClass( CertificateReadingTrustManager.class );
+
+    private final X509Utils.ReadCertificateFlag[] readCertificateFlags;
+    private final X509TrustManager wrappedTrustManager;
+    private final TrustManagerSettings trustManagerSettings;
+
+    private List<X509Certificate> certificates = new ArrayList<>();
+
+    CertificateReadingTrustManager( final Configuration configuration, final X509TrustManager wrappedTrustManager, final X509Utils.ReadCertificateFlag... readCertificateFlags )
+    {
+        this.readCertificateFlags = readCertificateFlags;
+        this.wrappedTrustManager = wrappedTrustManager;
+        this.trustManagerSettings = TrustManagerSettings.fromConfiguration( configuration );
+    }
+
+    public static CertificateReadingTrustManager newCertReaderTrustManager(
+            final Configuration configuration,
+            final X509Utils.ReadCertificateFlag... readCertificateFlags )
+    {
+        return new CertificateReadingTrustManager( configuration, PromiscuousTrustManager.createPromiscuousTrustManager(), readCertificateFlags );
+    }
+
+    public void checkClientTrusted( final X509Certificate[] chain, final String authType )
+            throws CertificateException
+    {
+        wrappedTrustManager.checkClientTrusted(  chain, authType );
+    }
+
+    public X509Certificate[] getAcceptedIssuers( )
+    {
+        return wrappedTrustManager.getAcceptedIssuers();
+    }
+
+    public void checkServerTrusted( final X509Certificate[] chain, final String authType )
+            throws CertificateException
+    {
+        certificates = Arrays.asList( chain );
+        wrappedTrustManager.checkServerTrusted( chain, authType );
+    }
+
+    public List<X509Certificate> getCertificates( )
+            throws PwmUnrecoverableException
+    {
+        if ( JavaHelper.isEmpty( certificates ) )
+        {
+            final String msg = "remote server did not present any certificates";
+            LOGGER.debug( () -> "ServerCertReader: " + msg );
+            throw PwmUnrecoverableException.newException( PwmError.ERROR_CERTIFICATE_ERROR, msg );
+        }
+
+        final boolean readOnlyRootCA = JavaHelper.enumArrayContainsValue( readCertificateFlags, X509Utils.ReadCertificateFlag.ReadOnlyRootCA );
+        if ( readOnlyRootCA )
+        {
+            final Optional<List<X509Certificate>> rootCA = X509Utils.extractRootCaCertificates( certificates );
+            if ( rootCA.isPresent() )
+            {
+                LOGGER.debug( () -> "ServerCertReader: read " + rootCA.get().size()
+                        + " remote CA ROOT certificate(s) from server: " + X509Utils.makeDebugTexts( rootCA.get() ) );
+                return Collections.unmodifiableList( rootCA.get() );
+            }
+            else
+            {
+                LOGGER.debug( () -> "ServerCertReader: " + "read " + certificates.size()
+                        + " certificate(s) from server but none are identified as a ROOT CA certificate." );
+                if ( !trustManagerSettings.isAllowSelfSigned() )
+                {
+                    final String msg = "remote server did not present a signed CA ROOT certificate"
+                            + " and self-signed certs are not permitted";
+                    LOGGER.debug( () -> "ServerCertReader: " + msg );
+                    throw PwmUnrecoverableException.newException( PwmError.ERROR_CERTIFICATE_ERROR, msg );
+                }
+            }
+        }
+
+        LOGGER.debug( () -> "ServerCertReader: read self-signed certificates from remote server: "
+                + JsonUtil.serialize( new ArrayList<>( X509Utils.makeDebugInfoMap( certificates ) ) ) );
+        return Collections.unmodifiableList( certificates );
+    }
+}

+ 5 - 6
server/src/main/java/password/pwm/util/secure/HttpsServerCertificateManager.java

@@ -79,9 +79,9 @@ public class HttpsServerCertificateManager
 {
     private static final PwmLogger LOGGER = PwmLogger.forClass( HttpsServerCertificateManager.class );
 
-    private static boolean bouncyCastleInitialized;
+    private static volatile boolean bouncyCastleInitialized;
 
-    private static void initBouncyCastleProvider( )
+    private static synchronized void initBouncyCastleProvider( )
     {
         if ( !bouncyCastleInitialized )
         {
@@ -98,8 +98,7 @@ public class HttpsServerCertificateManager
     )
             throws PwmUnrecoverableException
     {
-        KeyStore keyStore = null;
-        keyStore = exportKey( pwmApplication.getConfig(), KeyStoreFormat.JKS, passwordData, alias );
+        KeyStore keyStore = exportKey( pwmApplication.getConfig(), KeyStoreFormat.JKS, passwordData, alias );
 
         if ( keyStore == null )
         {
@@ -371,9 +370,9 @@ public class HttpsServerCertificateManager
             final String effectiveAlias;
             {
                 final List<String> allAliases = new ArrayList<>();
-                for ( final Enumeration enu = keyStore.aliases(); enu.hasMoreElements(); )
+                for ( final Enumeration<String> aliasEnum = keyStore.aliases(); aliasEnum.hasMoreElements(); )
                 {
-                    final String value = ( String ) enu.nextElement();
+                    final String value = aliasEnum.nextElement();
                     allAliases.add( value );
                 }
                 effectiveAlias = allAliases.size() == 1 ? allAliases.iterator().next() : alias;

+ 74 - 0
server/src/main/java/password/pwm/util/secure/PromiscuousTrustManager.java

@@ -0,0 +1,74 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2019 The PWM Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package password.pwm.util.secure;
+
+import password.pwm.util.logging.PwmLogger;
+
+import javax.net.ssl.X509TrustManager;
+import java.security.cert.X509Certificate;
+
+public class PromiscuousTrustManager implements X509TrustManager
+{
+    private static final PwmLogger LOGGER = PwmLogger.forClass( PromiscuousTrustManager.class );
+
+    private PromiscuousTrustManager()
+    {
+    }
+
+    public static PromiscuousTrustManager createPromiscuousTrustManager()
+    {
+        return new PromiscuousTrustManager();
+    }
+
+    public X509Certificate[] getAcceptedIssuers( )
+    {
+        return new X509Certificate[ 0 ];
+    }
+
+    public void checkClientTrusted( final X509Certificate[] certs, final String authType )
+    {
+        logMsg( certs, authType );
+    }
+
+    public void checkServerTrusted( final X509Certificate[] certs, final String authType )
+    {
+        logMsg( certs, authType );
+    }
+
+    private void logMsg( final X509Certificate[] certs, final String authType )
+    {
+        if ( certs != null )
+        {
+            for ( final X509Certificate cert : certs )
+            {
+                try
+                {
+                    LOGGER.debug( () -> "promiscuous trusting certificate during authType=" + authType + ", subject=" + cert.getSubjectDN().toString() );
+                }
+                catch ( final Exception e )
+                {
+                    LOGGER.error( "error while decoding certificate: " + e.getMessage() );
+                    throw new IllegalStateException( e );
+                }
+            }
+        }
+    }
+}

+ 201 - 0
server/src/main/java/password/pwm/util/secure/PwmTrustManager.java

@@ -0,0 +1,201 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2019 The PWM Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package password.pwm.util.secure;
+
+import password.pwm.AppProperty;
+import password.pwm.PwmConstants;
+import password.pwm.config.Configuration;
+import password.pwm.error.PwmError;
+import password.pwm.error.PwmUnrecoverableException;
+import password.pwm.util.java.JavaHelper;
+import password.pwm.util.logging.PwmLogger;
+
+import javax.net.ssl.X509TrustManager;
+import java.security.InvalidKeyException;
+import java.security.NoSuchAlgorithmException;
+import java.security.NoSuchProviderException;
+import java.security.SignatureException;
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Optional;
+
+public class PwmTrustManager implements X509TrustManager
+{
+    private static final PwmLogger LOGGER = PwmLogger.forClass( PwmTrustManager.class );
+
+    private final List<X509Certificate> trustedCertificates;
+    private final TrustManagerSettings settings;
+
+    private PwmTrustManager( final TrustManagerSettings trustManagerSettings, final List<X509Certificate> trustedCertificates )
+    {
+        this.trustedCertificates = new ArrayList<>( trustedCertificates );
+        this.settings = trustManagerSettings;
+    }
+
+    public static PwmTrustManager createPwmTrustManager( final Configuration config, final List<X509Certificate> trustedCertificates )
+    {
+        final TrustManagerSettings trustManagerSettings = TrustManagerSettings.fromConfiguration( config );
+
+        return new PwmTrustManager( trustManagerSettings, trustedCertificates );
+    }
+
+    public static PwmTrustManager createPwmTrustManager( final TrustManagerSettings trustManagerSettings, final List<X509Certificate> trustedCertificates )
+    {
+        return new PwmTrustManager( trustManagerSettings, trustedCertificates );
+    }
+
+    @Override
+    public void checkClientTrusted( final X509Certificate[] x509Certificates, final String s ) throws CertificateException
+    {
+    }
+
+    @Override
+    public void checkServerTrusted( final X509Certificate[] x509Certificates, final String s ) throws CertificateException
+    {
+        switch ( settings.getCertificateMatchingMode() )
+        {
+            case CA_ONLY:
+                doRootCaValidation( trustedCertificates, Arrays.asList( x509Certificates ) );
+                break;
+
+            case CERTIFICATE_CHAIN:
+                doSelfSignedValidation( trustedCertificates, Arrays.asList( x509Certificates ) );
+                break;
+
+            default:
+                JavaHelper.unhandledSwitchStatement( settings.getCertificateMatchingMode() );
+        }
+    }
+
+    private void doRootCaValidation(
+            final List<X509Certificate> trustedCertificates,
+            final List<X509Certificate> presentedCertificates
+    )
+            throws CertificateException
+    {
+        final Optional<List<X509Certificate>> rootCa = X509Utils.extractRootCaCertificates( trustedCertificates );
+
+        if ( JavaHelper.isEmpty( trustedCertificates ) )
+        {
+            final String errorMsg = "no ROOT certificates in configuration trust store for this operation";
+            throw new CertificateException( errorMsg );
+        }
+
+        if ( rootCa.isPresent() )
+        {
+            for ( final X509Certificate presentedCert : presentedCertificates )
+            {
+                try
+                {
+                    doRootCaValidation( rootCa.get(), presentedCert );
+                }
+                catch ( final PwmUnrecoverableException e )
+                {
+                    throw new CertificateException( e.getMessage() );
+                }
+            }
+            return;
+        }
+
+        doSelfSignedValidation( trustedCertificates, presentedCertificates );
+    }
+
+    private static void doRootCaValidation(
+            final List<X509Certificate> rootCertificates,
+            final X509Certificate testCertificate
+    )
+            throws PwmUnrecoverableException
+    {
+        boolean passed = false;
+        final StringBuilder errorText = new StringBuilder(  );
+        for ( final X509Certificate rootCA : rootCertificates )
+        {
+            if ( !passed )
+            {
+                try
+                {
+                    testCertificate.verify( rootCA.getPublicKey() );
+                    passed = true;
+                }
+                catch ( final NoSuchAlgorithmException | SignatureException | NoSuchProviderException | InvalidKeyException | CertificateException e )
+                {
+                    final String msg = "server certificate " + X509Utils.makeDebugText( testCertificate )
+                            + " is not trusted by ROOT CA " + X509Utils.makeDebugText( rootCA );
+                    LOGGER.trace( () -> msg );
+                    errorText.append( msg ).append( "  " );
+                }
+            }
+        }
+
+        if ( !passed )
+        {
+            final String errorMsg = "server certificate " + X509Utils.makeDebugText( testCertificate )
+                    + " is not signed by configured ROOT CA certificate(s): " + errorText.toString();
+            throw PwmUnrecoverableException.newException( PwmError.ERROR_CERTIFICATE_ERROR, errorMsg );
+        }
+    }
+
+    private void doSelfSignedValidation(
+            final List<X509Certificate> trustedCertificates,
+            final List<X509Certificate> presentedCertificates
+    )
+            throws CertificateException
+    {
+        if ( !settings.isAllowSelfSigned() )
+        {
+            final String msg = "unable to trust self-signed certificate due to app property '"
+                    + AppProperty.SECURITY_CERTIFICATES_ALLOW_SELF_SIGNED.getKey() + "'";
+            throw new CertificateException( msg );
+        }
+
+        for ( final X509Certificate loopCert : presentedCertificates )
+        {
+            boolean certTrusted = false;
+            for ( final X509Certificate storedCert : trustedCertificates )
+            {
+                if ( loopCert.equals( storedCert ) )
+                {
+                    if ( settings.isValidateTimestamps() )
+                    {
+                        loopCert.checkValidity();
+                    }
+                    certTrusted = true;
+                }
+            }
+            if ( !certTrusted )
+            {
+                final String errorMsg = "server certificate {subject=" + loopCert.getSubjectDN().getName() + "} does not match a certificate in the "
+                        + PwmConstants.PWM_APP_NAME + " configuration trust store.";
+                throw new CertificateException( errorMsg );
+            }
+            //LOGGER.trace("trusting configured certificate: " + makeDebugText(loopCert));
+        }
+    }
+
+    @Override
+    public X509Certificate[] getAcceptedIssuers( )
+    {
+        return new X509Certificate[ 0 ];
+    }
+}

+ 44 - 0
server/src/main/java/password/pwm/util/secure/TrustManagerSettings.java

@@ -0,0 +1,44 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2019 The PWM Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package password.pwm.util.secure;
+
+import lombok.Value;
+import password.pwm.AppProperty;
+import password.pwm.config.Configuration;
+import password.pwm.config.option.CertificateMatchingMode;
+
+@Value
+class TrustManagerSettings
+{
+    private final boolean validateTimestamps;
+    private final boolean allowSelfSigned;
+    private final CertificateMatchingMode certificateMatchingMode;
+
+    public static TrustManagerSettings fromConfiguration( final Configuration config )
+    {
+        final boolean validateTimestamps = config != null && Boolean.parseBoolean( config.readAppProperty( AppProperty.SECURITY_CERTIFICATES_VALIDATE_TIMESTAMPS ) );
+        final boolean allowSelfSigned = config != null && Boolean.parseBoolean( config.readAppProperty( AppProperty.SECURITY_CERTIFICATES_ALLOW_SELF_SIGNED ) );
+        final CertificateMatchingMode certificateMatchingMode = config == null
+                ? CertificateMatchingMode.CERTIFICATE_CHAIN
+                : config.readCertificateMatchingMode();
+        return new TrustManagerSettings( validateTimestamps, allowSelfSigned, certificateMatchingMode );
+    }
+}

+ 49 - 215
server/src/main/java/password/pwm/util/secure/X509Utils.java

@@ -20,7 +20,6 @@
 
 package password.pwm.util.secure;
 
-import password.pwm.AppProperty;
 import password.pwm.PwmApplication;
 import password.pwm.PwmConstants;
 import password.pwm.bean.SessionLabel;
@@ -37,7 +36,6 @@ import password.pwm.svc.httpclient.PwmHttpClient;
 import password.pwm.svc.httpclient.PwmHttpClientConfiguration;
 import password.pwm.svc.httpclient.PwmHttpClientRequest;
 import password.pwm.util.java.JavaHelper;
-import password.pwm.util.java.JsonUtil;
 import password.pwm.util.java.StringUtil;
 import password.pwm.util.logging.PwmLogger;
 
@@ -46,7 +44,6 @@ import javax.net.ssl.SSLSocket;
 import javax.net.ssl.SSLSocketFactory;
 import javax.net.ssl.TrustManager;
 import javax.net.ssl.TrustManagerFactory;
-import javax.net.ssl.X509TrustManager;
 import java.io.ByteArrayInputStream;
 import java.io.IOException;
 import java.io.OutputStream;
@@ -60,18 +57,27 @@ import java.security.cert.CertificateException;
 import java.security.cert.CertificateFactory;
 import java.security.cert.X509Certificate;
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.Collections;
 import java.util.LinkedHashMap;
 import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
+import java.util.Optional;
 import java.util.Set;
 
-public abstract class X509Utils
+/**
+ *
+ */
+public class X509Utils
 {
     private static final PwmLogger LOGGER = PwmLogger.forClass( X509Utils.class );
 
+    private X509Utils()
+    {
+    }
+
+
     public static List<X509Certificate> readRemoteCertificates(
             final URI uri,
             final Configuration configuration
@@ -92,7 +98,7 @@ public abstract class X509Utils
             throws PwmOperationalException
     {
         LOGGER.debug( () -> "ServerCertReader: beginning certificate read procedure to import certificates from host=" + host + ", port=" + port );
-        final CertReaderTrustManager certReaderTm = new CertReaderTrustManager( new PromiscuousTrustManager(), readCertificateFlagsFromConfig( configuration ) );
+        final CertificateReadingTrustManager certReaderTm = CertificateReadingTrustManager.newCertReaderTrustManager( configuration, readCertificateFlagsFromConfig( configuration ) );
         try
         {
             // use custom trust manager to read the certificates
@@ -130,7 +136,17 @@ public abstract class X509Utils
             );
             throw new PwmOperationalException( errorInformation );
         }
-        final List<X509Certificate> certs = certReaderTm.getCertificates();
+
+        final List<X509Certificate> certs;
+        try
+        {
+            certs = certReaderTm.getCertificates();
+        }
+        catch ( final PwmUnrecoverableException e )
+        {
+            throw new PwmOperationalException( e.getErrorInformation() );
+        }
+
         if ( JavaHelper.isEmpty( certs ) )
         {
             LOGGER.debug( () -> "ServerCertReader: unable to read certificates: null returned from CertReaderTrustManager.getCertificates()" );
@@ -149,8 +165,7 @@ public abstract class X509Utils
     public static List<X509Certificate> readRemoteHttpCertificates(
             final PwmApplication pwmApplication,
             final SessionLabel sessionLabel,
-            final URI uri,
-            final Configuration configuration
+            final URI uri
     )
             throws PwmUnrecoverableException
     {
@@ -231,200 +246,6 @@ public abstract class X509Utils
         return false;
     }
 
-    public static class CertReaderTrustManager implements X509TrustManager
-    {
-        private final ReadCertificateFlag[] readCertificateFlags;
-        private final X509TrustManager wrappedTrustManager;
-
-        private List<X509Certificate> certificates = new ArrayList<>();
-
-        public CertReaderTrustManager( final X509TrustManager wrappedTrustManager, final ReadCertificateFlag... readCertificateFlags )
-        {
-            this.readCertificateFlags = readCertificateFlags;
-            this.wrappedTrustManager = wrappedTrustManager;
-        }
-
-        public void checkClientTrusted( final X509Certificate[] chain, final String authType )
-                throws CertificateException
-        {
-            wrappedTrustManager.checkClientTrusted(  chain, authType );
-        }
-
-        public X509Certificate[] getAcceptedIssuers( )
-        {
-            return wrappedTrustManager.getAcceptedIssuers();
-        }
-
-        public void checkServerTrusted( final X509Certificate[] chain, final String authType )
-                throws CertificateException
-        {
-            final List<X509Certificate> asList = Arrays.asList( chain );
-            certificates.addAll( JavaHelper.enumArrayContainsValue( readCertificateFlags, ReadCertificateFlag.ReadOnlyRootCA )
-                    ? identifyRootCACertificate( asList )
-                    : asList );
-            wrappedTrustManager.checkServerTrusted( chain, authType );
-        }
-
-        public List<X509Certificate> getCertificates( )
-        {
-            LOGGER.debug( () -> "read certificates from remote server: "
-                    + JsonUtil.serialize( new ArrayList<>( X509Utils.makeDebugInfoMap( certificates ) ) ) );
-            return Collections.unmodifiableList( certificates );
-        }
-    }
-
-    public static class PromiscuousTrustManager implements X509TrustManager
-    {
-        public X509Certificate[] getAcceptedIssuers( )
-        {
-            return new X509Certificate[ 0 ];
-        }
-
-        public void checkClientTrusted( final X509Certificate[] certs, final String authType )
-        {
-            logMsg( certs, authType );
-        }
-
-        public void checkServerTrusted( final X509Certificate[] certs, final String authType )
-        {
-            logMsg( certs, authType );
-        }
-
-        private void logMsg( final X509Certificate[] certs, final String authType )
-        {
-            if ( certs != null )
-            {
-                for ( final X509Certificate cert : certs )
-                {
-                    try
-                    {
-                        LOGGER.debug( () -> "promiscuous trusting certificate during authType=" + authType + ", subject=" + cert.getSubjectDN().toString() );
-                    }
-                    catch ( final Exception e )
-                    {
-                        LOGGER.error( "error while decoding certificate: " + e.getMessage() );
-                        throw new IllegalStateException( e );
-                    }
-                }
-            }
-        }
-    }
-
-    public static class CertMatchingTrustManager implements X509TrustManager
-    {
-        final List<X509Certificate> trustedCertificates;
-        final boolean validateTimestamps;
-        final boolean allowSelfSigned;
-        final CertificateMatchingMode certificateMatchingMode;
-
-        public CertMatchingTrustManager( final Configuration config, final List<X509Certificate> trustedCertificates )
-        {
-            this.trustedCertificates = new ArrayList<>( trustedCertificates );
-            validateTimestamps = config != null && Boolean.parseBoolean( config.readAppProperty( AppProperty.SECURITY_CERTIFICATES_VALIDATE_TIMESTAMPS ) );
-            allowSelfSigned = config != null && Boolean.parseBoolean( config.readAppProperty( AppProperty.SECURITY_CERTIFICATES_ALLOW_SELF_SIGNED ) );
-            certificateMatchingMode = config == null
-                    ? CertificateMatchingMode.CERTIFICATE_CHAIN
-                    : config.readCertificateMatchingMode();
-        }
-
-        @Override
-        public void checkClientTrusted( final X509Certificate[] x509Certificates, final String s ) throws CertificateException
-        {
-        }
-
-        @Override
-        public void checkServerTrusted( final X509Certificate[] x509Certificates, final String s ) throws CertificateException
-        {
-            final List<X509Certificate> trustedRootCA = X509Utils.identifyRootCACertificate( trustedCertificates );
-            final List<X509Certificate> remoteCertificates = Arrays.asList( x509Certificates );
-            if ( trustedCertificates.size() == 1 && trustedRootCA.isEmpty() && remoteCertificates.size() == 1 )
-            {
-                if ( allowSelfSigned )
-                {
-                    doValidation( remoteCertificates, trustedCertificates, validateTimestamps );
-                    return;
-                }
-                else
-                {
-                    final String msg = "unable to trust self-signed certificate due to app property '"
-                            + AppProperty.SECURITY_CERTIFICATES_ALLOW_SELF_SIGNED.getKey() + "'";
-                    throw new CertificateException( msg );
-                }
-            }
-
-
-            switch ( certificateMatchingMode )
-            {
-                case CERTIFICATE_CHAIN:
-                {
-                    doValidation( trustedCertificates, remoteCertificates, validateTimestamps );
-                    break;
-                }
-
-                case CA_ONLY:
-                {
-                    if ( trustedRootCA.isEmpty() )
-                    {
-                        final String errorMsg = "no root CA certificates in configuration trust store for this operation";
-                        throw new CertificateException( errorMsg );
-                    }
-                    doValidation(
-                            trustedRootCA,
-                            X509Utils.identifyRootCACertificate( remoteCertificates ),
-                            validateTimestamps
-                    );
-                    break;
-                }
-
-                default:
-                    JavaHelper.unhandledSwitchStatement( certificateMatchingMode );
-            }
-        }
-
-        private static void doValidation(
-                final List<X509Certificate> trustedCertificates,
-                final List<X509Certificate> certificates,
-                final boolean validateTimestamps
-        )
-                throws CertificateException
-        {
-            if ( JavaHelper.isEmpty( trustedCertificates ) )
-            {
-                final String errorMsg = "no certificates in configuration trust store for this operation";
-                throw new CertificateException( errorMsg );
-            }
-
-            for ( final X509Certificate loopCert : certificates )
-            {
-                boolean certTrusted = false;
-                for ( final X509Certificate storedCert : trustedCertificates )
-                {
-                    if ( loopCert.equals( storedCert ) )
-                    {
-                        if ( validateTimestamps )
-                        {
-                            loopCert.checkValidity();
-                        }
-                        certTrusted = true;
-                    }
-                }
-                if ( !certTrusted )
-                {
-                    final String errorMsg = "server certificate {subject=" + loopCert.getSubjectDN().getName() + "} does not match a certificate in the "
-                            + PwmConstants.PWM_APP_NAME + " configuration trust store.";
-                    throw new CertificateException( errorMsg );
-                }
-                //LOGGER.trace("trusting configured certificate: " + makeDebugText(loopCert));
-            }
-        }
-
-        @Override
-        public X509Certificate[] getAcceptedIssuers( )
-        {
-            return new X509Certificate[ 0 ];
-        }
-    }
-
     public static String hexSerial( final X509Certificate x509Certificate )
     {
         String result = x509Certificate.getSerialNumber().toString( 16 ).toUpperCase();
@@ -446,6 +267,20 @@ public abstract class X509Utils
                 + "\n:IsRootCA: " + certIsRootCA( x509Certificate );
     }
 
+    public static String makeDebugTexts( final List<X509Certificate> x509Certificates )
+    {
+        final StringBuilder sb = new StringBuilder();
+        sb.append( "Certificates: " );
+        for ( final X509Certificate x509Certificate : x509Certificates )
+        {
+            sb.append( "[" );
+            sb.append( makeDebugText( x509Certificate ) );
+            sb.append( "]" );
+
+        }
+        return sb.toString();
+    }
+
     public static String makeDebugText( final X509Certificate x509Certificate )
     {
         return "subject=" + x509Certificate.getSubjectDN().getName() + ", serial=" + x509Certificate.getSerialNumber();
@@ -540,29 +375,28 @@ public abstract class X509Utils
         return StringUtil.base64Encode( certificate.getEncoded() );
     }
 
-    private static List<X509Certificate> identifyRootCACertificate( final List<X509Certificate> certificates )
+    static Optional<List<X509Certificate>> extractRootCaCertificates(
+            final List<X509Certificate> certificates
+    )
     {
+        Objects.requireNonNull( certificates );
+
+        final List<X509Certificate> returnList = new ArrayList<>( );
+
         for ( final X509Certificate certificate : certificates )
         {
             if ( certIsRootCA( certificate ) )
             {
-                return Collections.singletonList( certificate );
+                returnList.add( certificate );
             }
         }
 
-        if ( certificates.size() == 1 )
-        {
-            LOGGER.debug( () -> "ServerCertReader: treating single certificate as ROOT CA certificate: "
-                    + X509Utils.makeDebugText(  certificates.iterator().next() ) );
-            return Collections.unmodifiableList( certificates );
-        }
-
-        if ( !certificates.isEmpty() )
+        if ( !returnList.isEmpty() )
         {
-            LOGGER.debug( () -> "ServerCertReader: no certificates in read certificate chain are detected as CA cert" );
+            return Optional.of( Collections.unmodifiableList( returnList ) );
         }
 
-        return Collections.emptyList();
+        return Optional.empty();
     }
 
     private static boolean certIsRootCA( final X509Certificate certificate )

+ 29 - 0
server/src/test/java/password/pwm/util/secure/PromiscuousTrustManagerTest.java

@@ -0,0 +1,29 @@
+package password.pwm.util.secure;
+
+import org.junit.Test;
+
+import javax.net.ssl.X509TrustManager;
+import java.io.IOException;
+import java.security.cert.CertificateException;
+import java.security.cert.X509Certificate;
+import java.util.ResourceBundle;
+
+public class PromiscuousTrustManagerTest
+{
+
+    @Test
+    public void createPromiscuousTrustManager() throws CertificateException, IOException
+    {
+        final ResourceBundle bundle = ResourceBundle.getBundle( PromiscuousTrustManagerTest.class.getName() );
+        final String cert1data = bundle.getString( "cert1" );
+        final X509Certificate cert1 = X509Utils.certificateFromBase64( cert1data );
+
+        final X509TrustManager trustManager = PromiscuousTrustManager.createPromiscuousTrustManager();
+        final X509Certificate[] certificates = new X509Certificate[]
+                {
+                        cert1
+                };
+        trustManager.checkServerTrusted( certificates, "test" );
+    }
+
+}

+ 19 - 0
server/src/test/resources/password/pwm/util/secure/PromiscuousTrustManagerTest.properties

@@ -0,0 +1,19 @@
+cert1=-----BEGIN CERTIFICATE-----\
+MIIDBzCCAe+gAwIBAgIJAI0GS0VAJiXHMA0GCSqGSIb3DQEBBQUAMBoxGDAWBgNV\
+BAMMD3d3dy5leGFtcGxlLmNvbTAeFw0yMDAyMjMwMTEzNTNaFw0zMDAyMjAwMTEz\
+NTNaMBoxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTCCASIwDQYJKoZIhvcNAQEB\
+BQADggEPADCCAQoCggEBAM2Byf419Vwos7RwxR2VAnaqx0bdH6EWWukXGqF/dF6A\
+tFfuaSREDp+K440g4NxEOStbq+VCOslKBNTIeEw9138LZetzCol8uNSCoSZc8z+N\
+BJVpBIwBJXaHB316S2XpX4FmpFwtBXG0k+wXtfhFndFA8k+q4rWYI6kjUTrZmneP\
+Hn06fkx1upQxLlCJv/pQ6lTB7LPSVvpeLMpkGZcZCS9J9oYtbiOanL/5bLN+DEOB\
+EHxDHveYsx2xoyjPQe5ddikVCT6XF+dDNh+CKvd0cuEX0igmuappQvEn/1nPuyjw\
+JsR9cCoQrZvMQsNWu2WPl2In0vMRGM7BqAEWs8wOVc0CAwEAAaNQME4wHQYDVR0O\
+BBYEFJtnC+EqmbEmQGa4rr5Y5Hc0m9d2MB8GA1UdIwQYMBaAFJtnC+EqmbEmQGa4\
+rr5Y5Hc0m9d2MAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQEFBQADggEBAI+FWyYA\
+LJSfnYKZDKJOZJtO/UC6DrrBtlBMhg/oql2pE1dcLHPaiycVY44dIaozMcJs5rEb\
+jnJIeD9C68+gE5gR5UgcxcDCI5xHeAGmJKNUFbeFxS951Ah43yI4six0HLKO0J3n\
+PHkhB8433gxeHx7BvRyaAVR+b3OmRDcZ7oJHTGsH4LeuVeDBhU0WLcanZsQ0U3VI\
+O7JS7Zj9tp5hSpp4RVuO7dNX30N+KoDfvOV5Bqg5PV5315jdblnJEM1RFfacua7d\
+KUjGqNaR95xDaTV6ucGHFhnq0phQnEbGhxpllvqZv59XqJOFEItdwlwQWX4JEwfz\
+XIcfkWcJ8BjzRRA=\
+-----END CERTIFICATE-----