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.security.GeneralSecurityException;
024import java.security.KeyStore;
025import java.security.Security;
026import java.security.cert.PKIXBuilderParameters;
027import java.security.cert.X509CertSelector;
028import java.time.Duration;
029import java.util.Arrays;
030import java.util.Objects;
031import java.util.concurrent.atomic.AtomicReference;
032import javax.net.ssl.CertPathTrustManagerParameters;
033import javax.net.ssl.KeyManager;
034import javax.net.ssl.KeyManagerFactory;
035import javax.net.ssl.TrustManager;
036import javax.net.ssl.TrustManagerFactory;
037import javax.net.ssl.X509ExtendedTrustManager;
038import javax.net.ssl.X509KeyManager;
039import javax.net.ssl.X509TrustManager;
040import org.apache.hadoop.conf.Configuration;
041import org.apache.hadoop.hbase.exceptions.KeyManagerException;
042import org.apache.hadoop.hbase.exceptions.SSLContextException;
043import org.apache.hadoop.hbase.exceptions.TrustManagerException;
044import org.apache.hadoop.hbase.exceptions.X509Exception;
045import org.apache.hadoop.hbase.io.FileChangeWatcher;
046import org.apache.yetus.audience.InterfaceAudience;
047import org.slf4j.Logger;
048import org.slf4j.LoggerFactory;
049
050import org.apache.hbase.thirdparty.io.netty.handler.ssl.IdentityCipherSuiteFilter;
051import org.apache.hbase.thirdparty.io.netty.handler.ssl.OpenSsl;
052import org.apache.hbase.thirdparty.io.netty.handler.ssl.SslContext;
053import org.apache.hbase.thirdparty.io.netty.handler.ssl.SslContextBuilder;
054import org.apache.hbase.thirdparty.io.netty.handler.ssl.SslProvider;
055
056/**
057 * Utility code for X509 handling Default cipher suites: Performance testing done by Facebook
058 * engineers shows that on Intel x86_64 machines, Java9 performs better with GCM and Java8 performs
059 * better with CBC, so these seem like reasonable defaults.
060 * <p/>
061 * This file has been copied from the Apache ZooKeeper project.
062 * @see <a href=
063 *      "https://github.com/apache/zookeeper/blob/c74658d398cdc1d207aa296cb6e20de00faec03e/zookeeper-server/src/main/java/org/apache/zookeeper/common/X509Util.java">Base
064 *      revision</a>
065 */
066@InterfaceAudience.Private
067public final class X509Util {
068
069  private static final Logger LOG = LoggerFactory.getLogger(X509Util.class);
070  private static final char[] EMPTY_CHAR_ARRAY = new char[0];
071
072  //
073  // Common tls configs across both server and client
074  //
075  static final String CONFIG_PREFIX = "hbase.rpc.tls.";
076  public static final String TLS_CONFIG_PROTOCOL = CONFIG_PREFIX + "protocol";
077  public static final String TLS_CONFIG_KEYSTORE_LOCATION = CONFIG_PREFIX + "keystore.location";
078  public static final String TLS_CONFIG_KEYSTORE_TYPE = CONFIG_PREFIX + "keystore.type";
079  public static final String TLS_CONFIG_KEYSTORE_PASSWORD = CONFIG_PREFIX + "keystore.password";
080  public static final String TLS_CONFIG_TRUSTSTORE_LOCATION = CONFIG_PREFIX + "truststore.location";
081  public static final String TLS_CONFIG_TRUSTSTORE_TYPE = CONFIG_PREFIX + "truststore.type";
082  public static final String TLS_CONFIG_TRUSTSTORE_PASSWORD = CONFIG_PREFIX + "truststore.password";
083  public static final String TLS_CONFIG_CLR = CONFIG_PREFIX + "clr";
084  public static final String TLS_CONFIG_OCSP = CONFIG_PREFIX + "ocsp";
085  public static final String TLS_CONFIG_REVERSE_DNS_LOOKUP_ENABLED =
086    CONFIG_PREFIX + "host-verification.reverse-dns.enabled";
087  public static final String TLS_ENABLED_PROTOCOLS = CONFIG_PREFIX + "enabledProtocols";
088  public static final String TLS_CIPHER_SUITES = CONFIG_PREFIX + "ciphersuites";
089  public static final String TLS_CERT_RELOAD = CONFIG_PREFIX + "certReload";
090  public static final String TLS_USE_OPENSSL = CONFIG_PREFIX + "useOpenSsl";
091
092  //
093  // Server-side specific configs
094  //
095  public static final String HBASE_SERVER_NETTY_TLS_ENABLED = "hbase.server.netty.tls.enabled";
096  public static final String HBASE_SERVER_NETTY_TLS_CLIENT_AUTH_MODE =
097    "hbase.server.netty.tls.client.auth.mode";
098  public static final String HBASE_SERVER_NETTY_TLS_VERIFY_CLIENT_HOSTNAME =
099    "hbase.server.netty.tls.verify.client.hostname";
100  public static final String HBASE_SERVER_NETTY_TLS_SUPPORTPLAINTEXT =
101    "hbase.server.netty.tls.supportplaintext";
102
103  /**
104   * Set the SSL wrapSize for netty. This is only a maximum wrap size. Buffers smaller than this
105   * will not be consolidated, but buffers larger than this will be split into multiple wrap
106   * buffers. The netty default of 16k is not great for hbase which tends to return larger payloads
107   * than that, meaning most responses end up getting chunked up. This leads to more memory
108   * contention in netty's PoolArena. See https://github.com/netty/netty/pull/13551
109   */
110  public static final String HBASE_SERVER_NETTY_TLS_WRAP_SIZE = "hbase.server.netty.tls.wrapSize";
111  public static final int DEFAULT_HBASE_SERVER_NETTY_TLS_WRAP_SIZE = 1024 * 1024;
112  //
113  // Client-side specific configs
114  //
115  public static final String HBASE_CLIENT_NETTY_TLS_ENABLED = "hbase.client.netty.tls.enabled";
116  public static final String HBASE_CLIENT_NETTY_TLS_VERIFY_SERVER_HOSTNAME =
117    "hbase.client.netty.tls.verify.server.hostname";
118  public static final String HBASE_CLIENT_NETTY_TLS_HANDSHAKETIMEOUT =
119    "hbase.client.netty.tls.handshaketimeout";
120  public static final int DEFAULT_HANDSHAKE_DETECTION_TIMEOUT_MILLIS = 5000;
121
122  public static final String HBASE_TLS_FILEPOLL_INTERVAL_MILLIS =
123    CONFIG_PREFIX + "filepoll.interval.millis";
124  // 1 minute
125  private static final long DEFAULT_FILE_POLL_INTERVAL = Duration.ofSeconds(60).toMillis();
126
127  /**
128   * Enum specifying the client auth requirement of server-side TLS sockets created by this
129   * X509Util.
130   * <ul>
131   * <li>NONE - do not request a client certificate.</li>
132   * <li>WANT - request a client certificate, but allow anonymous clients to connect.</li>
133   * <li>NEED - require a client certificate, disconnect anonymous clients.</li>
134   * </ul>
135   * If the config property is not set, the default value is NEED.
136   */
137  public enum ClientAuth {
138    NONE(org.apache.hbase.thirdparty.io.netty.handler.ssl.ClientAuth.NONE),
139    WANT(org.apache.hbase.thirdparty.io.netty.handler.ssl.ClientAuth.OPTIONAL),
140    NEED(org.apache.hbase.thirdparty.io.netty.handler.ssl.ClientAuth.REQUIRE);
141
142    private final org.apache.hbase.thirdparty.io.netty.handler.ssl.ClientAuth nettyAuth;
143
144    ClientAuth(org.apache.hbase.thirdparty.io.netty.handler.ssl.ClientAuth nettyAuth) {
145      this.nettyAuth = nettyAuth;
146    }
147
148    /**
149     * Converts a property value to a ClientAuth enum. If the input string is empty or null, returns
150     * <code>ClientAuth.NEED</code>.
151     * @param prop the property string.
152     * @return the ClientAuth.
153     * @throws IllegalArgumentException if the property value is not "NONE", "WANT", "NEED", or
154     *                                  empty/null.
155     */
156    public static ClientAuth fromPropertyValue(String prop) {
157      if (prop == null || prop.length() == 0) {
158        return NEED;
159      }
160      return ClientAuth.valueOf(prop.toUpperCase());
161    }
162
163    public org.apache.hbase.thirdparty.io.netty.handler.ssl.ClientAuth toNettyClientAuth() {
164      return nettyAuth;
165    }
166  }
167
168  private X509Util() {
169    // disabled
170  }
171
172  public static SslContext createSslContextForClient(Configuration config)
173    throws X509Exception, IOException {
174
175    SslContextBuilder sslContextBuilder = SslContextBuilder.forClient();
176
177    configureOpenSslIfAvailable(sslContextBuilder, config);
178    String keyStoreLocation = config.get(TLS_CONFIG_KEYSTORE_LOCATION, "");
179    char[] keyStorePassword = config.getPassword(TLS_CONFIG_KEYSTORE_PASSWORD);
180    String keyStoreType = config.get(TLS_CONFIG_KEYSTORE_TYPE, "");
181
182    if (keyStoreLocation.isEmpty()) {
183      LOG.warn(TLS_CONFIG_KEYSTORE_LOCATION + " not specified");
184    } else {
185      sslContextBuilder
186        .keyManager(createKeyManager(keyStoreLocation, keyStorePassword, keyStoreType));
187    }
188
189    String trustStoreLocation = config.get(TLS_CONFIG_TRUSTSTORE_LOCATION, "");
190    char[] trustStorePassword = config.getPassword(TLS_CONFIG_TRUSTSTORE_PASSWORD);
191    String trustStoreType = config.get(TLS_CONFIG_TRUSTSTORE_TYPE, "");
192
193    boolean sslCrlEnabled = config.getBoolean(TLS_CONFIG_CLR, false);
194    boolean sslOcspEnabled = config.getBoolean(TLS_CONFIG_OCSP, false);
195
196    boolean verifyServerHostname =
197      config.getBoolean(HBASE_CLIENT_NETTY_TLS_VERIFY_SERVER_HOSTNAME, true);
198    boolean allowReverseDnsLookup = config.getBoolean(TLS_CONFIG_REVERSE_DNS_LOOKUP_ENABLED, true);
199
200    if (trustStoreLocation.isEmpty()) {
201      LOG.warn(TLS_CONFIG_TRUSTSTORE_LOCATION + " not specified");
202    } else {
203      sslContextBuilder
204        .trustManager(createTrustManager(trustStoreLocation, trustStorePassword, trustStoreType,
205          sslCrlEnabled, sslOcspEnabled, verifyServerHostname, allowReverseDnsLookup));
206    }
207
208    sslContextBuilder.enableOcsp(sslOcspEnabled);
209    String[] enabledProtocols = getEnabledProtocols(config);
210    if (enabledProtocols != null) {
211      sslContextBuilder.protocols(enabledProtocols);
212    }
213    String[] cipherSuites = getCipherSuites(config);
214    if (cipherSuites == null) {
215      /*
216       * if cipher list is not explicitly defined, we use the most inclusive cipher list at the
217       * client side
218       */
219      sslContextBuilder.ciphers(null,
220        IdentityCipherSuiteFilter.INSTANCE_DEFAULTING_TO_SUPPORTED_CIPHERS);
221    } else {
222      sslContextBuilder.ciphers(Arrays.asList(cipherSuites));
223    }
224
225    return sslContextBuilder.build();
226  }
227
228  /**
229   * Adds SslProvider.OPENSSL if OpenSsl is available and enabled. In order to make it available,
230   * one must ensure that a properly shaded netty-tcnative is on the classpath. Properly shaded
231   * means relocated to be prefixed with "org.apache.hbase.thirdparty" like the rest of the netty
232   * classes. We make available org.apache.hbase:hbase-openssl as a convenience module which one can
233   * use to pull in a shaded netty-tcnative statically linked against boringssl.
234   */
235  private static boolean configureOpenSslIfAvailable(SslContextBuilder sslContextBuilder,
236    Configuration conf) {
237    boolean openSslEnabled = conf.getBoolean(TLS_USE_OPENSSL, true);
238    if (openSslEnabled && OpenSsl.isAvailable()) {
239      LOG.debug("Using netty-tcnative to accelerate SSL handling");
240      sslContextBuilder.sslProvider(SslProvider.OPENSSL);
241      return true;
242    } else {
243      LOG.debug("Using default JDK SSL provider because netty-tcnative is not {}",
244        openSslEnabled ? "available" : "enabled");
245      sslContextBuilder.sslProvider(SslProvider.JDK);
246      return false;
247    }
248  }
249
250  public static SslContext createSslContextForServer(Configuration config)
251    throws X509Exception, IOException {
252    String keyStoreLocation = config.get(TLS_CONFIG_KEYSTORE_LOCATION, "");
253    char[] keyStorePassword = config.getPassword(TLS_CONFIG_KEYSTORE_PASSWORD);
254    String keyStoreType = config.get(TLS_CONFIG_KEYSTORE_TYPE, "");
255
256    if (keyStoreLocation.isEmpty()) {
257      throw new SSLContextException(
258        "Keystore is required for SSL server: " + TLS_CONFIG_KEYSTORE_LOCATION);
259    }
260
261    SslContextBuilder sslContextBuilder;
262    sslContextBuilder = SslContextBuilder
263      .forServer(createKeyManager(keyStoreLocation, keyStorePassword, keyStoreType));
264
265    configureOpenSslIfAvailable(sslContextBuilder, config);
266    String trustStoreLocation = config.get(TLS_CONFIG_TRUSTSTORE_LOCATION, "");
267    char[] trustStorePassword = config.getPassword(TLS_CONFIG_TRUSTSTORE_PASSWORD);
268    String trustStoreType = config.get(TLS_CONFIG_TRUSTSTORE_TYPE, "");
269
270    boolean sslCrlEnabled = config.getBoolean(TLS_CONFIG_CLR, false);
271    boolean sslOcspEnabled = config.getBoolean(TLS_CONFIG_OCSP, false);
272
273    ClientAuth clientAuth =
274      ClientAuth.fromPropertyValue(config.get(HBASE_SERVER_NETTY_TLS_CLIENT_AUTH_MODE));
275    boolean verifyClientHostname =
276      config.getBoolean(HBASE_SERVER_NETTY_TLS_VERIFY_CLIENT_HOSTNAME, true);
277    boolean allowReverseDnsLookup = config.getBoolean(TLS_CONFIG_REVERSE_DNS_LOOKUP_ENABLED, true);
278
279    if (trustStoreLocation.isEmpty()) {
280      LOG.warn(TLS_CONFIG_TRUSTSTORE_LOCATION + " not specified");
281    } else {
282      sslContextBuilder
283        .trustManager(createTrustManager(trustStoreLocation, trustStorePassword, trustStoreType,
284          sslCrlEnabled, sslOcspEnabled, verifyClientHostname, allowReverseDnsLookup));
285    }
286
287    sslContextBuilder.enableOcsp(sslOcspEnabled);
288    String[] enabledProtocols = getEnabledProtocols(config);
289    if (enabledProtocols != null) {
290      sslContextBuilder.protocols(enabledProtocols);
291    }
292    String[] cipherSuites = getCipherSuites(config);
293    if (cipherSuites != null) {
294      sslContextBuilder.ciphers(Arrays.asList(cipherSuites));
295    }
296    sslContextBuilder.clientAuth(clientAuth.toNettyClientAuth());
297
298    return sslContextBuilder.build();
299  }
300
301  /**
302   * Creates a key manager by loading the key store from the given file of the given type,
303   * optionally decrypting it using the given password.
304   * @param keyStoreLocation the location of the key store file.
305   * @param keyStorePassword optional password to decrypt the key store. If empty, assumes the key
306   *                         store is not encrypted.
307   * @param keyStoreType     must be JKS, PEM, PKCS12, BCFKS or null. If null, attempts to
308   *                         autodetect the key store type from the file extension (e.g. .jks /
309   *                         .pem).
310   * @return the key manager.
311   * @throws KeyManagerException if something goes wrong.
312   */
313  static X509KeyManager createKeyManager(String keyStoreLocation, char[] keyStorePassword,
314    String keyStoreType) throws KeyManagerException {
315
316    if (keyStorePassword == null) {
317      keyStorePassword = EMPTY_CHAR_ARRAY;
318    }
319
320    try {
321      KeyStoreFileType storeFileType =
322        KeyStoreFileType.fromPropertyValueOrFileName(keyStoreType, keyStoreLocation);
323      KeyStore ks = FileKeyStoreLoaderBuilderProvider.getBuilderForKeyStoreFileType(storeFileType)
324        .setKeyStorePath(keyStoreLocation).setKeyStorePassword(keyStorePassword).build()
325        .loadKeyStore();
326
327      KeyManagerFactory kmf = KeyManagerFactory.getInstance("PKIX");
328      kmf.init(ks, keyStorePassword);
329
330      for (KeyManager km : kmf.getKeyManagers()) {
331        if (km instanceof X509KeyManager) {
332          return (X509KeyManager) km;
333        }
334      }
335      throw new KeyManagerException("Couldn't find X509KeyManager");
336    } catch (IOException | GeneralSecurityException | IllegalArgumentException e) {
337      throw new KeyManagerException(e);
338    }
339  }
340
341  /**
342   * Creates a trust manager by loading the trust store from the given file of the given type,
343   * optionally decrypting it using the given password.
344   * @param trustStoreLocation    the location of the trust store file.
345   * @param trustStorePassword    optional password to decrypt the trust store (only applies to JKS
346   *                              trust stores). If empty, assumes the trust store is not encrypted.
347   * @param trustStoreType        must be JKS, PEM, PKCS12, BCFKS or null. If null, attempts to
348   *                              autodetect the trust store type from the file extension (e.g. .jks
349   *                              / .pem).
350   * @param crlEnabled            enable CRL (certificate revocation list) checks.
351   * @param ocspEnabled           enable OCSP (online certificate status protocol) checks.
352   * @param verifyHostName        if true, ssl peer hostname must match name in certificate
353   * @param allowReverseDnsLookup if true, allow falling back to reverse dns lookup in verifying
354   *                              hostname
355   * @return the trust manager.
356   * @throws TrustManagerException if something goes wrong.
357   */
358  static X509TrustManager createTrustManager(String trustStoreLocation, char[] trustStorePassword,
359    String trustStoreType, boolean crlEnabled, boolean ocspEnabled, boolean verifyHostName,
360    boolean allowReverseDnsLookup) throws TrustManagerException {
361
362    if (trustStorePassword == null) {
363      trustStorePassword = EMPTY_CHAR_ARRAY;
364    }
365
366    try {
367      KeyStoreFileType storeFileType =
368        KeyStoreFileType.fromPropertyValueOrFileName(trustStoreType, trustStoreLocation);
369      KeyStore ts = FileKeyStoreLoaderBuilderProvider.getBuilderForKeyStoreFileType(storeFileType)
370        .setTrustStorePath(trustStoreLocation).setTrustStorePassword(trustStorePassword).build()
371        .loadTrustStore();
372
373      PKIXBuilderParameters pbParams = new PKIXBuilderParameters(ts, new X509CertSelector());
374      if (crlEnabled || ocspEnabled) {
375        pbParams.setRevocationEnabled(true);
376        System.setProperty("com.sun.net.ssl.checkRevocation", "true");
377        if (crlEnabled) {
378          System.setProperty("com.sun.security.enableCRLDP", "true");
379        }
380        if (ocspEnabled) {
381          Security.setProperty("ocsp.enable", "true");
382        }
383      } else {
384        pbParams.setRevocationEnabled(false);
385      }
386
387      // Revocation checking is only supported with the PKIX algorithm
388      TrustManagerFactory tmf = TrustManagerFactory.getInstance("PKIX");
389      tmf.init(new CertPathTrustManagerParameters(pbParams));
390
391      for (final TrustManager tm : tmf.getTrustManagers()) {
392        if (tm instanceof X509ExtendedTrustManager) {
393          return new HBaseTrustManager((X509ExtendedTrustManager) tm, verifyHostName,
394            allowReverseDnsLookup);
395        }
396      }
397      throw new TrustManagerException("Couldn't find X509TrustManager");
398    } catch (IOException | GeneralSecurityException | IllegalArgumentException e) {
399      throw new TrustManagerException(e);
400    }
401  }
402
403  private static String[] getEnabledProtocols(Configuration config) {
404    String enabledProtocolsInput = config.get(TLS_ENABLED_PROTOCOLS);
405    if (enabledProtocolsInput == null) {
406      enabledProtocolsInput = config.get(TLS_CONFIG_PROTOCOL);
407    }
408    if (enabledProtocolsInput != null) {
409      return enabledProtocolsInput.split(",");
410    } else {
411      return null;
412    }
413  }
414
415  private static String[] getCipherSuites(Configuration config) {
416    String cipherSuitesInput = config.get(TLS_CIPHER_SUITES);
417    if (cipherSuitesInput == null) {
418      return null;
419    } else {
420      return cipherSuitesInput.split(",");
421    }
422  }
423
424  /**
425   * Enable certificate file reloading by creating FileWatchers for keystore and truststore.
426   * AtomicReferences will be set with the new instances. resetContext - if not null - will be
427   * called when the file has been modified.
428   * @param keystoreWatcher   Reference to keystoreFileWatcher.
429   * @param trustStoreWatcher Reference to truststoreFileWatcher.
430   * @param resetContext      Callback for file changes.
431   */
432  public static void enableCertFileReloading(Configuration config,
433    AtomicReference<FileChangeWatcher> keystoreWatcher,
434    AtomicReference<FileChangeWatcher> trustStoreWatcher, Runnable resetContext)
435    throws IOException {
436    String keyStoreLocation = config.get(TLS_CONFIG_KEYSTORE_LOCATION, "");
437    keystoreWatcher.set(newFileChangeWatcher(config, keyStoreLocation, resetContext));
438    String trustStoreLocation = config.get(TLS_CONFIG_TRUSTSTORE_LOCATION, "");
439    // we are using the same callback for both. there's no reason to kick off two
440    // threads if keystore/truststore are both at the same location
441    if (!keyStoreLocation.equals(trustStoreLocation)) {
442      trustStoreWatcher.set(newFileChangeWatcher(config, trustStoreLocation, resetContext));
443    }
444  }
445
446  private static FileChangeWatcher newFileChangeWatcher(Configuration config, String fileLocation,
447    Runnable resetContext) throws IOException {
448    if (fileLocation == null || fileLocation.isEmpty() || resetContext == null) {
449      return null;
450    }
451    final Path filePath = Paths.get(fileLocation).toAbsolutePath();
452    FileChangeWatcher fileChangeWatcher =
453      new FileChangeWatcher(filePath, Objects.toString(filePath.getFileName()),
454        Duration
455          .ofMillis(config.getLong(HBASE_TLS_FILEPOLL_INTERVAL_MILLIS, DEFAULT_FILE_POLL_INTERVAL)),
456        watchEventFilePath -> handleWatchEvent(watchEventFilePath, resetContext));
457    fileChangeWatcher.start();
458    return fileChangeWatcher;
459  }
460
461  /**
462   * Handler for watch events that let us know a file we may care about has changed on disk.
463   */
464  private static void handleWatchEvent(Path filePath, Runnable resetContext) {
465    LOG.info("Attempting to reset default SSL context after receiving watch event on file {}",
466      filePath);
467    resetContext.run();
468  }
469}