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