From 13e3181d4528a6b74c09792725edfac382d09c00 Mon Sep 17 00:00:00 2001 From: Ashpan Raskar Date: Fri, 19 Nov 2021 14:17:47 -0500 Subject: [PATCH] [WFCORE-5859] Add Filesystem integrity support --- .../security/elytron-base/main/module.xml | 1 + .../elytron/ElytronDescriptionConstants.java | 3 + .../elytron/ElytronSubsystemParser16_0.java | 7 + .../elytron/ElytronSubsystemTransformers.java | 8 + .../elytron/FileSystemRealmDefinition.java | 173 +++++++++++++++++- .../elytron/KeyStoreServiceUtil.java | 48 +++++ .../extension/elytron/RealmParser.java | 30 +++ .../_private/ElytronSubsystemMessages.java | 22 +++ .../elytron/LocalDescriptions.properties | 5 + .../resources/schema/wildfly-elytron_16_0.xsd | 15 ++ .../extension/elytron/RealmsTestCase.java | 116 ++++++++++++ .../elytron/SubsystemTransformerTestCase.java | 2 + .../elytron-transformers-13.0-reject.xml | 8 + .../u/user-OVZWK4Q.xml | 13 ++ .../u/user2-OVZWK4RS.xml | 9 + .../org/wildfly/extension/elytron/keystore | Bin 0 -> 1230 bytes .../wildfly/extension/elytron/realms-test.xml | 15 +- 17 files changed, 465 insertions(+), 10 deletions(-) create mode 100644 elytron/src/main/java/org/wildfly/extension/elytron/KeyStoreServiceUtil.java create mode 100644 elytron/src/test/resources/org/wildfly/extension/elytron/filesystem-realm-integrity/u/user-OVZWK4Q.xml create mode 100644 elytron/src/test/resources/org/wildfly/extension/elytron/filesystem-realm-integrity/u/user2-OVZWK4RS.xml create mode 100644 elytron/src/test/resources/org/wildfly/extension/elytron/keystore diff --git a/core-feature-pack/common/src/main/resources/modules/system/layers/base/org/wildfly/security/elytron-base/main/module.xml b/core-feature-pack/common/src/main/resources/modules/system/layers/base/org/wildfly/security/elytron-base/main/module.xml index 8d414d49f28..0a782c8dc4f 100644 --- a/core-feature-pack/common/src/main/resources/modules/system/layers/base/org/wildfly/security/elytron-base/main/module.xml +++ b/core-feature-pack/common/src/main/resources/modules/system/layers/base/org/wildfly/security/elytron-base/main/module.xml @@ -109,6 +109,7 @@ + diff --git a/elytron/src/main/java/org/wildfly/extension/elytron/ElytronDescriptionConstants.java b/elytron/src/main/java/org/wildfly/extension/elytron/ElytronDescriptionConstants.java index 1b4a25633c3..9c12ffd4f68 100644 --- a/elytron/src/main/java/org/wildfly/extension/elytron/ElytronDescriptionConstants.java +++ b/elytron/src/main/java/org/wildfly/extension/elytron/ElytronDescriptionConstants.java @@ -104,6 +104,7 @@ interface ElytronDescriptionConstants { String CHAINED_PRINCIPAL_TRANSFORMER = "chained-principal-transformer"; String CHANGE_ACCOUNT_KEY = "change-account-key"; String CHANGE_ALIAS = "change-alias"; + String UPDATE_KEY_PAIR = "update-key-pair"; String CIPHER_SUITE = "cipher-suite"; String CIPHER_SUITE_FILTER = "cipher-suite-filter"; String CIPHER_SUITE_NAMES = "cipher-suite-names"; @@ -288,6 +289,7 @@ interface ElytronDescriptionConstants { String KEY_MAP = "key-map"; String KEY_SIZE = "key-size"; String KEY_STORE = "key-store"; + String KEY_STORE_ALIAS = "key-store-alias"; String KEY_STORE_REALM = "key-store-realm"; String KEY_STORES = "key-stores"; String KID = "kid"; @@ -603,6 +605,7 @@ interface ElytronDescriptionConstants { String VALUE = "value"; String VERBOSE = "verbose"; String VERIFIABLE = "verifiable"; + String VERIFY_INTEGRITY = "verify-integrity"; String VERSION = "version"; String VERSION_COMPARISON = "version-comparison"; diff --git a/elytron/src/main/java/org/wildfly/extension/elytron/ElytronSubsystemParser16_0.java b/elytron/src/main/java/org/wildfly/extension/elytron/ElytronSubsystemParser16_0.java index bb7b97caaee..317ac710254 100644 --- a/elytron/src/main/java/org/wildfly/extension/elytron/ElytronSubsystemParser16_0.java +++ b/elytron/src/main/java/org/wildfly/extension/elytron/ElytronSubsystemParser16_0.java @@ -18,6 +18,8 @@ package org.wildfly.extension.elytron; +import org.jboss.as.controller.PersistentResourceXMLDescription; + /** * The subsystem parser, which uses stax to read and write to and from xml. * @@ -30,5 +32,10 @@ String getNameSpace() { return ElytronExtension.NAMESPACE_16_0; } + @Override + PersistentResourceXMLDescription getRealmParser() { + return new RealmParser().realmParser_16; + } + } diff --git a/elytron/src/main/java/org/wildfly/extension/elytron/ElytronSubsystemTransformers.java b/elytron/src/main/java/org/wildfly/extension/elytron/ElytronSubsystemTransformers.java index 81c1a76d3f3..b4d39d73c48 100644 --- a/elytron/src/main/java/org/wildfly/extension/elytron/ElytronSubsystemTransformers.java +++ b/elytron/src/main/java/org/wildfly/extension/elytron/ElytronSubsystemTransformers.java @@ -32,6 +32,8 @@ import static org.wildfly.extension.elytron.ElytronDescriptionConstants.HASH_CHARSET; import static org.wildfly.extension.elytron.ElytronDescriptionConstants.HASH_ENCODING; import static org.wildfly.extension.elytron.ElytronDescriptionConstants.JDBC_REALM; +import static org.wildfly.extension.elytron.ElytronDescriptionConstants.KEY_STORE; +import static org.wildfly.extension.elytron.ElytronDescriptionConstants.KEY_STORE_ALIAS; import static org.wildfly.extension.elytron.ElytronDescriptionConstants.LDAP_REALM; import static org.wildfly.extension.elytron.ElytronDescriptionConstants.MODULAR_CRYPT_MAPPER; import static org.wildfly.extension.elytron.ElytronDescriptionConstants.PERIODIC_ROTATING_FILE_AUDIT_LOG; @@ -141,6 +143,12 @@ public void registerTransformers(SubsystemTransformerRegistration registration) private static void from16(ChainedTransformationDescriptionBuilder chainedBuilder) { ResourceTransformationDescriptionBuilder builder = chainedBuilder.createBuilder(ELYTRON_16_0_0, ELYTRON_15_1_0); + builder.addChildResource(PathElement.pathElement(FILESYSTEM_REALM)) + .getAttributeBuilder() + .setDiscard(DiscardAttributeChecker.UNDEFINED, KEY_STORE) + .setDiscard(DiscardAttributeChecker.UNDEFINED, KEY_STORE_ALIAS) + .addRejectCheck(RejectAttributeChecker.DEFINED, KEY_STORE) + .addRejectCheck(RejectAttributeChecker.DEFINED, KEY_STORE_ALIAS); } private static void from15_1(ChainedTransformationDescriptionBuilder chainedBuilder) { diff --git a/elytron/src/main/java/org/wildfly/extension/elytron/FileSystemRealmDefinition.java b/elytron/src/main/java/org/wildfly/extension/elytron/FileSystemRealmDefinition.java index a3a7536c920..162f966849f 100644 --- a/elytron/src/main/java/org/wildfly/extension/elytron/FileSystemRealmDefinition.java +++ b/elytron/src/main/java/org/wildfly/extension/elytron/FileSystemRealmDefinition.java @@ -21,19 +21,29 @@ import static org.jboss.as.controller.capability.RuntimeCapability.buildDynamicCapabilityName; import static org.wildfly.extension.elytron.Capabilities.CREDENTIAL_STORE_API_CAPABILITY; import static org.wildfly.extension.elytron.Capabilities.CREDENTIAL_STORE_CAPABILITY; +import static org.wildfly.extension.elytron.Capabilities.KEY_STORE_CAPABILITY; import static org.wildfly.extension.elytron.Capabilities.MODIFIABLE_SECURITY_REALM_RUNTIME_CAPABILITY; import static org.wildfly.extension.elytron.Capabilities.SECURITY_REALM_CAPABILITY; import static org.wildfly.extension.elytron.Capabilities.SECURITY_REALM_RUNTIME_CAPABILITY; import static org.wildfly.extension.elytron.ElytronDescriptionConstants.BASE64; import static org.wildfly.extension.elytron.ElytronDescriptionConstants.HEX; import static org.wildfly.extension.elytron.ElytronDescriptionConstants.UTF_8; +import static org.wildfly.extension.elytron.ElytronExtension.getRequiredService; +import static org.wildfly.extension.elytron.ElytronExtension.isServerOrHostController; import static org.wildfly.extension.elytron.FileAttributeDefinitions.pathName; import static org.wildfly.extension.elytron.FileAttributeDefinitions.pathResolver; +import static org.wildfly.extension.elytron.KeyStoreServiceUtil.getModifiableKeyStoreService; import static org.wildfly.extension.elytron._private.ElytronSubsystemMessages.ROOT_LOGGER; +import java.io.IOException; import java.nio.charset.Charset; import java.nio.file.Path; import java.security.KeyStore; +import java.security.KeyStoreException; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.security.UnrecoverableKeyException; import javax.crypto.SecretKey; import org.jboss.as.controller.AbstractAddStepHandler; @@ -42,11 +52,14 @@ import org.jboss.as.controller.OperationContext; import org.jboss.as.controller.OperationFailedException; import org.jboss.as.controller.OperationStepHandler; +import org.jboss.as.controller.PathAddress; import org.jboss.as.controller.PathElement; import org.jboss.as.controller.ResourceDefinition; import org.jboss.as.controller.SimpleAttributeDefinition; import org.jboss.as.controller.SimpleAttributeDefinitionBuilder; +import org.jboss.as.controller.SimpleOperationDefinitionBuilder; import org.jboss.as.controller.SimpleResourceDefinition; +import org.jboss.as.controller.descriptions.ResourceDescriptionResolver; import org.jboss.as.controller.operations.validation.CharsetValidator; import org.jboss.as.controller.operations.validation.StringAllowedValuesValidator; import org.jboss.as.controller.registry.AttributeAccess; @@ -56,18 +69,23 @@ import org.jboss.as.controller.services.path.PathManagerService; import org.jboss.dmr.ModelNode; import org.jboss.dmr.ModelType; +import org.jboss.msc.Service; import org.jboss.msc.service.ServiceBuilder; +import org.jboss.msc.service.ServiceController; import org.jboss.msc.service.ServiceName; +import org.jboss.msc.service.ServiceRegistry; import org.jboss.msc.service.ServiceTarget; import org.jboss.msc.service.StartException; import org.jboss.msc.value.InjectedValue; import org.wildfly.common.function.ExceptionFunction; +import org.wildfly.common.function.ExceptionSupplier; import org.wildfly.extension.elytron.FileAttributeDefinitions.PathResolver; import org.wildfly.security.auth.realm.FileSystemSecurityRealm; import org.wildfly.security.auth.realm.FileSystemSecurityRealmBuilder; import org.wildfly.security.auth.server.NameRewriter; import org.wildfly.security.auth.server.SecurityRealm; import org.wildfly.security.credential.SecretKeyCredential; +import org.wildfly.security.credential.source.CredentialSource; import org.wildfly.security.credential.store.CredentialStore; import org.wildfly.security.credential.store.CredentialStoreException; import org.wildfly.security.password.spec.Encoding; @@ -140,7 +158,25 @@ class FileSystemRealmDefinition extends SimpleResourceDefinition { .setRestartAllServices() .build(); - static final AttributeDefinition[] ATTRIBUTES = new AttributeDefinition[]{PATH, RELATIVE_TO, LEVELS, ENCODED, HASH_ENCODING, HASH_CHARSET, CREDENTIAL_STORE, SECRET_KEY}; + static final SimpleAttributeDefinition KEY_STORE = + new SimpleAttributeDefinitionBuilder(ElytronDescriptionConstants.KEY_STORE, ModelType.STRING, true) + .setAllowExpression(true) + .setRequires(ElytronDescriptionConstants.KEY_STORE_ALIAS) + .setMinSize(1) + .setRestartAllServices() + .setCapabilityReference(KEY_STORE_CAPABILITY, SECURITY_REALM_CAPABILITY, true) + .setFlags(AttributeAccess.Flag.RESTART_RESOURCE_SERVICES) + .build(); + + static final SimpleAttributeDefinition KEY_STORE_ALIAS = + new SimpleAttributeDefinitionBuilder(ElytronDescriptionConstants.KEY_STORE_ALIAS, ModelType.STRING, true) + .setAllowExpression(true) + .setRequires(ElytronDescriptionConstants.KEY_STORE) + .setMinSize(1) + .setRestartAllServices() + .build(); + + static final AttributeDefinition[] ATTRIBUTES = new AttributeDefinition[]{PATH, RELATIVE_TO, LEVELS, ENCODED, HASH_ENCODING, HASH_CHARSET, CREDENTIAL_STORE, SECRET_KEY, KEY_STORE, KEY_STORE_ALIAS}; private static final AbstractAddStepHandler ADD = new RealmAddHandler(); private static final OperationStepHandler REMOVE = new TrivialCapabilityServiceRemoveHandler(ADD, MODIFIABLE_SECURITY_REALM_RUNTIME_CAPABILITY, SECURITY_REALM_RUNTIME_CAPABILITY); @@ -163,13 +199,76 @@ public void registerAttributes(ManagementResourceRegistration resourceRegistrati } } + @Override + public void registerOperations(ManagementResourceRegistration resourceRegistration) { + super.registerOperations(resourceRegistration); + ResourceDescriptionResolver resolver = ElytronExtension.getResourceDescriptionResolver(ElytronDescriptionConstants.FILESYSTEM_REALM); + if (isServerOrHostController(resourceRegistration)) { // server-only operations + UpdateKeyPairHandler.register(resourceRegistration, resolver); + VerifyRealmIntegrity.register(resourceRegistration, resolver); + } + } + + static class UpdateKeyPairHandler extends ElytronRuntimeOnlyHandler { + + static void register(ManagementResourceRegistration resourceRegistration, ResourceDescriptionResolver descriptionResolver) { + resourceRegistration.registerOperationHandler( + new SimpleOperationDefinitionBuilder(ElytronDescriptionConstants.UPDATE_KEY_PAIR, descriptionResolver) + .setRuntimeOnly() + .build(), + new FileSystemRealmDefinition.UpdateKeyPairHandler()); + } + + @Override + protected void executeRuntimeStep(final OperationContext context, final ModelNode operation) throws OperationFailedException { + TrivialService filesystemService = (TrivialService) getFileSystemService(context); + FileSystemSecurityRealm fileSystemRealm = filesystemService.getValue(); + try { + if (! fileSystemRealm.hasIntegrityEnabled()) { + throw ROOT_LOGGER.filesystemMissingKeypair(); + } + fileSystemRealm.updateRealmKeyPair(); + } catch (IOException e) { + throw ROOT_LOGGER.unableToVerifyIntegrity(e, e.getLocalizedMessage()); + } + } + } + + static class VerifyRealmIntegrity extends ElytronRuntimeOnlyHandler { + + static void register(ManagementResourceRegistration resourceRegistration, ResourceDescriptionResolver descriptionResolver) { + resourceRegistration.registerOperationHandler( + new SimpleOperationDefinitionBuilder(ElytronDescriptionConstants.VERIFY_INTEGRITY, descriptionResolver) + .setRuntimeOnly() + .build(), + new FileSystemRealmDefinition.VerifyRealmIntegrity()); + } + + @Override + protected void executeRuntimeStep(final OperationContext context, final ModelNode operation) throws OperationFailedException { + TrivialService filesystemService = (TrivialService) getFileSystemService(context); + FileSystemSecurityRealm fileSystemRealm = filesystemService.getValue(); + try { + if (! fileSystemRealm.hasIntegrityEnabled()) { + throw ROOT_LOGGER.filesystemMissingKeypair(); + } + FileSystemSecurityRealm.IntegrityResult result = fileSystemRealm.verifyRealmIntegrity(); + if(!result.isValid()) { + throw ROOT_LOGGER.filesystemIntegrityInvalid(result.getIdentityNames()); + } + } catch (IOException e) { + throw ROOT_LOGGER.unableToVerifyIntegrity(e, e.getLocalizedMessage()); + } + } + } + private static class RealmAddHandler extends BaseAddHandler { private RealmAddHandler() { super(SECURITY_REALM_RUNTIME_CAPABILITY, ATTRIBUTES); } - private SecretKey getKey(OperationContext context, String credentialStoreReference, String alias) throws OperationFailedException{ + private static SecretKey getSecretKey(OperationContext context, String credentialStoreReference, String alias) throws OperationFailedException { ExceptionFunction credentialStoreApi = context.getCapabilityRuntimeAPI(CREDENTIAL_STORE_API_CAPABILITY, credentialStoreReference, ExceptionFunction.class); CredentialStore credentialStoreResource = credentialStoreApi.apply(context); try { @@ -182,6 +281,14 @@ private SecretKey getKey(OperationContext context, String credentialStoreReferen throw ROOT_LOGGER.unableToLoadCredentialStore(e); } } + private static char[] getKeyStorePassword(KeyStoreService keyStoreService) throws RuntimeException { + InjectedValue> credentialSourceSupplierInjector = new InjectedValue<>(); + try { + return keyStoreService.resolveKeyPassword(credentialSourceSupplierInjector.getOptionalValue()); + } catch (Exception e) { + throw ROOT_LOGGER.unableToGetKeyStorePassword(); + } + } @Override protected void performRuntime(OperationContext context, ModelNode operation, ModelNode model) @@ -203,20 +310,25 @@ protected void performRuntime(OperationContext context, ModelNode operation, Mod final String hashCharset = HASH_CHARSET.resolveModelAttribute(context, model).asString(); final String credentialStore = CREDENTIAL_STORE.resolveModelAttribute(context, model).asStringOrNull(); final String secretKey = SECRET_KEY.resolveModelAttribute(context, model).asStringOrNull(); + final String keyStoreName = KEY_STORE.resolveModelAttribute(context, model).asStringOrNull(); + final String keyPairAlias = KEY_STORE_ALIAS.resolveModelAttribute(context, model).asStringOrNull(); - SecretKey key = null; - if(credentialStore != null && secretKey != null) { - key = getKey(context, credentialStore, secretKey); - } - SecretKey finalKey = key; - + final InjectedValue keyStoreInjector = new InjectedValue<>(); final InjectedValue pathManagerInjector = new InjectedValue<>(); final InjectedValue nameRewriterInjector = new InjectedValue<>(); + SecretKey key = null; + if (credentialStore != null && secretKey != null) { + key = getSecretKey(context, credentialStore, secretKey); + } + final SecretKey finalKey = key; + ServiceRegistry keyStoreServiceRegistry = context.getServiceRegistry(true); + TrivialService fileSystemRealmService = new TrivialService<>( new TrivialService.ValueSupplier() { private PathResolver pathResolver; + ModifiableKeyStoreService keyStoreService; @Override public SecurityRealm get() throws StartException { @@ -229,6 +341,28 @@ public SecurityRealm get() throws StartException { if (nameRewriter == null) { nameRewriter = NameRewriter.IDENTITY_REWRITER; } + KeyStore keyStore = keyStoreInjector.getOptionalValue(); + PrivateKey privateKey = null; + PublicKey publicKey = null; + if (keyStore != null) { + try { + keyStoreService = getModifiableKeyStoreService(keyStoreServiceRegistry, keyStoreName); + char[] keyPassword = getKeyStorePassword((KeyStoreService) keyStoreService); + if(! keyStore.containsAlias(keyPairAlias)) { + throw ROOT_LOGGER.keyStoreMissingAlias(keyPairAlias); + } + privateKey = (PrivateKey) keyStore.getKey(keyPairAlias, keyPassword); + publicKey = keyStore.getCertificate(keyPairAlias).getPublicKey(); + if (privateKey == null) { + throw ROOT_LOGGER.missingPrivateKey(keyStoreName, keyPairAlias); + } else if (publicKey == null) { + throw ROOT_LOGGER.missingPublicKey(keyStoreName, keyPairAlias); + } + } catch (KeyStoreException | NoSuchAlgorithmException | UnrecoverableKeyException | OperationFailedException e) { + throw ROOT_LOGGER.unableToAccessEntryFromKeyStore(keyPairAlias, keyStoreName); + } + } + FileSystemSecurityRealmBuilder fileSystemRealmBuilder = FileSystemSecurityRealm.builder() .setRoot(rootPath) .setNameRewriter(nameRewriter) @@ -241,6 +375,10 @@ public SecurityRealm get() throws StartException { fileSystemRealmBuilder.setSecretKey(finalKey); } + if (privateKey != null && publicKey != null) { + fileSystemRealmBuilder.setPrivateKey(privateKey); + fileSystemRealmBuilder.setPublicKey(publicKey); + } return fileSystemRealmBuilder.build(); } @@ -260,7 +398,11 @@ public void dispose() { if (credentialStore != null) { serviceBuilder.requires(context.getCapabilityServiceName(buildDynamicCapabilityName(CREDENTIAL_STORE_CAPABILITY, credentialStore), CredentialStore.class)); } - + if (keyStoreName != null) { + serviceBuilder.addDependency(context.getCapabilityServiceName( + buildDynamicCapabilityName(KEY_STORE_CAPABILITY, keyStoreName), KeyStore.class), + KeyStore.class, keyStoreInjector); + } if (relativeTo != null) { serviceBuilder.addDependency(PathManagerService.SERVICE_NAME, PathManager.class, pathManagerInjector); serviceBuilder.requires(pathName(relativeTo)); @@ -270,4 +412,17 @@ public void dispose() { } + private static Service getFileSystemService(OperationContext context) throws OperationFailedException { + ServiceRegistry serviceRegistry = context.getServiceRegistry(true); + PathAddress currentAddress = context.getCurrentAddress(); + ServiceName mainServiceName = MODIFIABLE_SECURITY_REALM_RUNTIME_CAPABILITY.fromBaseCapability(currentAddress.getLastElement().getValue()).getCapabilityServiceName(); + + ServiceController serviceContainer = getRequiredService(serviceRegistry, mainServiceName, SecurityRealm.class); + ServiceController.State serviceState = serviceContainer.getState(); + if (serviceState != ServiceController.State.UP) { + throw ROOT_LOGGER.requiredServiceNotUp(mainServiceName, serviceState); + } + return serviceContainer.getService(); + } + } diff --git a/elytron/src/main/java/org/wildfly/extension/elytron/KeyStoreServiceUtil.java b/elytron/src/main/java/org/wildfly/extension/elytron/KeyStoreServiceUtil.java new file mode 100644 index 00000000000..01edb15b0dc --- /dev/null +++ b/elytron/src/main/java/org/wildfly/extension/elytron/KeyStoreServiceUtil.java @@ -0,0 +1,48 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2022 Red Hat, Inc., and individual contributors + * as indicated by the @author tags. + * + * Licensed 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.wildfly.extension.elytron; + +import static org.wildfly.extension.elytron.Capabilities.KEY_STORE_RUNTIME_CAPABILITY; +import static org.wildfly.extension.elytron.ElytronExtension.getRequiredService; +import static org.wildfly.extension.elytron._private.ElytronSubsystemMessages.ROOT_LOGGER; + +import java.security.KeyStore; +import org.jboss.as.controller.OperationFailedException; +import org.jboss.as.controller.capability.RuntimeCapability; +import org.jboss.msc.service.ServiceController; +import org.jboss.msc.service.ServiceName; +import org.jboss.msc.service.ServiceRegistry; + +/** + * @author Ashpan Raskar + */ + +class KeyStoreServiceUtil { + public static ModifiableKeyStoreService getModifiableKeyStoreService(ServiceRegistry serviceRegistry, String keyStoreName) throws OperationFailedException { + RuntimeCapability runtimeCapability = KEY_STORE_RUNTIME_CAPABILITY.fromBaseCapability(keyStoreName); + ServiceName serviceName = runtimeCapability.getCapabilityServiceName(); + + ServiceController serviceContainer = getRequiredService(serviceRegistry, serviceName, KeyStore.class); + ServiceController.State serviceState = serviceContainer.getState(); + if (serviceState != ServiceController.State.UP) { + throw ROOT_LOGGER.requiredServiceNotUp(serviceName, serviceState); + } + + return (ModifiableKeyStoreService) serviceContainer.getService(); + } +} diff --git a/elytron/src/main/java/org/wildfly/extension/elytron/RealmParser.java b/elytron/src/main/java/org/wildfly/extension/elytron/RealmParser.java index 6a2cff81ade..017e0894c4e 100644 --- a/elytron/src/main/java/org/wildfly/extension/elytron/RealmParser.java +++ b/elytron/src/main/java/org/wildfly/extension/elytron/RealmParser.java @@ -109,6 +109,19 @@ class RealmParser { .addAttributes(FileSystemRealmDefinition.CREDENTIAL_STORE) // new .addAttributes(FileSystemRealmDefinition.SECRET_KEY) // new .build(); + private final PersistentResourceXMLDescription filesystemRealmParser_16 = builder(PathElement.pathElement(ElytronDescriptionConstants.FILESYSTEM_REALM), null) + .addAttributes(FileSystemRealmDefinition.PATH) + .addAttributes(FileSystemRealmDefinition.RELATIVE_TO) + .addAttributes(FileSystemRealmDefinition.LEVELS) + .addAttributes(FileSystemRealmDefinition.ENCODED) + .addAttributes(FileSystemRealmDefinition.HASH_ENCODING) + .addAttributes(FileSystemRealmDefinition.HASH_CHARSET) + .addAttributes(FileSystemRealmDefinition.CREDENTIAL_STORE) + .addAttributes(FileSystemRealmDefinition.SECRET_KEY) + .addAttribute(FileSystemRealmDefinition.KEY_STORE) //new + .addAttribute(FileSystemRealmDefinition.KEY_STORE_ALIAS) //new + .build(); + private final PersistentResourceXMLDescription tokenRealmParser = builder(PathElement.pathElement(ElytronDescriptionConstants.TOKEN_REALM), null) .addAttributes(TokenRealmDefinition.ATTRIBUTES) .build(); @@ -233,6 +246,23 @@ class RealmParser { .addChild(jaasRealmParser) .build(); + final PersistentResourceXMLDescription realmParser_16 = decorator(ElytronDescriptionConstants.SECURITY_REALMS) + .addChild(aggregateRealmParser_8_0) + .addChild(customRealmParser) + .addChild(customModifiableRealmParser) + .addChild(identityRealmParser) + .addChild(jdbcRealmParser_14_0) + .addChild(keyStoreRealmParser) + .addChild(propertiesRealmParser_14_0) + .addChild(ldapRealmParser) + .addChild(filesystemRealmParser_16) + .addChild(tokenRealmParser) + .addChild(cachingRealmParser) + .addChild(distributedRealmParser) + .addChild(failoverRealmParser) + .addChild(jaasRealmParser) + .build(); + RealmParser() { } diff --git a/elytron/src/main/java/org/wildfly/extension/elytron/_private/ElytronSubsystemMessages.java b/elytron/src/main/java/org/wildfly/extension/elytron/_private/ElytronSubsystemMessages.java index e567466abd1..e0bf0796413 100644 --- a/elytron/src/main/java/org/wildfly/extension/elytron/_private/ElytronSubsystemMessages.java +++ b/elytron/src/main/java/org/wildfly/extension/elytron/_private/ElytronSubsystemMessages.java @@ -23,6 +23,7 @@ import java.io.IOException; import java.net.UnknownHostException; import java.security.KeyStore; +import java.security.KeyStoreException; import java.security.NoSuchProviderException; import java.security.Policy; import java.security.Provider; @@ -700,6 +701,27 @@ public interface ElytronSubsystemMessages extends BasicLogger { @Message(id = 1211, value = "Unable to load the credential store.") OperationFailedException unableToLoadCredentialStore(@Cause Throwable cause); + @Message(id = 1212, value = "KeyStore does not contain a PrivateKey for KeyStore: [%s] and alias: [%s].") + StartException missingPrivateKey(String keyStore, String alias); + + @Message(id = 1213, value = "KeyStore does not contain a PublicKey for KeyStore: [%s] and alias: [%s].") + StartException missingPublicKey(String keyStore, String alias); + + @Message(id = 1214, value = "Unable to verify the integrity of the filesystem realm: %s") + OperationFailedException unableToVerifyIntegrity(@Cause Exception cause, String causeMessage); + + @Message(id = 1215, value = "Filesystem realm is missing key pair configuration, integrity checking is not enabled") + OperationFailedException filesystemMissingKeypair(); + + @Message(id = 1216, value = "Filesystem realm is unable to obtain key store password") + RuntimeException unableToGetKeyStorePassword(); + + @Message(id = 1217, value = "Realm verification failed, invalid signatures for the identities: %s") + OperationFailedException filesystemIntegrityInvalid(String identities); + + @Message(id = 1218, value = "Keystore used by filesystem realm does not contain the alias: %s") + KeyStoreException keyStoreMissingAlias(String alias); + /* * Don't just add new errors to the end of the file, there may be an appropriate section above for the resource. * diff --git a/elytron/src/main/resources/org/wildfly/extension/elytron/LocalDescriptions.properties b/elytron/src/main/resources/org/wildfly/extension/elytron/LocalDescriptions.properties index 4a600135f97..5d864cc3f71 100644 --- a/elytron/src/main/resources/org/wildfly/extension/elytron/LocalDescriptions.properties +++ b/elytron/src/main/resources/org/wildfly/extension/elytron/LocalDescriptions.properties @@ -888,6 +888,11 @@ elytron.filesystem-realm.hash-encoding=The string format for the password if it elytron.filesystem-realm.hash-charset=The character set to use when converting the password string to a byte array. elytron.filesystem-realm.credential-store=The reference to the credential store that contains the secret key to encrypt and decrypt the realm. elytron.filesystem-realm.secret-key=The alias of the secret key to encrypt and decrypt the realm. +elytron.filesystem-realm.key-store=The reference to the key store that contains the key pair to use to verify integrity. +elytron.filesystem-realm.key-store-alias=The alias that identifies the PrivateKeyEntry within the key store to use to verify integrity. +# Operations +elytron.filesystem-realm.update-key-pair=Updates the filesystem realm to make use of the new key pair to verify integrity. +elytron.filesystem-realm.verify-integrity=Verify the integrity of the entire filesystem realm. elytron.token-realm=A security realm definition capable of validating and extracting identities from security tokens. # Operations diff --git a/elytron/src/main/resources/schema/wildfly-elytron_16_0.xsd b/elytron/src/main/resources/schema/wildfly-elytron_16_0.xsd index 86e3b4f6d83..c2b613f0384 100644 --- a/elytron/src/main/resources/schema/wildfly-elytron_16_0.xsd +++ b/elytron/src/main/resources/schema/wildfly-elytron_16_0.xsd @@ -1907,6 +1907,21 @@ + + + + A reference to the key store that contains the key pair to perform filesystem integrity checks. + + + + + + + The alias within the key-store that identifies the PrivateKeyEntry to use to perform filesystem integrity checks + + + + diff --git a/elytron/src/test/java/org/wildfly/extension/elytron/RealmsTestCase.java b/elytron/src/test/java/org/wildfly/extension/elytron/RealmsTestCase.java index ca8d0d8c590..e71696f9ad2 100644 --- a/elytron/src/test/java/org/wildfly/extension/elytron/RealmsTestCase.java +++ b/elytron/src/test/java/org/wildfly/extension/elytron/RealmsTestCase.java @@ -26,6 +26,7 @@ import static org.junit.Assert.assertTrue; import java.io.File; +import java.io.FileInputStream; import java.io.IOException; import java.net.URL; import java.nio.charset.Charset; @@ -35,17 +36,26 @@ import java.nio.file.Paths; import java.nio.file.StandardOpenOption; import java.security.AccessController; +import java.security.KeyPairGenerator; +import java.security.KeyStore; import java.security.Principal; import java.security.PrivilegedAction; import java.security.Provider; +import java.security.PublicKey; import java.security.Security; import java.security.spec.KeySpec; import java.util.Arrays; +import java.util.Collections; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.concurrent.TimeUnit; +import javax.xml.crypto.dsig.Reference; +import javax.xml.crypto.dsig.XMLSignature; +import javax.xml.crypto.dsig.XMLSignatureFactory; +import javax.xml.crypto.dsig.dom.DOMValidateContext; +import javax.xml.parsers.DocumentBuilderFactory; import org.jboss.as.controller.client.helpers.ClientConstants; import org.jboss.as.subsystem.test.KernelServices; import org.jboss.dmr.ModelNode; @@ -53,6 +63,8 @@ import org.junit.Assert; import org.junit.BeforeClass; import org.junit.Test; +import org.w3c.dom.Document; +import org.w3c.dom.NodeList; import org.wildfly.common.iteration.ByteIterator; import org.wildfly.common.iteration.CodePointIterator; import org.wildfly.security.WildFlyElytronProvider; @@ -72,6 +84,7 @@ import org.wildfly.security.credential.PasswordCredential; import org.wildfly.security.evidence.PasswordGuessEvidence; import org.wildfly.security.password.PasswordFactory; +import org.wildfly.security.password.WildFlyElytronPasswordProvider; import org.wildfly.security.password.interfaces.BCryptPassword; import org.wildfly.security.password.interfaces.ClearPassword; import org.wildfly.security.password.interfaces.DigestPassword; @@ -346,6 +359,109 @@ public void testFilesystemRealmEncrypted() throws Exception { identityEmpty.dispose(); } + /** + * Test the signature of the filesystem realm when enabling integrity support + */ + @Test + public void testFilesystemRealmIntegrity() throws Exception { + KernelServices services = super.createKernelServicesBuilder(new TestEnvironment()).setSubsystemXmlResource("realms-test.xml").build(); + if (!services.isSuccessfulBoot()) { + Assert.fail(services.getBootError().toString()); + } + ServiceName serviceName = Capabilities.SECURITY_REALM_RUNTIME_CAPABILITY.getCapabilityServiceName("FilesystemRealmIntegrity"); + ModifiableSecurityRealm securityRealm = (ModifiableSecurityRealm) services.getContainer().getService(serviceName).getValue(); + ServiceName serviceNameBoth = Capabilities.SECURITY_REALM_RUNTIME_CAPABILITY.getCapabilityServiceName("FilesystemRealmIntegrityAndEncryption"); + ModifiableSecurityRealm securityRealmBoth = (ModifiableSecurityRealm) services.getContainer().getService(serviceNameBoth).getValue(); + Assert.assertNotNull(securityRealm); + Assert.assertNotNull(securityRealmBoth); + + String targetDir = Paths.get("target/test-classes/org/wildfly/extension/elytron/").toAbsolutePath().toString(); + KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType()); + char[] keyStorePassword = "secret".toCharArray(); + keyStore.load(new FileInputStream(targetDir + "/keystore"), keyStorePassword); + PublicKey publicKey = keyStore.getCertificate("localhost").getPublicKey(); + PublicKey invalidPublicKey = KeyPairGenerator.getInstance("RSA").generateKeyPair().getPublic(); + + + char[] password = "password".toCharArray(); + ModifiableRealmIdentity identity = securityRealm.getRealmIdentityForUpdate(fromName("user")); + Assert.assertTrue(identity.exists()); + Assert.assertTrue(identity.verifyEvidence(new PasswordGuessEvidence(password))); + Assert.assertFalse(identity.verifyEvidence(new PasswordGuessEvidence("secretPassword123".toCharArray()))); + + + File identityFile = new File(targetDir + "/filesystem-realm-integrity/u/user-OVZWK4Q.xml"); + assertTrue(validateDigitalSignature(identityFile, publicKey)); + assertFalse(validateDigitalSignature(identityFile, invalidPublicKey)); + + MapAttributes newAttributes = new MapAttributes(); + newAttributes.addFirst("firstName", "John"); + newAttributes.addFirst("lastName", "Smith"); + newAttributes.addAll("roles", Arrays.asList("Employee", "Manager", "Admin")); + identity.setAttributes(newAttributes); + // Test that the publicKey still works correctly after signature is changed + assertTrue(validateDigitalSignature(identityFile, publicKey)); + + // Verify that an identity with an incorrect signature doesn't validate + RealmIdentity identity2 = securityRealm.getRealmIdentity(fromName("user2")); + Assert.assertTrue(identity.exists()); + File identityFile2 = new File(targetDir + "/filesystem-realm-integrity/u/user2-OVZWK4RS.xml"); + assertFalse(validateDigitalSignature(identityFile2, publicKey)); + + // Verify a new identity generates a signature and is correct + ModifiableRealmIdentity identity3 = securityRealm.getRealmIdentityForUpdate(new NamePrincipal("user3")); + identity3.create(); + identity3.setAttributes(newAttributes); + PasswordFactory factory = PasswordFactory.getInstance(ClearPassword.ALGORITHM_CLEAR, WildFlyElytronPasswordProvider.getInstance()); + ClearPassword clearPassword = (ClearPassword) factory.generatePassword(new ClearPasswordSpec(password)); + identity3.setCredentials(Collections.singleton(new PasswordCredential(clearPassword))); + File identityFile3 = new File(targetDir + "/filesystem-realm-integrity/u/user3-OVZWK4RT.xml"); + assertTrue(validateDigitalSignature(identityFile3, publicKey)); + + identity.dispose(); + identity2.dispose(); + identity3.dispose(); + + // Verify that this works for a realm with encryption and integrity enabled + identity = securityRealmBoth.getRealmIdentityForUpdate(new NamePrincipal("user1")); + identity.create(); + identity.setAttributes(newAttributes); + identity.setCredentials(Collections.singleton(new PasswordCredential(clearPassword))); + identityFile = new File(targetDir + "/filesystem-realm-integrity-encryption/O/OVZWK4RR.xml"); + assertTrue(validateDigitalSignature(identityFile, publicKey)); + identity.dispose(); + } + + private boolean validateDigitalSignature(File path, PublicKey publicKey) { + try { + DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); + dbf.setNamespaceAware(true); + Document doc = dbf.newDocumentBuilder().parse(path); + NodeList nl = doc.getElementsByTagNameNS(XMLSignature.XMLNS, "Signature"); + if (nl.getLength() == 0) { + return false; + } + XMLSignatureFactory fac = XMLSignatureFactory.getInstance("DOM"); + DOMValidateContext valContext = new DOMValidateContext(publicKey, nl.item(0)); + XMLSignature signature = fac.unmarshalXMLSignature(valContext); + boolean coreValidity = signature.validate(valContext); + if (!coreValidity) { + boolean sv = signature.getSignatureValue().validate(valContext); + // check the validation status of each Reference + Iterator i = signature.getSignedInfo().getReferences().iterator(); + for (int j = 0; i.hasNext(); j++) { + ((Reference) i.next()).validate(valContext); + } + return false; + } else { + return true; + } + } catch (Exception e) { + return false; + } + } + + private void testAbstractFilesystemRealm(SecurityRealm securityRealm, String username, String password) throws Exception { Assert.assertNotNull(securityRealm); diff --git a/elytron/src/test/java/org/wildfly/extension/elytron/SubsystemTransformerTestCase.java b/elytron/src/test/java/org/wildfly/extension/elytron/SubsystemTransformerTestCase.java index 517e8839a8d..17201029fb3 100644 --- a/elytron/src/test/java/org/wildfly/extension/elytron/SubsystemTransformerTestCase.java +++ b/elytron/src/test/java/org/wildfly/extension/elytron/SubsystemTransformerTestCase.java @@ -226,6 +226,8 @@ public void testRejectingTransformersEAP740() throws Exception { FailedOperationTransformationConfig.REJECTED_RESOURCE) .addFailedAttribute(SUBSYSTEM_ADDRESS.append(PathElement.pathElement(ElytronDescriptionConstants.FILESYSTEM_REALM, "FilesystemRealmEncrypted")), FailedOperationTransformationConfig.REJECTED_RESOURCE) + .addFailedAttribute(SUBSYSTEM_ADDRESS.append(PathElement.pathElement(ElytronDescriptionConstants.FILESYSTEM_REALM, "FilesystemRealmIntegrity")), + FailedOperationTransformationConfig.REJECTED_RESOURCE) .addFailedAttribute(SUBSYSTEM_ADDRESS.append(PathElement.pathElement(ElytronDescriptionConstants.JDBC_REALM, "JDBCRealmCharset")), FailedOperationTransformationConfig.REJECTED_RESOURCE) .addFailedAttribute(SUBSYSTEM_ADDRESS.append(PathElement.pathElement(ElytronDescriptionConstants.LDAP_REALM, "LDAPRealmEncodingCharset")), diff --git a/elytron/src/test/resources/org/wildfly/extension/elytron/elytron-transformers-13.0-reject.xml b/elytron/src/test/resources/org/wildfly/extension/elytron/elytron-transformers-13.0-reject.xml index 3dda6928f3b..e412deee510 100644 --- a/elytron/src/test/resources/org/wildfly/extension/elytron/elytron-transformers-13.0-reject.xml +++ b/elytron/src/test/resources/org/wildfly/extension/elytron/elytron-transformers-13.0-reject.xml @@ -17,6 +17,11 @@ + + + + + @@ -64,6 +69,9 @@ + + + diff --git a/elytron/src/test/resources/org/wildfly/extension/elytron/filesystem-realm-integrity/u/user-OVZWK4Q.xml b/elytron/src/test/resources/org/wildfly/extension/elytron/filesystem-realm-integrity/u/user-OVZWK4Q.xml new file mode 100644 index 00000000000..d0c159f4020 --- /dev/null +++ b/elytron/src/test/resources/org/wildfly/extension/elytron/filesystem-realm-integrity/u/user-OVZWK4Q.xml @@ -0,0 +1,13 @@ + + + + AXBhc3N3b3Jk + + + + +x2JtLa+FLMUtnj4RTwZqkCaUeUXd65v0OPKNHTWaavA=CCGeQs4Tlnu1dIwg27KUpKerZPiqeZVzAG44AAptOflJQUbrHUBVx74I6OAQXHoxEc6Y6Pwsx57f +AOrjbmRwAx2TYvGYzu1ccK/KiOuOmMs1mvufhECK9CWpGCIT1LdOmi39za9txaFKOfuHpbQDE5Au +E6/NPCq+F3UzhbwjtMA=gtRFtSoBpoP0zSCN5ZCvJG4G9G5zO80aEJAJpcR2H7w7AiWuJ6LpATbwAWWVlbFR2+4UyxwbVzn6 +aezoo13l4STSWALyf2jB/XkWV/vV6D+u/q1ig3yM0O0y0Z5ANMgAJ65D2trh+J2xbiOk3ojL/yAS +YDOX+jziihXDPsFq5t8=AQAB \ No newline at end of file diff --git a/elytron/src/test/resources/org/wildfly/extension/elytron/filesystem-realm-integrity/u/user2-OVZWK4RS.xml b/elytron/src/test/resources/org/wildfly/extension/elytron/filesystem-realm-integrity/u/user2-OVZWK4RS.xml new file mode 100644 index 00000000000..415a3be0323 --- /dev/null +++ b/elytron/src/test/resources/org/wildfly/extension/elytron/filesystem-realm-integrity/u/user2-OVZWK4RS.xml @@ -0,0 +1,9 @@ + + + + AgV1c2VyMgpmaWxlc3lzdGVtOi73QqGkmSCHhUlhCQEvRQ== + JkcICC/dLeLA4kJmuld5Sv89OZYrQH24n8Am3FZSgcI=STPAuobZcYE+9EA4XSfX2P5bhSBkKbC1+b8aqcPXp8oAnfoWN4354KpLHOR2j4CSsjl8eff5d/jL + CmCISgST3RZ0k3Aj/vEsLb/UH1nV/k7GQC3gFQtuDSscXUJx9LfsWThSnVaeLwpUg60v30H6l2nJ + zgnFYxvkulNUwP9KTgI=gtRFtSoBpoP0zSCN5ZCvJG4G9G5zO80aEJAJpcR2H7w7AiWuJ6LpATbwAWWVlbFR2+4UyxwbVzn6 + aezoo13l4STSWALyf2jB/XkWV/vV6D+u/q1ig3yM0O0y0Z5ANMgAJ65D2trh+J2xbiOk3ojL/yAS + YDOX+jziihXDPsFq5t8=AQAB \ No newline at end of file diff --git a/elytron/src/test/resources/org/wildfly/extension/elytron/keystore b/elytron/src/test/resources/org/wildfly/extension/elytron/keystore new file mode 100644 index 0000000000000000000000000000000000000000..fb79f1591235cbafefaf6709e0f3c05aef7ee79c GIT binary patch literal 1230 zcmezO_TO6u1_mY|W&~r-oc!d(oQ(Y95}-(3WWif6pzJP#CZ=r$d~96WY>X_7T1)hNoT4Bpxe>vMJ8kA%{Gx+A9^@VUOA}=C3}Tc)my4fXjPz>H5goAuHdlGM*$lW8#@WFV^|9+1d`hyKqjE zJ#NnFEb}=@ib)=JK%Yc?k~ z6)6ZsSguZto343`Yt^JWW9h!N87@brszixLtdyOn=x$VWq49H-yKJb$(XH?8aw-+3 ze2PAPCVMLTpWwN%PcI%n^Fp+hDOK2WdIw{T%v?1i%(Om;|fNtcg-g;BBodxpn--Q`hs?9fDdmWbpmAx@op7d7eV!-2HBW*YOw8*wAE?IXs>}?)5zfsP^&2O5HtyfE`zo?VNy>nyj!b9Lh%^IO+YG4UW z(5DQV7>@z5;R0qRMkXc>yQ}*@$+&rJGvH?9)N1o+`_9YA$Za5EC}beO#vIDRER4tw z21*kAMg~Sk28MA5mX&LEo z%U_vqUM!A%`cUOk1k3&ah>^>7SBLm}NMFV*QS)h+)`B=nQL{yemroPXd zb+}B9=Y{$#_lU{7J9JS(h#BZ`mw%OVM{G5 z%AR#4THF7x#SA}|-=B}>^fGW8G_DL$;92*gIOgvc&TSQ6HO_N0-RqlXdYk8W^Ra!h Ye$u%zS`FE<4p}}gmM&RyKBOuY06f?e(EtDd literal 0 HcmV?d00001 diff --git a/elytron/src/test/resources/org/wildfly/extension/elytron/realms-test.xml b/elytron/src/test/resources/org/wildfly/extension/elytron/realms-test.xml index 16a5364a736..a59e123e54f 100644 --- a/elytron/src/test/resources/org/wildfly/extension/elytron/realms-test.xml +++ b/elytron/src/test/resources/org/wildfly/extension/elytron/realms-test.xml @@ -48,10 +48,18 @@ - + + + + + + + + + @@ -85,6 +93,11 @@ + + + + +