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}