În cadrul acestui laborator vom implementa Mutual TLS (mTLS) pentru aplicația Android, asigurând autentificarea bidirecțională între clientul mobil și brokerul MQTT.
Mutual TLS (mTLS) este un mecanism de autentificare bidirecțională în care:
Acest lucru asigură că:
| 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ș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 |
sudo apt install openssl pe Linux)Î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"
Î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
Î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
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
Î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
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
Creați sau editați fișierul 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țilorcertfile/keyfile — certificatul propriu al serverului și cheia privatărequire_certificate true — activează mTLS: clienții trebuie să prezinte un certificat validuse_subject_as_username true — folosește CN-ul certificatului ca nume de utilizator MQTT
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
res/raw/ este accesibil din cod prin R.raw.truststore și R.raw.keystore.
Adăugați constantele necesare pentru TLS în 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"
}
Creați o clasă nouă care gestionează crearea contextului SSL:
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:
ca_certs din Python).certfile + keyfile din Python).
Modificați constructorul și metoda connect() din MqttHandler.kt pentru a suporta atât conexiuni simple (TCP), cât și conexiuni securizate (SSL/TLS):
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:
SSLSocketFactoryssl: pentru TLS, tcp: pentru conexiuni simpleMqttConnectOptions.socketFactory este configurat cu SSLSocketFactory pentru handshake-ul TLSModificați ecranul de setup pentru a permite utilizatorului să aleagă între conexiune simplă și conexiune securizată:
Adăugați un switch pentru activarea TLS în activity_setup.xml, înainte de butonul de conectare:
<!-- ... 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" />
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)
}
}
}
Modificați crearea MqttHandler în MainActivity pentru a include configurația TLS:
// Î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)
}
}
)
mosquitto -c mosquitto_mtls.conf -v
8883.# 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').
openssl x509 -in android.crt -noout -dates
res/raw/netstat -tlnp | grep 8883
10.0.2.2 în loc de 127.0.0.1 (adresa specială a emulatorului Android pentru host-ul gazdă)certs/ în .gitignoreBuildConfig sau fișiere secrets.properties adăugate în .gitignoreca.key) nu trebuie să părăsească niciodată mașina pe care a fost generatăBuildConfig cu valori din local.properties).