韌館-LearnHouse

[Android]MQTT如何設置雙向認證與APP實作

MQTT的安裝和Publisher/Subscriber運作方式,我相信已經有雙向認證需求的應該都已經很清楚了。這我就不多做贅述,只講配置和Android程式實作的部分。

所謂的雙向認證就是Clinet端會需要驗證Server的憑證,而Server需要驗證Client是否是使用他允許的憑證。這時就需要建立三張憑證來達到這個效果。CA憑證,Server端憑證和Client憑證。Mosquitto會配置CA憑證和Server端憑證,而Publisher/Subscriber會配置Client憑證,如下圖。

首先就是先產生上述所說的憑證,使用openssl指令

# 生成CA私鑰與自簽名證書
openssl genpkey -algorithm RSA -out ca.key -pkeyopt rsa_keygen_bits:2048
openssl req -new -x509 -key ca.key -out ca.crt -days 25550

# 生成伺服器的私鑰與服務器證書簽名請求
openssl genpkey -algorithm RSA -out server.key -pkeyopt rsa_keygen_bits:2048
openssl req -new -key server.key -out server.csr

# 生成客戶端的私鑰與客戶端證書簽名請求
openssl genpkey -algorithm RSA -out client.key -pkeyopt rsa_keygen_bits:2048
openssl req -new -key client.key -out client.csr

# 使用CA證書簽署伺服器證書和客戶端證書(-days參數是憑證效期天數,可自行修改)
openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out server.crt -days 25550
openssl x509 -req -in client.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out client.crt -days 25550

上述過程比較需注意的是生成伺服務器證書簽名請求server.csr時所需輸入的內容,其中Common Name需要填入你伺服器的完整地domain name,至於CA.crt和client.csr則隨便輸入沒關係,但三張不能有重複的FQDN,以下是我輸入內容供參考:

You are about to be asked to enter information that will be incorporated
into your certificate request.
What you are about to enter is what is called a Distinguished Name or a DN.
There are quite a few fields but you can leave some blank
For some fields there will be a default value,
If you enter '.', the field will be left blank.
-----
Country Name (2 letter code) [AU]:TW
State or Province Name (full name) [Some-State]:Taiwan
Locality Name (eg, city) []:Hsinchu
Organization Name (eg, company) [Internet Widgits Pty Ltd]:LearnHouse
Organizational Unit Name (eg, section) []:SW
Common Name (e.g. server FQDN or YOUR name) []:learn-house.idv.tw
Email Address []:mr.yuchin@gmail.com

Please enter the following 'extra' attributes
to be sent with your certificate request
A challenge password []:
An optional company name []:

Broker Server端配置/etc/mosquitto/mosquitto.conf
將上述產生的金鑰和憑證放入相對應的位置

# MQTT-over-SSL port 8883
listener 8883

# Certificate配置
certfile /etc/mosquitto/certs/server.crt
keyfile /etc/mosquitto/certs/server.key
cafile /etc/mosquitto/ca_certificates/ca.crt

# 開啟雙向驗證
require_certificate true
use_identity_as_username true

Android APP的實作方面,
首先在app的build.grade加入bouncycastle

dependencies {
...
    implementation 'org.eclipse.paho:org.eclipse.paho.client.mqttv3:1.2.5'
    implementation 'org.eclipse.paho:org.eclipse.paho.android.service:1.1.1'
    implementation 'org.bouncycastle:bcprov-jdk15on:1.70'
    implementation 'org.bouncycastle:bcpkix-jdk15on:1.70'
}

在原始的options加入TLS/SSL支援

MqttConnectOptions options = new MqttConnectOptions();
...
SSLSocketFactory socketFactory = getSSLSocketFactory(R.raw.ca, R.raw.client_crt, R.raw.client_key, "");
options.setSocketFactory(socketFactory);

由於執行時都會出現:
MqttException (0) - javax.net.ssl.SSLHandshakeException: No subjectAltNames on the certificate match
正常來說,出現這樣的錯誤訊息主要是你連接的主機名不匹配,但我很確定我的server那張憑證的Common Name是learn-house.idv.tw。猜測應該是自簽憑證的關係,因此加入CustomHostnameVerifier來忽略主機名驗證和TrustManager來接受所有服務器證書。

// 自定義的 HostnameVerifier,忽略主機名驗證
private class CustomHostnameVerifier implements HostnameVerifier {
    @Override
    public boolean verify(String hostname, SSLSession session) {
        return true;
    }
}

private SSLSocketFactory getSSLSocketFactory(int caResId, int clientCertResId, int clientKeyResId, String clientKeyPassword) throws Exception {
    // 加載 CA 證書
    CertificateFactory cf = CertificateFactory.getInstance("X.509");
    ByteArrayInputStream caInput = new ByteArrayInputStream(readFromResource(caResId));
    Certificate ca = cf.generateCertificate(caInput);
    caInput.close();

    // 創建金鑰庫並導入 CA 證書
    KeyStore caKs = KeyStore.getInstance(KeyStore.getDefaultType());
    caKs.load(null, null);
    caKs.setCertificateEntry("ca-certificate", ca);

    // 創建自定義的 TrustManager
    TrustManager[] trustManagers = new TrustManager[] {
            new X509TrustManager() {
                @Override
                public void checkClientTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException {
                    // 不做任何檢查,意味著信任所有客戶端證書
                }

                @Override
                public X509Certificate[] getAcceptedIssuers() {
                    return null; // 通常返回 null 或一個空陣列表示不信任任何證書發行機構
                }

                @Override
                public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
                    // 不做任何檢查,意味著信任所有伺服器證書
                }
            }
    };

    // 加載客戶端證書和私鑰
    KeyStore clientKs = KeyStore.getInstance(KeyStore.getDefaultType());
    clientKs.load(null, null);

    // 讀取客戶端證書
    ByteArrayInputStream clientCertInput = new ByteArrayInputStream(readFromResource(clientCertResId));
    Certificate clientCert = cf.generateCertificate(clientCertInput);
    clientCertInput.close();

    // 讀取並解析私鑰
    byte[] clientKeyBytes = readFromResource(clientKeyResId);
    PrivateKey privateKey = getPrivateKey(clientKeyBytes, "RSA");

    // 將客戶端證書和私鑰存入金鑰庫
    clientKs.setCertificateEntry("client-cert", clientCert);
    clientKs.setKeyEntry("client-key", privateKey, clientKeyPassword.toCharArray(), new Certificate[]{clientCert});

    // 創建 KeyManager
    KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
    kmf.init(clientKs, clientKeyPassword.toCharArray());

    // 創建 SSLContext
    SSLContext sslContext = SSLContext.getInstance("TLS");
    sslContext.init(kmf.getKeyManagers(), trustManagers, null);

    return sslContext.getSocketFactory();
}

options修改如下:

MqttConnectOptions options = new MqttConnectOptions();
...
SSLSocketFactory socketFactory = getSSLSocketFactory(R.raw.ca, R.raw.client_crt, R.raw.client_key, "");
options.setSocketFactory(socketFactory);
options.setSSLHostnameVerifier(new CustomHostnameVerifier());

這樣APP就可以連到Broker Server並接收subscribe的topic了。

2024年12 月 posted by admin in 程式&軟體 and have No Comments

Place your comment

Please fill your data and comment below.
名稱:
信箱:
網站:
您的評論: