blob: dffc20911822f7548ecdc1a7921301b834746d80 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.omid.tls;
import io.netty.handler.ssl.SslContext;
import io.netty.handler.ssl.SslContextBuilder;
import org.apache.omid.tls.X509Exception;
import org.apache.omid.tls.X509Exception.KeyManagerException;
import org.apache.omid.tls.X509Exception.SSLContextException;
import org.apache.omid.tls.X509Exception.TrustManagerException;
import org.apache.phoenix.thirdparty.com.google.common.collect.ObjectArrays;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.net.ssl.*;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.security.Security;
import java.security.cert.PKIXBuilderParameters;
import java.security.cert.X509CertSelector;
import java.util.Arrays;
import java.util.Objects;
/**
* Utility code for X509 handling Default cipher suites: Performance testing done by Facebook
* engineers shows that on Intel x86_64 machines, Java9 performs better with GCM and Java8 performs
* better with CBC, so these seem like reasonable defaults.
* <p/>
* This file has is based on the one in HBase project.
* @see <a href=
* "https://github.com/apache/hbase/blob/d2b0074f7ad4c43d31a1a511a0d74feda72451d1/hbase-common/src/main/java/org/apache/hadoop/hbase/io/crypto/tls/X509Util.java">Base
* revision</a>
*/
public final class X509Util {
private static final Logger LOG = LoggerFactory.getLogger(X509Util.class);
private static final char[] EMPTY_CHAR_ARRAY = new char[0];
// Config
public static final int DEFAULT_HANDSHAKE_DETECTION_TIMEOUT_MILLIS = 5000;
public static final String DEFAULT_PROTOCOL = "TLSv1.2";
private static String[] getGCMCiphers() {
return new String[] { "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256",
"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384",
"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384" };
}
private static String[] getCBCCiphers() {
return new String[] { "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256",
"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256", "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA",
"TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA", "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384",
"TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384", "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA",
"TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA" };
}
// On Java 8, prefer CBC ciphers since AES-NI support is lacking and GCM is slower than CBC.
private static final String[] DEFAULT_CIPHERS_JAVA8 =
ObjectArrays.concat(getCBCCiphers(), getGCMCiphers(), String.class);
// On Java 9 and later, prefer GCM ciphers due to improved AES-NI support.
// Note that this performance assumption might not hold true for architectures other than x86_64.
private static final String[] DEFAULT_CIPHERS_JAVA9 =
ObjectArrays.concat(getGCMCiphers(), getCBCCiphers(), String.class);
private X509Util() {
// disabled
}
static String[] getDefaultCipherSuites() {
return getDefaultCipherSuitesForJavaVersion(System.getProperty("java.specification.version"));
}
static String[] getDefaultCipherSuitesForJavaVersion(String javaVersion) {
Objects.requireNonNull(javaVersion);
if (javaVersion.matches("\\d+")) {
// Must be Java 9 or later
LOG.debug("Using Java9+ optimized cipher suites for Java version {}", javaVersion);
return DEFAULT_CIPHERS_JAVA9;
} else if (javaVersion.startsWith("1.")) {
// Must be Java 1.8 or earlier
LOG.debug("Using Java8 optimized cipher suites for Java version {}", javaVersion);
return DEFAULT_CIPHERS_JAVA8;
} else {
LOG.debug("Could not parse java version {}, using Java8 optimized cipher suites",
javaVersion);
return DEFAULT_CIPHERS_JAVA8;
}
}
public static SslContext createSslContextForClient(String keyStoreLocation, char[] keyStorePassword,
String keyStoreType, String trustStoreLocation, char[] trustStorePassword, String trustStoreType,
boolean sslCrlEnabled, boolean sslOcspEnabled, String enabledProtocols, String cipherSuites, String tlsConfigProtocols)
throws X509Exception, IOException {
SslContextBuilder sslContextBuilder = SslContextBuilder.forClient();
if (keyStoreLocation.isEmpty()) {
LOG.warn("keyStoreLocation is not specified");
} else {
sslContextBuilder
.keyManager(createKeyManager(keyStoreLocation, keyStorePassword, keyStoreType));
}
if (trustStoreLocation.isEmpty()) {
LOG.warn("trustStoreLocation is not specified");
} else {
sslContextBuilder.trustManager(createTrustManager(trustStoreLocation, trustStorePassword,
trustStoreType, sslCrlEnabled, sslOcspEnabled));
}
sslContextBuilder.enableOcsp(sslOcspEnabled);
sslContextBuilder.protocols(getEnabledProtocols(enabledProtocols, tlsConfigProtocols));
sslContextBuilder.ciphers(Arrays.asList(getCipherSuites(cipherSuites)));
return sslContextBuilder.build();
}
public static SslContext createSslContextForServer(String keyStoreLocation, char[] keyStorePassword,
String keyStoreType, String trustStoreLocation, char[] trustStorePassword, String trustStoreType,
boolean sslCrlEnabled, boolean sslOcspEnabled, String enabledProtocols, String cipherSuites, String tlsConfigProtocols)
throws X509Exception, IOException {
if (keyStoreLocation.isEmpty()) {
throw new SSLContextException(
"keyStoreLocation is required for SSL server: ");
}
SslContextBuilder sslContextBuilder;
sslContextBuilder = SslContextBuilder
.forServer(createKeyManager(keyStoreLocation, keyStorePassword, keyStoreType));
if (trustStoreLocation.isEmpty()) {
LOG.warn("trustStoreLocation is not specified");
} else {
sslContextBuilder.trustManager(createTrustManager(trustStoreLocation, trustStorePassword,
trustStoreType, sslCrlEnabled, sslOcspEnabled));
}
sslContextBuilder.enableOcsp(sslOcspEnabled);
sslContextBuilder.protocols(getEnabledProtocols(enabledProtocols, tlsConfigProtocols));
sslContextBuilder.ciphers(Arrays.asList(getCipherSuites(cipherSuites)));
return sslContextBuilder.build();
}
/**
* Creates a key manager by loading the key store from the given file of the given type,
* optionally decrypting it using the given password.
* @param keyStoreLocation the location of the key store file.
* @param keyStorePassword optional password to decrypt the key store. If empty, assumes the key
* store is not encrypted.
* @param keyStoreType must be JKS, PEM, PKCS12, BCFKS or null. If null, attempts to
* autodetect the key store type from the file extension (e.g. .jks /
* .pem).
* @return the key manager.
* @throws KeyManagerException if something goes wrong.
*/
static X509KeyManager createKeyManager(String keyStoreLocation, char[] keyStorePassword,
String keyStoreType) throws KeyManagerException {
if (keyStoreType == null) {
keyStoreType = "jks";
}
if (keyStorePassword == null) {
keyStorePassword = EMPTY_CHAR_ARRAY;
}
try {
KeyStore ks = KeyStore.getInstance(keyStoreType);
try (InputStream inputStream = Files.newInputStream(new File(keyStoreLocation).toPath())) {
ks.load(inputStream, keyStorePassword);
}
KeyManagerFactory kmf = KeyManagerFactory.getInstance("PKIX");
kmf.init(ks, keyStorePassword);
for (KeyManager km : kmf.getKeyManagers()) {
if (km instanceof X509KeyManager) {
return (X509KeyManager) km;
}
}
throw new KeyManagerException("Couldn't find X509KeyManager");
} catch (IOException | GeneralSecurityException | IllegalArgumentException e) {
throw new KeyManagerException(e);
}
}
/**
* Creates a trust manager by loading the trust store from the given file of the given type,
* optionally decrypting it using the given password.
* @param trustStoreLocation the location of the trust store file.
* @param trustStorePassword optional password to decrypt the trust store (only applies to JKS
* trust stores). If empty, assumes the trust store is not encrypted.
* @param trustStoreType must be JKS, PEM, PKCS12, BCFKS or null. If null, attempts to
* autodetect the trust store type from the file extension (e.g. .jks /
* .pem).
* @param crlEnabled enable CRL (certificate revocation list) checks.
* @param ocspEnabled enable OCSP (online certificate status protocol) checks.
* @return the trust manager.
* @throws TrustManagerException if something goes wrong.
*/
static X509TrustManager createTrustManager(String trustStoreLocation, char[] trustStorePassword,
String trustStoreType, boolean crlEnabled, boolean ocspEnabled) throws TrustManagerException {
if (trustStoreType == null) {
trustStoreType = "jks";
}
if (trustStorePassword == null) {
trustStorePassword = EMPTY_CHAR_ARRAY;
}
try {
KeyStore ts = KeyStore.getInstance(trustStoreType);
try (InputStream inputStream = Files.newInputStream(new File(trustStoreLocation).toPath())) {
ts.load(inputStream, trustStorePassword);
}
PKIXBuilderParameters pbParams = new PKIXBuilderParameters(ts, new X509CertSelector());
if (crlEnabled || ocspEnabled) {
pbParams.setRevocationEnabled(true);
System.setProperty("com.sun.net.ssl.checkRevocation", "true");
if (crlEnabled) {
System.setProperty("com.sun.security.enableCRLDP", "true");
}
if (ocspEnabled) {
Security.setProperty("ocsp.enable", "true");
}
} else {
pbParams.setRevocationEnabled(false);
}
// Revocation checking is only supported with the PKIX algorithm
TrustManagerFactory tmf = TrustManagerFactory.getInstance("PKIX");
tmf.init(new CertPathTrustManagerParameters(pbParams));
for (final TrustManager tm : tmf.getTrustManagers()) {
if (tm instanceof X509ExtendedTrustManager) {
return (X509ExtendedTrustManager) tm;
}
}
throw new TrustManagerException("Couldn't find X509TrustManager");
} catch (IOException | GeneralSecurityException | IllegalArgumentException e) {
throw new TrustManagerException(e);
}
}
private static String[] getEnabledProtocols(String enabledProtocolsInput, String tlsConfigProtocols) {
if (enabledProtocolsInput == null) {
return new String[] {tlsConfigProtocols};
}
return enabledProtocolsInput.split(",");
}
private static String[] getCipherSuites(String cipherSuitesInput) {
if (cipherSuitesInput == null) {
return getDefaultCipherSuites();
} else {
return cipherSuitesInput.split(",");
}
}
}