Skip to content

Commit

Permalink
[ELY-2496] Add integrity to existing filesystem realms using Elytron …
Browse files Browse the repository at this point in the history
…Tool
  • Loading branch information
jessicarod7 committed May 23, 2023
1 parent babbb20 commit 161a2fe
Show file tree
Hide file tree
Showing 7 changed files with 1,292 additions and 19 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@


import java.util.List;

import org.wildfly.common.Assert;
import org.wildfly.security.auth.principal.NamePrincipal;
import org.wildfly.security.auth.server.ModifiableRealmIdentity;
Expand All @@ -31,34 +32,34 @@
* A utility class to utilize methods from the {@code FileSystemSecurityRealm} class for the Elytron Tool.
*
* @author <a href="mailto:[email protected]">Ashpan Raskar</a>
* @author <a href="mailto:[email protected]">Cameron Rodriguez</a>
*/
public class FileSystemRealmUtil {

/**
* Converts a pre-existing unencrypted {@code FileSystemSecurityRealm} to a newly created encrypted {@code FileSystemSecurityRealm}
* Copies identities from an existing {@code FileSystemSecurityRealm} to a new one.
*
* @param unencryptedRealm the {@code FileSystemSecurityRealm} without any encryption applied
* @param encryptedRealm the {@code FileSystemSecurityRealm} configured with a SecretKey to encrypt identity data
* @throws RealmUnavailableException if either realm is unavailable
* @param oldRealm the existing {@code FileSystemSecurityRealm} with the identities
* @param newRealm the new {@code FileSystemSecurityRealm}
* @throws RealmUnavailableException if either realm is unavailable or an operation fails
*/
public static void createEncryptedRealmFromUnencrypted(FileSystemSecurityRealm unencryptedRealm, FileSystemSecurityRealm encryptedRealm) throws RealmUnavailableException {
Assert.checkNotNullParam("unencryptedRealm", unencryptedRealm);
Assert.checkNotNullParam("encryptedRealm", encryptedRealm);
public static void cloneIdentitiesToNewRealm(FileSystemSecurityRealm oldRealm, FileSystemSecurityRealm newRealm) throws RealmUnavailableException {
Assert.checkNotNullParam("Old FileSystem Realm", oldRealm);
Assert.checkNotNullParam("New FileSystem Realm", newRealm);

ModifiableRealmIdentityIterator realmIterator = unencryptedRealm.getRealmIdentityIterator();
ModifiableRealmIdentityIterator realmIterator = oldRealm.getRealmIdentityIterator();

while (realmIterator.hasNext()) {
ModifiableRealmIdentity identity = realmIterator.next();
List<Credential> credentials = ((FileSystemSecurityRealm.Identity) identity).loadCredentials();
Attributes attributes = identity.getAttributes();
ModifiableRealmIdentity oldIdentity = realmIterator.next();
List<Credential> credentials = ((FileSystemSecurityRealm.Identity) oldIdentity).loadCredentials();
Attributes attributes = oldIdentity.getAttributes();

ModifiableRealmIdentity newIdentity = encryptedRealm.getRealmIdentityForUpdate(new NamePrincipal(identity.getRealmIdentityPrincipal().getName()));
ModifiableRealmIdentity newIdentity = newRealm.getRealmIdentityForUpdate(new NamePrincipal(oldIdentity.getRealmIdentityPrincipal().getName()));
newIdentity.create();
newIdentity.setCredentials(credentials);
newIdentity.setAttributes(attributes);
newIdentity.dispose();
}
realmIterator.close();
}

}
93 changes: 92 additions & 1 deletion tool/src/main/java/org/wildfly/security/tool/Command.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,23 @@
import java.io.BufferedReader;
import java.io.Console;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOError;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.security.KeyPair;
import java.security.KeyStore;
import java.security.KeyStoreException;
import java.security.GeneralSecurityException;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.Provider;
import java.security.Security;
import java.security.UnrecoverableKeyException;
import java.security.cert.Certificate;
import java.security.cert.CertificateException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
Expand All @@ -41,6 +51,7 @@
import java.util.ServiceLoader;
import java.util.Set;
import java.util.function.Supplier;
import java.util.regex.Pattern;

import javax.crypto.SecretKey;

Expand All @@ -50,7 +61,11 @@
import org.wildfly.security.credential.store.UnsupportedCredentialTypeException;
import org.wildfly.security.credential.store.impl.PropertiesCredentialStore;
import org.wildfly.security.encryption.SecretKeyUtil;
import org.wildfly.security.keystore.AtomicLoadKeyStore;
import org.wildfly.security.keystore.KeyStoreUtil;
import org.wildfly.security.keystore.WildFlyElytronKeyStoreProvider;
import org.wildfly.security.password.WildFlyElytronPasswordProvider;
import org.wildfly.security.provider.util.ProviderUtil;

/**
* Base command class
Expand All @@ -67,7 +82,8 @@ public abstract class Command {

public static final int INPUT_DATA_NOT_CONFIRMED = 3;

public static Supplier<Provider[]> ELYTRON_PASSWORD_PROVIDERS = () -> new Provider[] {
public static Supplier<Provider[]> ELYTRON_KS_PASS_PROVIDERS = () -> new Provider[] {
WildFlyElytronKeyStoreProvider.getInstance(),
WildFlyElytronPasswordProvider.getInstance()
};

Expand Down Expand Up @@ -378,10 +394,79 @@ SecretKey getSecretKey(Boolean createCredentialStore, String credentialStoreLoca

return key;
}

/**
* Acquire a given keypair from a {@link KeyStore}.
*
* @param keyPairAlias the name for a keypair within the keyStore
* @param descriptorBlockCount index of a descriptor block from bulk conversion files
* @return the requested {@link KeyPair}, or {@code null} if it could not be retrieved
* @throws CertificateException if the KeyStore cannot be loaded
* @throws NoSuchAlgorithmException if the algorithm to verify the KeyStore's integrity is not available
*/
KeyPair getKeyPair(Path keyStorePath, String keyStoreType, String keyPairAlias, char[] password,
int descriptorBlockCount) throws CertificateException, NoSuchAlgorithmException {
AtomicLoadKeyStore keyStore = null;
if (keyStoreType != null) {
keyStore = AtomicLoadKeyStore.newInstance(
keyStoreType,
ProviderUtil.findProvider(ProviderUtil.INSTALLED_PROVIDERS, null, KeyStore.class, keyStoreType)
);
}

File resolvedPath = null;
if (keyStorePath != null && keyStorePath.toFile().exists()) {
try {
resolvedPath = keyStorePath.toAbsolutePath().toFile();
} catch (IOError | SecurityException e) {
warningHandler(ElytronToolMessages.msg.skippingDescriptorBlockInvalidKeyStorePath(descriptorBlockCount));
return null;
}
}
if (resolvedPath == null){
warningHandler(ElytronToolMessages.msg.skippingDescriptorBlockInvalidKeyStorePath(descriptorBlockCount));
return null;
}

try (FileInputStream is = new FileInputStream(resolvedPath)) {
if (keyStoreType != null) {
keyStore.load(is, password);
} else {
KeyStore detected = KeyStoreUtil.loadKeyStore(ProviderUtil.INSTALLED_PROVIDERS, null,
is, resolvedPath.getPath(), password);
keyStore = AtomicLoadKeyStore.atomize(detected);
}
} catch (IOException | KeyStoreException e) {
warningHandler(ElytronToolMessages.msg.skippingDescriptorBlockKeyStoreNotLoaded(descriptorBlockCount));
return null;
}

PrivateKey privateKey;
Certificate publicKeyCert;
try {
privateKey = (PrivateKey) keyStore.getKey(keyPairAlias, password);
publicKeyCert = keyStore.getCertificate(keyPairAlias);
} catch (KeyStoreException | NoSuchAlgorithmException | UnrecoverableKeyException e) {
warningHandler(ElytronToolMessages.msg.skippingDescriptorBlockKeyPairFail(descriptorBlockCount));
return null;
}

if (privateKey == null) {
warningHandler(ElytronToolMessages.msg.skippingDescriptorBlockMissingPrivateKey(descriptorBlockCount));
return null;
}
if (publicKeyCert == null) {
warningHandler(ElytronToolMessages.msg.skippingDescriptorBlockMissingPublicKey(descriptorBlockCount));
return null;
}

return new KeyPair(publicKeyCert.getPublicKey(), privateKey);
}
}

class Params {
static final String ALIAS_PARAM = "alias";
static final String BOOLEAN_PARAM = "true/false";
static final String BULK_CONVERT_PARAM = "bulk-convert";
static final String CREDENTIAL_STORE_LOCATION_PARAM = "credential-store";
static final String CREATE_CREDENTIAL_STORE_PARAM = "create";
Expand All @@ -397,12 +482,16 @@ class Params {
static final String IMPLEMENTATION_PROPERTIES_PARAM = "properties";
static final String INPUT_LOCATION_PARAM = "input-location";
static final String ITERATION_PARAM = "iteration";
static final String KEY_PAIR_ALIAS_PARAM = "key-pair";
static final String KEYSTORE_PARAM = "keystore";
static final String KEYSTORE_TYPE_PARAM = "type";
static final String LEVELS_PARAM = "levels";
static final String NAME_PARAM = "name";
static final String NUMBER_PARAM = "number";
static final String OTHER_PROVIDERS_PARAM = "other-providers";
static final String OUTPUT_LOCATION_PARAM = "output-location";
static final String PASSWORD_PARAM = "password";
static final String PASSWORD_ENV_PARAM = "password-env";
static final String REALM_NAME_PARAM = "realm-name";
static final String SALT_PARAM = "salt";
static final String SECRET_KEY_ALIAS_PARAM = "secret-key";
Expand All @@ -411,6 +500,8 @@ class Params {
static final String SUMMARY_PARAM = "summary";

// Other constants
static final Pattern BOOLEAN_ARG_REGEX = Pattern.compile("(true|false)", Pattern.CASE_INSENSITIVE | Pattern.UNICODE_CASE);
static final String DEFAULT_KEY_PAIR_ALIAS = "integrity-key";
static final Integer DEFAULT_LEVELS = 2;
static final String DEFAULT_SECRET_KEY_ALIAS = "key";
static final String FILE_SEPARATOR = File.separator;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ public ElytronTool() {
commandRegistry.put(VaultCommand.VAULT_COMMAND, new VaultCommand()); // uses exit code 7
commandRegistry.put(FileSystemRealmCommand.FILE_SYSTEM_REALM_COMMAND, new FileSystemRealmCommand()); // uses exit code 7
commandRegistry.put(FileSystemEncryptRealmCommand.FILE_SYSTEM_ENCRYPT_COMMAND, new FileSystemEncryptRealmCommand()); // uses exit code 7
commandRegistry.put(FileSystemRealmIntegrityCommand.FILE_SYSTEM_REALM_INTEGRITY_COMMAND, new FileSystemRealmIntegrityCommand()); // uses exit code 7
}

/**
Expand Down
107 changes: 107 additions & 0 deletions tool/src/main/java/org/wildfly/security/tool/ElytronToolMessages.java
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,12 @@ public interface ElytronToolMessages extends BasicLogger {
@Message(id = NONE, value = "Password for credential store")
String cmdLineCredentialStorePassword();

@Message(id = NONE, value = "Password for KeyStore. Can also be provided by console prompt.")
String cmdLineKeyStorePassword();

@Message(id = NONE, value = "Name of an environment variable from which to resolve the KeyStore password.")
String cmdLineKeyStorePasswordEnv();

@Message(id = NONE, value = "Salt to apply for final masked password of the credential store")
String cmdLineSaltDesc();

Expand Down Expand Up @@ -481,16 +487,81 @@ public interface ElytronToolMessages extends BasicLogger {
"Blocks of options must be separated by a blank line.")
String cmdFileSystemRealmEncryptBulkConvertDesc();

@Message(id = NONE, value = "Bulk conversion with options listed in description file. (Action)" +
"Optional options have defaults and can be skipped ([type, default_or_NULL]), required options do not (<type>). %n" +
"One of either password or password-env is required. %n" +
"Blocks of options must be separated by a blank line; order is not important. Syntax: %n" +
"input-location:<directory>%n" + "output-location:[directory,NULL]%n" + "realm-name:[name,filesystem-realm-with-integrity]%n" +
"keystore:<file>%n" + "type:[name,NULL]%n" + "password:[password,NULL]%n" + "password-env:[name,NULL]%n" +
"key-pair:[name,integrity-key]%n" + "credential-store:[file,NULL]%n" + "secret-key:[name,NULL]%n" +
"levels:[number,2]%n" + "hash-encoding:[name,BASE64]%n" + "hash-charset:[name,UTF-8]%n" + "encoded:[bool,true]")
String cmdFileSystemRealmIntegrityBulkConvertDesc();

// filesystem-realm encrypt command
@Message(id = NONE, value = "'FileSystemRealmEncrypt' command is used to convert un-encrypted FileSystemSecurityRealm(s) to encrypted FileSystemSecurityRealm(s) with a SecretKey.")
String cmdFileSystemEncryptHelpHeader();

@Message(id = NONE, value = "Secret Key was not found in the Credential Store at %s, and populate option was not set. Skipping descriptor file block number %d.")
String cmdFileSystemEncryptionNoSecretKey(String credentialStorePath, Integer blockNumber);

@Message(id = NONE, value = "The directory where the new filesystem realm resides. If not provided, realm will be upgraded in-place (with backup), %n" +
"and realm-name option will not be used in file path.")
String cmdFileSystemRealmIntegrityOutputLocationDesc();

@Message(id = NONE, value = "The name of the new filesystem-realm. Will be appended to output-location path (if output-location is provided). %n" +
"When not set, nothing is appended to the path, and `filesystem-realm-with-integrity` is used for the WildFly resource name.%n")
String cmdFileSystemRealmIntegrityNewRealmDesc();

@Message(id = NONE, value = "The relative or absolute path to the KeyStore file that contains the key pair.")
String cmdFileSystemRealmIntegrityKeyStoreDesc();

@Message(id = NONE, value = "The type of KeyStore to be used. Optional.")
String cmdFileSystemRealmIntegrityKeyStoreTypeDesc();

@Message(id = NONE, value = "The alias of the key pair to be used, within the KeyStore. Set to integrity-key by default.")
String cmdFileSystemRealmIntegrityKeyPairAliasDesc();

@Message(id = NONE, value = "The relative or absolute path to the secret-key-credential-store file. Only %n" +
"applicable if the filesystem realm is encrypted.")
String cmdFileSystemRealmIntegrityCredentialStoreDesc();

@Message(id = NONE, value = "The alias of the secret key stored in the credential store file. Set to key by default, only %n" +
"applicable if the filesystem realm is encrypted.")
String cmdFileSystemRealmIntegritySecretKeyDesc();

@Message(id = NONE, value = "The number of levels used in the input filesystem realm. Set to 2 by default.")
String cmdFileSystemRealmIntegrityLevelsDesc();

@Message(id = NONE, value = "The hash encoding used in the input filesystem realm. Set to BASE64 by default.%n" +
"Regardless of setting, the output realm will always be BASE64-encoded.")
String cmdFileSystemRealmIntegrityHashEncodingDesc();

@Message(id = NONE, value = "The character set used to convert the password string to a byte array. Defaults to UTF-8.")
String cmdFileSystemRealmIntegrityHashCharsetDesc();

@Message(id = NONE, value = "Indicates if the original realm used Base32-encoded identities as file names.%n" +
"Set to true by default. Regardless of setting, the output realm will always use Base32-encoding in file names.")
String cmdFileSystemRealmIntegrityEncodedDesc();

@Message(id = NONE, value = "Both --bulk-convert and at least one other realm configuration option was specified. %n" +
"The bulk-convert option may only be used with --help, --debug, --silent, and --summary options.")
MissingOptionException mutuallyExclusiveOptionsIntegritySpecified();

@Message(id = NONE, value = "'FileSystem Realm Integrity' command is used to sign existing, non-empty FileSystem Security Realms with a key pair, for future integrity validation.")
String cmdFileSystemIntegrityHelpHeader();

@Message(id = NONE, value = "KeyStore password: ")
String keyStorePasswordPrompt();

@Message(id = NONE, value = "KeyStore path not specified.")
MissingArgumentException keyStorePathNotSpecified();

@Message(id = NONE, value = "KeyStore does not exist.")
MissingArgumentException keyStoreDoesNotExist();

@Message(id = NONE, value = "Encoded option must be set to 'true' or 'false'.")
IllegalArgumentException encodedMustBeBoolean();

@Message(id = NONE, value = "Suppresses all output except errors and prompts.")
String cmdFileSystemRealmSilentDesc();

Expand Down Expand Up @@ -518,6 +589,10 @@ public interface ElytronToolMessages extends BasicLogger {
@Message(id = NONE, value = "Could not find the specified file %s.")
FileNotFoundException fileNotFound(String file);

@Message(id = NONE, value = "Could not copy input filesystem realm at %s for in-place upgrade.%n" +
"Output filesystem will be placed at %s")
String unableToUpgradeInPlace(String inputPath, String newOutputPath);

@Message(id = NONE, value = "Skipping descriptor file block number %d due to %s.")
String skippingDescriptorBlock(Integer blockNumber, String reason);

Expand All @@ -531,12 +606,44 @@ public interface ElytronToolMessages extends BasicLogger {
@Message(id = NONE, value = "Skipping descriptor file block number %d due to missing output realm location.")
String skippingDescriptorBlockOutputLocation(Integer blockNumber);

@Message(id = NONE, value = "Skipping descriptor file block number %d due to missing KeyStore path.")
String skippingDescriptorBlockKeyStorePath(Integer blockName);

@Message(id = NONE, value = "Skipping descriptor file block number %d due to missing KeyStore password.")
String skippingDescriptorBlockPassword(Integer blockName);

@Message(id = NONE, value = "Skipping descriptor file block number %d due to invalid KeyStore path.")
String skippingDescriptorBlockInvalidKeyStorePath(Integer blockNumber);

@Message(id = NONE, value = "Skipping descriptor file block number %d due to failure to load KeyStore.")
String skippingDescriptorBlockKeyStoreNotLoaded(Integer blockNumber);

@Message(id = NONE, value = "Skipping descriptor file block number %d due to failure to load key pair.")
String skippingDescriptorBlockKeyPairFail(Integer blockNumber);

@Message(id = NONE, value = "Skipping descriptor file block number %d due to missing private key.")
String skippingDescriptorBlockMissingPrivateKey(Integer blockNumber);

@Message(id = NONE, value = "Skipping descriptor file block number %d due to missing public key.")
String skippingDescriptorBlockMissingPublicKey(Integer blockNumber);

@Message(id = NONE, value = "Skipping descriptor file block number %d due to missing new filesystem realm name.")
String skippingDescriptorBlockFilesystemRealmName(Integer blockNumber);

@Message(id = NONE, value = "Skipping descriptor file block number %d due to no identities present in filesystem realm.%n" +
"Keys for this realm can be added via the management client.")
String skippingDescriptorBlockEmptyRealm(Integer blockNumber);

@Message(id = NONE, value = "Creating encrypted realm for: %s")
String fileSystemRealmEncryptCreatingRealm(String realmName);

@Message(id = NONE, value = "Creating filesystem realm with integrity verification for: %s")
String fileSystemRealmIntegrityCreatingRealm(String realmName);

@Message(id = NONE, value = "In-place upgrade for descriptor block %d: filesystem realm backed up at %s%n" +
"Realm name will not be used in output realm path.")
String fileSystemRealmIntegrityInPlaceBackup(Integer blockNumber, String backupLocation);

@Message(id = NONE, value = "Should file %s be overwritten? (y/n) ")
String shouldFileBeOverwritten(String file);

Expand Down
Loading

0 comments on commit 161a2fe

Please sign in to comment.