Skip to content

Commit

Permalink
[WFCORE-5859] Add Filesystem integrity support
Browse files Browse the repository at this point in the history
  • Loading branch information
Ashpan committed Jul 26, 2022
1 parent df63169 commit 0914f8a
Show file tree
Hide file tree
Showing 17 changed files with 465 additions and 10 deletions.
Expand Up @@ -109,6 +109,7 @@
<module name="java.security.sasl"/>
<module name="java.sql"/>
<module name="java.xml"/>
<module name="java.xml.crypto"/>
<module name="javax.json.api"/>
<module name="jdk.security.auth"/>
<module name="jdk.unsupported"/>
Expand Down
Expand Up @@ -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";
Expand Down Expand Up @@ -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";
Expand Down Expand Up @@ -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";

Expand Down
Expand Up @@ -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.
*
Expand All @@ -30,5 +32,10 @@ String getNameSpace() {
return ElytronExtension.NAMESPACE_16_0;
}

@Override
PersistentResourceXMLDescription getRealmParser() {
return new RealmParser().realmParser_16;
}

}

Expand Up @@ -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;
Expand Down Expand Up @@ -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) {
Expand Down

Large diffs are not rendered by default.

@@ -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 <a href="mailto:araskar@redhat.com">Ashpan Raskar</a>
*/

class KeyStoreServiceUtil {
public static ModifiableKeyStoreService getModifiableKeyStoreService(ServiceRegistry serviceRegistry, String keyStoreName) throws OperationFailedException {
RuntimeCapability<Void> runtimeCapability = KEY_STORE_RUNTIME_CAPABILITY.fromBaseCapability(keyStoreName);
ServiceName serviceName = runtimeCapability.getCapabilityServiceName();

ServiceController<KeyStore> 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();
}
}
Expand Up @@ -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();
Expand Down Expand Up @@ -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() {

}
Expand Down
Expand Up @@ -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;
Expand Down Expand Up @@ -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.
*
Expand Down
Expand Up @@ -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
Expand Down
15 changes: 15 additions & 0 deletions elytron/src/main/resources/schema/wildfly-elytron_16_0.xsd
Expand Up @@ -1907,6 +1907,21 @@
</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="key-store" type="xs:string" use="optional">
<xs:annotation>
<xs:documentation>
A reference to the key store that contains the key pair to perform filesystem integrity checks.
</xs:documentation>
</xs:annotation>
</xs:attribute>
<xs:attribute name="key-store-alias" type="xs:string" use="optional">
<xs:annotation>
<xs:documentation>
The alias within the key-store that identifies the PrivateKeyEntry to use to perform filesystem integrity checks
</xs:documentation>
</xs:annotation>
</xs:attribute>

</xs:extension>
</xs:complexContent>
</xs:complexType>
Expand Down
Expand Up @@ -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;
Expand All @@ -35,24 +36,35 @@
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;
import org.jboss.msc.service.ServiceName;
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;
Expand All @@ -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;
Expand Down Expand Up @@ -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);

Expand Down

0 comments on commit 0914f8a

Please sign in to comment.