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了。
Place your comment