Skip to content
Snippets Groups Projects
Verified Commit b9b43664 authored by Janne Mareike Koschinski's avatar Janne Mareike Koschinski
Browse files

Use system hostname verifier

parent b622ad63
Branches
No related tags found
1 merge request!2Draft: Jetpack compose rewrite
Showing
with 54 additions and 146 deletions
...@@ -28,7 +28,6 @@ import androidx.core.app.RemoteInput ...@@ -28,7 +28,6 @@ import androidx.core.app.RemoteInput
import androidx.lifecycle.Observer import androidx.lifecycle.Observer
import com.github.pwittchen.reactivenetwork.library.rx2.ReactiveNetwork import com.github.pwittchen.reactivenetwork.library.rx2.ReactiveNetwork
import de.kuschku.libquassel.connection.ConnectionState import de.kuschku.libquassel.connection.ConnectionState
import de.kuschku.libquassel.connection.HostnameVerifier
import de.kuschku.libquassel.connection.SocketAddress import de.kuschku.libquassel.connection.SocketAddress
import de.kuschku.libquassel.protocol.* import de.kuschku.libquassel.protocol.*
import de.kuschku.libquassel.quassel.BufferInfo import de.kuschku.libquassel.quassel.BufferInfo
...@@ -68,6 +67,7 @@ import de.kuschku.quasseldroid.util.ui.LocaleHelper ...@@ -68,6 +67,7 @@ import de.kuschku.quasseldroid.util.ui.LocaleHelper
import io.reactivex.subjects.BehaviorSubject import io.reactivex.subjects.BehaviorSubject
import org.threeten.bp.Instant import org.threeten.bp.Instant
import javax.inject.Inject import javax.inject.Inject
import javax.net.ssl.HostnameVerifier
import javax.net.ssl.X509TrustManager import javax.net.ssl.X509TrustManager
class QuasselService : DaggerLifecycleService(), class QuasselService : DaggerLifecycleService(),
......
...@@ -19,25 +19,16 @@ ...@@ -19,25 +19,16 @@
package de.kuschku.quasseldroid.ssl package de.kuschku.quasseldroid.ssl
import de.kuschku.libquassel.connection.HostnameVerifier
import de.kuschku.libquassel.connection.QuasselSecurityException
import de.kuschku.libquassel.connection.SocketAddress
import de.kuschku.libquassel.ssl.BrowserCompatibleHostnameVerifier
import de.kuschku.quasseldroid.ssl.custom.QuasselHostnameManager import de.kuschku.quasseldroid.ssl.custom.QuasselHostnameManager
import java.security.cert.X509Certificate import javax.net.ssl.HostnameVerifier
import javax.net.ssl.SSLException import javax.net.ssl.HttpsURLConnection
import javax.net.ssl.SSLSession
class QuasselHostnameVerifier( class QuasselHostnameVerifier(
private val hostnameManager: QuasselHostnameManager, private val hostnameManager: QuasselHostnameManager,
private val hostnameVerifier: HostnameVerifier = BrowserCompatibleHostnameVerifier() private val hostnameVerifier: HostnameVerifier = HttpsURLConnection.getDefaultHostnameVerifier()
) : HostnameVerifier { ) : HostnameVerifier {
override fun checkValid(address: SocketAddress, chain: Array<out X509Certificate>) { override fun verify(hostname: String?, session: SSLSession?): Boolean {
try { return hostnameManager.verify(hostname, session) || hostnameVerifier.verify(hostname, session)
if (!hostnameManager.isValid(address, chain)) {
hostnameVerifier.checkValid(address, chain)
}
} catch (e: SSLException) {
throw QuasselSecurityException.Hostname(chain, address, e)
}
} }
} }
...@@ -19,17 +19,19 @@ ...@@ -19,17 +19,19 @@
package de.kuschku.quasseldroid.ssl.custom package de.kuschku.quasseldroid.ssl.custom
import de.kuschku.libquassel.connection.SocketAddress import de.kuschku.libquassel.ssl.toJavaCertificate
import de.kuschku.quasseldroid.persistence.dao.SslHostnameWhitelistDao import de.kuschku.quasseldroid.persistence.dao.SslHostnameWhitelistDao
import de.kuschku.quasseldroid.util.helper.sha1Fingerprint import de.kuschku.quasseldroid.util.helper.sha1Fingerprint
import java.security.cert.X509Certificate import javax.net.ssl.HostnameVerifier
import javax.net.ssl.SSLSession
class QuasselHostnameManager( class QuasselHostnameManager(
private val hostnameWhitelist: SslHostnameWhitelistDao private val hostnameWhitelist: SslHostnameWhitelistDao
) { ) : HostnameVerifier {
fun isValid(address: SocketAddress, chain: Array<out X509Certificate>): Boolean { override fun verify(hostname: String?, session: SSLSession?): Boolean {
val leafCertificate = chain.firstOrNull() ?: return false val chain = session?.peerCertificateChain?.toJavaCertificate()
val whitelistEntry = hostnameWhitelist.find(leafCertificate.sha1Fingerprint, address.host) val leafCertificate = chain?.firstOrNull() ?: return false
val whitelistEntry = hostnameWhitelist.find(leafCertificate.sha1Fingerprint, hostname ?: "")
return whitelistEntry != null return whitelistEntry != null
} }
} }
...@@ -526,7 +526,7 @@ class ChatActivity : ServiceBoundActivity(), SharedPreferences.OnSharedPreferenc ...@@ -526,7 +526,7 @@ class ChatActivity : ServiceBoundActivity(), SharedPreferences.OnSharedPreferenc
.show() .show()
} else { } else {
val leafCertificate = it.certificateChain?.firstOrNull() val leafCertificate = it.certificateChain?.firstOrNull()
if (leafCertificate == null) { if (leafCertificate == null || it is QuasselSecurityException.NoCertificate) {
// No certificate exists in the chain // No certificate exists in the chain
MaterialDialog.Builder(this) MaterialDialog.Builder(this)
.title(R.string.label_error_certificate) .title(R.string.label_error_certificate)
...@@ -630,7 +630,7 @@ class ChatActivity : ServiceBoundActivity(), SharedPreferences.OnSharedPreferenc ...@@ -630,7 +630,7 @@ class ChatActivity : ServiceBoundActivity(), SharedPreferences.OnSharedPreferenc
.show() .show()
} }
// Certificate not valid for this hostname // Certificate not valid for this hostname
it is QuasselSecurityException.Hostname -> { it is QuasselSecurityException.WrongHostname -> {
MaterialDialog.Builder(this) MaterialDialog.Builder(this)
.title(R.string.label_error_certificate) .title(R.string.label_error_certificate)
.content( .content(
......
...@@ -29,7 +29,6 @@ import de.kuschku.libquassel.protocol.primitive.serializer.VariantListSerializer ...@@ -29,7 +29,6 @@ import de.kuschku.libquassel.protocol.primitive.serializer.VariantListSerializer
import de.kuschku.libquassel.quassel.ProtocolFeature import de.kuschku.libquassel.quassel.ProtocolFeature
import de.kuschku.libquassel.quassel.QuasselFeatures import de.kuschku.libquassel.quassel.QuasselFeatures
import de.kuschku.libquassel.session.ProtocolHandler import de.kuschku.libquassel.session.ProtocolHandler
import de.kuschku.libquassel.ssl.BrowserCompatibleHostnameVerifier
import de.kuschku.libquassel.ssl.TrustManagers import de.kuschku.libquassel.ssl.TrustManagers
import de.kuschku.libquassel.util.Optional import de.kuschku.libquassel.util.Optional
import de.kuschku.libquassel.util.compatibility.CompatibilityUtils import de.kuschku.libquassel.util.compatibility.CompatibilityUtils
...@@ -50,6 +49,8 @@ import java.lang.Thread.UncaughtExceptionHandler ...@@ -50,6 +49,8 @@ import java.lang.Thread.UncaughtExceptionHandler
import java.net.Socket import java.net.Socket
import java.net.SocketException import java.net.SocketException
import java.nio.ByteBuffer import java.nio.ByteBuffer
import javax.net.ssl.HostnameVerifier
import javax.net.ssl.HttpsURLConnection
import javax.net.ssl.SSLSession import javax.net.ssl.SSLSession
import javax.net.ssl.X509TrustManager import javax.net.ssl.X509TrustManager
...@@ -60,7 +61,7 @@ class CoreConnection( ...@@ -60,7 +61,7 @@ class CoreConnection(
private val features: Features = Features(clientData.clientFeatures, QuasselFeatures.empty()), private val features: Features = Features(clientData.clientFeatures, QuasselFeatures.empty()),
private val handlerService: HandlerService = JavaHandlerService(), private val handlerService: HandlerService = JavaHandlerService(),
private val trustManager: X509TrustManager = TrustManagers.default(), private val trustManager: X509TrustManager = TrustManagers.default(),
private val hostnameVerifier: HostnameVerifier = BrowserCompatibleHostnameVerifier() private val hostnameVerifier: HostnameVerifier = HttpsURLConnection.getDefaultHostnameVerifier()
) : Thread(), Closeable { ) : Thread(), Closeable {
companion object { companion object {
private const val TAG = "CoreConnection" private const val TAG = "CoreConnection"
......
...@@ -31,11 +31,14 @@ sealed class QuasselSecurityException( ...@@ -31,11 +31,14 @@ sealed class QuasselSecurityException(
cause: Exception cause: Exception
) : QuasselSecurityException(certificateChain, cause) ) : QuasselSecurityException(certificateChain, cause)
class Hostname( class WrongHostname(
certificateChain: Array<out X509Certificate>?, certificateChain: Array<out X509Certificate>?,
val address: SocketAddress, val address: SocketAddress
cause: Exception ) : QuasselSecurityException(certificateChain, null)
) : QuasselSecurityException(certificateChain, cause)
class NoCertificate(
val address: SocketAddress
) : QuasselSecurityException(emptyArray(), null)
object NoSsl : QuasselSecurityException(emptyArray(), null) object NoSsl : QuasselSecurityException(emptyArray(), null)
} }
...@@ -26,7 +26,6 @@ import de.kuschku.libquassel.protocol.message.SignalProxyMessage ...@@ -26,7 +26,6 @@ import de.kuschku.libquassel.protocol.message.SignalProxyMessage
import de.kuschku.libquassel.quassel.ExtendedFeature import de.kuschku.libquassel.quassel.ExtendedFeature
import de.kuschku.libquassel.quassel.QuasselFeatures import de.kuschku.libquassel.quassel.QuasselFeatures
import de.kuschku.libquassel.quassel.syncables.* import de.kuschku.libquassel.quassel.syncables.*
import de.kuschku.libquassel.ssl.BrowserCompatibleHostnameVerifier
import de.kuschku.libquassel.ssl.TrustManagers import de.kuschku.libquassel.ssl.TrustManagers
import de.kuschku.libquassel.util.compatibility.HandlerService import de.kuschku.libquassel.util.compatibility.HandlerService
import de.kuschku.libquassel.util.compatibility.LoggingHandler.Companion.log import de.kuschku.libquassel.util.compatibility.LoggingHandler.Companion.log
...@@ -37,6 +36,8 @@ import io.reactivex.Observable ...@@ -37,6 +36,8 @@ import io.reactivex.Observable
import io.reactivex.subjects.BehaviorSubject import io.reactivex.subjects.BehaviorSubject
import io.reactivex.subjects.PublishSubject import io.reactivex.subjects.PublishSubject
import org.threeten.bp.Instant import org.threeten.bp.Instant
import javax.net.ssl.HostnameVerifier
import javax.net.ssl.HttpsURLConnection
import javax.net.ssl.X509TrustManager import javax.net.ssl.X509TrustManager
class Session( class Session(
...@@ -44,7 +45,7 @@ class Session( ...@@ -44,7 +45,7 @@ class Session(
private var userData: Pair<String, String>, private var userData: Pair<String, String>,
requireSsl: Boolean = false, requireSsl: Boolean = false,
trustManager: X509TrustManager = TrustManagers.default(), trustManager: X509TrustManager = TrustManagers.default(),
hostnameVerifier: HostnameVerifier = BrowserCompatibleHostnameVerifier(), hostnameVerifier: HostnameVerifier = HttpsURLConnection.getDefaultHostnameVerifier(),
clientData: ClientData = ClientData.DEFAULT, clientData: ClientData = ClientData.DEFAULT,
private val handlerService: HandlerService = JavaHandlerService(), private val handlerService: HandlerService = JavaHandlerService(),
heartBeatFactory: () -> HeartBeatRunner = ::JavaHeartBeatRunner, heartBeatFactory: () -> HeartBeatRunner = ::JavaHeartBeatRunner,
......
...@@ -19,9 +19,9 @@ ...@@ -19,9 +19,9 @@
package de.kuschku.libquassel.session.manager package de.kuschku.libquassel.session.manager
import de.kuschku.libquassel.connection.HostnameVerifier
import de.kuschku.libquassel.connection.SocketAddress import de.kuschku.libquassel.connection.SocketAddress
import de.kuschku.libquassel.protocol.ClientData import de.kuschku.libquassel.protocol.ClientData
import javax.net.ssl.HostnameVerifier
import javax.net.ssl.X509TrustManager import javax.net.ssl.X509TrustManager
data class ConnectionInfo( data class ConnectionInfo(
......
/*
* Quasseldroid - Quassel client for Android
*
* Copyright (c) 2020 Janne Mareike Koschinski
* Copyright (c) 2020 The Quassel Project
*
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 3 as published
* by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package de.kuschku.libquassel.ssl
import de.kuschku.libquassel.connection.HostnameVerifier
import de.kuschku.libquassel.connection.SocketAddress
import de.kuschku.libquassel.ssl.X509Helper.hostnames
import java.net.IDN
import java.security.cert.X509Certificate
import javax.net.ssl.SSLException
class BrowserCompatibleHostnameVerifier : HostnameVerifier {
override fun checkValid(address: SocketAddress, chain: Array<out X509Certificate>) {
val leafCertificate = chain.firstOrNull() ?: throw SSLException("No Certificate found")
val hostnames = hostnames(leafCertificate).toList()
if (hostnames.none { matches(it, address.host) })
throw SSLException("Hostname does not match")
}
private fun matches(name: String, host: String): Boolean {
// First we normalize both by removing trailing dots (absolute DNS names), splitting into DNS
// labels, and punycoding all unicode parts.
val normalizedName = name.trimEnd('.').split('.').map(IDN::toASCII)
val normalizedHost = host.trimEnd('.').split('.').map(IDN::toASCII)
// Only if both have the same number of DNS labels they can match
if (normalizedHost.size != normalizedName.size) return false
// Hosts with size of zero are invalid
if (normalizedHost.isEmpty()) return false
val both = normalizedName.zip(normalizedHost)
// The first label has to either match exactly, or be *
if (!both.take(1).all { (target, actual) ->
target.equals(actual, ignoreCase = true) || target == "*"
}) return false
// All other labels have to match exactly.
if (!both.drop(1).all { (target, actual) ->
target.equals(actual, ignoreCase = true)
}) return false
return true
}
}
/*
* Quasseldroid - Quassel client for Android
*
* Copyright (c) 2020 Janne Mareike Koschinski
* Copyright (c) 2020 The Quassel Project
*
* This program is free software: you can redistribute it and/or modify it
* under the terms of the GNU General Public License version 3 as published
* by the Free Software Foundation.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package de.kuschku.libquassel.ssl
import de.kuschku.libquassel.connection.HostnameVerifier
import de.kuschku.libquassel.connection.SocketAddress
import java.security.cert.X509Certificate
class TrustAllHostnameVerifier : HostnameVerifier {
override fun checkValid(address: SocketAddress, chain: Array<out X509Certificate>) = Unit
}
...@@ -17,12 +17,9 @@ ...@@ -17,12 +17,9 @@
* with this program. If not, see <http://www.gnu.org/licenses/>. * with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
package de.kuschku.libquassel.connection package de.kuschku.libquassel.ssl
import java.security.cert.X509Certificate import javax.security.cert.X509Certificate
import javax.net.ssl.SSLException
interface HostnameVerifier { fun X509Certificate.toJavaCertificate() = X509Helper.convert(this)
@Throws(SSLException::class) fun Array<X509Certificate>.toJavaCertificate() = X509Helper.convert(this)
fun checkValid(address: SocketAddress, chain: Array<out X509Certificate>)
}
...@@ -23,9 +23,8 @@ import java.io.ByteArrayInputStream ...@@ -23,9 +23,8 @@ import java.io.ByteArrayInputStream
import java.security.cert.CertificateFactory import java.security.cert.CertificateFactory
import java.security.cert.X509Certificate import java.security.cert.X509Certificate
// FIXME: re-read RFC and check it's actually secure
object X509Helper { object X509Helper {
val certificateFactory = CertificateFactory.getInstance("X.509") private val certificateFactory = CertificateFactory.getInstance("X.509")
fun hostnames(certificate: X509Certificate): Sequence<String> = fun hostnames(certificate: X509Certificate): Sequence<String> =
(sequenceOf(certificate.subjectX500Principal.commonName) + subjectAlternativeNames(certificate)) (sequenceOf(certificate.subjectX500Principal.commonName) + subjectAlternativeNames(certificate))
...@@ -48,6 +47,9 @@ object X509Helper { ...@@ -48,6 +47,9 @@ object X509Helper {
fun convert(certificate: javax.security.cert.X509Certificate) = fun convert(certificate: javax.security.cert.X509Certificate) =
certificateFactory.generateCertificate(ByteArrayInputStream(certificate.encoded)) as? X509Certificate certificateFactory.generateCertificate(ByteArrayInputStream(certificate.encoded)) as? X509Certificate
fun convert(chain: Array<out javax.security.cert.X509Certificate>): Array<X509Certificate>? =
chain.map { convert(it) ?: return null }.toTypedArray()
val COMMON_NAME = Regex("""(?:^|,\s?)(?:CN=("(?:[^"]|"")+"|[^,]+))""") val COMMON_NAME = Regex("""(?:^|,\s?)(?:CN=("(?:[^"]|"")+"|[^,]+))""")
val ORGANIZATION = Regex("""(?:^|,\s?)(?:O=("(?:[^"]|"")+"|[^,]+))""") val ORGANIZATION = Regex("""(?:^|,\s?)(?:O=("(?:[^"]|"")+"|[^,]+))""")
......
...@@ -19,8 +19,9 @@ ...@@ -19,8 +19,9 @@
package de.kuschku.libquassel.util.nio package de.kuschku.libquassel.util.nio
import de.kuschku.libquassel.connection.HostnameVerifier import de.kuschku.libquassel.connection.QuasselSecurityException
import de.kuschku.libquassel.connection.SocketAddress import de.kuschku.libquassel.connection.SocketAddress
import de.kuschku.libquassel.ssl.X509Helper
import de.kuschku.libquassel.util.compatibility.CompatibilityUtils import de.kuschku.libquassel.util.compatibility.CompatibilityUtils
import de.kuschku.libquassel.util.compatibility.LoggingHandler.Companion.log import de.kuschku.libquassel.util.compatibility.LoggingHandler.Companion.log
import de.kuschku.libquassel.util.compatibility.LoggingHandler.LogLevel import de.kuschku.libquassel.util.compatibility.LoggingHandler.LogLevel
...@@ -35,12 +36,8 @@ import java.nio.channels.ReadableByteChannel ...@@ -35,12 +36,8 @@ import java.nio.channels.ReadableByteChannel
import java.nio.channels.WritableByteChannel import java.nio.channels.WritableByteChannel
import java.security.GeneralSecurityException import java.security.GeneralSecurityException
import java.security.NoSuchAlgorithmException import java.security.NoSuchAlgorithmException
import java.security.cert.X509Certificate
import java.util.zip.InflaterInputStream import java.util.zip.InflaterInputStream
import javax.net.ssl.SSLContext import javax.net.ssl.*
import javax.net.ssl.SSLSocket
import javax.net.ssl.SSLSocketFactory
import javax.net.ssl.X509TrustManager
class WrappedChannel private constructor( class WrappedChannel private constructor(
private val socket: Socket, private val socket: Socket,
...@@ -91,8 +88,10 @@ class WrappedChannel private constructor( ...@@ -91,8 +88,10 @@ class WrappedChannel private constructor(
} }
@Throws(GeneralSecurityException::class, IOException::class) @Throws(GeneralSecurityException::class, IOException::class)
fun withSSL(certificateManager: X509TrustManager, hostnameVerifier: HostnameVerifier, fun withSSL(certificateManager: X509TrustManager,
address: SocketAddress): WrappedChannel { hostnameVerifier: HostnameVerifier,
address: SocketAddress
): WrappedChannel {
val tlsVersion = try { val tlsVersion = try {
selectBestTlsVersion(SSLContext.getDefault().defaultSSLParameters.protocols) selectBestTlsVersion(SSLContext.getDefault().defaultSSLParameters.protocols)
} catch (e: NoSuchAlgorithmException) { } catch (e: NoSuchAlgorithmException) {
...@@ -111,10 +110,13 @@ class WrappedChannel private constructor( ...@@ -111,10 +110,13 @@ class WrappedChannel private constructor(
val socket = factory.createSocket(socket, address.host, address.port, true) as SSLSocket val socket = factory.createSocket(socket, address.host, address.port, true) as SSLSocket
socket.useClientMode = true socket.useClientMode = true
socket.addHandshakeCompletedListener { socket.addHandshakeCompletedListener {
hostnameVerifier.checkValid( if (socket.session.peerCertificateChain.isEmpty()) {
address, throw QuasselSecurityException.NoCertificate(address)
socket.session.peerCertificates.map { it as X509Certificate }.toTypedArray() }
) if (!hostnameVerifier.verify(address.host, socket.session)) {
throw QuasselSecurityException.WrongHostname(X509Helper.convert(socket.session.peerCertificateChain),
address)
}
} }
socket.startHandshake() socket.startHandshake()
return ofSocket(socket) return ofSocket(socket)
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment