MQTT研究之EMQ:【SSL證書鏈驗證】

1. 建立證書鏈(shell腳本)html

客戶端證書鏈關係:java

rootCA-->chainca1-->chainca2-->chainca3
ca       caCert1     caCert2     caCert 
#!/bin/bash

set -e
dir=`pwd`
root_key=$dir/rootCA.key
root_crt=$dir/rootCA.crt
echo "rootKey: $root_key, rootCrt: $root_crt"

key_bits=2048
expire_days=3650
subj1=/C="CN"/ST="Hubei"/L="Wuhan"/O="Taikang"/OU="TKCloud"/CN="CAC1"
subj2=/C="CN"/ST="Hubei"/L="Wuhan"/O="Taikang"/OU="TKCloud"/CN="CAC2"
subj3=/C="CN"/ST="Hubei"/L="Wuhan"/O="Taikang"/OU="TKCloud"/CN="CAC3"
server="chainca"
param=$server
if [ -d $param ]; then
    rm -r $param
fi
mkdir -p $param
cd $param

ca1key_name=$param1.key
ca1csr_name=$param1.csr
ca1crt_name=$param1.crt
ca2key_name=$param2.key
ca2csr_name=$param2.csr
ca2crt_name=$param2.crt
ca3key_name=$param3.key
ca3csr_name=$param3.csr
ca3crt_name=$param3.crt

#cd -
#SUB1 CA
openssl genrsa -out $ca1key_name $key_bits
openssl req -new -key $ca1key_name -sha256 -out $ca1csr_name -subj $subj1 -days $expire_days
openssl ca -batch -in $ca1csr_name -out $ca1crt_name -cert $root_crt -keyfile $root_key
echo "===================Gen SUB1 CA OK===================="

#SUB2 CA
openssl genrsa -out $ca2key_name $key_bits
openssl req -new -key $ca2key_name -sha256 -out $ca2csr_name -subj $subj2 -days $expire_days
openssl ca -batch -in $ca2csr_name -out $ca2crt_name -cert $ca1crt_name -keyfile $ca1key_name
echo "===================Gen SUB2 CA OK===================="

#SUB3 CA
openssl genrsa -out $ca3key_name $key_bits
openssl req -new -key $ca3key_name -sha256 -out $ca3csr_name -subj $subj3 -days $expire_days
openssl ca -batch -in $ca3csr_name -out $ca3crt_name -cert $ca2crt_name -keyfile $ca2key_name
echo "===================Gen SUB3 CA OK===================="
rm -f *.csr

cat $root_crt $ca1crt_name $ca2crt_name |tee $param.pem
echo "===================Gen All OK===================="

 

2. emqttd配置/etc/emqttd/emq.confshell

EMQ服務端的配置,SSL模式,參考器的前一篇博客 MQTT研究之EMQ:【SSL雙向驗證】數據庫

CA和客戶端CA同樣,rootCA,證書server.crt, server.keyapache

## Path to the file containing the user's private PEM-encoded key.
##
## See: http://erlang.org/doc/man/ssl.html
##
## Value: File
#listener.ssl.external.keyfile = /etc/emqttd/certs/key.pem
listener.ssl.external.keyfile = /opt/certs/server.key

## Path to a file containing the user certificate.
##
## See: http://erlang.org/doc/man/ssl.html
##
## Value: File
#listener.ssl.external.certfile = /etc/emqttd/certs/cert.pem
listener.ssl.external.certfile = /opt/certs/server.crt

## Path to the file containing PEM-encoded CA certificates. The CA certificates
## are used during server authentication and when building the client certificate chain.
##
## Value: File
## listener.ssl.external.cacertfile = /etc/emqttd/certs/cacert.pem
listener.ssl.external.cacertfile = /opt/certs/rootCA.crt

## The Ephemeral Diffie-Helman key exchange is a very effective way of
## ensuring Forward Secrecy by exchanging a set of keys that never hit
## the wire. Since the DH key is effectively signed by the private key,
## it needs to be at least as strong as the private key. In addition,
## the default DH groups that most of the OpenSSL installations have
## are only a handful (since they are distributed with the OpenSSL
## package that has been built for the operating system it’s running on)
## and hence predictable (not to mention, 1024 bits only).
## In order to escape this situation, first we need to generate a fresh,
## strong DH group, store it in a file and then use the option above,
## to force our SSL application to use the new DH group. Fortunately,
## OpenSSL provides us with a tool to do that. Simply run:
## openssl dhparam -out dh-params.pem 2048
##
## Value: File
## listener.ssl.external.dhfile = /etc/emqttd/certs/dh-params.pem

## A server only does x509-path validation in mode verify_peer,
## as it then sends a certificate request to the client (this
## message is not sent if the verify option is verify_none).
## You can then also want to specify option fail_if_no_peer_cert.
## More information at: http://erlang.org/doc/man/ssl.html
##
## Value: verify_peer | verify_none
listener.ssl.external.verify = verify_peer

## Used together with {verify, verify_peer} by an SSL server. If set to true,
## the server fails if the client does not have a certificate to send, that is,
## sends an empty certificate.
##
## Value: true | false listener.ssl.external.fail_if_no_peer_cert = true

 

3. 基於paho的java客戶端(demo代碼)api

import com.taikang.iot.re.demo.PushCallback;
import org.eclipse.paho.client.mqttv3.MqttClient;
import org.eclipse.paho.client.mqttv3.MqttConnectOptions;
import org.eclipse.paho.client.mqttv3.MqttException;
import org.eclipse.paho.client.mqttv3.MqttTopic;
import org.eclipse.paho.client.mqttv3.persist.MemoryPersistence;

import javax.net.ssl.SSLSocketFactory;
import java.util.concurrent.ScheduledExecutorService;

public class SSLChainConsumer {
    public static final String HOST = "ssl://10.95.197.3:8883";
    public static final String TOPIC1 = "taikang/rulee";
    private static final String clientid = "client11";
    private MqttClient client;
    private MqttConnectOptions options;
    private String userName = "water";    //非必須
    private String passWord = "water";  //非必須
    @SuppressWarnings("unused")
    private ScheduledExecutorService scheduler;
    private String sslPemPath = "E:\\HOWTO\\emqtt-ssl\\self1\\";

    private void start() {
        try {
            // host爲主機名,clientid即鏈接MQTT的客戶端ID,通常以惟一標識符表示,MemoryPersistence設置clientid的保存形式,默認爲之內存保存
            client = new MqttClient(HOST, clientid, new MemoryPersistence());
            // MQTT的鏈接設置
            options = new MqttConnectOptions();
            //-----------security begin--------------
            SSLSocketFactory factory = SSLUtil.getSSLSocketFactory(sslPemPath + "rootCA.crt",sslPemPath +"chainca3.crt",sslPemPath + "chainca3.key","shihucx");
            options.setSocketFactory(factory);
            //-----------end of security ------------
            // 設置是否清空session,這裏若是設置爲false表示服務器會保留客戶端的鏈接記錄,設置爲true表示每次鏈接到服務器都以新的身份鏈接
            options.setCleanSession(false);
            // 設置鏈接的用戶名
            options.setUserName(userName);
            // 設置鏈接的密碼
            options.setPassword(passWord.toCharArray());
            // 設置超時時間 單位爲秒
            options.setConnectionTimeout(10);
            // 設置會話心跳時間 單位爲秒 服務器會每隔1.5*20秒的時間向客戶端發送個消息判斷客戶端是否在線,但這個方法並無重連的機制
            options.setKeepAliveInterval(20);
            // 設置重連機制
            options.setAutomaticReconnect(true);
            // 設置回調
            client.setCallback(new PushCallback());
            MqttTopic topic = client.getTopic(TOPIC1);
            //setWill方法,若是項目中須要知道客戶端是否掉線能夠調用該方法。設置最終端口的通知消息
            //options.setWill(topic, "close".getBytes(), 2, true);//遺囑
            client.connect(options);
            //訂閱消息
            int[] Qos  = {1};
            String[] topic1 = {TOPIC1};
            client.subscribe(topic1, Qos);

        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws MqttException {
        //System.setProperty("javax.net.debug", "ssl,handshake");
        SSLChainConsumer client = new SSLChainConsumer();
        client.start();
    }
}
package com.taikang.iot.re.security;

import org.apache.log4j.Logger;
import org.bouncycastle.asn1.pkcs.RSAPrivateKey;
import org.bouncycastle.util.io.pem.PemObject;
import org.bouncycastle.util.io.pem.PemReader;

import javax.net.ssl.*;
import java.io.*;
import java.security.*;
import java.security.cert.Certificate;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.RSAPrivateKeySpec;

/**
 * @Author: chengsh05
 * @Date: 2019/3/1 17:51
 */
public class SSLUtil {

    private static Logger logger = Logger.getLogger(OpensslHelper.class);

    /**
     * 利用開源的工具類解析openssl私鑰,openssl私鑰文件格式爲pem,須要去除頁眉頁腳後才能被java讀取
     *
     * @param file
     * @return
     */
    public static PrivateKey getPrivateKey(File file) {
        if (file == null) {
            return null;
        }
        PrivateKey privKey = null;
        PemReader pemReader = null;
        try {
            pemReader = new PemReader(new FileReader(file));
            PemObject pemObject = pemReader.readPemObject();
            byte[] pemContent = pemObject.getContent();
            //支持從PKCS#1或PKCS#8 格式的私鑰文件中提取私鑰
            if (pemObject.getType().endsWith("RSA PRIVATE KEY")) {
                /*
                 * 取得私鑰  for PKCS#1
                 * openssl genrsa 默認生成的私鑰就是PKCS1的編碼
                 */
                RSAPrivateKey asn1PrivKey = RSAPrivateKey.getInstance(pemContent);
                RSAPrivateKeySpec rsaPrivKeySpec = new RSAPrivateKeySpec(asn1PrivKey.getModulus(), asn1PrivKey.getPrivateExponent());
                KeyFactory keyFactory= KeyFactory.getInstance("rsa");
                privKey= keyFactory.generatePrivate(rsaPrivKeySpec);
            } else if (pemObject.getType().endsWith("PRIVATE KEY")) {
                /*
                 * 經過openssl pkcs8 -topk8轉換爲pkcs8,例如(-nocrypt不作額外加密操做):
                 * openssl pkcs8 -topk8 -in pri.key -out pri8.key -nocrypt
                 *
                 * 取得私鑰 for PKCS#8
                 */
                PKCS8EncodedKeySpec privKeySpec = new PKCS8EncodedKeySpec(pemContent);
                KeyFactory kf = KeyFactory.getInstance("rsa");
                privKey = kf.generatePrivate(privKeySpec);
            }
        } catch (FileNotFoundException e) {
            logger.error("read private key fail,the reason is the file not exist");
            e.printStackTrace();
        } catch (IOException e) {
            logger.error("read private key fail,the reason is :"+e.getMessage());
            e.printStackTrace();
        } catch (NoSuchAlgorithmException e) {
            logger.error("read private key fail,the reason is :"+e.getMessage());
            e.printStackTrace();
        } catch (InvalidKeySpecException e) {
            logger.error("read private key fail,the reason is :"+e.getMessage());
            e.printStackTrace();
        }  finally {
            try {
                if (pemReader != null) {
                    pemReader.close();
                }
            } catch (IOException e) {
                logger.error(e.getMessage());
            }
        }
        return privKey;
    }

    /**
     * 獲取SSLContext,基於CA, Certificate, key及密碼進行SSL上下文的建立
     *
     * @param caPath
     * @param crtPath
     * @param keyPath
     * @param password
     * @return
     * @throws Exception
     */
    private static SSLContext getSSLContext(String caPath, String crtPath, String keyPath, String password) throws Exception {
        /*
         * CA證書是用來認證服務端的,這裏的CA就是一個公認的認證證書
         * TrustManagerFactory 管理的是授信的CA證書,因此KeyStore裏面存放的不須要私鑰信息,一般也不可能有
         */
        CertificateFactory cAf = CertificateFactory.getInstance("X.509");
        FileInputStream caIn = new FileInputStream(caPath);
        X509Certificate ca = (X509Certificate) cAf.generateCertificate(caIn);
        KeyStore caKs = KeyStore.getInstance("JKS");
        caKs.load(null, password.toCharArray());
        caKs.setCertificateEntry("ca1", ca); //能夠經過設置alias不一樣,配置多個ca實例,即配置多個可信的root CA。
        TrustManagerFactory tmf = TrustManagerFactory.getInstance("PKIX");
        tmf.init(caKs);
        caIn.close();

        //這個客戶端證書,是用來發送給服務端的,準備作雙向驗證用的。
        CertificateFactory cf = CertificateFactory.getInstance("X.509");
        FileInputStream crtIn = new FileInputStream(crtPath);
        X509Certificate caCert = (X509Certificate) cf.generateCertificate(crtIn);
        crtIn.close();

        //客戶端私鑰,是用來處理雙向SSL驗證中服務端用客戶端證書加密的數據的解密(解析簽名)工具
        KeyStore ks = KeyStore.getInstance("JKS");
        ks.load(null, password.toCharArray());
        ks.setCertificateEntry("certificate3", caCert);
        String sslPath = "E:\\HOWTO\\emqtt-ssl\\self1\\";
        FileInputStream crtIn1 = new FileInputStream(sslPath + "chainca1.crt");
        FileInputStream crtIn2 = new FileInputStream(sslPath + "chainca2.crt");
        X509Certificate caCert1 = (X509Certificate) cf.generateCertificate(crtIn1);
        X509Certificate caCert2 = (X509Certificate) cf.generateCertificate(crtIn2);
        crtIn1.close();
        crtIn2.close();
        ks.setCertificateEntry("certificate1", caCert1);
        ks.setCertificateEntry("certificate2", caCert2);

        PrivateKey privateKey = getPrivateKey(new File(keyPath));
        /*
         * 注意:下面這行代碼中很是重要的一點是:
         * setKeyEntry這個函數的第二個參數 password,他不是指私鑰的加密密碼,只是KeyStore對這個私鑰進行管理設置的密碼
         *
         * setKeyEntry中最後一個參數,chain的順序是證書鏈中越靠近當前privateKey節點的證書,越靠近數字下標0的位置。即chain[0]是privateKey對應的證書,
         * chain[1]是簽發chain[0]的證書,以此類推,有chain[i+1]簽發chain[i]的關係。
         */
        ks.setKeyEntry("private-key", privateKey, password.toCharArray(), new Certificate[]{caCert, caCert2, caCert1});

        /*
         * KeyManagerFactory必須是證書和私鑰配對使用,即KeyStore裏面裝載客戶端證書以及對應的私鑰,雙向SSL驗證須要。
         */
        KeyManagerFactory kmf = KeyManagerFactory.getInstance("PKIX");
        kmf.init(ks, password.toCharArray());

        /*
         * 最後建立SSL套接字工廠 SSLSocketFactory
         * 注意:這裏,SSLContext不支持TLSv2建立
         */
        SSLContext context = SSLContext.getInstance("TLSv1");
        KeyManager[] kms = kmf.getKeyManagers();
        TrustManager[] tms = tmf.getTrustManagers();
        context.init(kms, tms, new SecureRandom());
        return context;
    }

    /**
     * 基於給定的CA文件,客戶端證書文件以及客戶端私鑰文件,進行SSL上下文環境的構建, 此處建立的SSLSocketFactory是支持雙向SSL驗證的。
     *
     * NOTE: 證書及祕鑰文件,都是經過openssl建立獲取的。
     *
     * @param caPath CA證書文件
     * @param crtPath 客戶證書文件
     * @param keyPath 客戶私鑰文件
     * @param password KeyStore存儲私鑰配置的安全密碼,相似數據庫存了數據,想訪問,須要密碼同樣。
     * @return
     * @throws Exception
     */
    public static SSLSocketFactory getSSLSocketFactory(String caPath, String crtPath, String keyPath, String password) throws Exception {
        SSLContext ctx = getSSLContext(caPath, crtPath, keyPath, password);
        SSLSocketFactory factory = ctx.getSocketFactory();
        return factory;
    }
}
package com.taikang.iot.re.demo;

import org.eclipse.paho.client.mqttv3.IMqttDeliveryToken;
import org.eclipse.paho.client.mqttv3.MqttCallback;
import org.eclipse.paho.client.mqttv3.MqttMessage;

/**
 * @Author: chengsh05
 * @Date: 2019/1/15 10:02
 */
public class PushCallback implements MqttCallback {

    public void connectionLost(Throwable cause) {
        // 鏈接丟失後,通常在這裏面進行重連
        System.out.println("鏈接斷開,能夠作重連");
    }

    public void deliveryComplete(IMqttDeliveryToken token) {
        System.out.println("deliveryComplete---------" + token.isComplete());
    }

    public void messageArrived(String topic, MqttMessage message) throws Exception {
        // subscribe後獲得的消息會執行到這裏面
        System.out.println("Qos : " + message.getQos() + ", Topic :" + topic);
        System.out.println("Sub : " + new String(message.getPayload()) + "\r\n");
    }
}

 

這裏我要重點說明的是KeyStore裏面的函數setKeyEntry,下面是JDK8的api說明:數組

KeyStore的配置用來作雙向驗證的關鍵部分:
public final void setKeyEntry(String alias,
                              Key key,
                              char[] password,
                              Certificate[] chain)
                       throws KeyStoreException將給定的密鑰分配給給定的別名,並使用給定的密碼進行保護。 
若是給定的密鑰是類型java.security.PrivateKey ,它必須附有一個證書鏈,證實相應的公鑰。 

若是給定的別名已經存在,與它相關聯的密鑰庫信息將被給定的密鑰(也多是證書鏈)覆蓋。 

參數 
alias - 別名 
key - 與別名 key的關鍵 
password - 密碼保護密鑰 
chain - 相應公鑰的證書鏈(僅當給定鍵爲 java.security.PrivateKey類型 java.security.PrivateKey )。 

KeyStore中的注意事項
1. setKeyEntry這個函數的第二個參數 password,他不是指私鑰的加密密碼,只是KeyStore對這個私鑰進行管理設置的密碼
2. setKeyEntry中最後一個參數,chain的順序是證書鏈中越靠近當前privateKey節點的證書,越靠近數字下標0的位置。即chain[0]是privateKey對應的證書,
chain[1]是簽發chain[0]的證書,以此類推,有chain[i+1]簽發chain[i]的關係安全

正確的配置方式以下:
ks.setKeyEntry("private-key", privateKey, password.toCharArray(), new Certificate[]{caCert,caCert2,caCert1});bash

正確配置下,paho客戶端和emqtt服務端消息交互的wireshark的截圖以下:服務器

 

對應setKeyEntry中的第三個參數,certificate類型的數組,證書鏈的關係能夠涵蓋rootCA,也能夠不涵蓋rootCA,可是,從當前證書到根證書之間的中間證書必需要在這個證書鏈中,且順序必須正確,不然會出現:

1)證書鏈節點不全,即rootCA以前的節點有缺失,會出現服務端認證身份識別

ks.setKeyEntry("private-key", privateKey, password.toCharArray(), new Certificate[]{caCert, caCert2});

錯誤信息以下:
Warning: no suitable certificate found - continuing without client authentication
*** Certificate chain
<Empty> ssl: Ignoring alias private-key: issuers do not match ssl: KeyMgr: no matching key found


。。。。。。。。。。。。。。。。。。。。。。。。。


MqttException (
0) - javax.net.ssl.SSLHandshakeException: Received fatal alert: handshake_failure at org.eclipse.paho.client.mqttv3.internal.ExceptionHelper.createMqttException(ExceptionHelper.java:38) at org.eclipse.paho.client.mqttv3.internal.ClientComms$ConnectBG.run(ClientComms.java:715) at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511) at java.util.concurrent.FutureTask.run(FutureTask.java:266) at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.access$201(ScheduledThreadPoolExecutor.java:180) at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:293) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617) at java.lang.Thread.run(Thread.java:745) Caused by: javax.net.ssl.SSLHandshakeException: Received fatal alert: handshake_failure at sun.security.ssl.Alerts.getSSLException(Alerts.java:192) at sun.security.ssl.Alerts.getSSLException(Alerts.java:154) at sun.security.ssl.SSLSocketImpl.recvAlert(SSLSocketImpl.java:2023) at sun.security.ssl.SSLSocketImpl.readRecord(SSLSocketImpl.java:1125) at sun.security.ssl.SSLSocketImpl.performInitialHandshake(SSLSocketImpl.java:1375) at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1403) at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1387) at org.eclipse.paho.client.mqttv3.internal.SSLNetworkModule.start(SSLNetworkModule.java:108) at org.eclipse.paho.client.mqttv3.internal.ClientComms$ConnectBG.run(ClientComms.java:701) ... 7 more
 

 

 

2)證書鏈節點是全的,可是順序不對

ks.setKeyEntry("private-key", privateKey, password.toCharArray(), new Certificate[]{caCert1, caCert2, caCert});

 
MqttException (0) - javax.net.ssl.SSLHandshakeException: Received fatal alert: unknown_ca
    at org.eclipse.paho.client.mqttv3.internal.ExceptionHelper.createMqttException(ExceptionHelper.java:38)
    at org.eclipse.paho.client.mqttv3.internal.ClientComms$ConnectBG.run(ClientComms.java:715)
    at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)
    at java.util.concurrent.FutureTask.run(FutureTask.java:266)
    at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.access$201(ScheduledThreadPoolExecutor.java:180)
    at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:293)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
    at java.lang.Thread.run(Thread.java:745)
Caused by: javax.net.ssl.SSLHandshakeException: Received fatal alert: unknown_ca
    at sun.security.ssl.Alerts.getSSLException(Alerts.java:192)
    at sun.security.ssl.Alerts.getSSLException(Alerts.java:154)
    at sun.security.ssl.SSLSocketImpl.recvAlert(SSLSocketImpl.java:2023)
    at sun.security.ssl.SSLSocketImpl.readRecord(SSLSocketImpl.java:1125)
    at sun.security.ssl.SSLSocketImpl.performInitialHandshake(SSLSocketImpl.java:1375)
    at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1403)
    at sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:1387)
    at org.eclipse.paho.client.mqttv3.internal.SSLNetworkModule.start(SSLNetworkModule.java:108)
    at org.eclipse.paho.client.mqttv3.internal.ClientComms$ConnectBG.run(ClientComms.java:701)
    ... 7 more



此博文到此,有須要探討的,歡迎關注個人博客,共同進步,安全非小事,點滴積累吧
相關文章
相關標籤/搜索