/*
* 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.kafka.common.security.ssl;
import org.apache.kafka.common.Configurable;
import org.apache.kafka.common.KafkaException;
import org.apache.kafka.common.config.SslConfigs;
import org.apache.kafka.common.network.Mode;
import org.apache.kafka.common.config.types.Password;
import java.io.FileInputStream;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.security.KeyStore;
import java.security.SecureRandom;
import java.util.List;
import java.util.Map;
import javax.net.ssl.KeyManager;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLEngine;
import javax.net.ssl.SSLParameters;
import javax.net.ssl.TrustManagerFactory;
public class SslFactory implements Configurable {
private final Mode mode;
private final String clientAuthConfigOverride;
private String protocol;
private String provider;
private String kmfAlgorithm;
private String tmfAlgorithm;
private SecurityStore keystore = null;
private Password keyPassword;
private SecurityStore truststore;
private String[] cipherSuites;
private String[] enabledProtocols;
private String endpointIdentification;
private SecureRandom secureRandomImplementation;
private SSLContext sslContext;
private boolean needClientAuth;
private boolean wantClientAuth;
public SslFactory(Mode mode) {
this(mode, null);
}
public SslFactory(Mode mode, String clientAuthConfigOverride) {
this.mode = mode;
this.clientAuthConfigOverride = clientAuthConfigOverride;
}
@Override
public void configure(Map<String, ?> configs) throws KafkaException {
this.protocol = (String) configs.get(SslConfigs.SSL_PROTOCOL_CONFIG);
this.provider = (String) configs.get(SslConfigs.SSL_PROVIDER_CONFIG);
@SuppressWarnings("unchecked")
List<String> cipherSuitesList = (List<String>) configs.get(SslConfigs.SSL_CIPHER_SUITES_CONFIG);
if (cipherSuitesList != null)
this.cipherSuites = cipherSuitesList.toArray(new String[cipherSuitesList.size()]);
@SuppressWarnings("unchecked")
List<String> enabledProtocolsList = (List<String>) configs.get(SslConfigs.SSL_ENABLED_PROTOCOLS_CONFIG);
if (enabledProtocolsList != null)
this.enabledProtocols = enabledProtocolsList.toArray(new String[enabledProtocolsList.size()]);
String endpointIdentification = (String) configs.get(SslConfigs.SSL_ENDPOINT_IDENTIFICATION_ALGORITHM_CONFIG);
if (endpointIdentification != null)
this.endpointIdentification = endpointIdentification;
String secureRandomImplementation = (String) configs.get(SslConfigs.SSL_SECURE_RANDOM_IMPLEMENTATION_CONFIG);
if (secureRandomImplementation != null) {
try {
this.secureRandomImplementation = SecureRandom.getInstance(secureRandomImplementation);
} catch (GeneralSecurityException e) {
throw new KafkaException(e);
}
}
String clientAuthConfig = clientAuthConfigOverride;
if (clientAuthConfig == null)
clientAuthConfig = (String) configs.get(SslConfigs.SSL_CLIENT_AUTH_CONFIG);
if (clientAuthConfig != null) {
if (clientAuthConfig.equals("required"))
this.needClientAuth = true;
else if (clientAuthConfig.equals("requested"))
this.wantClientAuth = true;
}
this.kmfAlgorithm = (String) configs.get(SslConfigs.SSL_KEYMANAGER_ALGORITHM_CONFIG);
this.tmfAlgorithm = (String) configs.get(SslConfigs.SSL_TRUSTMANAGER_ALGORITHM_CONFIG);
createKeystore((String) configs.get(SslConfigs.SSL_KEYSTORE_TYPE_CONFIG),
(String) configs.get(SslConfigs.SSL_KEYSTORE_LOCATION_CONFIG),
(Password) configs.get(SslConfigs.SSL_KEYSTORE_PASSWORD_CONFIG),
(Password) configs.get(SslConfigs.SSL_KEY_PASSWORD_CONFIG));
createTruststore((String) configs.get(SslConfigs.SSL_TRUSTSTORE_TYPE_CONFIG),
(String) configs.get(SslConfigs.SSL_TRUSTSTORE_LOCATION_CONFIG),
(Password) configs.get(SslConfigs.SSL_TRUSTSTORE_PASSWORD_CONFIG));
try {
this.sslContext = createSSLContext();
} catch (Exception e) {
throw new KafkaException(e);
}
}
private SSLContext createSSLContext() throws GeneralSecurityException, IOException {
SSLContext sslContext;
if (provider != null)
sslContext = SSLContext.getInstance(protocol, provider);
else
sslContext = SSLContext.getInstance(protocol);
KeyManager[] keyManagers = null;
if (keystore != null) {
String kmfAlgorithm = this.kmfAlgorithm != null ? this.kmfAlgorithm : KeyManagerFactory.getDefaultAlgorithm();
KeyManagerFactory kmf = KeyManagerFactory.getInstance(kmfAlgorithm);
KeyStore ks = keystore.load();
Password keyPassword = this.keyPassword != null ? this.keyPassword : keystore.password;
kmf.init(ks, keyPassword.value().toCharArray());
keyManagers = kmf.getKeyManagers();
}
String tmfAlgorithm = this.tmfAlgorithm != null ? this.tmfAlgorithm : TrustManagerFactory.getDefaultAlgorithm();
TrustManagerFactory tmf = TrustManagerFactory.getInstance(tmfAlgorithm);
KeyStore ts = truststore == null ? null : truststore.load();
tmf.init(ts);
sslContext.init(keyManagers, tmf.getTrustManagers(), this.secureRandomImplementation);
return sslContext;
}
public SSLEngine createSslEngine(String peerHost, int peerPort) {
SSLEngine sslEngine = sslContext.createSSLEngine(peerHost, peerPort);
if (cipherSuites != null) sslEngine.setEnabledCipherSuites(cipherSuites);
if (enabledProtocols != null) sslEngine.setEnabledProtocols(enabledProtocols);
if (mode == Mode.SERVER) {
sslEngine.setUseClientMode(false);
if (needClientAuth)
sslEngine.setNeedClientAuth(needClientAuth);
else
sslEngine.setWantClientAuth(wantClientAuth);
} else {
sslEngine.setUseClientMode(true);
SSLParameters sslParams = sslEngine.getSSLParameters();
sslParams.setEndpointIdentificationAlgorithm(endpointIdentification);
sslEngine.setSSLParameters(sslParams);
}
return sslEngine;
}
/**
* Returns a configured SSLContext.
* @return SSLContext.
*/
public SSLContext sslContext() {
return sslContext;
}
private void createKeystore(String type, String path, Password password, Password keyPassword) {
if (path == null && password != null) {
throw new KafkaException("SSL key store is not specified, but key store password is specified.");
} else if (path != null && password == null) {
throw new KafkaException("SSL key store is specified, but key store password is not specified.");
} else if (path != null && password != null) {
this.keystore = new SecurityStore(type, path, password);
this.keyPassword = keyPassword;
}
}
private void createTruststore(String type, String path, Password password) {
if (path == null && password != null) {
throw new KafkaException("SSL trust store is not specified, but trust store password is specified.");
} else if (path != null) {
this.truststore = new SecurityStore(type, path, password);
}
}
private static class SecurityStore {
private final String type;
private final String path;
private final Password password;
private SecurityStore(String type, String path, Password password) {
this.type = type == null ? KeyStore.getDefaultType() : type;
this.path = path;
this.password = password;
}
private KeyStore load() throws GeneralSecurityException, IOException {
FileInputStream in = null;
try {
KeyStore ks = KeyStore.getInstance(type);
in = new FileInputStream(path);
// If a password is not set access to the truststore is still available, but integrity checking is disabled.
char[] passwordChars = password != null ? password.value().toCharArray() : null;
ks.load(in, passwordChars);
return ks;
} finally {
if (in != null) in.close();
}
}
}
}