From 8302ed5e76967f1d618b59ebe4ae11223e5037c3 Mon Sep 17 00:00:00 2001 From: Mathieu Baudier Date: Fri, 1 Jul 2022 09:43:15 +0200 Subject: [PATCH] SSL truststore working. --- .../servlet/internal/jetty/JettyConfig.java | 22 ++++-- .../internal/jetty/JettyHttpConstants.java | 6 ++ .../equinox/jetty/CmsJettyCustomizer.java | 24 +++++++ .../src/org/argeo/cms/CmsDeployProperty.java | 6 ++ .../http/client/SpnegoHttpClient.java | 28 ++------ .../cms/internal/runtime/CmsStateImpl.java | 72 +++++++++++++------ .../argeo/cms/internal/runtime/PkiUtils.java | 40 ++++++++--- .../org/argeo/util/naming/dns/DnsBrowser.java | 8 ++- 8 files changed, 145 insertions(+), 61 deletions(-) diff --git a/eclipse/org.argeo.cms.servlet/src/org/argeo/cms/servlet/internal/jetty/JettyConfig.java b/eclipse/org.argeo.cms.servlet/src/org/argeo/cms/servlet/internal/jetty/JettyConfig.java index 64e33d3ec..0e2a3e5ca 100644 --- a/eclipse/org.argeo.cms.servlet/src/org/argeo/cms/servlet/internal/jetty/JettyConfig.java +++ b/eclipse/org.argeo.cms.servlet/src/org/argeo/cms/servlet/internal/jetty/JettyConfig.java @@ -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) diff --git a/eclipse/org.argeo.cms.servlet/src/org/argeo/cms/servlet/internal/jetty/JettyHttpConstants.java b/eclipse/org.argeo.cms.servlet/src/org/argeo/cms/servlet/internal/jetty/JettyHttpConstants.java index 155e6c882..8ceb358dd 100644 --- a/eclipse/org.argeo.cms.servlet/src/org/argeo/cms/servlet/internal/jetty/JettyHttpConstants.java +++ b/eclipse/org.argeo.cms.servlet/src/org/argeo/cms/servlet/internal/jetty/JettyHttpConstants.java @@ -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"; + } diff --git a/eclipse/org.argeo.ext.equinox.jetty/src/org/argeo/equinox/jetty/CmsJettyCustomizer.java b/eclipse/org.argeo.ext.equinox.jetty/src/org/argeo/equinox/jetty/CmsJettyCustomizer.java index 9d15143d7..e34049506 100644 --- a/eclipse/org.argeo.ext.equinox.jetty/src/org/argeo/equinox/jetty/CmsJettyCustomizer.java +++ b/eclipse/org.argeo.ext.equinox.jetty/src/org/argeo/equinox/jetty/CmsJettyCustomizer.java @@ -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 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); + } + } diff --git a/org.argeo.cms/src/org/argeo/cms/CmsDeployProperty.java b/org.argeo.cms/src/org/argeo/cms/CmsDeployProperty.java index 243c22851..639e73e37 100644 --- a/org.argeo.cms/src/org/argeo/cms/CmsDeployProperty.java +++ b/org.argeo.cms/src/org/argeo/cms/CmsDeployProperty.java @@ -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 // diff --git a/org.argeo.cms/src/org/argeo/cms/internal/http/client/SpnegoHttpClient.java b/org.argeo.cms/src/org/argeo/cms/internal/http/client/SpnegoHttpClient.java index 674cfdf15..806a57569 100644 --- a/org.argeo.cms/src/org/argeo/cms/internal/http/client/SpnegoHttpClient.java +++ b/org.argeo.cms/src/org/argeo/cms/internal/http/client/SpnegoHttpClient.java @@ -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 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) { } diff --git a/org.argeo.cms/src/org/argeo/cms/internal/runtime/CmsStateImpl.java b/org.argeo.cms/src/org/argeo/cms/internal/runtime/CmsStateImpl.java index 126a7e68a..cb07f43eb 100644 --- a/org.argeo.cms/src/org/argeo/cms/internal/runtime/CmsStateImpl.java +++ b/org.argeo.cms/src/org/argeo/cms/internal/runtime/CmsStateImpl.java @@ -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); + } } } } diff --git a/org.argeo.cms/src/org/argeo/cms/internal/runtime/PkiUtils.java b/org.argeo.cms/src/org/argeo/cms/internal/runtime/PkiUtils.java index a90d59891..3acc95eed 100644 --- a/org.argeo.cms/src/org/argeo/cms/internal/runtime/PkiUtils.java +++ b/org.argeo.cms/src/org/argeo/cms/internal/runtime/PkiUtils.java @@ -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) { diff --git a/org.argeo.util/src/org/argeo/util/naming/dns/DnsBrowser.java b/org.argeo.util/src/org/argeo/util/naming/dns/DnsBrowser.java index 9ed0b21c6..376c51edc 100644 --- a/org.argeo.util/src/org/argeo/util/naming/dns/DnsBrowser.java +++ b/org.argeo.util/src/org/argeo/util/naming/dns/DnsBrowser.java @@ -37,12 +37,16 @@ public class DnsBrowser implements Closeable { Hashtable 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); } -- 2.30.2