Browse Source

add certificate verification mode setting

jrivard@gmail.com 6 years ago
parent
commit
5db9e8552f

+ 9 - 0
server/src/main/java/password/pwm/config/Configuration.java

@@ -28,6 +28,7 @@ import password.pwm.PwmConstants;
 import password.pwm.bean.EmailItemBean;
 import password.pwm.bean.PrivateKeyCertificate;
 import password.pwm.config.option.ADPolicyComplexity;
+import password.pwm.config.option.CertificateMatchingMode;
 import password.pwm.config.option.DataStorageMethod;
 import password.pwm.config.option.MessageSendMethod;
 import password.pwm.config.option.TokenStorageMethod;
@@ -1211,5 +1212,13 @@ public class Configuration implements SettingReader
         return returnSet;
     }
 
+    public CertificateMatchingMode readCertificateMatchingMode()
+    {
+        final CertificateMatchingMode mode = readSettingAsEnum( PwmSetting.CERTIFICATE_VALIDATION_MODE, CertificateMatchingMode.class );
+        return mode == null
+                ? CertificateMatchingMode.CA_ONLY
+                : mode;
+
+    }
 
 }

+ 2 - 1
server/src/main/java/password/pwm/config/PwmSetting.java

@@ -549,7 +549,8 @@ public enum PwmSetting
             "display.showDetailedErrors", PwmSettingSyntax.BOOLEAN, PwmSettingCategory.APP_SECURITY ),
     SESSION_MAX_SECONDS(
             "session.maxSeconds", PwmSettingSyntax.DURATION, PwmSettingCategory.APP_SECURITY ),
-
+    CERTIFICATE_VALIDATION_MODE(
+            "security.certificate.validationMode", PwmSettingSyntax.SELECT, PwmSettingCategory.APP_SECURITY ),
 
     // web security
     SECURITY_ENABLE_FORM_NONCE(

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

@@ -23,6 +23,7 @@
 package password.pwm.config.function;
 
 import password.pwm.bean.UserIdentity;
+import password.pwm.config.Configuration;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.SettingUIFunction;
 import password.pwm.config.stored.StoredConfigurationImpl;
@@ -61,11 +62,12 @@ abstract class AbstractUriCertImportFunction implements SettingUIFunction
             final URI uri = URI.create( urlString );
             if ( "https".equalsIgnoreCase( uri.getScheme() ) )
             {
-                certs = X509Utils.readRemoteHttpCertificates( pwmRequest.getPwmApplication(), pwmSession.getLabel(), uri );
+                certs = X509Utils.readRemoteHttpCertificates( pwmRequest.getPwmApplication(), pwmSession.getLabel(), uri, new Configuration( storedConfiguration ) );
             }
             else
             {
-                certs = X509Utils.readRemoteCertificates( URI.create( urlString ) );
+                final Configuration configuration = new Configuration( storedConfiguration );
+                certs = X509Utils.readRemoteCertificates( URI.create( urlString ), configuration );
             }
         }
         catch ( Exception e )

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

@@ -70,7 +70,7 @@ public class LdapCertImportFunction implements SettingUIFunction
                 for ( final String ldapUrlString : ldapUrlStrings )
                 {
                     final URI ldapURI = new URI( ldapUrlString );
-                    final List<X509Certificate> certs = X509Utils.readRemoteCertificates( ldapURI );
+                    final List<X509Certificate> certs = X509Utils.readRemoteCertificates( ldapURI, pwmApplication.getConfig() );
                     if ( certs != null )
                     {
                         resultCertificates.addAll( certs );

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

@@ -24,6 +24,7 @@ package password.pwm.config.function;
 
 import password.pwm.PwmApplication;
 import password.pwm.bean.UserIdentity;
+import password.pwm.config.Configuration;
 import password.pwm.config.PwmSetting;
 import password.pwm.config.SettingUIFunction;
 import password.pwm.config.stored.StoredConfigurationImpl;
@@ -75,7 +76,11 @@ public class SyslogCertImportFunction implements SettingUIFunction
                     {
                         try
                         {
-                            final List<X509Certificate> certs = X509Utils.readRemoteCertificates( syslogConfig.getHost(), syslogConfig.getPort() );
+                            final List<X509Certificate> certs = X509Utils.readRemoteCertificates(
+                                    syslogConfig.getHost(),
+                                    syslogConfig.getPort(),
+                                    new Configuration( storedConfiguration )
+                            );
                             if ( certs != null )
                             {
                                 resultCertificates.addAll( certs );

+ 29 - 0
server/src/main/java/password/pwm/config/option/CertificateMatchingMode.java

@@ -0,0 +1,29 @@
+/*
+ * Password Management Servlets (PWM)
+ * http://www.pwm-project.org
+ *
+ * Copyright (c) 2006-2009 Novell, Inc.
+ * Copyright (c) 2009-2018 The PWM Project
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation; either version 2 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 General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program; if not, write to the Free Software
+ * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
+ */
+
+package password.pwm.config.option;
+
+public enum CertificateMatchingMode
+{
+    CA_ONLY,
+    CERTIFICATE_CHAIN,
+}

+ 2 - 1
server/src/main/java/password/pwm/http/servlet/configguide/ConfigGuideServlet.java

@@ -183,7 +183,8 @@ public class ConfigGuideServlet extends ControlledPwmServlet
                 final URI ldapServerUri = new URI( ldapServerString );
                 if ( "ldaps".equalsIgnoreCase( ldapServerUri.getScheme() ) )
                 {
-                    configGuideBean.setLdapCertificates( X509Utils.readRemoteCertificates( ldapServerUri ) );
+                    final Configuration tempConfig = new Configuration( ConfigGuideForm.generateStoredConfig( configGuideBean ) );
+                    configGuideBean.setLdapCertificates( X509Utils.readRemoteCertificates( ldapServerUri, tempConfig ) );
                     configGuideBean.setCertsTrustedbyKeystore( X509Utils.testIfLdapServerCertsInDefaultKeystore( ldapServerUri ) );
                 }
                 else

+ 3 - 1
server/src/main/java/password/pwm/http/servlet/configguide/ConfigGuideUtils.java

@@ -29,6 +29,7 @@ import org.apache.commons.fileupload.servlet.ServletFileUpload;
 import password.pwm.PwmApplication;
 import password.pwm.PwmApplicationMode;
 import password.pwm.PwmConstants;
+import password.pwm.config.Configuration;
 import password.pwm.config.stored.ConfigurationProperty;
 import password.pwm.config.stored.ConfigurationReader;
 import password.pwm.config.stored.StoredConfigurationImpl;
@@ -202,7 +203,8 @@ public class ConfigGuideUtils
 
         if ( Boolean.parseBoolean( formData.get( ConfigGuideFormField.PARAM_LDAP_SECURE ) ) )
         {
-            X509Utils.readRemoteCertificates( host, port );
+            final Configuration tempConfig = new Configuration( ConfigGuideForm.generateStoredConfig( configGuideBean ) );
+            X509Utils.readRemoteCertificates( host, port, tempConfig );
         }
     }
 

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

@@ -126,7 +126,7 @@ public class LdapSchemaExtendCommand extends AbstractCliCommand
         if ( isSecureLDAP( url ) )
         {
             out( "ldaps certificates from: " + url );
-            final List<X509Certificate> certificateList = X509Utils.readRemoteCertificates( new URI ( url ) );
+            final List<X509Certificate> certificateList = X509Utils.readRemoteCertificates( new URI ( url ), cliEnvironment.getConfig() );
             out( JsonUtil.serializeCollection( X509Utils.makeDebugInfoMap( certificateList ), JsonUtil.Flag.PrettyPrint ) );
             return certificateList;
         }

+ 105 - 17
server/src/main/java/password/pwm/util/secure/X509Utils.java

@@ -27,6 +27,7 @@ import password.pwm.PwmApplication;
 import password.pwm.PwmConstants;
 import password.pwm.bean.SessionLabel;
 import password.pwm.config.Configuration;
+import password.pwm.config.option.CertificateMatchingMode;
 import password.pwm.error.ErrorInformation;
 import password.pwm.error.PwmError;
 import password.pwm.error.PwmException;
@@ -68,23 +69,27 @@ public abstract class X509Utils
 {
     private static final PwmLogger LOGGER = PwmLogger.forClass( X509Utils.class );
 
-    public static List<X509Certificate> readRemoteCertificates( final URI uri )
+    public static List<X509Certificate> readRemoteCertificates(
+            final URI uri,
+            final Configuration configuration
+    )
             throws PwmOperationalException
     {
         final String host = uri.getHost();
         final int port = PwmURL.portForUriSchema( uri );
 
-        return readRemoteCertificates( host, port );
+        return readRemoteCertificates( host, port, configuration );
     }
 
     public static List<X509Certificate> readRemoteHttpCertificates(
             final PwmApplication pwmApplication,
             final SessionLabel sessionLabel,
-            final URI uri
+            final URI uri,
+            final Configuration configuration
     )
             throws PwmUnrecoverableException
     {
-        final CertReaderTrustManager certReaderTrustManager = new CertReaderTrustManager();
+        final CertReaderTrustManager certReaderTrustManager = new CertReaderTrustManager( readCertificateFlagsFromConfig( configuration ) );
         final PwmHttpClientConfiguration pwmHttpClientConfiguration = PwmHttpClientConfiguration.builder()
                 .trustManager( certReaderTrustManager )
                 .build();
@@ -105,7 +110,7 @@ public abstract class X509Utils
 
         if ( certReaderTrustManager.getCertificates() != null )
         {
-            return Arrays.asList( certReaderTrustManager.getCertificates() );
+            return certReaderTrustManager.getCertificates();
         }
 
         {
@@ -122,11 +127,15 @@ public abstract class X509Utils
         throw new PwmUnrecoverableException( requestError );
     }
 
-    public static List<X509Certificate> readRemoteCertificates( final String host, final int port )
+    public static List<X509Certificate> readRemoteCertificates(
+            final String host,
+            final int port,
+            final Configuration configuration
+    )
             throws PwmOperationalException
     {
         LOGGER.debug( () -> "ServerCertReader: beginning certificate read procedure to import certificates from host=" + host + ", port=" + port );
-        final CertReaderTrustManager certReaderTm = new CertReaderTrustManager();
+        final CertReaderTrustManager certReaderTm = new CertReaderTrustManager( readCertificateFlagsFromConfig( configuration ) );
         try
         {
             // use custom trust manager to read the certificates
@@ -164,8 +173,8 @@ public abstract class X509Utils
             );
             throw new PwmOperationalException( errorInformation );
         }
-        final X509Certificate[] certs = certReaderTm.getCertificates();
-        if ( certs == null )
+        final List<X509Certificate> certs = certReaderTm.getCertificates();
+        if ( JavaHelper.isEmpty( certs ) )
         {
             LOGGER.debug( () -> "ServerCertReader: unable to read certificates: null returned from CertReaderTrustManager.getCertificates()" );
         }
@@ -177,7 +186,15 @@ public abstract class X509Utils
             }
         }
         LOGGER.debug( () -> "ServerCertReader: process completed" );
-        return certs == null ? Collections.emptyList() : Arrays.asList( certs );
+        return certs == null ? Collections.emptyList() : certs;
+    }
+
+    private static ReadCertificateFlag[] readCertificateFlagsFromConfig( final Configuration configuration )
+    {
+        final CertificateMatchingMode mode = configuration.readCertificateMatchingMode();
+        return mode == CertificateMatchingMode.CA_ONLY
+                ? Collections.singletonList( X509Utils.ReadCertificateFlag.ReadOnlyRootCA ).toArray( new X509Utils.ReadCertificateFlag[0] )
+                : new X509Utils.ReadCertificateFlag[0];
     }
 
     public static boolean testIfLdapServerCertsInDefaultKeystore( final URI serverURI )
@@ -211,8 +228,15 @@ public abstract class X509Utils
 
     private static class CertReaderTrustManager implements X509TrustManager
     {
+        private final ReadCertificateFlag[] readCertificateFlags;
+
         private X509Certificate[] certificates;
 
+        CertReaderTrustManager( final ReadCertificateFlag[] readCertificateFlags )
+        {
+            this.readCertificateFlags = readCertificateFlags;
+        }
+
         public void checkClientTrusted( final X509Certificate[] chain, final String authType )
                 throws CertificateException
         {
@@ -233,9 +257,13 @@ public abstract class X509Utils
                     + JsonUtil.serialize( new ArrayList<>( certDebugInfo ) ) );
         }
 
-        public X509Certificate[] getCertificates( )
+        public List<X509Certificate> getCertificates( )
         {
-            return certificates;
+            if ( JavaHelper.enumArrayContainsValue( readCertificateFlags, ReadCertificateFlag.ReadOnlyRootCA ) )
+            {
+                return Collections.unmodifiableList( identifyRootCACertificate( Arrays.asList( certificates ) ) );
+            }
+            return Collections.unmodifiableList( Arrays.asList( certificates ) );
         }
     }
 
@@ -278,13 +306,15 @@ public abstract class X509Utils
 
     public static class CertMatchingTrustManager implements X509TrustManager
     {
-        final List<X509Certificate> certificates;
+        final List<X509Certificate> trustedCertificates;
         final boolean validateTimestamps;
+        final CertificateMatchingMode certificateMatchingMode;
 
-        public CertMatchingTrustManager( final Configuration config, final List<X509Certificate> certificates )
+        public CertMatchingTrustManager( final Configuration config, final List<X509Certificate> trustedCertificates )
         {
-            this.certificates = new ArrayList<>( certificates );
+            this.trustedCertificates = new ArrayList<>( trustedCertificates );
             validateTimestamps = config != null && Boolean.parseBoolean( config.readAppProperty( AppProperty.SECURITY_CERTIFICATES_VALIDATE_TIMESTAMPS ) );
+            certificateMatchingMode = config.readCertificateMatchingMode();
         }
 
         @Override
@@ -301,10 +331,46 @@ public abstract class X509Utils
                 throw new CertificateException( errorMsg );
             }
 
-            for ( final X509Certificate loopCert : x509Certificates )
+            switch ( certificateMatchingMode )
+            {
+                case CERTIFICATE_CHAIN:
+                {
+                    doValidation( trustedCertificates, Arrays.asList( x509Certificates ), validateTimestamps );
+                    break;
+                }
+
+                case CA_ONLY:
+                {
+                    final List<X509Certificate> trustedRootCA = X509Utils.identifyRootCACertificate( trustedCertificates );
+                    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( Arrays.asList( x509Certificates ) ),
+                            validateTimestamps
+                    );
+                    break;
+                }
+
+                default:
+                    JavaHelper.unhandledSwitchStatement( certificateMatchingMode );
+            }
+        }
+
+        private static void doValidation(
+                final List<X509Certificate> trustedCertificates,
+                final List<X509Certificate> certificates,
+                final boolean validateTimestamps
+        )
+                throws CertificateException
+        {
+            for ( final X509Certificate loopCert : certificates )
             {
                 boolean certTrusted = false;
-                for ( final X509Certificate storedCert : certificates )
+                for ( final X509Certificate storedCert : trustedCertificates )
                 {
                     if ( loopCert.equals( storedCert ) )
                     {
@@ -372,6 +438,11 @@ public abstract class X509Utils
         detail,
     }
 
+    public enum ReadCertificateFlag
+    {
+        ReadOnlyRootCA
+    }
+
     public enum DebugInfoFlag
     {
         IncludeCertificateDetail
@@ -443,4 +514,21 @@ public abstract class X509Utils
     {
         return StringUtil.base64Encode( certificate.getEncoded() );
     }
+
+    private static List<X509Certificate> identifyRootCACertificate( final List<X509Certificate> certificates )
+    {
+        final int keyCertSignBitPosition = 5;
+        for ( final X509Certificate certificate : certificates )
+        {
+            final boolean[] keyUsages = certificate.getKeyUsage();
+            if ( keyUsages.length > keyCertSignBitPosition - 1 )
+            {
+                if ( keyUsages[keyCertSignBitPosition] )
+                {
+                    return Collections.singletonList( certificate );
+                }
+            }
+        }
+        return Collections.emptyList();
+    }
 }

+ 9 - 0
server/src/main/resources/password/pwm/config/PwmSetting.xml

@@ -1574,6 +1574,15 @@
             <value>28800</value>
         </default>
     </setting>
+    <setting hidden="false" key="security.certificate.validationMode" level="2">
+        <default>
+            <value>CERTIFICATE_CHAIN</value>
+        </default>
+        <options>
+            <option value="CA_ONLY">Root Certificate Only</option>
+            <option value="CERTIFICATE_CHAIN">Entire Certificate Chain</option>
+        </options>
+    </setting>
     <setting hidden="false" key="nodeService.enable" level="2">
         <default>
             <value>true</value>

+ 2 - 0
server/src/main/resources/password/pwm/i18n/PwmSetting.properties

@@ -669,6 +669,7 @@ Setting_Description_security.preventFraming=Enable this option to prevent browse
 Setting_Description_security.redirectUrl.whiteList=Specify a list of partial URL fragments. Any attempt to set the forwardURL or logoutURL via request parameter must match a URL fragment listed here. <ul><li>@PwmAppName@ attempts to match each item from the <b>beginning</b> of the requested URL string.</li><li>@PwmAppName@ decodes and parses the redirect URL before checking it against the whitelist.</li>          <li>If an error occurs when setting a redirect URL, set the debug logs to TRACE and watch the output as the error occurs.</li><li>@PwmAppName@ does not permit wildcards or case mis-matches, the values must match exactly.</li><li>If a fragment has the prefix <i>regex\:</i>, @PwmAppName@ treats the remainder of the fragment as a regular expression.  Regular expression matches must match the entire URL.</li></ul> <table><tr><td>Example</td><td>Matches</td><td>Not Matched</td></tr><td>https\://www.example.com</td><td>https\://www.example.com<br/>https\://www.example.com/<br/>https\://www.example.com/path</td><td>http\://www.example.com<br/>https\://mail.example.com</td></tr><td>http\://www.example.com/p1</td><td>http\://www.example.com/p1<br/>http\://www.example.com/p1/p2<br/>http\://www.example.com/p1?a1\=v1</td><td>https\://www.example.com/p1<br/>http\://www.example.com/p2</td></tr><td>/path1</td><td>/path1<br/>/path1/path2<br/>/path1/path2/?param\=v1</td><td>www.example.com/path1/<br/>https\://www.example.com/path1<br/>/path2</td></tr><td>regex\:^(https?\:\\/\\/)[a-z]*\\.example\\.com.*?$</td><td>http\://www.example.com<br/>https\://www.example.com<br/>http\://www.example.com/p1<br/>http\://mail.example.com/p1</td><td>www.example.com<br/>http\://www.example.org</td></tr></table>
 Setting_Description_security.sso.authHeaderName=Specify the name of the HTTP header that configures @PwmAppName@ to use an upstream server to allow automatic logins with a only a user name, a password is not required.  This setting controls the name of the HTTP header.  When used, @PwmAppName@ prompts users for their passwords to access certain functionality.
 Setting_Description_session.maxSeconds=Specify the maximum duration of a session (in seconds).  Having a maximum session lifetime prevents certain types of long-term session fixation attacks.
+Setting_Description_security.certificate.validationMode=Specify how outbound SSL/TLS certificate validation will be performed by @PwmAppName@.
 Setting_Description_shortcut.enable=Enable this option to enable the shortcuts module.
 Setting_Description_shortcut.httpHeaders=Specify the HTTP Headers to use to control the visible list of shortcuts.  If this header is present, @PwmAppName@ uses these values to determine which of the configured shortcuts are available to a user.  The values must correspond to the label values specified as part of the shortcut items.  When this header is present, @PwmAppName@ does not use the ldapQuery portion of the shortcut items and instead displays the shortcuts only if the label is present in the header.<br/><br/>Values can be set in multiple headers, or by comma separating the values.<br/><br/>A blank value disables this feature.
 Setting_Description_shortcut.items=Specify the list of available shortcuts.<br/><br/>Format\: <b>label\:\:url\:\:ldapQuery\:\:description</b><table style\="width\: 400px"><tr><td>label</td><td>Label to show to users</td></tr><tr><td>ldapQuery</td><td>Valid LDAP syntax style query, if the user matches this query, then @PwmAppName@ shows the shortcut to the users.</td></tr><tr><td>url</td><td>http shortcut to direct the user to</td></tr><tr><td>description</td><td>Long description of the shortcut</td></tr></table>
@@ -1186,6 +1187,7 @@ Setting_Label_security.preventFraming=Prevent HTML Framing
 Setting_Label_security.redirectUrl.whiteList=Redirect Whitelist
 Setting_Label_security.sso.authHeaderName=SSO Authentication Header Name
 Setting_Label_session.maxSeconds=Maximum Session Duration
+Setting_Label_security.certificate.validationMode=Certificate Validation Mode
 Setting_Label_shortcut.enable=Enable Shortcuts
 Setting_Label_shortcut.httpHeaders=Shortcut Headers
 Setting_Label_shortcut.items=Shortcut Items