blob: 5a58029f98a295d8775bbe46e7d12413cbf4619c [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.knox.gateway.services.security.impl;
import static org.apache.knox.gateway.services.security.AliasService.NO_CLUSTER_NAME;
import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;
import org.apache.knox.gateway.GatewayMessages;
import org.apache.knox.gateway.GatewayResources;
import org.apache.knox.gateway.config.GatewayConfig;
import org.apache.knox.gateway.i18n.messages.MessagesFactory;
import org.apache.knox.gateway.i18n.resources.ResourcesFactory;
import org.apache.knox.gateway.services.Service;
import org.apache.knox.gateway.services.ServiceLifecycleException;
import org.apache.knox.gateway.services.security.KeystoreService;
import org.apache.knox.gateway.services.security.KeystoreServiceException;
import org.apache.knox.gateway.services.security.MasterService;
import org.apache.knox.gateway.util.X509CertificateUtil;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.InetAddress;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import java.security.GeneralSecurityException;
import java.security.Key;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;
import java.security.UnrecoverableKeyException;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.text.MessageFormat;
import java.util.Collections;
import java.util.HashSet;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import javax.crypto.spec.SecretKeySpec;
public class DefaultKeystoreService implements KeystoreService, Service {
private static final String DN_TEMPLATE = "CN={0},OU=Test,O=Hadoop,L=Test,ST=Test,C=US";
private static final String CREDENTIALS_SUFFIX = "-credentials.jceks";
private static final String CREDENTIALS_STORE_TYPE = "JCEKS";
private static final String CERT_GEN_MODE = "hadoop.gateway.cert.gen.mode";
private static final String CERT_GEN_MODE_LOCALHOST = "localhost";
private static final String CERT_GEN_MODE_HOSTNAME = "hostname";
private static GatewayMessages LOG = MessagesFactory.get(GatewayMessages.class);
private static GatewayResources RES = ResourcesFactory.get(GatewayResources.class);
// Let's configure the cache with hard-coded attributes now; we can introduce new gateway configuration later on if
// needed visible for testing
final Cache<CacheKey, String> cache = Caffeine.newBuilder().expireAfterAccess(10, TimeUnit.MINUTES).maximumSize(1000).build();
private GatewayConfig config;
private MasterService masterService;
private Path keyStoreDirPath;
public void setMasterService(MasterService ms) {
this.masterService = ms;
}
@Override
public void init(GatewayConfig config, Map<String, String> options)
throws ServiceLifecycleException {
this.config = config;
this.keyStoreDirPath = Paths.get(config.getGatewayKeystoreDir());
if (Files.notExists(keyStoreDirPath)) {
try {
// This will attempt to create all missing directories. No failures will occur if the
// directories already exist.
Files.createDirectories(keyStoreDirPath);
} catch (IOException e) {
throw new ServiceLifecycleException(RES.failedToCreateKeyStoreDirectory(keyStoreDirPath.toString()));
}
}
}
@Override
public void start() throws ServiceLifecycleException {
}
@Override
public void stop() throws ServiceLifecycleException {
}
@Override
public void createKeystoreForGateway() throws KeystoreServiceException {
createKeyStore(Paths.get(config.getIdentityKeystorePath()), config.getIdentityKeystoreType(),
getKeyStorePassword(config.getIdentityKeystorePasswordAlias()));
}
@Override
public KeyStore getKeystoreForGateway() throws KeystoreServiceException {
return getKeystore(Paths.get(config.getIdentityKeystorePath()), config.getIdentityKeystoreType(), config.getIdentityKeystorePasswordAlias(), true);
}
@Override
public KeyStore getTruststoreForHttpClient() throws KeystoreServiceException {
String trustStorePath = config.getHttpClientTruststorePath();
if (trustStorePath == null) {
return null;
} else {
return getKeystore(Paths.get(trustStorePath), config.getHttpClientTruststoreType(), config.getHttpClientTruststorePasswordAlias(), true);
}
}
@Override
public KeyStore getSigningKeystore() throws KeystoreServiceException {
return getSigningKeystore(null);
}
@Override
public KeyStore getSigningKeystore(String keystoreName) throws KeystoreServiceException {
Path keyStoreFile;
String keyStoreType;
String passwordAlias;
if(keystoreName != null) {
keyStoreFile = keyStoreDirPath.resolve(keystoreName + ".jks");
keyStoreType = "jks";
passwordAlias = null;
} else {
keyStoreFile = Paths.get(config.getSigningKeystorePath());
keyStoreType = config.getSigningKeystoreType();
passwordAlias = config.getSigningKeystorePasswordAlias();
}
return getKeystore(keyStoreFile, keyStoreType, passwordAlias, true);
}
@Override
public void addSelfSignedCertForGateway(String alias, char[] passphrase) throws KeystoreServiceException {
addSelfSignedCertForGateway(alias, passphrase, null);
}
@Override
public void addSelfSignedCertForGateway(String alias, char[] passphrase, String hostname)
throws KeystoreServiceException {
addCertForGateway(alias, passphrase, hostname);
}
private synchronized void addCertForGateway(String alias, char[] passphrase, String hostname)
throws KeystoreServiceException {
KeyPairGenerator keyPairGenerator;
try {
keyPairGenerator = KeyPairGenerator.getInstance("RSA");
keyPairGenerator.initialize(2048);
KeyPair KPair = keyPairGenerator.generateKeyPair();
if (hostname == null) {
hostname = System.getProperty(CERT_GEN_MODE, CERT_GEN_MODE_LOCALHOST);
}
X509Certificate cert;
if(hostname.equals(CERT_GEN_MODE_HOSTNAME)) {
String dn = buildDistinguishedName(InetAddress.getLocalHost().getHostName());
cert = X509CertificateUtil.generateCertificate(dn, KPair, 365, "SHA1withRSA");
}
else {
String dn = buildDistinguishedName(hostname);
cert = X509CertificateUtil.generateCertificate(dn, KPair, 365, "SHA1withRSA");
}
KeyStore privateKS = getKeystoreForGateway();
privateKS.setKeyEntry(alias, KPair.getPrivate(),
passphrase,
new java.security.cert.Certificate[]{cert});
writeKeyStoreToFile(privateKS, Paths.get(config.getIdentityKeystorePath()),
getKeyStorePassword(config.getIdentityKeystorePasswordAlias()));
} catch (GeneralSecurityException | IOException e) {
LOG.failedToAddSeflSignedCertForGateway( alias, e );
throw new KeystoreServiceException(e);
}
}
private String buildDistinguishedName(String hostname) {
MessageFormat headerFormatter = new MessageFormat(DN_TEMPLATE, Locale.ROOT);
String[] paramArray = new String[1];
paramArray[0] = hostname;
return headerFormatter.format(paramArray);
}
@Override
public void createCredentialStoreForCluster(String clusterName) throws KeystoreServiceException {
createKeyStore(keyStoreDirPath.resolve(clusterName + CREDENTIALS_SUFFIX),
CREDENTIALS_STORE_TYPE, masterService.getMasterSecret());
}
@Override
public boolean isCredentialStoreForClusterAvailable(String clusterName) throws KeystoreServiceException {
final Path keyStoreFilePath = keyStoreDirPath.resolve(clusterName + CREDENTIALS_SUFFIX);
try {
return isKeyStoreAvailable(keyStoreFilePath, CREDENTIALS_STORE_TYPE, masterService.getMasterSecret());
} catch (KeyStoreException | IOException e) {
throw new KeystoreServiceException(e);
}
}
@Override
public boolean isKeystoreForGatewayAvailable() throws KeystoreServiceException {
final Path keyStoreFilePath = Paths.get(config.getIdentityKeystorePath());
try {
return isKeyStoreAvailable(keyStoreFilePath, config.getIdentityKeystoreType(),
getKeyStorePassword(config.getIdentityKeystorePasswordAlias()));
} catch (KeyStoreException | IOException e) {
throw new KeystoreServiceException(e);
}
}
@Override
public Key getKeyForGateway(char[] passphrase) throws KeystoreServiceException {
return getKeyForGateway(config.getIdentityKeyAlias(), passphrase);
}
@Override
public Key getKeyForGateway(String alias, char[] passphrase) throws KeystoreServiceException {
return getKeyFromKeystore(getKeystoreForGateway(), alias, passphrase);
}
@Override
public Certificate getCertificateForGateway() throws KeystoreServiceException, KeyStoreException {
KeyStore ks = getKeystoreForGateway();
return (ks == null) ? null : ks.getCertificate(config.getIdentityKeyAlias());
}
@Override
public Key getSigningKey(String alias, char[] passphrase) throws KeystoreServiceException {
return getSigningKey(null, alias, passphrase);
}
@Override
public Key getSigningKey(String keystoreName, String alias, char[] passphrase) throws KeystoreServiceException {
return getKeyFromKeystore(getSigningKeystore(keystoreName), alias, passphrase);
}
private Key getKeyFromKeystore(KeyStore ks, String alias, char[] passphrase) {
Key key = null;
if (passphrase == null) {
passphrase = masterService.getMasterSecret();
LOG.assumingKeyPassphraseIsMaster();
}
if (ks != null) {
try {
key = ks.getKey(alias, passphrase);
} catch (UnrecoverableKeyException | NoSuchAlgorithmException | KeyStoreException e) {
LOG.failedToGetKeyForGateway( alias, e );
}
}
return key;
}
@Override
public KeyStore getCredentialStoreForCluster(String clusterName)
throws KeystoreServiceException {
// Do not fail getting the credential store if the keystore file does not exist. The returned
// KeyStore will be empty. This seems like a potential bug, but is the behavior before KNOX-1812
return getKeystore(keyStoreDirPath.resolve(clusterName + CREDENTIALS_SUFFIX),
CREDENTIALS_STORE_TYPE, null, false);
}
@Override
public void addCredentialForCluster(String clusterName, String alias, String value)
throws KeystoreServiceException {
addCredentialsForCluster(clusterName, Collections.singletonMap(alias, value));
}
@Override
public void addCredentialsForCluster(String clusterName, Map<String, String> credentials)
throws KeystoreServiceException {
// Needed to prevent read then write synchronization issue where alias is not added
synchronized (this) {
removeFromCache(clusterName, credentials.keySet());
KeyStore ks = getCredentialStoreForCluster(clusterName);
if (ks != null) {
try {
// Add all the credential keys to the keystore
for (Map.Entry<String, String> credential : credentials.entrySet()) {
final Key key = new SecretKeySpec(credential.getValue().getBytes(StandardCharsets.UTF_8), "AES");
ks.setKeyEntry(credential.getKey(), key, masterService.getMasterSecret(), null);
}
// Write all the changes once
final Path keyStoreFilePath = keyStoreDirPath.resolve(clusterName + CREDENTIALS_SUFFIX);
writeKeyStoreToFile(ks, keyStoreFilePath, masterService.getMasterSecret());
addToCache(clusterName, credentials);
} catch (KeyStoreException | IOException | CertificateException | NoSuchAlgorithmException e) {
LOG.failedToAddCredentialForCluster(clusterName, e);
}
}
}
}
@Override
public char[] getCredentialForCluster(String clusterName, String alias)
throws KeystoreServiceException {
char[] credential;
synchronized (this) {
credential = checkCache(clusterName, alias);
if (credential == null) {
KeyStore ks = getCredentialStoreForCluster(clusterName);
if (ks != null) {
try {
char[] masterSecret = masterService.getMasterSecret();
Key credentialKey = ks.getKey(alias, masterSecret);
if (credentialKey != null) {
byte[] credentialBytes = credentialKey.getEncoded();
String credentialString = new String(credentialBytes, StandardCharsets.UTF_8);
credential = credentialString.toCharArray();
addToCache(clusterName, alias, credentialString);
}
} catch (UnrecoverableKeyException | NoSuchAlgorithmException | KeyStoreException e) {
LOG.failedToGetCredentialForCluster(clusterName, e);
}
}
}
}
return credential;
}
@Override
public void removeCredentialForCluster(String clusterName, String alias) throws KeystoreServiceException {
removeCredentialsForCluster(clusterName, Collections.singleton(alias));
}
@Override
public void removeCredentialsForCluster(String clusterName, Set<String> aliases) throws KeystoreServiceException {
// Needed to prevent read then write synchronization issue where alias is not removed
synchronized (this) {
KeyStore ks = getCredentialStoreForCluster(clusterName);
if (ks != null) {
try {
// Delete all the entries
for (String alias : aliases) {
if (ks.containsAlias(alias)) {
ks.deleteEntry(alias);
}
}
removeFromCache(clusterName, aliases);
// Update the keystore file once to reflect all the alias deletions
final Path keyStoreFilePath = keyStoreDirPath.resolve(clusterName + CREDENTIALS_SUFFIX);
writeKeyStoreToFile(ks, keyStoreFilePath, masterService.getMasterSecret());
} catch (KeyStoreException | IOException | CertificateException | NoSuchAlgorithmException e) {
LOG.failedToRemoveCredentialForCluster(clusterName, e);
}
}
}
}
/**
* Called only from within critical sections of other methods above.
*/
private char[] checkCache(String clusterName, String alias) {
final String cachedCredential = cache.getIfPresent(CacheKey.of(clusterName, alias));
return cachedCredential == null ? null : cachedCredential.toCharArray();
}
/**
* Called only from within critical sections of other methods above.
*/
private void addToCache(String clusterName, String alias, String credentialString) {
cache.put(CacheKey.of(clusterName, alias), credentialString);
}
/**
* Called only from within critical sections of other methods above.
*/
private void addToCache(String clusterName, Map<String, String> credentials) {
for (String alias : credentials.keySet()) {
cache.put(CacheKey.of(clusterName, alias), credentials.get(alias));
}
}
/**
* Called only from within critical sections of other methods above.
*/
private void removeFromCache(String clusterName, String alias) {
cache.invalidate(CacheKey.of(clusterName, alias));
}
/**
* Called only from within critical sections of other methods above.
*/
private void removeFromCache(String clusterName, Set<String> aliases) {
Set<CacheKey> keys = new HashSet<>();
for (String alias : aliases) {
keys.add(CacheKey.of(clusterName, alias));
}
cache.invalidateAll(keys);
}
@Override
public String getKeystorePath() {
return config.getIdentityKeystorePath();
}
/**
* Loads a keystore file.
* <p>
* if <code>failIfNotAccessible</code> is <code>true</code>, then the path to the keystore file
* (keystorePath) is validated such that it exists, is a file and can be read by the process. If
* any of these checks fail, a {@link KeystoreServiceException} is thrown in dictating the exact
* reason.
* <p>
* Before the keystore file is loaded, the service's read lock is locked to prevent concurrent
* reads on the file.
*
* @param keystorePath the path to the keystore file
* @param keystoreType the type of keystore file
* @param alias the alias for the password to the keystore file (see {@link #getKeyStorePassword(String)})
* @param failIfNotAccessible <code>true</code> to ensure the keystore file exists and is readable; <code>false</code> to not check
* @return a {@link KeyStore}, or <code>null</code> if the requested keystore cannot be created
* @throws KeystoreServiceException if an error occurs loading the keystore file
*/
private synchronized KeyStore getKeystore(Path keystorePath, String keystoreType, String alias,
boolean failIfNotAccessible)
throws KeystoreServiceException {
if (failIfNotAccessible) {
if (Files.notExists(keystorePath)) {
LOG.keystoreFileDoesNotExist(keystorePath.toString());
throw new KeystoreServiceException("The keystore file does not exist: " + keystorePath.toString());
} else if (!Files.isRegularFile(keystorePath)) {
LOG.keystoreFileIsNotAFile(keystorePath.toString());
throw new KeystoreServiceException("The keystore file is not a file: " + keystorePath.toString());
} else if (!Files.isReadable(keystorePath)) {
LOG.keystoreFileIsNotAccessible(keystorePath.toString());
throw new KeystoreServiceException("The keystore file cannot be read: " + keystorePath.toString());
}
}
return loadKeyStore(keystorePath, keystoreType, getKeyStorePassword(alias));
}
private synchronized boolean isKeyStoreAvailable(final Path keyStoreFilePath, String storeType,
char[] password)
throws KeyStoreException, IOException {
if (Files.exists(keyStoreFilePath) &&
Files.isRegularFile(keyStoreFilePath) &&
Files.isReadable(keyStoreFilePath)) {
try (InputStream input = Files.newInputStream(keyStoreFilePath)) {
final KeyStore keyStore = KeyStore.getInstance(storeType);
keyStore.load(input, password);
return true;
} catch (NoSuchAlgorithmException | CertificateException e) {
LOG.failedToLoadKeystore(keyStoreFilePath.toString(), storeType, e);
} catch (IOException | KeyStoreException e) {
LOG.failedToLoadKeystore(keyStoreFilePath.toString(), storeType, e);
throw e;
}
}
return false;
}
// Package private for unit test access
// We need this to be synchronized to prevent multiple threads from using at once
synchronized KeyStore createKeyStore(Path keystoreFilePath, String keystoreType, char[] password)
throws KeystoreServiceException {
if (Files.notExists(keystoreFilePath)) {
// Ensure the parent directory exists...
try {
// This will attempt to create all missing directories. No failures will occur if the
// directories already exist.
Files.createDirectories(keystoreFilePath.getParent());
} catch (IOException e) {
LOG.failedToCreateKeystore(keystoreFilePath.toString(), keystoreType, e);
throw new KeystoreServiceException(e);
}
}
try {
KeyStore ks = KeyStore.getInstance(keystoreType);
ks.load(null, null);
writeKeyStoreToFile(ks, keystoreFilePath, password);
return ks;
} catch (NoSuchAlgorithmException | CertificateException | KeyStoreException | IOException e) {
LOG.failedToCreateKeystore(keystoreFilePath.toString(), keystoreType, e);
throw new KeystoreServiceException(e);
}
}
// Package private for unit test access
synchronized KeyStore loadKeyStore(final Path keyStoreFilePath, final String storeType,
final char[] password) throws KeystoreServiceException {
try {
final KeyStore keyStore = KeyStore.getInstance(storeType);
// If the file does not exist, create an empty keystore
if (Files.exists(keyStoreFilePath)) {
try (FileChannel fileChannel = FileChannel.open(keyStoreFilePath, StandardOpenOption.READ)) {
fileChannel.lock(0L, Long.MAX_VALUE, true);
try (InputStream input = Channels.newInputStream(fileChannel)) {
keyStore.load(input, password);
}
}
} else {
keyStore.load(null, password);
}
return keyStore;
} catch (CertificateException | NoSuchAlgorithmException | KeyStoreException | IOException e) {
LOG.failedToLoadKeystore(keyStoreFilePath.toString(), storeType, e);
throw new KeystoreServiceException(e);
}
}
// Package private for unit test access
synchronized void writeKeyStoreToFile(final KeyStore keyStore, final Path path, char[] password)
throws KeyStoreException, IOException, NoSuchAlgorithmException, CertificateException {
// TODO: backup the keystore on disk before attempting a write and restore on failure
try (FileChannel fileChannel = FileChannel.open(path, StandardOpenOption.WRITE,
StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING)) {
fileChannel.lock();
try (OutputStream out = Channels.newOutputStream(fileChannel)) {
keyStore.store(out, password);
}
}
}
private char[] getKeyStorePassword(String alias) throws KeystoreServiceException {
char[] password = null;
if (alias != null && !alias.isEmpty()) {
password = getCredentialForCluster(NO_CLUSTER_NAME, alias);
}
return (password == null) ? masterService.getMasterSecret() : password;
}
private static class CacheKey {
private final String clusterName;
private final String alias;
private CacheKey(String clusterName, String alias) {
this.clusterName = clusterName;
this.alias = alias;
}
private static CacheKey of(String clusterName, String alias) {
return new CacheKey(clusterName, alias);
}
@Override
public int hashCode() {
return HashCodeBuilder.reflectionHashCode(this);
}
@Override
public boolean equals(Object obj) {
return EqualsBuilder.reflectionEquals(this, obj);
}
}
}