SSL truststore working.
authorMathieu Baudier <mbaudier@argeo.org>
Fri, 1 Jul 2022 07:43:15 +0000 (09:43 +0200)
committerMathieu Baudier <mbaudier@argeo.org>
Fri, 1 Jul 2022 07:43:15 +0000 (09:43 +0200)
eclipse/org.argeo.cms.servlet/src/org/argeo/cms/servlet/internal/jetty/JettyConfig.java
eclipse/org.argeo.cms.servlet/src/org/argeo/cms/servlet/internal/jetty/JettyHttpConstants.java
eclipse/org.argeo.ext.equinox.jetty/src/org/argeo/equinox/jetty/CmsJettyCustomizer.java
org.argeo.cms/src/org/argeo/cms/CmsDeployProperty.java
org.argeo.cms/src/org/argeo/cms/internal/http/client/SpnegoHttpClient.java
org.argeo.cms/src/org/argeo/cms/internal/runtime/CmsStateImpl.java
org.argeo.cms/src/org/argeo/cms/internal/runtime/PkiUtils.java
org.argeo.util/src/org/argeo/util/naming/dns/DnsBrowser.java

index 64e33d3ecd5147a99175922e699a94dc97b37c36..0e2a3e5cab4e8594d9df364ffbe8d911f3799165 100644 (file)
@@ -31,7 +31,8 @@ public class JettyConfig {
 
        private final BundleContext bc = FrameworkUtil.getBundle(JettyConfig.class).getBundleContext();
 
-       //private static final String JETTY_PROPERTY_PREFIX = "org.eclipse.equinox.http.jetty.";
+       // private static final String JETTY_PROPERTY_PREFIX =
+       // "org.eclipse.equinox.http.jetty.";
 
        public void start() {
                // We need to start asynchronously so that Jetty bundle get started by lazy init
@@ -104,9 +105,10 @@ public class JettyConfig {
                        }
                }
 
-               int tryCount = 30;
+               long begin = System.currentTimeMillis();
+               int tryCount = 60;
                try {
-                       tryGettyJetty: while (tryCount > 0) {
+                       while (tryCount > 0) {
                                try {
                                        // FIXME deal with multiple ids
                                        JettyConfigurator.startServer(CmsConstants.DEFAULT, new Hashtable<>(config));
@@ -118,7 +120,7 @@ public class JettyConfig {
                                        // Explicitly starts Jetty OSGi HTTP bundle, so that it gets triggered if OSGi
                                        // configuration is not cleaned
                                        FrameworkUtil.getBundle(JettyConfigurator.class).start();
-                                       break tryGettyJetty;
+                                       return;
                                } catch (IllegalStateException e) {
                                        // e.printStackTrace();
                                        // Jetty may not be ready
@@ -129,6 +131,8 @@ public class JettyConfig {
                                        }
                                        tryCount--;
                                }
+                               long duration = System.currentTimeMillis() - begin;
+                               log.error("Gave up with starting Jetty server after " + (duration / 1000) + " s");
                        }
                } catch (Exception e) {
                        log.error("Cannot start default Jetty server with config " + properties, e);
@@ -169,10 +173,18 @@ public class JettyConfig {
                                if (httpHost != null)
                                        props.put(JettyHttpConstants.HTTPS_HOST, httpHost);
 
-                               props.put(JettyHttpConstants.SSL_KEYSTORETYPE,  getFrameworkProp(CmsDeployProperty.SSL_KEYSTORETYPE));
+                               // keystore
+                               props.put(JettyHttpConstants.SSL_KEYSTORETYPE, getFrameworkProp(CmsDeployProperty.SSL_KEYSTORETYPE));
                                props.put(JettyHttpConstants.SSL_KEYSTORE, getFrameworkProp(CmsDeployProperty.SSL_KEYSTORE));
                                props.put(JettyHttpConstants.SSL_PASSWORD, getFrameworkProp(CmsDeployProperty.SSL_PASSWORD));
 
+                               // truststore
+                               props.put(JettyHttpConstants.SSL_TRUSTSTORETYPE,
+                                               getFrameworkProp(CmsDeployProperty.SSL_TRUSTSTORETYPE));
+                               props.put(JettyHttpConstants.SSL_TRUSTSTORE, getFrameworkProp(CmsDeployProperty.SSL_TRUSTSTORE));
+                               props.put(JettyHttpConstants.SSL_TRUSTSTOREPASSWORD,
+                                               getFrameworkProp(CmsDeployProperty.SSL_TRUSTSTOREPASSWORD));
+
                                // client certificate authentication
                                String wantClientAuth = getFrameworkProp(CmsDeployProperty.SSL_WANTCLIENTAUTH);
                                if (wantClientAuth != null)
index 155e6c882593df3e8b2f315fc495c34b969917c5..8ceb358ddc068d3e9f6eea41e8ae501f702f30b8 100644 (file)
@@ -16,4 +16,10 @@ interface JettyHttpConstants {
        static final String SSL_PROTOCOL = "ssl.protocol";
        static final String SSL_ALGORITHM = "ssl.algorithm";
        static final String SSL_KEYSTORETYPE = "ssl.keystoretype";
+
+       // Argeo
+       static final String SSL_TRUSTSTORE = "ssl.truststore";
+       static final String SSL_TRUSTSTOREPASSWORD = "ssl.truststorepassword";
+       static final String SSL_TRUSTSTORETYPE = "ssl.truststoretype";
+
 }
index 9d15143d790a56a4c4eb1981d530bd2ff835940f..e34049506b553ee3bd6948dab80de0092dc20c7f 100644 (file)
@@ -7,7 +7,11 @@ import javax.websocket.DeploymentException;
 import javax.websocket.server.ServerContainer;
 
 import org.eclipse.equinox.http.jetty.JettyCustomizer;
+import org.eclipse.jetty.server.ConnectionFactory;
+import org.eclipse.jetty.server.ServerConnector;
+import org.eclipse.jetty.server.SslConnectionFactory;
 import org.eclipse.jetty.servlet.ServletContextHandler;
+import org.eclipse.jetty.util.ssl.SslContextFactory;
 import org.eclipse.jetty.websocket.javax.server.config.JavaxWebSocketServletContainerInitializer;
 import org.eclipse.jetty.websocket.javax.server.config.JavaxWebSocketServletContainerInitializer.Configurator;
 import org.osgi.framework.BundleContext;
@@ -15,6 +19,10 @@ import org.osgi.framework.FrameworkUtil;
 
 /** Customises the Jetty HTTP server. */
 public class CmsJettyCustomizer extends JettyCustomizer {
+       static final String SSL_TRUSTSTORE = "ssl.truststore";
+       static final String SSL_TRUSTSTOREPASSWORD = "ssl.truststorepassword";
+       static final String SSL_TRUSTSTORETYPE = "ssl.truststoretype";
+
        private BundleContext bc = FrameworkUtil.getBundle(CmsJettyCustomizer.class).getBundleContext();
 
        public final static String WEBSOCKET_ENABLED = "argeo.websocket.enabled";
@@ -37,4 +45,20 @@ public class CmsJettyCustomizer extends JettyCustomizer {
                return super.customizeContext(context, settings);
 
        }
+
+       @Override
+       public Object customizeHttpsConnector(Object connector, Dictionary<String, ?> settings) {
+               ServerConnector httpsConnector = (ServerConnector) connector;
+               for (ConnectionFactory connectionFactory : httpsConnector.getConnectionFactories()) {
+                       if (connectionFactory instanceof SslConnectionFactory) {
+                               SslContextFactory.Server sslConnectionFactory = ((SslConnectionFactory) connectionFactory)
+                                               .getSslContextFactory();
+                               sslConnectionFactory.setTrustStorePath((String) settings.get(SSL_TRUSTSTORE));
+                               sslConnectionFactory.setTrustStoreType((String) settings.get(SSL_TRUSTSTORETYPE));
+                               sslConnectionFactory.setTrustStorePassword((String) settings.get(SSL_TRUSTSTOREPASSWORD));
+                       }
+               }
+               return super.customizeHttpsConnector(connector, settings);
+       }
+
 }
index 243c228513f54c7af549727966fd515507b7e020..639e73e37a59e2f5fecdff0b19f989624d1ec53a 100644 (file)
@@ -55,6 +55,12 @@ public enum CmsDeployProperty {
        SSL_PROTOCOL("argeo.ssl.protocol"),
        /** SSL algorithm to use. */
        SSL_ALGORITHM("argeo.ssl.algorithm"),
+       /** Custom SSL trust store. */
+       SSL_TRUSTSTORE("argeo.ssl.truststore"),
+       /** Custom SSL trust store type. */
+       SSL_TRUSTSTORETYPE("argeo.ssl.truststoretype"),
+       /** Custom SSL trust store type. */
+       SSL_TRUSTSTOREPASSWORD("argeo.ssl.truststorepassword"),
        //
        // WEBSOCKET
        //
index 674cfdf150a55fff6ec1fda1a9528fdfad538c72..806a57569449f8fec9bd4565411df03756d08227 100644 (file)
@@ -69,33 +69,15 @@ public class SpnegoHttpClient {
        }
 
        private static HttpClient openHttpClient(Subject subject) {
-               // disable https check
-               // jdk.internal.httpclient.disableHostnameVerification=true
-               HttpClient client = HttpClient.newBuilder().sslContext(insecureContext())
-//                             .authenticator(new Authenticator() {
-//                     public PasswordAuthentication getPasswordAuthentication() {
-//                             return null;
-//                     }
-//
-//             })
-                               .version(HttpClient.Version.HTTP_1_1).build();
+               HttpClient client = HttpClient.newBuilder() //
+//                             .sslContext(insecureContext()) //
+                               .version(HttpClient.Version.HTTP_1_1) //
+                               .build();
 
                return client;
-
-               // return client;
-//                     AuthPolicy.registerAuthScheme(SpnegoAuthScheme.NAME, SpnegoAuthScheme.class);
-//                     HttpParams params = DefaultHttpParams.getDefaultParams();
-//                     ArrayList<String> schemes = new ArrayList<>();
-//                     schemes.add(SpnegoAuthScheme.NAME);
-//                     params.setParameter(AuthPolicy.AUTH_SCHEME_PRIORITY, schemes);
-//                     params.setParameter(CredentialsProvider.PROVIDER, new HttpCredentialProvider());
-//                     HttpClient httpClient = new HttpClient();
-//                     httpClient.executeMethod(new GetMethod(("https://" + server + "/ipa/session/json")));
-//                     return httpClient;
-
        }
 
-       private static SSLContext insecureContext() {
+       static SSLContext insecureContext() {
                TrustManager[] noopTrustManager = new TrustManager[] { new X509TrustManager() {
                        public void checkClientTrusted(X509Certificate[] xcs, String string) {
                        }
index 126a7e68af8b308b23dfcd1e837bbd2095156166..cb07f43eb17eea2af0bf290c3445916fbb3198f8 100644 (file)
@@ -55,11 +55,17 @@ public class CmsStateImpl implements CmsState {
                deployPropertyDefaults.put(CmsDeployProperty.NODE_INIT, "../../init");
                deployPropertyDefaults.put(CmsDeployProperty.LOCALE, Locale.getDefault().toString());
 
+               // certificates
                deployPropertyDefaults.put(CmsDeployProperty.SSL_KEYSTORETYPE, PkiUtils.PKCS12);
-               deployPropertyDefaults.put(CmsDeployProperty.SSL_PASSWORD, "changeit");
+               deployPropertyDefaults.put(CmsDeployProperty.SSL_PASSWORD, PkiUtils.DEFAULT_KEYSTORE_PASSWORD);
                Path keyStorePath = getDataPath(PkiUtils.DEFAULT_KEYSTORE_PATH);
                deployPropertyDefaults.put(CmsDeployProperty.SSL_KEYSTORE, keyStorePath.toAbsolutePath().toString());
 
+               Path trustStorePath = getDataPath(PkiUtils.DEFAULT_TRUSTSTORE_PATH);
+               deployPropertyDefaults.put(CmsDeployProperty.SSL_TRUSTSTORETYPE, PkiUtils.PKCS12);
+               deployPropertyDefaults.put(CmsDeployProperty.SSL_TRUSTSTOREPASSWORD, PkiUtils.DEFAULT_KEYSTORE_PASSWORD);
+               deployPropertyDefaults.put(CmsDeployProperty.SSL_TRUSTSTORE, trustStorePath.toAbsolutePath().toString());
+
                this.deployPropertyDefaults = Collections.unmodifiableMap(deployPropertyDefaults);
        }
 
@@ -141,10 +147,12 @@ public class CmsStateImpl implements CmsState {
                Path pemCertPath = getDataPath(PkiUtils.DEFAULT_PEM_CERT_PATH);
                char[] keyStorePassword = getDeployProperty(CmsDeployProperty.SSL_PASSWORD).toCharArray();
 
+               // Keystore
                // if PEM files both exists, update the PKCS12 file
                if (Files.exists(pemCertPath) && Files.exists(pemKeyPath)) {
                        // TODO check certificate update time? monitor changes?
-                       KeyStore keyStore = PkiUtils.getKeyStore(keyStorePath, keyStorePassword, PkiUtils.PKCS12);
+                       KeyStore keyStore = PkiUtils.getKeyStore(keyStorePath, keyStorePassword,
+                                       getDeployProperty(CmsDeployProperty.SSL_KEYSTORETYPE));
                        try (Reader key = Files.newBufferedReader(pemKeyPath, StandardCharsets.US_ASCII);
                                        Reader cert = Files.newBufferedReader(pemCertPath, StandardCharsets.US_ASCII);) {
                                PkiUtils.loadPem(keyStore, key, keyStorePassword, cert);
@@ -156,6 +164,25 @@ public class CmsStateImpl implements CmsState {
                        }
                }
 
+               // Truststore
+               Path trustStorePath = Paths.get(getDeployProperty(CmsDeployProperty.SSL_TRUSTSTORE));
+               char[] trustStorePassword = getDeployProperty(CmsDeployProperty.SSL_TRUSTSTOREPASSWORD).toCharArray();
+
+               // IPA CA
+               Path ipaCaCertPath = Paths.get(PkiUtils.IPA_PEM_CA_CERT_PATH);
+               if (Files.exists(ipaCaCertPath)) {
+                       KeyStore trustStore = PkiUtils.getKeyStore(trustStorePath, trustStorePassword,
+                                       getDeployProperty(CmsDeployProperty.SSL_TRUSTSTORETYPE));
+                       try (Reader cert = Files.newBufferedReader(ipaCaCertPath, StandardCharsets.US_ASCII);) {
+                               PkiUtils.loadPem(trustStore, null, trustStorePassword, cert);
+                               PkiUtils.saveKeyStore(trustStorePath, trustStorePassword, trustStore);
+                               if (log.isDebugEnabled())
+                                       log.debug("IPA CA certificate stored in " + trustStorePath);
+                       } catch (IOException e) {
+                               log.error("Cannot trust CA certificate", e);
+                       }
+               }
+
                if (!Files.exists(keyStorePath))
                        PkiUtils.createSelfSignedKeyStore(keyStorePath, keyStorePassword, PkiUtils.PKCS12);
 //             props.put(JettyHttpConstants.SSL_KEYSTORETYPE, PkiUtils.PKCS12);
@@ -245,25 +272,30 @@ public class CmsStateImpl implements CmsState {
                        // try defaults
                        if (deployPropertyDefaults.containsKey(deployProperty)) {
                                value = deployPropertyDefaults.get(deployProperty);
+                               if (deployProperty.isSystemPropertyOnly())
+                                       System.setProperty(deployProperty.getProperty(), value);
                        }
-                       // try legacy properties
-                       String legacyProperty = switch (deployProperty) {
-                       case DIRECTORY -> "argeo.node.useradmin.uris";
-                       case DB_URL -> "argeo.node.dburl";
-                       case DB_USER -> "argeo.node.dbuser";
-                       case DB_PASSWORD -> "argeo.node.dbpassword";
-                       case HTTP_PORT -> "org.osgi.service.http.port";
-                       case HTTPS_PORT -> "org.osgi.service.http.port.secure";
-                       case HOST -> "org.eclipse.equinox.http.jetty.http.host";
-                       case LOCALE -> "argeo.i18n.defaultLocale";
-
-                       default -> null;
-                       };
-                       if (legacyProperty != null) {
-                               value = doGetDeployProperty(legacyProperty);
-                               if (value != null) {
-                                       log.warn("Retrieved deploy property " + deployProperty.getProperty()
-                                                       + " through deprecated property " + legacyProperty);
+
+                       if (value == null) {
+                               // try legacy properties
+                               String legacyProperty = switch (deployProperty) {
+                               case DIRECTORY -> "argeo.node.useradmin.uris";
+                               case DB_URL -> "argeo.node.dburl";
+                               case DB_USER -> "argeo.node.dbuser";
+                               case DB_PASSWORD -> "argeo.node.dbpassword";
+                               case HTTP_PORT -> "org.osgi.service.http.port";
+                               case HTTPS_PORT -> "org.osgi.service.http.port.secure";
+                               case HOST -> "org.eclipse.equinox.http.jetty.http.host";
+                               case LOCALE -> "argeo.i18n.defaultLocale";
+
+                               default -> null;
+                               };
+                               if (legacyProperty != null) {
+                                       value = doGetDeployProperty(legacyProperty);
+                                       if (value != null) {
+                                               log.warn("Retrieved deploy property " + deployProperty.getProperty()
+                                                               + " through deprecated property " + legacyProperty);
+                                       }
                                }
                        }
                }
index a90d598912ceacfaa93eb49862c7a92d962960b1..3acc95eedef8f632163dc97662cd4b2a4aa1d8b5 100644 (file)
@@ -12,6 +12,7 @@ import java.security.GeneralSecurityException;
 import java.security.KeyPair;
 import java.security.KeyPairGenerator;
 import java.security.KeyStore;
+import java.security.KeyStore.TrustedCertificateEntry;
 import java.security.KeyStoreException;
 import java.security.PrivateKey;
 import java.security.SecureRandom;
@@ -49,17 +50,29 @@ import org.bouncycastle.pkcs.PKCSException;
 class PkiUtils {
        private final static CmsLog log = CmsLog.getLog(PkiUtils.class);
 
-       public final static String PKCS12 = "PKCS12";
-       public static final String DEFAULT_KEYSTORE_PATH = KernelConstants.DIR_NODE + '/' + CmsConstants.NODE + ".p12";
+       final static String PKCS12 = "PKCS12";
+       final static String JKS = "JKS";
 
-       public static final String DEFAULT_PEM_KEY_PATH = KernelConstants.DIR_NODE + '/' + CmsConstants.NODE + ".key";
+       static final String DEFAULT_KEYSTORE_PATH = KernelConstants.DIR_NODE + '/' + CmsConstants.NODE + ".p12";
 
-       public static final String DEFAULT_PEM_CERT_PATH = KernelConstants.DIR_NODE + '/' + CmsConstants.NODE + ".crt";
+       static final String DEFAULT_TRUSTSTORE_PATH = KernelConstants.DIR_NODE + "/trusted.p12";
+
+       static final String DEFAULT_PEM_KEY_PATH = KernelConstants.DIR_NODE + '/' + CmsConstants.NODE + ".key";
+
+       static final String DEFAULT_PEM_CERT_PATH = KernelConstants.DIR_NODE + '/' + CmsConstants.NODE + ".crt";
+
+       static final String IPA_PEM_CA_CERT_PATH = "/etc/ipa/ca.crt";
+
+       static final String DEFAULT_KEYSTORE_PASSWORD = "changeit";
 
        private final static String SECURITY_PROVIDER;
+       private final static String BC_PROVIDER;
        static {
                Security.addProvider(new BouncyCastleProvider());
-               SECURITY_PROVIDER = "BC";
+               // BouncyCastle does not store trusted certificates properly
+               // TODO report it
+               BC_PROVIDER = "BC";
+               SECURITY_PROVIDER = "SUN";
        }
 
        public static X509Certificate generateSelfSignedCertificate(KeyStore keyStore, X500Principal x500Principal,
@@ -89,7 +102,7 @@ class PkiUtils {
 
        public static KeyStore getKeyStore(Path keyStoreFile, char[] keyStorePassword, String keyStoreType) {
                try {
-                       KeyStore store = KeyStore.getInstance(keyStoreType, SECURITY_PROVIDER);
+                       KeyStore store = KeyStore.getInstance(keyStoreType, "SunJSSE");
                        if (Files.exists(keyStoreFile)) {
                                try (InputStream fis = Files.newInputStream(keyStoreFile)) {
                                        store.load(fis, keyStorePassword);
@@ -150,11 +163,16 @@ class PkiUtils {
 //     }
 
        public static void loadPem(KeyStore keyStore, Reader key, char[] keyPassword, Reader cert) {
-               PrivateKey privateKey = loadPemPrivateKey(key, keyPassword);
-               X509Certificate certificate = loadPemCertificate(cert);
                try {
-                       keyStore.setKeyEntry(certificate.getSubjectX500Principal().getName(), privateKey, keyPassword,
-                                       new java.security.cert.Certificate[] { certificate });
+                       X509Certificate certificate = loadPemCertificate(cert);
+                       if (key != null) {
+                               PrivateKey privateKey = loadPemPrivateKey(key, keyPassword);
+                               keyStore.setKeyEntry(certificate.getSubjectX500Principal().getName(), privateKey, keyPassword,
+                                               new java.security.cert.Certificate[] { certificate });
+                       } else {
+                               TrustedCertificateEntry trustedCertificateEntry = new TrustedCertificateEntry(certificate);
+                               keyStore.setEntry(certificate.getSubjectX500Principal().getName(), trustedCertificateEntry, null);
+                       }
                } catch (KeyStoreException e) {
                        throw new RuntimeException("Cannot store PEM certificate", e);
                }
@@ -162,7 +180,7 @@ class PkiUtils {
 
        public static PrivateKey loadPemPrivateKey(Reader reader, char[] keyPassword) {
                try (PEMParser pemParser = new PEMParser(reader)) {
-                       JcaPEMKeyConverter converter = new JcaPEMKeyConverter().setProvider("BC");
+                       JcaPEMKeyConverter converter = new JcaPEMKeyConverter().setProvider(BC_PROVIDER);
                        Object object = pemParser.readObject();
                        PrivateKeyInfo privateKeyInfo;
                        if (object instanceof PKCS8EncryptedPrivateKeyInfo) {
index 9ed0b21c6ecebb60ae62442964da29691e3b21a3..376c51edc4b1fbc0b53cb1f109d7d8fdf25186fc 100644 (file)
@@ -37,12 +37,16 @@ public class DnsBrowser implements Closeable {
                Hashtable<String, Object> env = new Hashtable<>();
                env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.dns.DnsContextFactory");
                if (!dnsServerUrls.isEmpty()) {
+                       boolean specified = false;
                        StringJoiner providerUrl = new StringJoiner(" ");
                        for (String dnsUrl : dnsServerUrls) {
-                               if (dnsUrl != null)
+                               if (dnsUrl != null) {
                                        providerUrl.add(dnsUrl);
+                                       specified = true;
+                               }
                        }
-                       env.put(Context.PROVIDER_URL, providerUrl.toString());
+                       if (specified)
+                               env.put(Context.PROVIDER_URL, providerUrl.toString());
                }
                initialCtx = new InitialDirContext(env);
        }