| /* |
| * 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); |
| } |
| } |
| } |