001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *     http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018package org.apache.hadoop.hbase.io.crypto.tls;
019
020import java.io.IOException;
021import java.nio.file.Path;
022import java.nio.file.Paths;
023import java.nio.file.StandardWatchEventKinds;
024import java.nio.file.WatchEvent;
025import java.security.GeneralSecurityException;
026import java.security.KeyStore;
027import java.security.Security;
028import java.security.cert.PKIXBuilderParameters;
029import java.security.cert.X509CertSelector;
030import java.util.ArrayList;
031import java.util.Arrays;
032import java.util.List;
033import java.util.Objects;
034import java.util.Set;
035import java.util.concurrent.atomic.AtomicReference;
036import javax.net.ssl.CertPathTrustManagerParameters;
037import javax.net.ssl.KeyManager;
038import javax.net.ssl.KeyManagerFactory;
039import javax.net.ssl.TrustManager;
040import javax.net.ssl.TrustManagerFactory;
041import javax.net.ssl.X509ExtendedTrustManager;
042import javax.net.ssl.X509KeyManager;
043import javax.net.ssl.X509TrustManager;
044import org.apache.hadoop.conf.Configuration;
045import org.apache.hadoop.hbase.exceptions.KeyManagerException;
046import org.apache.hadoop.hbase.exceptions.SSLContextException;
047import org.apache.hadoop.hbase.exceptions.TrustManagerException;
048import org.apache.hadoop.hbase.exceptions.X509Exception;
049import org.apache.hadoop.hbase.io.FileChangeWatcher;
050import org.apache.yetus.audience.InterfaceAudience;
051import org.slf4j.Logger;
052import org.slf4j.LoggerFactory;
053
054import org.apache.hbase.thirdparty.com.google.common.collect.ObjectArrays;
055import org.apache.hbase.thirdparty.io.netty.handler.ssl.OpenSsl;
056import org.apache.hbase.thirdparty.io.netty.handler.ssl.SslContext;
057import org.apache.hbase.thirdparty.io.netty.handler.ssl.SslContextBuilder;
058import org.apache.hbase.thirdparty.io.netty.handler.ssl.SslProvider;
059
060/**
061 * Utility code for X509 handling Default cipher suites: Performance testing done by Facebook
062 * engineers shows that on Intel x86_64 machines, Java9 performs better with GCM and Java8 performs
063 * better with CBC, so these seem like reasonable defaults.
064 * <p/>
065 * This file has been copied from the Apache ZooKeeper project.
066 * @see <a href=
067 *      "https://github.com/apache/zookeeper/blob/c74658d398cdc1d207aa296cb6e20de00faec03e/zookeeper-server/src/main/java/org/apache/zookeeper/common/X509Util.java">Base
068 *      revision</a>
069 */
070@InterfaceAudience.Private
071public final class X509Util {
072
073  private static final Logger LOG = LoggerFactory.getLogger(X509Util.class);
074  private static final char[] EMPTY_CHAR_ARRAY = new char[0];
075
076  //
077  // Common tls configs across both server and client
078  //
079  static final String CONFIG_PREFIX = "hbase.rpc.tls.";
080  public static final String TLS_CONFIG_PROTOCOL = CONFIG_PREFIX + "protocol";
081  public static final String TLS_CONFIG_KEYSTORE_LOCATION = CONFIG_PREFIX + "keystore.location";
082  public static final String TLS_CONFIG_KEYSTORE_TYPE = CONFIG_PREFIX + "keystore.type";
083  public static final String TLS_CONFIG_KEYSTORE_PASSWORD = CONFIG_PREFIX + "keystore.password";
084  public static final String TLS_CONFIG_TRUSTSTORE_LOCATION = CONFIG_PREFIX + "truststore.location";
085  public static final String TLS_CONFIG_TRUSTSTORE_TYPE = CONFIG_PREFIX + "truststore.type";
086  public static final String TLS_CONFIG_TRUSTSTORE_PASSWORD = CONFIG_PREFIX + "truststore.password";
087  public static final String TLS_CONFIG_CLR = CONFIG_PREFIX + "clr";
088  public static final String TLS_CONFIG_OCSP = CONFIG_PREFIX + "ocsp";
089  public static final String TLS_CONFIG_REVERSE_DNS_LOOKUP_ENABLED =
090    CONFIG_PREFIX + "host-verification.reverse-dns.enabled";
091  public static final String TLS_ENABLED_PROTOCOLS = CONFIG_PREFIX + "enabledProtocols";
092  public static final String TLS_CIPHER_SUITES = CONFIG_PREFIX + "ciphersuites";
093  public static final String TLS_CERT_RELOAD = CONFIG_PREFIX + "certReload";
094  public static final String TLS_USE_OPENSSL = CONFIG_PREFIX + "useOpenSsl";
095  public static final String DEFAULT_PROTOCOL = "TLSv1.2";
096
097  //
098  // Server-side specific configs
099  //
100  public static final String HBASE_SERVER_NETTY_TLS_ENABLED = "hbase.server.netty.tls.enabled";
101  public static final String HBASE_SERVER_NETTY_TLS_CLIENT_AUTH_MODE =
102    "hbase.server.netty.tls.client.auth.mode";
103  public static final String HBASE_SERVER_NETTY_TLS_VERIFY_CLIENT_HOSTNAME =
104    "hbase.server.netty.tls.verify.client.hostname";
105  public static final String HBASE_SERVER_NETTY_TLS_SUPPORTPLAINTEXT =
106    "hbase.server.netty.tls.supportplaintext";
107
108  /**
109   * Set the SSL wrapSize for netty. This is only a maximum wrap size. Buffers smaller than this
110   * will not be consolidated, but buffers larger than this will be split into multiple wrap
111   * buffers. The netty default of 16k is not great for hbase which tends to return larger payloads
112   * than that, meaning most responses end up getting chunked up. This leads to more memory
113   * contention in netty's PoolArena. See https://github.com/netty/netty/pull/13551
114   */
115  public static final String HBASE_SERVER_NETTY_TLS_WRAP_SIZE = "hbase.server.netty.tls.wrapSize";
116  public static final int DEFAULT_HBASE_SERVER_NETTY_TLS_WRAP_SIZE = 1024 * 1024;
117  //
118  // Client-side specific configs
119  //
120  public static final String HBASE_CLIENT_NETTY_TLS_ENABLED = "hbase.client.netty.tls.enabled";
121  public static final String HBASE_CLIENT_NETTY_TLS_VERIFY_SERVER_HOSTNAME =
122    "hbase.client.netty.tls.verify.server.hostname";
123  public static final String HBASE_CLIENT_NETTY_TLS_HANDSHAKETIMEOUT =
124    "hbase.client.netty.tls.handshaketimeout";
125  public static final int DEFAULT_HANDSHAKE_DETECTION_TIMEOUT_MILLIS = 5000;
126
127  private static String[] getTls13Ciphers() {
128    return new String[] { "TLS_AES_128_GCM_SHA256", "TLS_AES_256_GCM_SHA384" };
129  }
130
131  private static String[] getGCMCiphers() {
132    return new String[] { "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
133      "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
134      "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384" };
135  }
136
137  private static String[] getCBCCiphers() {
138    return new String[] { "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256",
139      "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256", "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA",
140      "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA", "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384",
141      "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384", "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA",
142      "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA" };
143  }
144
145  // On Java 8, prefer CBC ciphers since AES-NI support is lacking and GCM is slower than CBC.
146  private static final String[] DEFAULT_CIPHERS_JAVA8 =
147    ObjectArrays.concat(getCBCCiphers(), getGCMCiphers(), String.class);
148  // On Java 9 and later, prefer GCM ciphers due to improved AES-NI support.
149  // Note that this performance assumption might not hold true for architectures other than x86_64.
150  private static final String[] DEFAULT_CIPHERS_JAVA9 =
151    ObjectArrays.concat(getGCMCiphers(), getCBCCiphers(), String.class);
152  private static final String[] DEFAULT_CIPHERS_JAVA11 =
153    ObjectArrays.concat(ObjectArrays.concat(getTls13Ciphers(), getGCMCiphers(), String.class),
154      getCBCCiphers(), String.class);
155
156  private static final String[] DEFAULT_CIPHERS_OPENSSL = getOpenSslFilteredDefaultCiphers();
157
158  /**
159   * Not all of our default ciphers are available in OpenSSL. Takes our default cipher lists and
160   * filters them to only those available in OpenSsl. Prefers TLS 1.3, then GCM, then CBC because
161   * GCM tends to be better and faster, and we don't need to worry about the java8 vs 9 performance
162   * issue if OpenSSL is handling it.
163   */
164  private static String[] getOpenSslFilteredDefaultCiphers() {
165    if (!OpenSsl.isAvailable()) {
166      return new String[0];
167    }
168
169    Set<String> openSslSuites = OpenSsl.availableJavaCipherSuites();
170    List<String> defaultSuites = new ArrayList<>();
171    Arrays.stream(getTls13Ciphers()).filter(openSslSuites::contains).forEach(defaultSuites::add);
172    Arrays.stream(getGCMCiphers()).filter(openSslSuites::contains).forEach(defaultSuites::add);
173    Arrays.stream(getCBCCiphers()).filter(openSslSuites::contains).forEach(defaultSuites::add);
174    return defaultSuites.toArray(new String[0]);
175  }
176
177  /**
178   * Enum specifying the client auth requirement of server-side TLS sockets created by this
179   * X509Util.
180   * <ul>
181   * <li>NONE - do not request a client certificate.</li>
182   * <li>WANT - request a client certificate, but allow anonymous clients to connect.</li>
183   * <li>NEED - require a client certificate, disconnect anonymous clients.</li>
184   * </ul>
185   * If the config property is not set, the default value is NEED.
186   */
187  public enum ClientAuth {
188    NONE(org.apache.hbase.thirdparty.io.netty.handler.ssl.ClientAuth.NONE),
189    WANT(org.apache.hbase.thirdparty.io.netty.handler.ssl.ClientAuth.OPTIONAL),
190    NEED(org.apache.hbase.thirdparty.io.netty.handler.ssl.ClientAuth.REQUIRE);
191
192    private final org.apache.hbase.thirdparty.io.netty.handler.ssl.ClientAuth nettyAuth;
193
194    ClientAuth(org.apache.hbase.thirdparty.io.netty.handler.ssl.ClientAuth nettyAuth) {
195      this.nettyAuth = nettyAuth;
196    }
197
198    /**
199     * Converts a property value to a ClientAuth enum. If the input string is empty or null, returns
200     * <code>ClientAuth.NEED</code>.
201     * @param prop the property string.
202     * @return the ClientAuth.
203     * @throws IllegalArgumentException if the property value is not "NONE", "WANT", "NEED", or
204     *                                  empty/null.
205     */
206    public static ClientAuth fromPropertyValue(String prop) {
207      if (prop == null || prop.length() == 0) {
208        return NEED;
209      }
210      return ClientAuth.valueOf(prop.toUpperCase());
211    }
212
213    public org.apache.hbase.thirdparty.io.netty.handler.ssl.ClientAuth toNettyClientAuth() {
214      return nettyAuth;
215    }
216  }
217
218  private X509Util() {
219    // disabled
220  }
221
222  static String[] getDefaultCipherSuites(boolean useOpenSsl) {
223    if (useOpenSsl) {
224      return DEFAULT_CIPHERS_OPENSSL;
225    }
226    return getDefaultCipherSuitesForJavaVersion(System.getProperty("java.specification.version"));
227  }
228
229  static String[] getDefaultCipherSuitesForJavaVersion(String javaVersion) {
230    Objects.requireNonNull(javaVersion);
231
232    if (javaVersion.matches("\\d+")) {
233      // Must be Java 9 or later
234      int javaVersionInt = Integer.parseInt(javaVersion);
235      if (javaVersionInt >= 11) {
236        LOG.debug(
237          "Using Java11+ optimized cipher suites for Java version {}, including TLSv1.3 support",
238          javaVersion);
239        return DEFAULT_CIPHERS_JAVA11;
240      } else {
241        LOG.debug("Using Java9+ optimized cipher suites for Java version {}", javaVersion);
242        return DEFAULT_CIPHERS_JAVA9;
243      }
244    } else if (javaVersion.startsWith("1.")) {
245      // Must be Java 1.8 or earlier
246      LOG.debug("Using Java8 optimized cipher suites for Java version {}", javaVersion);
247      return DEFAULT_CIPHERS_JAVA8;
248    } else {
249      LOG.debug("Could not parse java version {}, using Java8 optimized cipher suites",
250        javaVersion);
251      return DEFAULT_CIPHERS_JAVA8;
252    }
253  }
254
255  public static SslContext createSslContextForClient(Configuration config)
256    throws X509Exception, IOException {
257
258    SslContextBuilder sslContextBuilder = SslContextBuilder.forClient();
259
260    boolean useOpenSsl = configureOpenSslIfAvailable(sslContextBuilder, config);
261    String keyStoreLocation = config.get(TLS_CONFIG_KEYSTORE_LOCATION, "");
262    char[] keyStorePassword = config.getPassword(TLS_CONFIG_KEYSTORE_PASSWORD);
263    String keyStoreType = config.get(TLS_CONFIG_KEYSTORE_TYPE, "");
264
265    if (keyStoreLocation.isEmpty()) {
266      LOG.warn(TLS_CONFIG_KEYSTORE_LOCATION + " not specified");
267    } else {
268      sslContextBuilder
269        .keyManager(createKeyManager(keyStoreLocation, keyStorePassword, keyStoreType));
270    }
271
272    String trustStoreLocation = config.get(TLS_CONFIG_TRUSTSTORE_LOCATION, "");
273    char[] trustStorePassword = config.getPassword(TLS_CONFIG_TRUSTSTORE_PASSWORD);
274    String trustStoreType = config.get(TLS_CONFIG_TRUSTSTORE_TYPE, "");
275
276    boolean sslCrlEnabled = config.getBoolean(TLS_CONFIG_CLR, false);
277    boolean sslOcspEnabled = config.getBoolean(TLS_CONFIG_OCSP, false);
278
279    boolean verifyServerHostname =
280      config.getBoolean(HBASE_CLIENT_NETTY_TLS_VERIFY_SERVER_HOSTNAME, true);
281    boolean allowReverseDnsLookup = config.getBoolean(TLS_CONFIG_REVERSE_DNS_LOOKUP_ENABLED, true);
282
283    if (trustStoreLocation.isEmpty()) {
284      LOG.warn(TLS_CONFIG_TRUSTSTORE_LOCATION + " not specified");
285    } else {
286      sslContextBuilder
287        .trustManager(createTrustManager(trustStoreLocation, trustStorePassword, trustStoreType,
288          sslCrlEnabled, sslOcspEnabled, verifyServerHostname, allowReverseDnsLookup));
289    }
290
291    sslContextBuilder.enableOcsp(sslOcspEnabled);
292    sslContextBuilder.protocols(getEnabledProtocols(config));
293    sslContextBuilder.ciphers(Arrays.asList(getCipherSuites(config, useOpenSsl)));
294
295    return sslContextBuilder.build();
296  }
297
298  /**
299   * Adds SslProvider.OPENSSL if OpenSsl is available and enabled. In order to make it available,
300   * one must ensure that a properly shaded netty-tcnative is on the classpath. Properly shaded
301   * means relocated to be prefixed with "org.apache.hbase.thirdparty" like the rest of the netty
302   * classes. We make available org.apache.hbase:hbase-openssl as a convenience module which one can
303   * use to pull in a shaded netty-tcnative statically linked against boringssl.
304   */
305  private static boolean configureOpenSslIfAvailable(SslContextBuilder sslContextBuilder,
306    Configuration conf) {
307    if (OpenSsl.isAvailable() && conf.getBoolean(TLS_USE_OPENSSL, true)) {
308      LOG.debug("Using netty-tcnative to accelerate SSL handling");
309      sslContextBuilder.sslProvider(SslProvider.OPENSSL);
310      return true;
311    } else {
312      if (LOG.isDebugEnabled()) {
313        LOG.debug("Using default JDK SSL provider because netty-tcnative is not {}",
314          OpenSsl.isAvailable() ? "enabled" : "available");
315      }
316      sslContextBuilder.sslProvider(SslProvider.JDK);
317      return false;
318    }
319  }
320
321  public static SslContext createSslContextForServer(Configuration config)
322    throws X509Exception, IOException {
323    String keyStoreLocation = config.get(TLS_CONFIG_KEYSTORE_LOCATION, "");
324    char[] keyStorePassword = config.getPassword(TLS_CONFIG_KEYSTORE_PASSWORD);
325    String keyStoreType = config.get(TLS_CONFIG_KEYSTORE_TYPE, "");
326
327    if (keyStoreLocation.isEmpty()) {
328      throw new SSLContextException(
329        "Keystore is required for SSL server: " + TLS_CONFIG_KEYSTORE_LOCATION);
330    }
331
332    SslContextBuilder sslContextBuilder;
333    sslContextBuilder = SslContextBuilder
334      .forServer(createKeyManager(keyStoreLocation, keyStorePassword, keyStoreType));
335
336    boolean useOpenSsl = configureOpenSslIfAvailable(sslContextBuilder, config);
337    String trustStoreLocation = config.get(TLS_CONFIG_TRUSTSTORE_LOCATION, "");
338    char[] trustStorePassword = config.getPassword(TLS_CONFIG_TRUSTSTORE_PASSWORD);
339    String trustStoreType = config.get(TLS_CONFIG_TRUSTSTORE_TYPE, "");
340
341    boolean sslCrlEnabled = config.getBoolean(TLS_CONFIG_CLR, false);
342    boolean sslOcspEnabled = config.getBoolean(TLS_CONFIG_OCSP, false);
343
344    ClientAuth clientAuth =
345      ClientAuth.fromPropertyValue(config.get(HBASE_SERVER_NETTY_TLS_CLIENT_AUTH_MODE));
346    boolean verifyClientHostname =
347      config.getBoolean(HBASE_SERVER_NETTY_TLS_VERIFY_CLIENT_HOSTNAME, true);
348    boolean allowReverseDnsLookup = config.getBoolean(TLS_CONFIG_REVERSE_DNS_LOOKUP_ENABLED, true);
349
350    if (trustStoreLocation.isEmpty()) {
351      LOG.warn(TLS_CONFIG_TRUSTSTORE_LOCATION + " not specified");
352    } else {
353      sslContextBuilder
354        .trustManager(createTrustManager(trustStoreLocation, trustStorePassword, trustStoreType,
355          sslCrlEnabled, sslOcspEnabled, verifyClientHostname, allowReverseDnsLookup));
356    }
357
358    sslContextBuilder.enableOcsp(sslOcspEnabled);
359    sslContextBuilder.protocols(getEnabledProtocols(config));
360    sslContextBuilder.ciphers(Arrays.asList(getCipherSuites(config, useOpenSsl)));
361    sslContextBuilder.clientAuth(clientAuth.toNettyClientAuth());
362
363    return sslContextBuilder.build();
364  }
365
366  /**
367   * Creates a key manager by loading the key store from the given file of the given type,
368   * optionally decrypting it using the given password.
369   * @param keyStoreLocation the location of the key store file.
370   * @param keyStorePassword optional password to decrypt the key store. If empty, assumes the key
371   *                         store is not encrypted.
372   * @param keyStoreType     must be JKS, PEM, PKCS12, BCFKS or null. If null, attempts to
373   *                         autodetect the key store type from the file extension (e.g. .jks /
374   *                         .pem).
375   * @return the key manager.
376   * @throws KeyManagerException if something goes wrong.
377   */
378  static X509KeyManager createKeyManager(String keyStoreLocation, char[] keyStorePassword,
379    String keyStoreType) throws KeyManagerException {
380
381    if (keyStorePassword == null) {
382      keyStorePassword = EMPTY_CHAR_ARRAY;
383    }
384
385    try {
386      KeyStoreFileType storeFileType =
387        KeyStoreFileType.fromPropertyValueOrFileName(keyStoreType, keyStoreLocation);
388      KeyStore ks = FileKeyStoreLoaderBuilderProvider.getBuilderForKeyStoreFileType(storeFileType)
389        .setKeyStorePath(keyStoreLocation).setKeyStorePassword(keyStorePassword).build()
390        .loadKeyStore();
391
392      KeyManagerFactory kmf = KeyManagerFactory.getInstance("PKIX");
393      kmf.init(ks, keyStorePassword);
394
395      for (KeyManager km : kmf.getKeyManagers()) {
396        if (km instanceof X509KeyManager) {
397          return (X509KeyManager) km;
398        }
399      }
400      throw new KeyManagerException("Couldn't find X509KeyManager");
401    } catch (IOException | GeneralSecurityException | IllegalArgumentException e) {
402      throw new KeyManagerException(e);
403    }
404  }
405
406  /**
407   * Creates a trust manager by loading the trust store from the given file of the given type,
408   * optionally decrypting it using the given password.
409   * @param trustStoreLocation    the location of the trust store file.
410   * @param trustStorePassword    optional password to decrypt the trust store (only applies to JKS
411   *                              trust stores). If empty, assumes the trust store is not encrypted.
412   * @param trustStoreType        must be JKS, PEM, PKCS12, BCFKS or null. If null, attempts to
413   *                              autodetect the trust store type from the file extension (e.g. .jks
414   *                              / .pem).
415   * @param crlEnabled            enable CRL (certificate revocation list) checks.
416   * @param ocspEnabled           enable OCSP (online certificate status protocol) checks.
417   * @param verifyHostName        if true, ssl peer hostname must match name in certificate
418   * @param allowReverseDnsLookup if true, allow falling back to reverse dns lookup in verifying
419   *                              hostname
420   * @return the trust manager.
421   * @throws TrustManagerException if something goes wrong.
422   */
423  static X509TrustManager createTrustManager(String trustStoreLocation, char[] trustStorePassword,
424    String trustStoreType, boolean crlEnabled, boolean ocspEnabled, boolean verifyHostName,
425    boolean allowReverseDnsLookup) throws TrustManagerException {
426
427    if (trustStorePassword == null) {
428      trustStorePassword = EMPTY_CHAR_ARRAY;
429    }
430
431    try {
432      KeyStoreFileType storeFileType =
433        KeyStoreFileType.fromPropertyValueOrFileName(trustStoreType, trustStoreLocation);
434      KeyStore ts = FileKeyStoreLoaderBuilderProvider.getBuilderForKeyStoreFileType(storeFileType)
435        .setTrustStorePath(trustStoreLocation).setTrustStorePassword(trustStorePassword).build()
436        .loadTrustStore();
437
438      PKIXBuilderParameters pbParams = new PKIXBuilderParameters(ts, new X509CertSelector());
439      if (crlEnabled || ocspEnabled) {
440        pbParams.setRevocationEnabled(true);
441        System.setProperty("com.sun.net.ssl.checkRevocation", "true");
442        if (crlEnabled) {
443          System.setProperty("com.sun.security.enableCRLDP", "true");
444        }
445        if (ocspEnabled) {
446          Security.setProperty("ocsp.enable", "true");
447        }
448      } else {
449        pbParams.setRevocationEnabled(false);
450      }
451
452      // Revocation checking is only supported with the PKIX algorithm
453      TrustManagerFactory tmf = TrustManagerFactory.getInstance("PKIX");
454      tmf.init(new CertPathTrustManagerParameters(pbParams));
455
456      for (final TrustManager tm : tmf.getTrustManagers()) {
457        if (tm instanceof X509ExtendedTrustManager) {
458          return new HBaseTrustManager((X509ExtendedTrustManager) tm, verifyHostName,
459            allowReverseDnsLookup);
460        }
461      }
462      throw new TrustManagerException("Couldn't find X509TrustManager");
463    } catch (IOException | GeneralSecurityException | IllegalArgumentException e) {
464      throw new TrustManagerException(e);
465    }
466  }
467
468  private static String[] getEnabledProtocols(Configuration config) {
469    String enabledProtocolsInput = config.get(TLS_ENABLED_PROTOCOLS);
470    if (enabledProtocolsInput == null) {
471      return new String[] { config.get(TLS_CONFIG_PROTOCOL, DEFAULT_PROTOCOL) };
472    }
473    return enabledProtocolsInput.split(",");
474  }
475
476  private static String[] getCipherSuites(Configuration config, boolean useOpenSsl) {
477    String cipherSuitesInput = config.get(TLS_CIPHER_SUITES);
478    if (cipherSuitesInput == null) {
479      return getDefaultCipherSuites(useOpenSsl);
480    } else {
481      return cipherSuitesInput.split(",");
482    }
483  }
484
485  /**
486   * Enable certificate file reloading by creating FileWatchers for keystore and truststore.
487   * AtomicReferences will be set with the new instances. resetContext - if not null - will be
488   * called when the file has been modified.
489   * @param keystoreWatcher   Reference to keystoreFileWatcher.
490   * @param trustStoreWatcher Reference to truststoreFileWatcher.
491   * @param resetContext      Callback for file changes.
492   */
493  public static void enableCertFileReloading(Configuration config,
494    AtomicReference<FileChangeWatcher> keystoreWatcher,
495    AtomicReference<FileChangeWatcher> trustStoreWatcher, Runnable resetContext)
496    throws IOException {
497    String keyStoreLocation = config.get(TLS_CONFIG_KEYSTORE_LOCATION, "");
498    keystoreWatcher.set(newFileChangeWatcher(keyStoreLocation, resetContext));
499    String trustStoreLocation = config.get(TLS_CONFIG_TRUSTSTORE_LOCATION, "");
500    // we are using the same callback for both. there's no reason to kick off two
501    // threads if keystore/truststore are both at the same location
502    if (!keyStoreLocation.equals(trustStoreLocation)) {
503      trustStoreWatcher.set(newFileChangeWatcher(trustStoreLocation, resetContext));
504    }
505  }
506
507  private static FileChangeWatcher newFileChangeWatcher(String fileLocation, Runnable resetContext)
508    throws IOException {
509    if (fileLocation == null || fileLocation.isEmpty() || resetContext == null) {
510      return null;
511    }
512    final Path filePath = Paths.get(fileLocation).toAbsolutePath();
513    Path parentPath = filePath.getParent();
514    if (parentPath == null) {
515      throw new IOException("Key/trust store path does not have a parent: " + filePath);
516    }
517    FileChangeWatcher fileChangeWatcher =
518      new FileChangeWatcher(parentPath, Objects.toString(filePath.getFileName()), watchEvent -> {
519        handleWatchEvent(filePath, watchEvent, resetContext);
520      });
521    fileChangeWatcher.start();
522    return fileChangeWatcher;
523  }
524
525  /**
526   * Handler for watch events that let us know a file we may care about has changed on disk.
527   * @param filePath the path to the file we are watching for changes.
528   * @param event    the WatchEvent.
529   */
530  private static void handleWatchEvent(Path filePath, WatchEvent<?> event, Runnable resetContext) {
531    boolean shouldResetContext = false;
532    Path dirPath = filePath.getParent();
533    if (event.kind().equals(StandardWatchEventKinds.OVERFLOW)) {
534      // If we get notified about possibly missed events, reload the key store / trust store just to
535      // be sure.
536      shouldResetContext = true;
537    } else if (
538      event.kind().equals(StandardWatchEventKinds.ENTRY_MODIFY)
539        || event.kind().equals(StandardWatchEventKinds.ENTRY_CREATE)
540    ) {
541      Path eventFilePath = dirPath.resolve((Path) event.context());
542      if (filePath.equals(eventFilePath)) {
543        shouldResetContext = true;
544      }
545    }
546    // Note: we don't care about delete events
547    if (shouldResetContext) {
548      LOG.info(
549        "Attempting to reset default SSL context after receiving watch event: {} with context: {}",
550        event.kind(), event.context());
551      resetContext.run();
552    } else {
553      if (LOG.isDebugEnabled()) {
554        LOG.debug(
555          "Ignoring watch event and keeping previous default SSL context. Event kind: {} with context: {}",
556          event.kind(), event.context());
557      }
558    }
559  }
560}