| /* |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you 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 org.apache.bookkeeper.tls; |
| |
| import com.google.common.base.Strings; |
| import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; |
| import io.netty.buffer.ByteBufAllocator; |
| import io.netty.handler.ssl.ClientAuth; |
| import io.netty.handler.ssl.OpenSsl; |
| import io.netty.handler.ssl.SslContext; |
| import io.netty.handler.ssl.SslContextBuilder; |
| import io.netty.handler.ssl.SslHandler; |
| import io.netty.handler.ssl.SslProvider; |
| import java.io.File; |
| import java.io.FileInputStream; |
| import java.io.IOException; |
| import java.nio.charset.StandardCharsets; |
| import java.security.KeyStore; |
| import java.security.KeyStoreException; |
| import java.security.NoSuchAlgorithmException; |
| import java.security.NoSuchProviderException; |
| import java.security.Provider; |
| import java.security.Security; |
| import java.security.UnrecoverableKeyException; |
| import java.security.cert.CertificateException; |
| import java.security.spec.InvalidKeySpecException; |
| import java.util.Arrays; |
| import java.util.concurrent.TimeUnit; |
| import javax.net.ssl.KeyManagerFactory; |
| import javax.net.ssl.SSLParameters; |
| import javax.net.ssl.TrustManagerFactory; |
| import lombok.extern.slf4j.Slf4j; |
| import org.apache.bookkeeper.conf.AbstractConfiguration; |
| import org.apache.bookkeeper.conf.ClientConfiguration; |
| import org.apache.bookkeeper.conf.ServerConfiguration; |
| import org.apache.commons.io.FileUtils; |
| |
| /** |
| * A factory to manage TLS contexts. |
| */ |
| @Slf4j |
| public class TLSContextFactory implements SecurityHandlerFactory { |
| |
| public static final Provider BC_PROVIDER = getProvider(); |
| public static final String BC_FIPS_PROVIDER_CLASS = "org.bouncycastle.jcajce.provider.BouncyCastleFipsProvider"; |
| public static final String BC_NON_FIPS_PROVIDER_CLASS = "org.bouncycastle.jce.provider.BouncyCastleProvider"; |
| |
| // Security.getProvider("BC") / Security.getProvider("BCFIPS"). |
| // also used to get Factories. e.g. CertificateFactory.getInstance("X.509", "BCFIPS") |
| public static final String BC_FIPS = "BCFIPS"; |
| public static final String BC = "BC"; |
| |
| /** |
| * Get Bouncy Castle provider, and call Security.addProvider(provider) if success. |
| */ |
| public static Provider getProvider() { |
| boolean isProviderInstalled = |
| Security.getProvider(BC) != null || Security.getProvider(BC_FIPS) != null; |
| |
| if (isProviderInstalled) { |
| Provider provider = Security.getProvider(BC) != null |
| ? Security.getProvider(BC) |
| : Security.getProvider(BC_FIPS); |
| if (log.isDebugEnabled()) { |
| log.debug("Already instantiated Bouncy Castle provider {}", provider.getName()); |
| } |
| return provider; |
| } |
| |
| // Not installed, try load from class path |
| try { |
| return getBCProviderFromClassPath(); |
| } catch (Exception e) { |
| log.warn("Not able to get Bouncy Castle provider for both FIPS and Non-FIPS from class path:", e); |
| throw new RuntimeException(e); |
| } |
| } |
| |
| /** |
| * Get Bouncy Castle provider from classpath, and call Security.addProvider. |
| * Throw Exception if failed. |
| */ |
| public static Provider getBCProviderFromClassPath() throws Exception { |
| Class clazz; |
| try { |
| clazz = Class.forName(BC_FIPS_PROVIDER_CLASS); |
| } catch (ClassNotFoundException cnf) { |
| if (log.isDebugEnabled()) { |
| log.debug("Not able to get Bouncy Castle provider: {}, try to get FIPS provider {}", |
| BC_NON_FIPS_PROVIDER_CLASS, BC_FIPS_PROVIDER_CLASS); |
| } |
| // attempt to use the NON_FIPS provider. |
| clazz = Class.forName(BC_NON_FIPS_PROVIDER_CLASS); |
| |
| } |
| |
| @SuppressWarnings("unchecked") |
| Provider provider = (Provider) clazz.getDeclaredConstructor().newInstance(); |
| Security.addProvider(provider); |
| if (log.isDebugEnabled()) { |
| log.debug("Found and Instantiated Bouncy Castle provider in classpath {}", provider.getName()); |
| } |
| return provider; |
| } |
| |
| /** |
| * Supported Key File Types. |
| */ |
| public enum KeyStoreType { |
| PKCS12("PKCS12"), |
| JKS("JKS"), |
| PEM("PEM"); |
| |
| private String str; |
| |
| KeyStoreType(String str) { |
| this.str = str; |
| } |
| |
| @Override |
| public String toString() { |
| return this.str; |
| } |
| } |
| |
| private static final String TLSCONTEXT_HANDLER_NAME = "tls"; |
| private NodeType type; |
| private String[] protocols; |
| private String[] ciphers; |
| private volatile SslContext sslContext; |
| private ByteBufAllocator allocator; |
| private AbstractConfiguration config; |
| private FileModifiedTimeUpdater tlsCertificateFilePath, tlsKeyStoreFilePath, tlsKeyStorePasswordFilePath, |
| tlsTrustStoreFilePath, tlsTrustStorePasswordFilePath; |
| private long certRefreshTime; |
| private volatile long certLastRefreshTime; |
| private boolean isServerCtx; |
| |
| private String getPasswordFromFile(String path) throws IOException { |
| byte[] pwd; |
| File passwdFile = new File(path); |
| if (passwdFile.length() == 0) { |
| return ""; |
| } |
| pwd = FileUtils.readFileToByteArray(passwdFile); |
| return new String(pwd, StandardCharsets.UTF_8); |
| } |
| |
| @SuppressFBWarnings( |
| value = "OBL_UNSATISFIED_OBLIGATION", |
| justification = "work around for java 9: https://github.com/spotbugs/spotbugs/issues/493") |
| private KeyStore loadKeyStore(String keyStoreType, String keyStoreLocation, String keyStorePassword) |
| throws KeyStoreException, NoSuchAlgorithmException, CertificateException, IOException { |
| KeyStore ks = KeyStore.getInstance(keyStoreType); |
| |
| try (FileInputStream ksin = new FileInputStream(keyStoreLocation)) { |
| ks.load(ksin, keyStorePassword.trim().toCharArray()); |
| } |
| return ks; |
| } |
| |
| @Override |
| public String getHandlerName() { |
| return TLSCONTEXT_HANDLER_NAME; |
| } |
| |
| private KeyManagerFactory initKeyManagerFactory(String keyStoreType, String keyStoreLocation, |
| String keyStorePasswordPath) throws SecurityException, KeyStoreException, NoSuchAlgorithmException, |
| CertificateException, IOException, UnrecoverableKeyException, InvalidKeySpecException { |
| KeyManagerFactory kmf = null; |
| |
| if (Strings.isNullOrEmpty(keyStoreLocation)) { |
| log.error("Key store location cannot be empty when Mutual Authentication is enabled!"); |
| throw new SecurityException("Key store location cannot be empty when Mutual Authentication is enabled!"); |
| } |
| |
| String keyStorePassword = ""; |
| if (!Strings.isNullOrEmpty(keyStorePasswordPath)) { |
| keyStorePassword = getPasswordFromFile(keyStorePasswordPath); |
| } |
| |
| // Initialize key file |
| KeyStore ks = loadKeyStore(keyStoreType, keyStoreLocation, keyStorePassword); |
| kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); |
| kmf.init(ks, keyStorePassword.trim().toCharArray()); |
| |
| return kmf; |
| } |
| |
| private TrustManagerFactory initTrustManagerFactory(String trustStoreType, String trustStoreLocation, |
| String trustStorePasswordPath) |
| throws KeyStoreException, NoSuchAlgorithmException, CertificateException, IOException, SecurityException { |
| TrustManagerFactory tmf; |
| |
| if (Strings.isNullOrEmpty(trustStoreLocation)) { |
| log.error("Trust Store location cannot be empty!"); |
| throw new SecurityException("Trust Store location cannot be empty!"); |
| } |
| |
| String trustStorePassword = ""; |
| if (!Strings.isNullOrEmpty(trustStorePasswordPath)) { |
| trustStorePassword = getPasswordFromFile(trustStorePasswordPath); |
| } |
| |
| // Initialize trust file |
| KeyStore ts = loadKeyStore(trustStoreType, trustStoreLocation, trustStorePassword); |
| tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); |
| tmf.init(ts); |
| |
| return tmf; |
| } |
| |
| private SslProvider getTLSProvider(String sslProvider) { |
| if (sslProvider.trim().equalsIgnoreCase("OpenSSL")) { |
| if (OpenSsl.isAvailable()) { |
| log.info("Security provider - OpenSSL"); |
| return SslProvider.OPENSSL; |
| } |
| |
| Throwable causeUnavailable = OpenSsl.unavailabilityCause(); |
| log.warn("OpenSSL Unavailable: ", causeUnavailable); |
| |
| log.info("Security provider - JDK"); |
| return SslProvider.JDK; |
| } |
| |
| log.info("Security provider - JDK"); |
| return SslProvider.JDK; |
| } |
| |
| private void createClientContext() |
| throws SecurityException, KeyStoreException, NoSuchAlgorithmException, CertificateException, IOException, |
| UnrecoverableKeyException, InvalidKeySpecException, NoSuchProviderException { |
| ClientConfiguration clientConf = (ClientConfiguration) config; |
| markAutoCertRefresh(clientConf.getTLSCertificatePath(), clientConf.getTLSKeyStore(), |
| clientConf.getTLSKeyStorePasswordPath(), clientConf.getTLSTrustStore(), |
| clientConf.getTLSTrustStorePasswordPath()); |
| updateClientContext(); |
| } |
| |
| private synchronized void updateClientContext() |
| throws SecurityException, KeyStoreException, NoSuchAlgorithmException, CertificateException, IOException, |
| UnrecoverableKeyException, InvalidKeySpecException, NoSuchProviderException { |
| final SslContextBuilder sslContextBuilder; |
| final ClientConfiguration clientConf; |
| final SslProvider provider; |
| final boolean clientAuthentication; |
| |
| // get key-file and trust-file locations and passwords |
| if (!(config instanceof ClientConfiguration)) { |
| throw new SecurityException("Client configruation not provided"); |
| } |
| |
| clientConf = (ClientConfiguration) config; |
| provider = getTLSProvider(clientConf.getTLSProvider()); |
| clientAuthentication = clientConf.getTLSClientAuthentication(); |
| |
| switch (KeyStoreType.valueOf(clientConf.getTLSTrustStoreType())) { |
| case PEM: |
| if (Strings.isNullOrEmpty(clientConf.getTLSTrustStore())) { |
| throw new SecurityException("CA Certificate required"); |
| } |
| |
| sslContextBuilder = SslContextBuilder.forClient() |
| .trustManager(new File(clientConf.getTLSTrustStore())) |
| .ciphers(null) |
| .sessionCacheSize(0) |
| .sessionTimeout(0) |
| .sslProvider(provider) |
| .clientAuth(ClientAuth.REQUIRE); |
| |
| break; |
| case JKS: |
| // falling thru, same as PKCS12 |
| case PKCS12: |
| TrustManagerFactory tmf = initTrustManagerFactory(clientConf.getTLSTrustStoreType(), |
| clientConf.getTLSTrustStore(), clientConf.getTLSTrustStorePasswordPath()); |
| |
| sslContextBuilder = SslContextBuilder.forClient() |
| .trustManager(tmf) |
| .ciphers(null) |
| .sessionCacheSize(0) |
| .sessionTimeout(0) |
| .sslProvider(provider) |
| .clientAuth(ClientAuth.REQUIRE); |
| |
| break; |
| default: |
| throw new SecurityException("Invalid Truststore type: " + clientConf.getTLSTrustStoreType()); |
| } |
| |
| if (clientAuthentication) { |
| switch (KeyStoreType.valueOf(clientConf.getTLSKeyStoreType())) { |
| case PEM: |
| final String keyPassword; |
| |
| if (Strings.isNullOrEmpty(clientConf.getTLSCertificatePath())) { |
| throw new SecurityException("Valid Certificate is missing"); |
| } |
| |
| if (Strings.isNullOrEmpty(clientConf.getTLSKeyStore())) { |
| throw new SecurityException("Valid Key is missing"); |
| } |
| |
| if (!Strings.isNullOrEmpty(clientConf.getTLSKeyStorePasswordPath())) { |
| keyPassword = getPasswordFromFile(clientConf.getTLSKeyStorePasswordPath()); |
| } else { |
| keyPassword = null; |
| } |
| |
| sslContextBuilder.keyManager(new File(clientConf.getTLSCertificatePath()), |
| new File(clientConf.getTLSKeyStore()), keyPassword); |
| break; |
| case JKS: |
| // falling thru, same as PKCS12 |
| case PKCS12: |
| KeyManagerFactory kmf = initKeyManagerFactory(clientConf.getTLSKeyStoreType(), |
| clientConf.getTLSKeyStore(), clientConf.getTLSKeyStorePasswordPath()); |
| |
| sslContextBuilder.keyManager(kmf); |
| break; |
| default: |
| throw new SecurityException("Invalid Keyfile type" + clientConf.getTLSKeyStoreType()); |
| } |
| } |
| |
| sslContext = sslContextBuilder.build(); |
| certLastRefreshTime = System.currentTimeMillis(); |
| } |
| |
| private void createServerContext() |
| throws SecurityException, KeyStoreException, NoSuchAlgorithmException, CertificateException, IOException, |
| UnrecoverableKeyException, InvalidKeySpecException, IllegalArgumentException { |
| isServerCtx = true; |
| ServerConfiguration clientConf = (ServerConfiguration) config; |
| markAutoCertRefresh(clientConf.getTLSCertificatePath(), clientConf.getTLSKeyStore(), |
| clientConf.getTLSKeyStorePasswordPath(), clientConf.getTLSTrustStore(), |
| clientConf.getTLSTrustStorePasswordPath()); |
| updateServerContext(); |
| } |
| |
| private synchronized SslContext getSSLContext() { |
| long now = System.currentTimeMillis(); |
| if ((certRefreshTime > 0 && now > (certLastRefreshTime + certRefreshTime))) { |
| if (tlsCertificateFilePath.checkAndRefresh() || tlsKeyStoreFilePath.checkAndRefresh() |
| || tlsKeyStorePasswordFilePath.checkAndRefresh() || tlsTrustStoreFilePath.checkAndRefresh() |
| || tlsTrustStorePasswordFilePath.checkAndRefresh()) { |
| try { |
| log.info("Updating tls certs certFile={}, keyStoreFile={}, trustStoreFile={}", |
| tlsCertificateFilePath.getFileName(), tlsKeyStoreFilePath.getFileName(), |
| tlsTrustStoreFilePath.getFileName()); |
| if (isServerCtx) { |
| updateServerContext(); |
| } else { |
| updateClientContext(); |
| } |
| } catch (Exception e) { |
| log.info("Failed to refresh tls certs", e); |
| } |
| } |
| } |
| return sslContext; |
| } |
| |
| private synchronized void updateServerContext() throws SecurityException, KeyStoreException, |
| NoSuchAlgorithmException, CertificateException, IOException, UnrecoverableKeyException, |
| InvalidKeySpecException, IllegalArgumentException { |
| final SslContextBuilder sslContextBuilder; |
| final ServerConfiguration serverConf; |
| final SslProvider provider; |
| final boolean clientAuthentication; |
| |
| // get key-file and trust-file locations and passwords |
| if (!(config instanceof ServerConfiguration)) { |
| throw new SecurityException("Server configruation not provided"); |
| } |
| |
| serverConf = (ServerConfiguration) config; |
| provider = getTLSProvider(serverConf.getTLSProvider()); |
| clientAuthentication = serverConf.getTLSClientAuthentication(); |
| |
| switch (KeyStoreType.valueOf(serverConf.getTLSKeyStoreType())) { |
| case PEM: |
| final String keyPassword; |
| |
| if (Strings.isNullOrEmpty(serverConf.getTLSKeyStore())) { |
| throw new SecurityException("Key path is required"); |
| } |
| |
| if (Strings.isNullOrEmpty(serverConf.getTLSCertificatePath())) { |
| throw new SecurityException("Certificate path is required"); |
| } |
| |
| if (!Strings.isNullOrEmpty(serverConf.getTLSKeyStorePasswordPath())) { |
| keyPassword = getPasswordFromFile(serverConf.getTLSKeyStorePasswordPath()); |
| } else { |
| keyPassword = null; |
| } |
| |
| sslContextBuilder = SslContextBuilder |
| .forServer(new File(serverConf.getTLSCertificatePath()), |
| new File(serverConf.getTLSKeyStore()), keyPassword) |
| .ciphers(null) |
| .sessionCacheSize(0) |
| .sessionTimeout(0) |
| .sslProvider(provider) |
| .startTls(true); |
| |
| break; |
| case JKS: |
| // falling thru, same as PKCS12 |
| case PKCS12: |
| KeyManagerFactory kmf = initKeyManagerFactory(serverConf.getTLSKeyStoreType(), |
| serverConf.getTLSKeyStore(), |
| serverConf.getTLSKeyStorePasswordPath()); |
| |
| sslContextBuilder = SslContextBuilder.forServer(kmf) |
| .ciphers(null) |
| .sessionCacheSize(0) |
| .sessionTimeout(0) |
| .sslProvider(provider) |
| .startTls(true); |
| |
| break; |
| default: |
| throw new SecurityException("Invalid Keyfile type" + serverConf.getTLSKeyStoreType()); |
| } |
| |
| if (clientAuthentication) { |
| sslContextBuilder.clientAuth(ClientAuth.REQUIRE); |
| |
| switch (KeyStoreType.valueOf(serverConf.getTLSTrustStoreType())) { |
| case PEM: |
| if (Strings.isNullOrEmpty(serverConf.getTLSTrustStore())) { |
| throw new SecurityException("CA Certificate chain is required"); |
| } |
| sslContextBuilder.trustManager(new File(serverConf.getTLSTrustStore())); |
| break; |
| case JKS: |
| // falling thru, same as PKCS12 |
| case PKCS12: |
| TrustManagerFactory tmf = initTrustManagerFactory(serverConf.getTLSTrustStoreType(), |
| serverConf.getTLSTrustStore(), serverConf.getTLSTrustStorePasswordPath()); |
| sslContextBuilder.trustManager(tmf); |
| break; |
| default: |
| throw new SecurityException("Invalid Truststore type" + serverConf.getTLSTrustStoreType()); |
| } |
| } |
| |
| sslContext = sslContextBuilder.build(); |
| certLastRefreshTime = System.currentTimeMillis(); |
| } |
| |
| @Override |
| public synchronized void init(NodeType type, AbstractConfiguration conf, ByteBufAllocator allocator) |
| throws SecurityException { |
| this.allocator = allocator; |
| this.config = conf; |
| this.type = type; |
| final String enabledProtocols; |
| final String enabledCiphers; |
| certRefreshTime = TimeUnit.SECONDS.toMillis(conf.getTLSCertFilesRefreshDurationSeconds()); |
| |
| enabledCiphers = conf.getTLSEnabledCipherSuites(); |
| enabledProtocols = conf.getTLSEnabledProtocols(); |
| |
| try { |
| switch (type) { |
| case Client: |
| createClientContext(); |
| break; |
| case Server: |
| createServerContext(); |
| break; |
| default: |
| throw new SecurityException(new IllegalArgumentException("Invalid NodeType")); |
| } |
| |
| if (enabledProtocols != null && !enabledProtocols.isEmpty()) { |
| protocols = enabledProtocols.split(","); |
| } |
| |
| if (enabledCiphers != null && !enabledCiphers.isEmpty()) { |
| ciphers = enabledCiphers.split(","); |
| } |
| } catch (KeyStoreException e) { |
| throw new RuntimeException("Standard keystore type missing", e); |
| } catch (NoSuchAlgorithmException e) { |
| throw new RuntimeException("Standard algorithm missing", e); |
| } catch (CertificateException e) { |
| throw new SecurityException("Unable to load keystore", e); |
| } catch (IOException e) { |
| throw new SecurityException("Error initializing SSLContext", e); |
| } catch (UnrecoverableKeyException e) { |
| throw new SecurityException("Unable to load key manager, possibly bad password", e); |
| } catch (InvalidKeySpecException e) { |
| throw new SecurityException("Unable to load key manager", e); |
| } catch (IllegalArgumentException e) { |
| throw new SecurityException("Invalid TLS configuration", e); |
| } catch (NoSuchProviderException e) { |
| throw new SecurityException("No such provider", e); |
| } |
| } |
| |
| @Override |
| public SslHandler newTLSHandler() { |
| return this.newTLSHandler(null, -1); |
| } |
| |
| @Override |
| public SslHandler newTLSHandler(String peer, int port) { |
| SslHandler sslHandler = getSSLContext().newHandler(allocator, peer, port); |
| |
| if (protocols != null && protocols.length != 0) { |
| sslHandler.engine().setEnabledProtocols(protocols); |
| } |
| if (log.isDebugEnabled()) { |
| log.debug("Enabled cipher protocols: {} ", Arrays.toString(sslHandler.engine().getEnabledProtocols())); |
| } |
| |
| if (ciphers != null && ciphers.length != 0) { |
| sslHandler.engine().setEnabledCipherSuites(ciphers); |
| } |
| if (log.isDebugEnabled()) { |
| log.debug("Enabled cipher suites: {} ", Arrays.toString(sslHandler.engine().getEnabledCipherSuites())); |
| } |
| |
| if (type == NodeType.Client && ((ClientConfiguration) config).getHostnameVerificationEnabled()) { |
| SSLParameters sslParameters = sslHandler.engine().getSSLParameters(); |
| sslParameters.setEndpointIdentificationAlgorithm("HTTPS"); |
| sslHandler.engine().setSSLParameters(sslParameters); |
| if (log.isDebugEnabled()) { |
| log.debug("Enabled endpointIdentificationAlgorithm: HTTPS"); |
| } |
| } |
| |
| return sslHandler; |
| } |
| |
| private void markAutoCertRefresh(String tlsCertificatePath, String tlsKeyStore, String tlsKeyStorePasswordPath, |
| String tlsTrustStore, String tlsTrustStorePasswordPath) { |
| tlsCertificateFilePath = new FileModifiedTimeUpdater(tlsCertificatePath); |
| tlsKeyStoreFilePath = new FileModifiedTimeUpdater(tlsKeyStore); |
| tlsKeyStorePasswordFilePath = new FileModifiedTimeUpdater(tlsKeyStorePasswordPath); |
| tlsTrustStoreFilePath = new FileModifiedTimeUpdater(tlsTrustStore); |
| tlsTrustStorePasswordFilePath = new FileModifiedTimeUpdater(tlsTrustStorePasswordPath); |
| } |
| } |