From c4f7abc8a9db22eaad66bab555884bb60e3b5e17 Mon Sep 17 00:00:00 2001 From: Ashpan Raskar Date: Tue, 26 Oct 2021 17:55:19 -0400 Subject: [PATCH] [ELY-2320] Add integrity support to FileSystemSecurityRealm --- auth/realm/base/pom.xml | 5 + .../security/auth/realm/ElytronMessages.java | 21 +- .../auth/realm/FileSystemSecurityRealm.java | 350 ++++++++- .../realm/FileSystemSecurityRealmBuilder.java | 54 +- .../auth/realm/IntegrityException.java | 71 ++ .../security/WildFlyElytronBaseProvider.java | 1 - pom.xml | 6 + .../auth/FileSystemSecurityRealmTest.java | 685 ++++++++++++------ ...ifiableSecurityRealmIdentityCacheTest.java | 2 +- .../u/s/e/user-OVZWK4Q.xml | 32 + 10 files changed, 984 insertions(+), 243 deletions(-) create mode 100644 auth/realm/base/src/main/java/org/wildfly/security/auth/realm/IntegrityException.java create mode 100644 tests/base/src/test/resources/filesystem-realm-exists/u/s/e/user-OVZWK4Q.xml 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..b063665025c 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,15 @@ 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.File; import java.io.FileNotFoundException; +import java.io.FileWriter; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; @@ -39,15 +44,20 @@ import java.nio.file.Files; import java.nio.file.NoSuchFileException; import java.nio.file.Path; +import java.nio.file.Paths; import java.security.AccessController; import java.security.GeneralSecurityException; +import java.security.InvalidAlgorithmParameterException; +import java.security.KeyException; import java.security.KeyFactory; 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 +75,45 @@ import java.util.Locale; import java.util.Map; import java.util.NoSuchElementException; +import java.util.Objects; 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 +147,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 +165,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 +205,8 @@ 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 ConcurrentHashMap realmIdentityLocks = new ConcurrentHashMap<>(); /** @@ -188,8 +230,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 +247,26 @@ 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; + + } + + /** + * 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 +282,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 +296,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 +317,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 +345,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 +365,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 +453,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); } public ModifiableRealmIdentityIterator getRealmIdentityIterator() throws RealmUnavailableException { @@ -512,6 +580,47 @@ 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.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 +636,10 @@ 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; - 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) { this.name = name; this.path = path; this.lock = lock; @@ -536,6 +647,8 @@ static class Identity implements ModifiableRealmIdentity { this.hashEncoding = hashEncoding; this.providers = providers; this.secretKey = secretKey; + this.privateKey = privateKey; + this.publicKey = publicKey; } public Principal getRealmIdentityPrincipal() { @@ -589,12 +702,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 +739,9 @@ public boolean verifyEvidence(final Evidence evidence) throws RealmUnavailableEx } List loadCredentials() throws RealmUnavailableException { + if (!verifyIntegrity()) { + throw new RealmUnavailableException(ElytronMessages.log.invalidIdentitySignature(name)); + } final LoadedIdentity loadedIdentity = loadIdentity(false, true); return loadedIdentity == null ? Collections.emptyList() : loadedIdentity.getCredentials(); } @@ -694,16 +823,33 @@ 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); + if (privateKey != null) { + streamWriter.writeCharacters("\n "); + streamWriter.writeStartElement("principal"); + streamWriter.writeAttribute("name", secretKey != null ? CipherUtil.encrypt(name, secretKey) : name); + streamWriter.writeEndElement(); + streamWriter.writeCharacters("\n"); + } streamWriter.writeEndElement(); streamWriter.writeEndDocument(); } catch (XMLStreamException e) { throw ElytronMessages.log.fileSystemRealmFailedToWrite(tempPath, name, e); + } catch (GeneralSecurityException e) { + throw new RuntimeException(e); } } catch (FileAlreadyExistsException ignored) { // try a new name @@ -728,6 +874,13 @@ private Void createPrivileged() throws RealmUnavailableException { } catch (IOException ignored) { // nothing we can do } + if(privateKey != null) { + try { + writeDigitalSignature(path.toString(), this.name); + } catch (RealmUnavailableException e) { + throw ElytronMessages.log.unableToGenerateSignature(path.toString()); + } + } return null; } } @@ -778,6 +931,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 +979,14 @@ private Void replaceIdentityPrivileged(final LoadedIdentity newIdentity) throws } catch (IOException ignored) { // nothing we can do } + + if (this.publicKey != null) { + try { + writeDigitalSignature(path.toString(), name); + } catch (RealmUnavailableException e) { + throw ElytronMessages.log.unableToGenerateSignature(path.toString()); + } + } return null; } catch (Throwable t) { try { @@ -840,22 +1004,28 @@ 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 { streamWriter.writeStartDocument(); streamWriter.writeCharacters("\n"); streamWriter.writeStartElement("identity"); - streamWriter.writeDefaultNamespace(requiredVersion(newIdentity).getNamespace()); + if (privateKey != null) { + streamWriter.writeCharacters("\n "); + streamWriter.writeStartElement("principal"); + streamWriter.writeAttribute("name", secretKey != null ? CipherUtil.encrypt(name, secretKey) : name); + streamWriter.writeEndElement(); + } + if (newIdentity.getCredentials().size() > 0) { streamWriter.writeCharacters("\n "); streamWriter.writeStartElement("credentials"); @@ -921,6 +1091,7 @@ private void writeIdentity(final XMLStreamWriter streamWriter, final LoadedIdent } while (entryIter.hasNext()); streamWriter.writeCharacters("\n "); streamWriter.writeEndElement(); + streamWriter.writeCharacters("\n"); } streamWriter.writeEndElement(); streamWriter.writeEndDocument(); @@ -955,6 +1126,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 +1152,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,16 +1172,21 @@ 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); } - if (! version.getNamespace().equals(streamReader.getNamespaceURI())) { + if (!(version.getNamespace().equals(streamReader.getNamespaceURI())) && !(XMLSignature.XMLNS.equals(streamReader.getNamespaceURI()))) { // Mixed versions unsupported. throw ElytronMessages.log.fileSystemRealmInvalidContent(path, streamReader.getLocation().getLineNumber(), name); } + + if (version.isAtLeast(Version.VERSION_1_2) && "principal".equals(streamReader.getLocalName())) { + consumeContent(streamReader); + } + if (! gotCredentials && "credentials".equals(streamReader.getLocalName())) { gotCredentials = true; if (skipCredentials) { @@ -1294,6 +1472,120 @@ private void consumeContent(final XMLStreamReader reader) throws XMLStreamExcept } } + private boolean verifyIntegrity() { + if (this.publicKey != null) { + return (validatePrincipalName(path.toString(), name) && validateDigitalSignature(path.toString(), name)); + } + 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 boolean validatePrincipalName(String path, String name) { + DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance(); + dbf.setNamespaceAware(true); + Document doc; + try { + doc = dbf.newDocumentBuilder().parse(path); + } catch (SAXException | IOException | ParserConfigurationException e) { + return false; + } + NodeList nl = doc.getElementsByTagName("principal"); + if (nl.getLength() == 0) { + return false; + } + String principalName = nl.item(0).getAttributes().getNamedItem("name").getNodeValue(); + if (secretKey != null) { + try { + principalName = CipherUtil.decrypt(principalName, secretKey); + } catch (GeneralSecurityException e) { + return false; + } + } + return Objects.equals(principalName, name); + } + + 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(Files.newInputStream(Paths.get(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); + } + } } 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/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} +