Laborator 9: Implementarea mTLS în aplicația mobilă

În cadrul acestui laborator vom implementa Mutual TLS (mTLS) pentru aplicația Android, asigurând autentificarea bidirecțională între clientul mobil și brokerul MQTT.

Ce este mTLS?

Mutual TLS (mTLS) este un mecanism de autentificare bidirecțională în care:

  • Serverul se autentifică față de client (TLS standard)
  • Clientul se autentifică față de server (autentificare mutuală)

Acest lucru asigură că:

  • Datele sunt criptate în tranzit
  • Doar dispozitivele autorizate se pot conecta la broker
  • Atacurile de tip man-in-the-middle sunt prevenite

Diferența față de TLS simplu

Aspect TLS simplu mTLS
Autentificare server DA DA
Autentificare client NU DA
Certificat client necesar NU DA
Protecție împotriva clienților neautorizați NU DA

Fișiere de modificat

Fișier Scop Modificări necesare
broker/mosquitto.conf Configurare broker Activarea mTLS pe portul 8883
MqttHandler.kt Client MQTT Android Configurarea SSLSocketFactory cu certificate client
SetupActivity.kt Ecran de setup Adăugarea opțiunii de selectare a protocolului (TCP/SSL)
MqttConstants.kt Constante Adăugarea constantelor pentru TLS
app/src/main/res/raw/ Resurse Stocarea certificatelor CA și client

Cerințe preliminare

  • OpenSSL instalat (sudo apt install openssl pe Linux)
  • Un broker MQTT Mosquitto funcțional
  • Proiectul Android funcțional cu conexiune MQTT simplă (laboratoarele anterioare)

Pasul 1: Generarea certificatelor

1.1 Crearea Autorității de Certificare (CA)

Într-un terminal:

mkdir -p certs && cd certs
 
# Generarea cheii private a CA (4096 biți)
openssl genrsa -out ca.key 4096
 
# Generarea certificatului CA (auto-semnat, valabil 10 ani)
openssl req -new -x509 -days 3650 -key ca.key -out ca.crt \
    -subj "/C=RO/ST=Romania/L=Bucharest/O=SSProject/OU=Security/CN=SSProject-CA"

1.2 Crearea certificatului de server (pentru brokerul Mosquitto)

Înlocuiți ADRESA_IP_BROKER cu IP-ul real al mașinii pe care rulează Mosquitto:

Într-un terminal:

# Generarea cheii private a serverului
openssl genrsa -out server.key 2048
 
# Generarea cererii de semnare (CSR) cu Subject Alternative Name
openssl req -new -key server.key -out server.csr \
    -subj "/C=RO/ST=Romania/L=Bucharest/O=SSProject/OU=Broker/CN=ADRESA_IP_BROKER" \
    -addext "subjectAltName=IP:ADRESA_IP_BROKER"
 
# Semnarea certificatului de server cu CA (valabil 1 an)
openssl x509 -req -days 365 -in server.csr -CA ca.crt -CAkey ca.key \
    -CAcreateserial -out server.crt -sha256 \
    -copy_extensions copyall
 
# Curățare CSR
rm server.csr

1.3 Crearea certificatului de client (pentru aplicația Android)

Într-un terminal:

# Generarea cheii private a clientului
openssl genrsa -out android.key 2048
 
# Generarea cererii de semnare a certificatului de client (CSR)
openssl req -new -key android.key -out android.csr \
    -subj "/C=RO/ST=Romania/L=Bucharest/O=SSProject/OU=MobileClient/CN=android"
 
# Semnarea certificatului de client cu CA (valabil 1 an)
openssl x509 -req -days 365 -in android.csr -CA ca.crt -CAkey ca.key \
    -CAcreateserial -out android.crt -sha256
 
# Curățare CSR
rm android.csr

1.4 Crearea fișierului BKS (BouncyCastle KeyStore) pentru Android

Android nu suportă nativ fișiere PEM. Trebuie să convertim certificatele într-un format compatibil. Vom crea două keystore-uri:

  • truststore.bks — conține certificatul CA (pentru a verifica identitatea serverului)
  • keystore.bks — conține certificatul și cheia clientului (pentru a ne autentifica față de server)

Într-un terminal:

# Crearea unui fișier PKCS12 cu certificatul și cheia clientului
openssl pkcs12 -export -in android.crt -inkey android.key \
    -out android.p12 -name "android-client" \
    -password pass:changeit
 
# Descărcarea BouncyCastle Provider (necesar pentru format BKS)
wget https://repo1.maven.org/maven2/org/bouncycastle/bcprov-jdk18on/1.80/bcprov-jdk18on-1.80.jar
 
# Crearea truststore-ului BKS (conține CA)
keytool -importcert -v -trustcacerts \
    -file ca.crt -alias ca \
    -keystore truststore.bks \
    -storetype BKS \
    -providerclass org.bouncycastle.jce.provider.BouncyCastleProvider \
    -providerpath ./bcprov-jdk18on-1.80.jar \
    -storepass changeit -noprompt
 
# Crearea keystore-ului BKS (conține certificatul client)
keytool -importkeystore \
    -srckeystore android.p12 -srcstoretype PKCS12 -srcstorepass changeit \
    -destkeystore keystore.bks -deststoretype BKS -deststorepass changeit \
    -providerclass org.bouncycastle.jce.provider.BouncyCastleProvider \
    -providerpath ./bcprov-jdk18on-1.80.jar

1.5 Verificarea certificatelor

Într-un terminal:

# Verificarea certificatului CA
openssl x509 -in ca.crt -text -noout | head -15
 
# Verificarea certificatului de server
openssl verify -CAfile ca.crt server.crt
 
# Verificarea certificatului de client
openssl verify -CAfile ca.crt android.crt
 
# Verificarea conținutului truststore-ului BKS
keytool -list -keystore truststore.bks -storetype BKS \
    -providerclass org.bouncycastle.jce.provider.BouncyCastleProvider \
    -providerpath ./bcprov-jdk18on-1.80.jar \
    -storepass changeit

1.6 Lista fișierelor generate

După generare, directorul certs/ ar trebui să conțină:

certs/
├── ca.crt          # Certificatul CA
├── ca.key          # Cheia privată CA (NU se distribuie!)
├── server.crt      # Certificatul serverului
├── server.key      # Cheia privată a serverului
├── android.crt     # Certificatul clientului Android
├── android.key     # Cheia privată a clientului Android
├── android.p12     # Fișier PKCS12 intermediar
├── truststore.bks  # Truststore BKS → va fi copiat în proiectul Android
└── keystore.bks    # Keystore BKS → va fi copiat în proiectul Android

Pasul 2: Configurarea brokerului Mosquitto cu mTLS

Creați sau editați fișierul mosquitto_mtls.conf:

mosquitto_mtls.conf
# MQTT securizat cu mTLS
listener 8883
cafile ./certs/ca.crt
certfile ./certs/server.crt
keyfile ./certs/server.key
require_certificate true
use_subject_as_username true
allow_anonymous true
 
# Logare
log_type all
connection_messages true
log_timestamp true

Porniți brokerul securizat:

Într-un terminal:

sudo systemctl stop mosquitto.service
mosquitto -c mosquitto_mtls.conf -v

Setări cheie:

  • cafile — certificatul CA pentru verificarea certificatelor clienților
  • certfile/keyfile — certificatul propriu al serverului și cheia privată
  • require_certificate trueactivează mTLS: clienții trebuie să prezinte un certificat valid
  • use_subject_as_username true — folosește CN-ul certificatului ca nume de utilizator MQTT

Pasul 3: Copierea certificatelor în proiectul Android

Copiați fișierele truststore.bks și keystore.bks în directorul de resurse raw al aplicației:

Într-un terminal:

mkdir -p app/src/main/res/raw
cp certs/truststore.bks app/src/main/res/raw/truststore.bks
cp certs/keystore.bks app/src/main/res/raw/keystore.bks

Directorul res/raw/ este accesibil din cod prin R.raw.truststore și R.raw.keystore.

Fișierele BKS sunt incluse în APK, deci nu trebuie să le distribuiți separat.

Pasul 4: Actualizarea constantelor MQTT

Adăugați constantele necesare pentru TLS în MqttConstants.kt:

MqttConstants.kt
package ro.pub.cs.systems.ssproject.mqtt
 
object MqttConstants {
    const val TAG = "MqttHandler"
    const val TOPIC_IMAGES = "ssproject/images"
    const val TOPIC_COMMANDS = "ssproject/commands"
 
    const val CMD_CAPTURE = "CAPTURE"
    const val CMD_START_LIVE = "START-LIVE"
    const val CMD_STOP_LIVE = "STOP-LIVE"
 
    // TLS
    const val BKS_STORE_TYPE = "BKS"
    const val TLS_PROTOCOL = "TLSv1.2"
    const val KEYSTORE_PASSWORD = "changeit"
    const val TRUSTSTORE_PASSWORD = "changeit"
    const val DEFAULT_TLS_PORT = "8883"
}

Pasul 5: Crearea clasei ''TlsHelper''

Creați o clasă nouă care gestionează crearea contextului SSL:

TlsHelper.kt
package ro.pub.cs.systems.ssproject.mqtt
 
import android.content.Context
import java.security.KeyStore
import javax.net.ssl.KeyManagerFactory
import javax.net.ssl.SSLContext
import javax.net.ssl.SSLSocketFactory
import javax.net.ssl.TrustManagerFactory
 
object TlsHelper {
 
    /**
     * Creează un SSLSocketFactory configurat pentru mTLS.
     *
     * @param context Contextul Android (necesar pentru accesarea resurselor raw)
     * @param trustStoreResId ID-ul resursei raw pentru truststore (R.raw.truststore)
     * @param keyStoreResId ID-ul resursei raw pentru keystore (R.raw.keystore)
     * @return SSLSocketFactory configurat cu certificatele client și CA
     */
    fun createMtlsSocketFactory(
        context: Context,
        trustStoreResId: Int,
        keyStoreResId: Int
    ): SSLSocketFactory {
        // 1. Încărcarea TrustStore-ului (conține certificatul CA)
        val trustStore = KeyStore.getInstance(MqttConstants.BKS_STORE_TYPE)
        context.resources.openRawResource(trustStoreResId).use { input ->
            trustStore.load(input, MqttConstants.TRUSTSTORE_PASSWORD.toCharArray())
        }
 
        val trustManagerFactory = TrustManagerFactory.getInstance(
            TrustManagerFactory.getDefaultAlgorithm()
        )
        trustManagerFactory.init(trustStore)
 
        // 2. Încărcarea KeyStore-ului (conține certificatul și cheia clientului)
        val keyStore = KeyStore.getInstance(MqttConstants.BKS_STORE_TYPE)
        context.resources.openRawResource(keyStoreResId).use { input ->
            keyStore.load(input, MqttConstants.KEYSTORE_PASSWORD.toCharArray())
        }
 
        val keyManagerFactory = KeyManagerFactory.getInstance(
            KeyManagerFactory.getDefaultAlgorithm()
        )
        keyManagerFactory.init(keyStore, MqttConstants.KEYSTORE_PASSWORD.toCharArray())
 
        // 3. Crearea contextului SSL cu ambele componente (mTLS)
        val sslContext = SSLContext.getInstance(MqttConstants.TLS_PROTOCOL)
        sslContext.init(
            keyManagerFactory.keyManagers,   // Autentificarea clientului
            trustManagerFactory.trustManagers, // Verificarea serverului
            null
        )
 
        return sslContext.socketFactory
    }
}

Explicarea codului:

  1. TrustStore — conține certificatul CA. Este folosit pentru a verifica identitatea serverului (echivalentul ca_certs din Python).
  2. KeyStore — conține certificatul și cheia privată a clientului. Este folosit pentru autentificarea clientului față de server (echivalentul certfile + keyfile din Python).
  3. SSLContext — combină cele două componente într-un context SSL complet pentru mTLS.

Pasul 6: Actualizarea ''MqttHandler'' pentru suportul TLS

Modificați constructorul și metoda connect() din MqttHandler.kt pentru a suporta atât conexiuni simple (TCP), cât și conexiuni securizate (SSL/TLS):

MqttHandler.kt
package ro.pub.cs.systems.ssproject.mqtt
 
import android.os.Build
import android.util.Log
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken
import org.eclipse.paho.client.mqttv3.MqttCallback
import org.eclipse.paho.client.mqttv3.MqttClient
import org.eclipse.paho.client.mqttv3.MqttConnectOptions
import org.eclipse.paho.client.mqttv3.MqttMessage
import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence
import javax.net.ssl.SSLSocketFactory
 
class MqttHandler(
    brokerIp: String,
    brokerPort: String,
    private val sslSocketFactory: SSLSocketFactory? = null,
    private val isConnectedCallback: (Boolean) -> Unit,
    private val onCommandReceived: (String) -> Unit
) : MqttCallback {
    // Determinăm protocolul pe baza prezenței SSLSocketFactory
    private val protocol = if (sslSocketFactory != null) "ssl" else "tcp"
    private val brokerUrl = "$protocol://$brokerIp:$brokerPort"
    private val clientId = "${Build.MANUFACTURER}_${Build.MODEL}_${MqttClient.generateClientId()}"
    private var client: MqttClient? = null
 
    suspend fun connect() {
        withContext(Dispatchers.IO) {
            if (client?.isConnected == true) {
                return@withContext
            }
 
            try {
                client = MqttClient(brokerUrl, clientId, MemoryPersistence())
                client?.setCallback(this@MqttHandler)
 
                val options = MqttConnectOptions().apply {
                    isCleanSession = true
                    connectionTimeout = 10
                    keepAliveInterval = 60
 
                    // Configurarea TLS dacă SSLSocketFactory este disponibil
                    if (sslSocketFactory != null) {
                        socketFactory = sslSocketFactory
                    }
                }
 
                client?.connect(options)
                Log.i(MqttConstants.TAG, "Connected to $brokerUrl")
 
                client?.subscribe(MqttConstants.TOPIC_COMMANDS, 1)
                Log.i(MqttConstants.TAG, "Subscribed to ${MqttConstants.TOPIC_COMMANDS}")
            } catch (e: Exception) {
                Log.e(MqttConstants.TAG, "Connection error: ${e.message}")
                e.printStackTrace()
            } finally {
                isConnectedCallback(isConnected())
            }
        }
    }
 
    // ... restul metodelor rămân neschimbate (disconnect, isConnected,
    //    publishImage, connectionLost, messageArrived, deliveryComplete)
}

Modificări cheie:

  • Constructorul primește opțional un SSLSocketFactory
  • Protocolul se determină automat: ssl: pentru TLS, tcp: pentru conexiuni simple
  • MqttConnectOptions.socketFactory este configurat cu SSLSocketFactory pentru handshake-ul TLS

Pasul 7: Actualizarea ''SetupActivity'' pentru selecția protocolului

Modificați ecranul de setup pentru a permite utilizatorului să aleagă între conexiune simplă și conexiune securizată:

7.1 Actualizarea layout-ului

Adăugați un switch pentru activarea TLS în activity_setup.xml, înainte de butonul de conectare:

activity_setup.xml
            <!-- ... după TextInputLayout-ul pentru port ... -->
 
            <com.google.android.material.materialswitch.MaterialSwitch
                android:id="@+id/setup_card_view_tls_switch"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginTop="8dp"
                android:layout_marginBottom="8dp"
                android:text="Conexiune securizată (mTLS)"
                android:checked="false" />
 
            <com.google.android.material.button.MaterialButton
                android:id="@+id/setup_card_view_connect_btn"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_gravity="end|center"
                android:text="@string/setup_connect_btn" />

7.2 Actualizarea ''SetupActivity.kt''

SetupActivity.kt
package ro.pub.cs.systems.ssproject.ui.setup
 
import android.content.Intent
import android.os.Bundle
import android.util.Patterns
import android.widget.Button
import android.widget.EditText
import androidx.activity.ComponentActivity
import androidx.activity.enableEdgeToEdge
import com.google.android.material.materialswitch.MaterialSwitch
import ro.pub.cs.systems.ssproject.mqtt.MqttConstants
import ro.pub.cs.systems.ssproject.ui.dashboard.MainActivity
import ro.pub.cs.systems.ssproject.R
 
class SetupActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContentView(R.layout.activity_setup)
 
        val ipField = findViewById<EditText>(R.id.setup_card_view_ip_field)
        val portField = findViewById<EditText>(R.id.setup_card_view_port_field)
        val tlsSwitch = findViewById<MaterialSwitch>(R.id.setup_card_view_tls_switch)
        val connect = findViewById<Button>(R.id.setup_card_view_connect_btn)
 
        // Actualizarea portului implicit la activarea TLS
        tlsSwitch.setOnCheckedChangeListener { _, isChecked ->
            if (isChecked && portField.text.toString() == "1883") {
                portField.setText(MqttConstants.DEFAULT_TLS_PORT)
            } else if (!isChecked && portField.text.toString() ==
                MqttConstants.DEFAULT_TLS_PORT) {
                portField.setText("1883")
            }
        }
 
        connect.setOnClickListener {
            val inputIp = ipField.text.toString().trim()
            val inputPort = portField.text.toString().trim()
 
            if (inputIp.isEmpty() || !Patterns.IP_ADDRESS.matcher(inputIp).matches()) {
                ipField.error = "Invalid IP address"
                ipField.requestFocus()
                return@setOnClickListener
            }
 
            val portNumber = inputPort.toIntOrNull()
            if (portNumber == null || portNumber !in 1..65535) {
                portField.error = "Invalid port"
                portField.requestFocus()
                return@setOnClickListener
            }
 
            val intent = Intent(this, MainActivity::class.java)
            intent.putExtra("brokerIp", inputIp)
            intent.putExtra("brokerPort", inputPort)
            intent.putExtra("useTls", tlsSwitch.isChecked)
            startActivity(intent)
        }
    }
}

7.3 Actualizarea ''MainActivity.kt''

Modificați crearea MqttHandler în MainActivity pentru a include configurația TLS:

MainActivity.kt
// În metoda onCreate(), înlocuiți blocul de creare a MqttHandler cu:
 
val useTls = intent.getBooleanExtra("useTls", false)
 
val sslSocketFactory = if (useTls) {
    TlsHelper.createMtlsSocketFactory(
        context = this,
        trustStoreResId = R.raw.truststore,
        keyStoreResId = R.raw.keystore
    )
} else {
    null
}
 
mqttHandler = MqttHandler(
    brokerIp,
    brokerPort,
    sslSocketFactory = sslSocketFactory,
    isConnectedCallback = { isConnected ->
        runOnUiThread {
            handleConnectionStateChange(isConnected)
        }
    },
    onCommandReceived = { command ->
        runOnUiThread {
            appendLog(getString(R.string.main_logs_command_received_entry, command))
            handleReceivedCommand(command)
        }
    }
)

Pasul 8: Testarea conexiunii mTLS

  • Porniți brokerul Mosquitto cu configurația mTLS:
mosquitto -c mosquitto_mtls.conf -v
  • Compilați și rulați aplicația pe dispozitivul Android / emulator.
  • În ecranul de setup, activați switch-ul Conexiune securizată (mTLS) și introduceți IP-ul brokerului. Portul se va actualiza automat la 8883.
  • Verificați log-urile brokerului pentru a confirma conexiunea mTLS:
# New connection from X.X.X.X:XXXXX on port 8883.
# New client connected from X.X.X.X:XXXXX as ... (p1, c1, k60, u'android').
  • Verificați cu Wireshark că traficul pe portul 8883 este criptat (comparativ cu portul 1883).

Depanare

''SSLHandshakeException: ...'' la conectare

  • Asigurați-vă că toate certificatele sunt semnate de același CA
  • Verificați datele de expirare:
openssl x509 -in android.crt -noout -dates
  • Verificați că fișierele BKS au fost generate corect și copiate în res/raw/

''Connection refused'' pe portul 8883

  • Verificați că brokerul Mosquitto ascultă pe portul 8883:
netstat -tlnp | grep 8883
  • Verificați setările firewall-ului pe mașina broker

''java.io.IOException: Wrong version of key store'' la încărcarea BKS

  • Asigurați-vă că folosiți versiunea corectă a BouncyCastle Provider
  • Recreați fișierele BKS cu comanda din Pasul 1.4

Emulator Android nu poate accesa broker-ul de pe host

  • Folosiți adresa 10.0.2.2 în loc de 127.0.0.1 (adresa specială a emulatorului Android pentru host-ul gazdă)

Bune practici de securitate

  • Nu comiteți niciodată certificate în git — adăugați certs/ în .gitignore
  • Nu hardcodați parolele keystore-urilor — folosiți BuildConfig sau fișiere secrets.properties adăugate în .gitignore
  • Folosiți certificate cu durată scurtă de viață — schimbați-le periodic
  • Folosiți dimensiuni mari ale cheilor — 4096 biți pentru CA, minim 2048 biți pentru server/client
  • Stocați cheile private în siguranță — cheia CA (ca.key) nu trebuie să părăsească niciodată mașina pe care a fost generată

Cerințe

  1. Urmăriți toți pașii din acest laborator pentru a implementa mTLS în aplicația Android.
  2. Demonstrați că aplicația se conectează cu succes la broker prin mTLS (screenshot cu log-urile brokerului care arată conexiunea securizată).
  3. Verificați cu Wireshark că traficul pe portul 8883 este criptat (comparativ cu portul 1883 din laboratoarele anterioare).
  4. Implementați un mecanism prin care parolele keystore-urilor să nu mai fie hardcodate în cod (de exemplu, folosind BuildConfig cu valori din local.properties).
ss/laboratoare/09.txt · Last modified: 2026/05/04 19:40 by ciprian.popescu0411
CC Attribution-Share Alike 3.0 Unported
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0