diff --git a/auth/realm/base/pom.xml b/auth/realm/base/pom.xml index 6085a25cbbc..268d6f70c7e 100644 --- a/auth/realm/base/pom.xml +++ b/auth/realm/base/pom.xml @@ -85,6 +85,11 @@ org.wildfly.common wildfly-common + + org.apache.santuario + xmlsec + + diff --git a/auth/realm/base/src/main/java/org/wildfly/security/auth/realm/ElytronMessages.java b/auth/realm/base/src/main/java/org/wildfly/security/auth/realm/ElytronMessages.java index 38fd26b646e..7f0101a9cf2 100644 --- a/auth/realm/base/src/main/java/org/wildfly/security/auth/realm/ElytronMessages.java +++ b/auth/realm/base/src/main/java/org/wildfly/security/auth/realm/ElytronMessages.java @@ -153,6 +153,23 @@ interface ElytronMessages extends BasicLogger { @Message(id = 13006, value = "Filesystem-backed realm unable to encrypt identity") RealmUnavailableException fileSystemRealmEncryptionFailed(@Cause Throwable cause); - @Message(id = 13007, value = "Filesystem-backed realm found an incompatible identity version. Requires at least version: %s") - RealmUnavailableException fileSystemRealmIncompatibleIdentityVersion(String expectedVersion); + @Message(id = 13007, value = "Signature for the following identity is invalid: %s.") + IntegrityException invalidIdentitySignature(String s); + + @Message(id = 13008, value = "Unable to create a signature for the file: %s.") + RealmUnavailableException unableToGenerateSignature(String s); + + @Message(id = 13009, value = "Unable to locate the signature element for the file: %s") + RealmUnavailableException cannotFindSignature(String s); + + @Message(id = 13010, value = "Both PrivateKey and PublicKey must be defined for realm at: %s") + IllegalArgumentException invalidKeyPairArgument(String s); + + @Message(id = 13011, value = "Unable to access master index file: %s") + IllegalStateException unableToAccessMainIndex(String s); + + @LogMessage(level = Logger.Level.TRACE) + @Message(id = 13012, value = "Unable to write checksum to main index") + void unableToWriteToMainIndex(); + } diff --git a/auth/realm/base/src/main/java/org/wildfly/security/auth/realm/FileSystemSecurityRealm.java b/auth/realm/base/src/main/java/org/wildfly/security/auth/realm/FileSystemSecurityRealm.java index 16e55db8ad7..986e61bca60 100644 --- a/auth/realm/base/src/main/java/org/wildfly/security/auth/realm/FileSystemSecurityRealm.java +++ b/auth/realm/base/src/main/java/org/wildfly/security/auth/realm/FileSystemSecurityRealm.java @@ -25,10 +25,18 @@ import static javax.xml.stream.XMLStreamConstants.END_ELEMENT; import static javax.xml.stream.XMLStreamConstants.START_ELEMENT; import static org.wildfly.security.provider.util.ProviderUtil.INSTALLED_PROVIDERS; +import static org.apache.xml.security.signature.XMLSignature.ALGO_ID_SIGNATURE_RSA_SHA256; +import static org.apache.xml.security.signature.XMLSignature.ALGO_ID_SIGNATURE_ECDSA_SHA256; +import static org.apache.xml.security.signature.XMLSignature.ALGO_ID_MAC_HMAC_SHA256; +import static org.apache.xml.security.signature.XMLSignature.ALGO_ID_SIGNATURE_DSA_SHA256; import java.io.BufferedOutputStream; +import java.io.BufferedReader; import java.io.File; +import java.io.FileInputStream; import java.io.FileNotFoundException; +import java.io.FileReader; +import java.io.FileWriter; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -41,13 +49,18 @@ import java.nio.file.Path; import java.security.AccessController; import java.security.GeneralSecurityException; +import java.security.InvalidAlgorithmParameterException; +import java.security.KeyException; import java.security.KeyFactory; +import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.Principal; +import java.security.PrivateKey; import java.security.PrivilegedAction; import java.security.PrivilegedActionException; import java.security.PrivilegedExceptionAction; import java.security.Provider; +import java.security.PublicKey; import java.security.cert.CertificateEncodingException; import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; @@ -65,16 +78,45 @@ import java.util.Locale; import java.util.Map; import java.util.NoSuchElementException; +import java.util.Scanner; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ThreadLocalRandom; import java.util.function.Consumer; import java.util.function.Supplier; import javax.crypto.SecretKey; +import javax.xml.crypto.MarshalException; +import javax.xml.crypto.dsig.CanonicalizationMethod; +import javax.xml.crypto.dsig.DigestMethod; +import javax.xml.crypto.dsig.Reference; +import javax.xml.crypto.dsig.SignedInfo; +import javax.xml.crypto.dsig.Transform; +import javax.xml.crypto.dsig.XMLSignature; +import javax.xml.crypto.dsig.XMLSignatureException; +import javax.xml.crypto.dsig.XMLSignatureFactory; +import javax.xml.crypto.dsig.dom.DOMSignContext; +import javax.xml.crypto.dsig.dom.DOMValidateContext; +import javax.xml.crypto.dsig.keyinfo.KeyInfo; +import javax.xml.crypto.dsig.keyinfo.KeyInfoFactory; +import javax.xml.crypto.dsig.keyinfo.KeyValue; +import javax.xml.crypto.dsig.spec.C14NMethodParameterSpec; +import javax.xml.crypto.dsig.spec.TransformParameterSpec; +import javax.xml.parsers.DocumentBuilder; +import javax.xml.parsers.DocumentBuilderFactory; +import javax.xml.parsers.ParserConfigurationException; import javax.xml.stream.XMLInputFactory; import javax.xml.stream.XMLOutputFactory; import javax.xml.stream.XMLStreamException; import javax.xml.stream.XMLStreamReader; import javax.xml.stream.XMLStreamWriter; +import javax.xml.transform.Transformer; +import javax.xml.transform.TransformerException; +import javax.xml.transform.TransformerFactory; +import javax.xml.transform.dom.DOMSource; +import javax.xml.transform.stream.StreamResult; +import org.w3c.dom.Document; +import org.w3c.dom.Element; +import org.w3c.dom.Node; +import org.w3c.dom.NodeList; import org.wildfly.common.Assert; import org.wildfly.common.bytes.ByteStringBuilder; import org.wildfly.common.codec.Base32Alphabet; @@ -108,6 +150,7 @@ import org.wildfly.security.password.spec.PasswordSpec; import org.wildfly.security.password.util.ModularCrypt; import org.wildfly.security.permission.ElytronPermission; +import org.xml.sax.SAXException; /** * A simple filesystem-backed security realm. @@ -125,7 +168,8 @@ private enum Version { VERSION_1_0("urn:elytron:1.0", null), VERSION_1_0_1("urn:elytron:1.0.1", VERSION_1_0), - VERSION_1_1("urn:elytron:identity:1.1", VERSION_1_0_1); + VERSION_1_1("urn:elytron:identity:1.1", VERSION_1_0_1), + VERSION_1_2("urn:elytron:identity:1.2", VERSION_1_1); final String namespace; @@ -164,7 +208,9 @@ boolean isAtLeast(Version version) { private final Charset hashCharset; private final Encoding hashEncoding; private final SecretKey secretKey; - + private final PrivateKey privateKey; + private final PublicKey publicKey; + private final File mainIndex; private final ConcurrentHashMap realmIdentityLocks = new ConcurrentHashMap<>(); /** @@ -188,8 +234,11 @@ public static FileSystemSecurityRealmBuilder builder() { * @param hashEncoding the string format for the hashed passwords. Uses Base64 by default. * @param providers The providers supplier * @param secretKey the SecretKey used to encrypt and decrypt the security realm (if {@code null}, the security realm will be unencrypted) + * @param privateKey the PrivateKey used to verify the integrity of the security realm (if {@code null}, the security realm will not verify integrity) + * @param publicKey the PublicKey used to verify the integrity of the security realm (if {@code null}, the security realm will not verify integrity) + * */ - public FileSystemSecurityRealm(final Path root, final NameRewriter nameRewriter, final int levels, final boolean encoded, final Encoding hashEncoding, final Charset hashCharset, final Supplier providers, final SecretKey secretKey) { + public FileSystemSecurityRealm(final Path root, final NameRewriter nameRewriter, final int levels, final boolean encoded, final Encoding hashEncoding, final Charset hashCharset, Supplier providers, final SecretKey secretKey, final PrivateKey privateKey, final PublicKey publicKey) { final SecurityManager sm = System.getSecurityManager(); if (sm != null) { sm.checkPermission(CREATE_SECURITY_REALM); @@ -202,6 +251,46 @@ public FileSystemSecurityRealm(final Path root, final NameRewriter nameRewriter, this.hashEncoding = hashEncoding != null ? hashEncoding : Encoding.BASE64; this.providers = providers != null ? providers : INSTALLED_PROVIDERS; this.secretKey = secretKey; + this.privateKey = privateKey; + this.publicKey = publicKey; + // mainIndex the index file used to store the checksums of each identity to + // verify integrity of the security realm + this.mainIndex = new ConcurrentHashMap<>(); + try { + ModifiableRealmIdentityIterator realmIterator = this.getRealmIdentityIterator(); + while (realmIterator.hasNext()) { + Identity identity = (Identity) realmIterator.next(); + if (identity.verifyIntegrity()) { + try { + Identity.writeToMainIndex(mainIndex, identity.path, secretKey); + } catch (IOException | GeneralSecurityException e) { + ElytronMessages.log.unableToWriteToMainIndex(); + } + } + identity.dispose(); + } + realmIterator.close(); + } catch (RealmUnavailableException e) { + ElytronMessages.log.tracef("Unable to initialize filesystem realm"); + } + + } + + /** + * Construct a new instance. + * + * Construction with enabled security manager requires {@code createSecurityRealm} {@link ElytronPermission}. + * + * @param root the root path of the identity store + * @param nameRewriter the name rewriter to apply to looked up names + * @param levels the number of levels of directory hashing to apply + * @param encoded whether identity names should be BASE32 encoded before using as filename + * @param hashCharset the character set to use when converting password strings to a byte array. Uses UTF-8 by default. + * @param hashEncoding the string format for the hashed passwords. Uses Base64 by default. + * @param secretKey the SecretKey used to encrypt and decrypt the security realm (if {@code null}, the security realm will be unencrypted) + */ + public FileSystemSecurityRealm(final Path root, final NameRewriter nameRewriter, final int levels, final boolean encoded, final Encoding hashEncoding, final Charset hashCharset, final SecretKey secretKey) { + this(root, nameRewriter, levels, encoded, hashEncoding, hashCharset, INSTALLED_PROVIDERS, secretKey, null, null); } /** @@ -217,7 +306,7 @@ public FileSystemSecurityRealm(final Path root, final NameRewriter nameRewriter, * @param hashEncoding the string format for the hashed passwords. Uses Base64 by default. */ public FileSystemSecurityRealm(final Path root, final NameRewriter nameRewriter, final int levels, final boolean encoded, final Encoding hashEncoding, final Charset hashCharset) { - this(root, nameRewriter, levels, encoded, hashEncoding, hashCharset, INSTALLED_PROVIDERS, null); + this(root, nameRewriter, levels, encoded, hashEncoding, hashCharset, INSTALLED_PROVIDERS, null, null, null); } /** @@ -231,7 +320,7 @@ public FileSystemSecurityRealm(final Path root, final NameRewriter nameRewriter, * @param encoded whether identity names should by BASE32 encoded before using as filename */ public FileSystemSecurityRealm(final Path root, final NameRewriter nameRewriter, final int levels, final boolean encoded) { - this(root, nameRewriter, levels, encoded, Encoding.BASE64, StandardCharsets.UTF_8, INSTALLED_PROVIDERS, null); + this(root, nameRewriter, levels, encoded, Encoding.BASE64, StandardCharsets.UTF_8, INSTALLED_PROVIDERS, null, null, null); } /** @@ -252,10 +341,10 @@ public FileSystemSecurityRealm(final Path root, final NameRewriter nameRewriter, * @param nameRewriter the name rewriter to apply to looked up names * @param levels the number of levels of directory hashing to apply * @param hashEncoding the string format for hashed passwords. Uses Base64 by default. - * @param hashCharset the character set to use when converting password strings to a byte array. Uses UTF-8 by default and must not be {@code null}. + * @param hashCharset the character set to use when converting password strings to a byte array. Uses UTF-8 by default and must not be {@code null}. */ public FileSystemSecurityRealm(final Path root, final NameRewriter nameRewriter, final int levels, final Encoding hashEncoding, final Charset hashCharset) { - this(root, nameRewriter, levels, true, hashEncoding, hashCharset, INSTALLED_PROVIDERS, null); + this(root, nameRewriter, levels, true, hashEncoding, hashCharset, INSTALLED_PROVIDERS, null, null, null); } @@ -280,7 +369,7 @@ public FileSystemSecurityRealm(final Path root, final int levels) { * @param hashCharset the character set to use when converting password strings to a byte array. Uses UTF-8 by default and must not be {@code null}. */ public FileSystemSecurityRealm(final Path root, final int levels, final Encoding hashEncoding, final Charset hashCharset) { - this(root, NameRewriter.IDENTITY_REWRITER, levels, true, hashEncoding, hashCharset, INSTALLED_PROVIDERS, null); + this(root, NameRewriter.IDENTITY_REWRITER, levels, true, hashEncoding, hashCharset, INSTALLED_PROVIDERS, null, null, null); } /** @@ -300,13 +389,16 @@ public FileSystemSecurityRealm(final Path root) { * @param hashCharset the character set to use when converting password strings to a byte array. Uses UTF-8 by default and must not be {@code null} */ public FileSystemSecurityRealm(final Path root, final Encoding hashEncoding, final Charset hashCharset) { - this(root, NameRewriter.IDENTITY_REWRITER, 2, true, hashEncoding, hashCharset, INSTALLED_PROVIDERS, null); + this(root, NameRewriter.IDENTITY_REWRITER, 2, true, hashEncoding, hashCharset, INSTALLED_PROVIDERS, null, null, null); } public FileSystemSecurityRealm(Path root, int levels, Supplier providers) { - this(root, NameRewriter.IDENTITY_REWRITER, levels, true, Encoding.BASE64, StandardCharsets.UTF_8, providers, null); + this(root, NameRewriter.IDENTITY_REWRITER, levels, true, Encoding.BASE64, StandardCharsets.UTF_8, providers, null, null, null); } + public boolean hasIntegrityEnabled() { + return privateKey != null && publicKey != null; + } private Path pathFor(String name) { assert name.codePointCount(0, name.length()) > 0; String normalizedName = name; @@ -385,7 +477,7 @@ private ModifiableRealmIdentity getRealmIdentity(final String name, final boolea } else { lock = realmIdentityLock.lockShared(); } - return new Identity(finalName, pathFor(finalName), lock, hashCharset, hashEncoding, providers, secretKey); + return new Identity(finalName, pathFor(finalName), lock, hashCharset, hashEncoding, providers, secretKey, privateKey, publicKey, mainIndex); } public ModifiableRealmIdentityIterator getRealmIdentityIterator() throws RealmUnavailableException { @@ -512,6 +604,48 @@ interface CredentialParseFunction { void parseCredential(String algorithm, String format, String body) throws RealmUnavailableException, XMLStreamException; } + /** + * Method to update all filesystem's identities signatures with the newly updated key-pair + * Designed to be by WildFly CLI when the {@code :write-attribute(...)} is used for {@code key-store} or {@code key-store-alias} + * + * @throws IOException + * @throws GeneralSecurityException + */ + public void updateRealmKeyPair() throws IOException, GeneralSecurityException { + if (! hasIntegrityEnabled()) { + throw ElytronMessages.log.invalidKeyPairArgument(root.toString()); + } + ModifiableRealmIdentityIterator realmIterator = this.getRealmIdentityIterator(); + while (realmIterator.hasNext()) { + Identity identity = (Identity) realmIterator.next(); + identity.writeDigitalSignature(String.valueOf(identity.path), identity.name); + Identity.writeToMainIndex(mainIndex, identity.path, identity.secretKey); + identity.dispose(); + } + realmIterator.close(); + } + + /** + * Method to iterate over all filesystem realm identities and verify it's integrity + * Designed to be by WildFly CLI when the {@code :verify-realm-integrity()} is used + * @return Boolean representing if the realm integrity is valid + */ + public boolean verifyRealmIntegrity() throws RealmUnavailableException { + if (! hasIntegrityEnabled()) { + throw ElytronMessages.log.invalidKeyPairArgument(root.toString()); + } + ModifiableRealmIdentityIterator realmIterator = this.getRealmIdentityIterator(); + while (realmIterator.hasNext()) { + Identity identity = (Identity) realmIterator.next(); + if(! identity.verifyIntegrity()) { + return false; + } + identity.dispose(); + } + realmIterator.close(); + return true; + } + static class Identity implements ModifiableRealmIdentity { private static final String ENCRYPTION_FORMAT = "enc_base64"; @@ -527,8 +661,11 @@ static class Identity implements ModifiableRealmIdentity { private final Charset hashCharset; private final Encoding hashEncoding; private final SecretKey secretKey; + private final PrivateKey privateKey; + private final PublicKey publicKey; + private final File mainIndex; - Identity(final String name, final Path path, final IdentityLock lock, final Charset hashCharset, final Encoding hashEncoding, Supplier providers, final SecretKey secretKey) { + Identity(final String name, final Path path, final IdentityLock lock, final Charset hashCharset, final Encoding hashEncoding, Supplier providers, final SecretKey secretKey, final PrivateKey privateKey, final PublicKey publicKey, final File mainIndex) { this.name = name; this.path = path; this.lock = lock; @@ -536,6 +673,9 @@ static class Identity implements ModifiableRealmIdentity { this.hashEncoding = hashEncoding; this.providers = providers; this.secretKey = secretKey; + this.privateKey = privateKey; + this.publicKey = publicKey; + this.mainIndex = mainIndex; } public Principal getRealmIdentityPrincipal() { @@ -589,12 +729,25 @@ public boolean verifyEvidence(final Evidence evidence) throws RealmUnavailableEx Assert.checkNotNullParam("evidence", evidence); if (ElytronMessages.log.isTraceEnabled()) { - final LoadedIdentity loadedIdentity = loadIdentity(false, true); - ElytronMessages.log.tracef("Trying to authenticate identity %s using FileSystemSecurityRealm", - (loadedIdentity != null) ? loadedIdentity.getName() : "null"); + try { + final LoadedIdentity loadedIdentity = loadIdentity(false, true); + ElytronMessages.log.tracef("Trying to authenticate identity %s using FileSystemSecurityRealm", (loadedIdentity != null) ? loadedIdentity.getName() : "null"); + } catch (RealmUnavailableException e) { + if (e.getCause() instanceof IntegrityException) { + return false; + } + throw e; + } + } + List credentials = null; + try { + credentials = loadCredentials(); + } catch (RealmUnavailableException e) { + if (e.getCause() instanceof IntegrityException) { + return false; + } + throw e; } - - List credentials = loadCredentials(); ElytronMessages.log.tracef("FileSystemSecurityRealm - verification evidence [%s] against [%d] credentials...", evidence, credentials.size()); for (Credential credential : credentials) { if (credential.canVerify(evidence)) { @@ -613,6 +766,9 @@ public boolean verifyEvidence(final Evidence evidence) throws RealmUnavailableEx } List loadCredentials() throws RealmUnavailableException { + if (!verifyIntegrity()) { + throw ElytronMessages.log.invalidIdentitySignature(name); + } final LoadedIdentity loadedIdentity = loadIdentity(false, true); return loadedIdentity == null ? Collections.emptyList() : loadedIdentity.getCredentials(); } @@ -627,6 +783,13 @@ public boolean exists() throws RealmUnavailableException { public void delete() throws RealmUnavailableException { if (System.getSecurityManager() == null) { deletePrivileged(); + try { + if(publicKey != null) { + deleteMainIndexEntry(mainIndex, path); + } + } catch (IOException e) { + throw ElytronMessages.log.unableToAccessMainIndex(mainIndex.toString()); + } return; } try { @@ -676,6 +839,19 @@ private Path tempPath() { public void create() throws RealmUnavailableException { if (System.getSecurityManager() == null) { createPrivileged(); + if(privateKey != null) { + try { + createDigitalSignature(path.toString(), this.name); + writeToMainIndex(this.mainIndex, this.path, this.secretKey); + } catch (IllegalStateException e) { + throw ElytronMessages.log.unableToGenerateSignature(path.toString()); + } catch (IOException | GeneralSecurityException e) { + throw ElytronMessages.log.unableToAccessMainIndex(this.mainIndex.toString()); + } + if (!verifyIntegrity()) { + throw ElytronMessages.log.invalidIdentitySignature(this.name); + } + } return; } try { @@ -694,12 +870,20 @@ private Void createPrivileged() throws RealmUnavailableException { final XMLOutputFactory xmlOutputFactory = XMLOutputFactory.newFactory(); try (OutputStream outputStream = new BufferedOutputStream(Files.newOutputStream(tempPath, WRITE, CREATE_NEW, DSYNC))) { try (AutoCloseableXMLStreamWriterHolder holder = new AutoCloseableXMLStreamWriterHolder(xmlOutputFactory.createXMLStreamWriter(outputStream))) { + String namespace = ""; + if (privateKey != null) { + namespace = Version.VERSION_1_2.getNamespace(); + } else if (secretKey != null) { + namespace = Version.VERSION_1_1.getNamespace(); + } else { + namespace = Version.VERSION_1_0.getNamespace(); + } final XMLStreamWriter streamWriter = holder.getXmlStreamWriter(); // create empty identity streamWriter.writeStartDocument(); streamWriter.writeCharacters("\n"); streamWriter.writeStartElement("identity"); - streamWriter.writeDefaultNamespace(secretKey != null ? Version.VERSION_1_1.getNamespace() : Version.VERSION_1_0.getNamespace()); + streamWriter.writeDefaultNamespace(namespace); streamWriter.writeEndElement(); streamWriter.writeEndDocument(); } catch (XMLStreamException e) { @@ -728,6 +912,16 @@ private Void createPrivileged() throws RealmUnavailableException { } catch (IOException ignored) { // nothing we can do } + if(privateKey != null) { + try { + writeDigitalSignature(path.toString(), this.name); + writeToMainIndex(this.mainIndex, this.path, this.secretKey); + } catch (RealmUnavailableException e) { + throw ElytronMessages.log.unableToGenerateSignature(path.toString()); + } catch (IOException | GeneralSecurityException e) { + throw ElytronMessages.log.unableToAccessMainIndex(this.mainIndex.toString()); + } + } return null; } } @@ -778,6 +972,9 @@ private void replaceIdentity(final LoadedIdentity newIdentity) throws RealmUnava } private Void replaceIdentityPrivileged(final LoadedIdentity newIdentity) throws RealmUnavailableException { + if (!verifyIntegrity()) { + throw new RealmUnavailableException(ElytronMessages.log.invalidIdentitySignature(name)); + } for (;;) { final Path tempPath = tempPath(); try { @@ -823,6 +1020,17 @@ private Void replaceIdentityPrivileged(final LoadedIdentity newIdentity) throws } catch (IOException ignored) { // nothing we can do } + + if (this.publicKey != null) { + try { + writeDigitalSignature(path.toString(), name); + writeToMainIndex(mainIndex, path, secretKey); + } catch (RealmUnavailableException e) { + throw ElytronMessages.log.unableToGenerateSignature(path.toString()); + } catch (GeneralSecurityException | IOException e) { + throw ElytronMessages.log.unableToAccessMainIndex(path.toString()); + } + } return null; } catch (Throwable t) { try { @@ -840,13 +1048,13 @@ private Version requiredVersion(final LoadedIdentity identityToWrite) { // if new functionality is used then use the required schema version otherwise fallback // to an older version. - if (secretKey != null) { + if (privateKey != null) { + return Version.VERSION_1_2; + } else if (secretKey != null) { return Version.VERSION_1_1; + } else { + return Version.VERSION_1_0; } - // We would not require 1.0.1 as no realm specific changed were made. - //return Version.VERSION_1_0_1; - - return Version.VERSION_1_0; } private void writeIdentity(final XMLStreamWriter streamWriter, final LoadedIdentity newIdentity) throws XMLStreamException, InvalidKeySpecException, NoSuchAlgorithmException, GeneralSecurityException { @@ -955,6 +1163,9 @@ private LoadedIdentity loadIdentity(final boolean skipCredentials, final boolean } protected LoadedIdentity loadIdentityPrivileged(final boolean skipCredentials, final boolean skipAttributes) throws RealmUnavailableException { + if (!verifyIntegrity()) { + throw new RealmUnavailableException(ElytronMessages.log.invalidIdentitySignature(name)); + } try (InputStream inputStream = Files.newInputStream(path, READ)) { final XMLInputFactory inputFactory = XMLInputFactory.newFactory(); inputFactory.setProperty(XMLInputFactory.IS_VALIDATING, Boolean.FALSE); @@ -978,8 +1189,7 @@ private LoadedIdentity parseIdentity(final XMLStreamReader streamReader, final b final int tag = streamReader.nextTag(); Version version; if (tag != START_ELEMENT || ((version = identifyVersion(streamReader)) == null) || ! "identity".equals(streamReader.getLocalName())) { - throw ElytronMessages.log.fileSystemRealmInvalidContent(path, streamReader.getLocation().getLineNumber(), name); - } + throw ElytronMessages.log.fileSystemRealmInvalidContent(path, streamReader.getLocation().getLineNumber(), name); } return parseIdentityContents(streamReader, version, skipCredentials, skipAttributes); } @@ -999,8 +1209,8 @@ private LoadedIdentity parseIdentityContents(final XMLStreamReader streamReader, for (;;) { if (streamReader.isEndElement()) { if (attributes == Attributes.EMPTY && !skipAttributes) { - //Since this could be a use-case wanting to modify the attributes, make sure that we have a - //modifiable version of Attributes; + // Since this could be a use-case wanting to modify the attributes, make sure that we have a + // modifiable version of Attributes; attributes = new MapAttributes(); } return new LoadedIdentity(name, credentials, attributes, hashEncoding); @@ -1294,6 +1504,222 @@ private void consumeContent(final XMLStreamReader reader) throws XMLStreamExcept } } + private boolean verifyIntegrity() { + if (this.publicKey != null) { + boolean isChecksumValid; + try { + isChecksumValid = validateChecksum(this.mainIndex, this.path, this.secretKey); + } catch (IOException e) { + ElytronMessages.log.tracef("Unable to calculate checksum of identity: %s", name); + return false; + } catch (GeneralSecurityException e) { + ElytronMessages.log.tracef("Unable to decrypt main index data"); + return false; + } + if (!isChecksumValid) { + boolean isSignatureValid = validateDigitalSignature(this.path.toString(), this.name); + if (isSignatureValid) { + try { + writeToMainIndex(mainIndex, path, secretKey); + } catch (IOException | GeneralSecurityException e) { + ElytronMessages.log.unableToWriteToMainIndex(); + } + } + return isSignatureValid; + } + return true; + } + return true; + } + + // Process for updating identity: + // 1. Validate current identity digital signature + // 2. Update identity with new data + // 3. Create new digital signature + private boolean validateDigitalSignature(String path, String name) { + 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) { + throw ElytronMessages.log.cannotFindSignature(path); + } + XMLSignatureFactory fac = XMLSignatureFactory.getInstance("DOM"); + DOMValidateContext valContext = new DOMValidateContext(publicKey, nl.item(0)); + XMLSignature signature = fac.unmarshalXMLSignature(valContext); + boolean coreValidity = signature.validate(valContext); + ElytronMessages.log.tracef("FileSystemSecurityRealm - verification against signature for credential [%s] = %b", name, coreValidity); + return coreValidity; + } catch (ParserConfigurationException | IOException | MarshalException | XMLSignatureException | SAXException e) { + return false; + } + } + + private void writeDigitalSignature(String path, String name) throws RealmUnavailableException { + try { + DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); + dbf.setNamespaceAware(true); + DocumentBuilder builder = dbf.newDocumentBuilder(); + Document doc = builder.parse(new FileInputStream(path)); + Element elem = doc.getDocumentElement(); + NodeList signatureNode = doc.getElementsByTagName("Signature"); + if (signatureNode.getLength() > 0) { + Node sig = signatureNode.item(0); + elem.removeChild(sig); + } + DOMSignContext dsc = new DOMSignContext(this.privateKey, elem); + XMLSignatureFactory fac = XMLSignatureFactory.getInstance("DOM"); + Reference ref = fac.newReference + ("", fac.newDigestMethod(DigestMethod.SHA256, null), + Collections.singletonList + (fac.newTransform(Transform.ENVELOPED, + (TransformParameterSpec) null)), null, null); + String signatureMethod = ""; + switch (this.publicKey.getAlgorithm()) { + case "DSA": + signatureMethod = ALGO_ID_SIGNATURE_DSA_SHA256; + break; + case "RSA": + signatureMethod = ALGO_ID_SIGNATURE_RSA_SHA256; + break; + case "HMAC": + signatureMethod = ALGO_ID_MAC_HMAC_SHA256; + break; + case "EC": + signatureMethod = ALGO_ID_SIGNATURE_ECDSA_SHA256; + break; + } + SignedInfo si = fac.newSignedInfo + (fac.newCanonicalizationMethod + (CanonicalizationMethod.INCLUSIVE, + (C14NMethodParameterSpec) null), + fac.newSignatureMethod(signatureMethod, null), + Collections.singletonList(ref)); + KeyInfoFactory kif = fac.getKeyInfoFactory(); + KeyValue kv = kif.newKeyValue(this.publicKey); + KeyInfo ki = kif.newKeyInfo(Collections.singletonList(kv)); + XMLSignature signature = fac.newXMLSignature(si, ki); + signature.sign(dsc); + TransformerFactory transformerFactory = TransformerFactory.newInstance(); + Transformer transformer = transformerFactory.newTransformer(); + DOMSource source = new DOMSource(doc); + FileWriter writer = new FileWriter(path); + StreamResult result = new StreamResult(writer); + transformer.transform(source, result); + ElytronMessages.log.tracef("FileSystemSecurityRealm - signature against file updated [%s]", name); + writer.close(); + + } catch (ParserConfigurationException | IOException | NoSuchAlgorithmException | InvalidAlgorithmParameterException | + KeyException | XMLSignatureException | MarshalException | TransformerException | SAXException e) { + throw ElytronMessages.log.unableToGenerateSignature(path); + } + } + + private static void writeToMainIndex(File mainIndex, Path path, SecretKey secretKey) throws IOException, GeneralSecurityException { + FileWriter mainIndexWriter = new FileWriter(mainIndex, true); + MessageDigest messageDigest = MessageDigest.getInstance("MD5"); + String checksum = getChecksum(messageDigest, new File(path.toString())); + String identityFilename = path.getFileName().toString(); + if(secretKey != null) { + checksum = CipherUtil.encrypt(checksum, secretKey); + identityFilename = CipherUtil.encrypt(identityFilename, secretKey); + } + mainIndexWriter.append(identityFilename).append(":").append(checksum).append('\n'); + mainIndexWriter.close(); + } + + private static void updateMainIndex(File mainIndex, Path path, SecretKey secretKey) throws IOException, GeneralSecurityException { + BufferedReader reader = new BufferedReader(new FileReader(mainIndex)); + MessageDigest messageDigest = MessageDigest.getInstance("MD5"); + String checksum = getChecksum(messageDigest, new File(path.toString())); + String identityFilename = path.getFileName().toString(); + String line; + ArrayList fileContents = new ArrayList(); + while((line = reader.readLine()) != null) { + fileContents.add(line); + } + reader.close(); + + for (int i = 0; i < fileContents.size(); i++) { + String currentIdentity = fileContents.get(i).split(":")[0]; + if (secretKey != null) { + currentIdentity = CipherUtil.decrypt(currentIdentity, secretKey); + checksum = CipherUtil.encrypt(checksum, secretKey); + } + if (currentIdentity.equals(identityFilename)) { + fileContents.set(i, fileContents.get(i).split(":")[0] + ":" + checksum); + break; + } + } + FileWriter writer = new FileWriter(mainIndex); + writer.write(String.join("\n", fileContents)); + writer.close(); + } + + private static boolean validateChecksum(File mainIndex, Path path, SecretKey secretKey) throws IOException, GeneralSecurityException { + try { + Scanner mainIndexScanner = new Scanner(mainIndex); + MessageDigest messageDigest = MessageDigest.getInstance("MD5"); + String currentFileChecksum = getChecksum(messageDigest, new File(path.toString())); + String identityFilename = path.getFileName().toString(); + String data, mainIndexFilename, mainIndexChecksum; + while (mainIndexScanner.hasNextLine()) { + data = mainIndexScanner.nextLine(); + mainIndexFilename = data.split(":")[0]; + mainIndexChecksum = data.split(":")[1]; + if(secretKey != null) { + mainIndexFilename = CipherUtil.decrypt(mainIndexFilename, secretKey); + mainIndexChecksum = CipherUtil.decrypt(mainIndexChecksum, secretKey); + } + if(mainIndexFilename.equals(identityFilename)) { + mainIndexScanner.close(); + return mainIndexChecksum.equals(currentFileChecksum); + } + } + mainIndexScanner.close(); + } catch (FileNotFoundException e) { + throw ElytronMessages.log.unableToAccessMainIndex(path.toString()); + } + return false; + } + + private static void deleteMainIndexEntry(File mainIndex, Path path) throws IOException { + BufferedReader reader = new BufferedReader(new FileReader(mainIndex)); + String line; + StringBuilder fileContents = new StringBuilder(); + while((line = reader.readLine()) != null) { + fileContents.append(line).append("\r\n"); + } + reader.close(); + int beginningIndex = fileContents.indexOf(path.getFileName().toString()); + int endIndex = fileContents.indexOf("\n", beginningIndex); + fileContents.delete(beginningIndex, endIndex+"\n".length()); + FileWriter writer = new FileWriter(mainIndex); + writer.write(fileContents.toString()); + writer.close(); + } + + private static String getChecksum(MessageDigest digest, File file) throws IOException { + FileInputStream fileInputStream = new FileInputStream(file); + // Create byte array to read data in chunks + byte[] byteArray = new byte[1024]; + int bytesCount = 0; + while ((bytesCount = fileInputStream.read(byteArray)) != -1) { + digest.update(byteArray, 0, bytesCount); + } + // close the input stream + fileInputStream.close(); + // store the bytes returned by the digest() method + byte[] bytes = digest.digest(); + + StringBuilder stringBuilder = new StringBuilder(); + for (byte eachByte : bytes) { + // converts the decimal into hexadecimal format + stringBuilder.append(Integer.toString((eachByte & 0xff) + 0x100, 16).substring(1)); + } + return stringBuilder.toString(); + } } protected static final class LoadedIdentity { diff --git a/auth/realm/base/src/main/java/org/wildfly/security/auth/realm/FileSystemSecurityRealmBuilder.java b/auth/realm/base/src/main/java/org/wildfly/security/auth/realm/FileSystemSecurityRealmBuilder.java index b0839aad96e..a976215754c 100644 --- a/auth/realm/base/src/main/java/org/wildfly/security/auth/realm/FileSystemSecurityRealmBuilder.java +++ b/auth/realm/base/src/main/java/org/wildfly/security/auth/realm/FileSystemSecurityRealmBuilder.java @@ -17,14 +17,16 @@ */ package org.wildfly.security.auth.realm; +import static org.wildfly.security.provider.util.ProviderUtil.INSTALLED_PROVIDERS; + import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.nio.file.Path; +import java.security.PrivateKey; import java.security.Provider; +import java.security.PublicKey; import java.util.function.Supplier; - import javax.crypto.SecretKey; - import org.wildfly.common.Assert; import org.wildfly.security.auth.server.NameRewriter; import org.wildfly.security.password.spec.Encoding; @@ -44,6 +46,8 @@ public class FileSystemSecurityRealmBuilder { private Charset hashCharset; private Encoding hashEncoding; private SecretKey secretKey; + private PrivateKey privateKey; + private PublicKey publicKey; private Supplier providers; FileSystemSecurityRealmBuilder() { @@ -120,6 +124,18 @@ public FileSystemSecurityRealmBuilder setHashEncoding(final Encoding hashEncodin return this; } + /** + * Set the providers to be used by the realm. + * + * @param providers the provider to be used (must not be {@code null}) + * @return this builder. + */ + public FileSystemSecurityRealmBuilder setProviders(final Supplier providers) { + Assert.checkNotNullParam("providers", providers); + this.providers = providers; + return this; + } + /** * Set the SecretKey to be used by the realm. * @@ -132,9 +148,27 @@ public FileSystemSecurityRealmBuilder setSecretKey(final SecretKey secretKey) { return this; } - public FileSystemSecurityRealmBuilder setProviders(final Supplier providers) { - Assert.checkNotNullParam("providers", providers); - this.providers = providers; + /** + * Set the PrivateKey to be used by the realm. + * + * @param privateKey the asymmetric PrivateKey used to sign the identity files used for file integrity (must not be {@code null}) + * @return this builder. + */ + public FileSystemSecurityRealmBuilder setPrivateKey(final PrivateKey privateKey) { + Assert.checkNotNullParam("privateKey", privateKey); + this.privateKey = privateKey; + return this; + } + + /** + * Set the PublicKey to be used by the realm. + * + * @param publicKey the asymmetric PublicKey used to verify the identity files used for file integrity (must not be {@code null}) + * @return this builder. + */ + public FileSystemSecurityRealmBuilder setPublicKey(final PublicKey publicKey) { + Assert.checkNotNullParam("publicKey", publicKey); + this.publicKey = publicKey; return this; } @@ -154,6 +188,14 @@ public FileSystemSecurityRealm build() { if (hashCharset == null) { hashCharset = StandardCharsets.UTF_8; } - return new FileSystemSecurityRealm(root, nameRewriter, levels, encoded, hashEncoding, hashCharset, providers, secretKey); + if (providers == null) { + providers = INSTALLED_PROVIDERS; + } + + if (privateKey == null ^ publicKey == null) { + throw ElytronMessages.log.invalidKeyPairArgument(root.toString()); + } + + return new FileSystemSecurityRealm(root, nameRewriter, levels, encoded, hashEncoding, hashCharset, providers, secretKey, privateKey, publicKey); } } diff --git a/auth/realm/base/src/main/java/org/wildfly/security/auth/realm/IntegrityException.java b/auth/realm/base/src/main/java/org/wildfly/security/auth/realm/IntegrityException.java new file mode 100644 index 00000000000..366d52056e1 --- /dev/null +++ b/auth/realm/base/src/main/java/org/wildfly/security/auth/realm/IntegrityException.java @@ -0,0 +1,71 @@ +/* + * JBoss, Home of Professional Open Source. + * Copyright 2020 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.security.auth.realm; + +import java.io.IOException; + +/** + * Exception to indicate a general failure related to the Integrity Verification of the Filesystem Realm. + * + * @author Ashpan Raskar + */ +public class IntegrityException extends IOException { + + + private static final long serialVersionUID = 8889252552074803941L; + + /** + * Constructs a new {@code IntegrityException} instance. The message is left blank ({@code null}), and no + * cause is specified. + */ + public IntegrityException() { + } + + /** + * Constructs a new {@code IntegrityException} instance with an initial message. No cause is specified. + * + * @param msg the message + */ + public IntegrityException(final String msg) { + super(msg); + } + + /** + * Constructs a new {@code IntegrityException} instance with an initial cause. If a non-{@code null} cause + * is specified, its message is used to initialize the message of this {@code IntegrityException}; otherwise + * the message is left blank ({@code null}). + * + * @param cause the cause + */ + public IntegrityException(final Throwable cause) { + super(cause); + } + + /** + * Constructs a new {@code IntegrityException} instance with an initial message and cause. + * + * @param msg the message + * @param cause the cause + */ + public IntegrityException(final String msg, final Throwable cause) { + super(msg, cause); + } + +} + diff --git a/auth/realm/base/src/main/resources/schema/elytron-identity-1_1.xsd b/auth/realm/base/src/main/resources/schema/elytron-identity-1_1.xsd index 2e0d8cfeece..118ee40a5fd 100644 --- a/auth/realm/base/src/main/resources/schema/elytron-identity-1_1.xsd +++ b/auth/realm/base/src/main/resources/schema/elytron-identity-1_1.xsd @@ -144,4 +144,6 @@ + + diff --git a/base/src/main/java/org/wildfly/security/WildFlyElytronBaseProvider.java b/base/src/main/java/org/wildfly/security/WildFlyElytronBaseProvider.java index d20c1ac32f1..445271ad9c0 100644 --- a/base/src/main/java/org/wildfly/security/WildFlyElytronBaseProvider.java +++ b/base/src/main/java/org/wildfly/security/WildFlyElytronBaseProvider.java @@ -26,7 +26,6 @@ import java.lang.reflect.Constructor; import java.security.NoSuchAlgorithmException; import java.security.Provider; -import java.security.Provider.Service; import java.util.Arrays; import java.util.Collection; import java.util.Collections; diff --git a/pom.xml b/pom.xml index 215288b514b..3d443f640b5 100644 --- a/pom.xml +++ b/pom.xml @@ -103,6 +103,7 @@ 17.0.0 4.3.3 2.40.0 + 2.3.0 INFO @@ -1027,6 +1028,11 @@ jose4j ${version.org.bitbucket.b_c.jose4j} + + org.apache.santuario + xmlsec + ${version.org.apache.santuario} +