天天看點

裝置CA憑證認證

作者:程式設計識堂

背景消息

  • 裝置證書是由CA根證書簽發給用戶端裝置使用的數字證書,用于用戶端和服務端連接配接時,服務端對用戶端進行安全認證。認證通過後服務端和用戶端可基于證書内的加密密鑰進行安全通信,若認證不通過則服務端拒絕用戶端接入。
  • 使用裝置證書認證時,必須保證簽發該裝置證書的CA憑證已在MQTT服務端中注冊。
  • 用戶端裝置使用裝置證書進行接入認證時,服務端會根據已注冊的CA憑證驗證裝置證書是否正确,若CA憑證和裝置證書比對成功,則用戶端認證通過,且系統會将該裝置證書自動注冊到服務端中。

雙向SSL/TLS 安全連接配接

作為基于現代密碼學公鑰算法的安全協定,TLS/SSL 能在計算機通訊網絡上保證傳輸安全,很多MQTT Broker 内置對 TLS/SSL 的支援,包括支援單/雙向認證、X.509 證書、負載均衡 SSL 等多種安全認證。

SSL/TLS 安全優勢

  • 強認證。 用 TLS 建立連接配接的時候,通訊雙方可以互相檢查對方的身份。在實踐中,很常見的一種身份檢查方式是檢查對方持有的 X.509 數字證書。這樣的數字證書通常是由一個授信機構頒發的,不可僞造。
  • 保證機密性。TLS 通訊的每次會話都會由會話密鑰加密,會話密鑰由通訊雙方協商産生。任何第三方都無法知曉通訊内容。即使一次會話的密鑰洩露,并不影響其他會話的安全性。
  • 完整性。 加密通訊中的資料很難被篡改而不被發現。

SSL/TLS 協定

TLS/SSL 協定下的通訊過程分為兩部分,第一部分是握手協定。握手協定的目的是鑒别對方身份并建立一個安全的通訊通道。握手完成之後雙方會協商出接下來使用的密碼套件和會話密鑰;第二部分是 record 協定,record 和其他資料傳輸協定非常類似,會攜帶内容類型,版本,長度和荷載等資訊,不同的是它所攜帶的資訊是加密了的。

下面的圖檔描述了 TLS/SSL 握手協定的過程,從用戶端的 "hello" 一直到伺服器的 "finished" 完成握手。有興趣的同學可以找更詳細的資料看。

裝置CA憑證認證

SSL/TLS 證書準備

在雙向認證中,一般都使用自簽名證書的方式來生成服務端和用戶端證書,是以本文就以自簽名證書為例。

通常來說,我們需要數字證書來保證 TLS 通訊的強認證。數字證書的使用本身是一個三方協定,除了通訊雙方,還有一個頒發證書的受信第三方,有時候這個受信第三方就是一個 CA。和 CA 的通訊,一般是以預先發行證書的方式進行的。也就是在開始 TLS 通訊的時候,我們需要至少有 2 個證書,一個 CA 的,一個 MQTT服務端 的, MQTT服務端的證書由 CA 頒發,并用 CA 的證書驗證。

在這裡,我們假設您的系統已經安裝了 OpenSSL。使用 OpenSSL 附帶的工具集就可以生成我們需要的證書了。

  • 已安裝OpenSSL v1.1.1i或以上版本。

使用OpenSSL建立生成CA憑證、伺服器、用戶端證書及密鑰

  1. 生成CA憑證
  1. 生成伺服器證書
  1. 生成用戶端證書
  • 對于SSL單向認證:伺服器需要CA憑證、server證書、server私鑰,用戶端需要CA證。
  • 對于SSL雙向認證:伺服器需要CA憑證、server證書、server私鑰,用戶端需要CA憑證,client證書、client私鑰。

各類證書與密鑰檔案字尾的解釋

總得來說這些檔案都與X.509證書和密鑰檔案有關,從檔案編碼上分,隻有兩大類:

PEM格式:使用Base64 ASCII進行編碼的純文字格式

DER格式:二機制格式

而CRT, CER,KEY這幾種證書和密鑰檔案,它們都有自己的schema,在存儲為實體檔案時,既可以是PEM格式,也可以DER格式。

CER:一般用于windows的證書檔案格式

CRT:一般用于Linux的證書,包含公鑰和主體資訊

KEY:一般用于密鑰,特别是私鑰, 與證書一一配對

打個比方:CER,CRT,KEY相當于論文,說明書等,有規定好的行文格式與規範,而PEM和DER相當于txt格式還是word格式。

CSR: Certificate Signing Request,即證書簽名請求檔案。證書申請者在生成私鑰的同時也生成證書請求檔案。把CSR檔案送出給證書頒發機構後,證書頒發機構使用其根證書私鑰簽名就生成了證書公鑰檔案,也就是頒發給使用者的證書。

證書生成

1.生成CA憑證

1.建立CA憑證私鑰

openssl genrsa -out ca.key 2048           

2.請求證書 證數各參數含義如下

  • C-----國家(Country Name)
  • ST----省份(State or Province Name)
  • L----城市(Locality Name)
  • O----公司(Organization Name)
  • OU----部門(Organizational Unit Name)
  • CN----産品名(Common Name)
  • emailAddress----郵箱(Email Address)
openssl req -new -sha256 -key ca.key -out ca.csr -subj "/C=CN/ST=SZ/L=SZ/O=C.X.L/OU=C.X.L/CN=CA/[email protected]"           

3.自簽署證書

openssl x509 -req -days 36500 -sha256 -extensions v3_ca -signkey ca.key -in ca.csr -out ca.crt           

2.生成服務端證書

1.建立伺服器私鑰

openssl genrsa -out server.key 2048

建立 openssl.cnf 檔案,

  • req_distinguished_name :根據情況進行修改,
  • alt_names:BROKER_ADDRESS 修改為 EMQ X 伺服器實際的 IP 或 DNS 位址,例如:IP.1 = 127.0.0.1,或 DNS.1 = broker.xxx.com
  • 注意:IP 和 DNS 二者保留其一即可,如果已購買域名,隻需保留 DNS 并修改為你所使用的域名位址。
[req]
default_bits = 2048
distinguished_name = req_distinguished_name
req_extensions = req_ext
x509_extensions = v3_req
prompt = no
[req_distinguished_name]
countryName = CN
stateOrProvinceName = SZ
organizationName = C.X.L
organizationalUnitName = C.X.L
commonName = service
[req_ext]
subjectAltName = @alt_names
[v3_req]
subjectAltName = @alt_names
[alt_names]
IP.1 = 127.0.0.1
IP.2 = 192.168.5.249           

2.請求證書

openssl req -new -sha256 -key server.key -config openssl.cnf -out server.csr           

3.使用CA憑證簽署伺服器證書

openssl x509 -req -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out server.crt -days 3650 -sha256 -extensions v3_req -extfile openssl.cnf           

4.驗證服務端證書

openssl verify -CAfile ca.crt server.crt           

5檢視服務端證書

openssl x509 -noout -text -in server.crt           

6.Netty需要支援PKCS8格式讀取私鑰

openssl pkcs8 -topk8 -nocrypt -in server.key -out pkcs8_key.pem           
裝置CA憑證認證

注:錯誤日志也很明确的列印了:at sun.security.pkcs.PKCS8Key.decode(PKCS8Key.java:351),采用PKCS8無法解析證書。這是因為部分MQTT broker使用的是netty,netty預設使用PKCS8格式對證書進行解析,然而我們使用openssl生成的服務端server.key是PKCS1格式的,是以MQTT broker采用PKCS8無法對證書進行解析。

問題處理

對證書進行格式轉行,将PKCS1格式轉換成PKCS8即可。

證書格式差別:

  1. PKCS1的檔案頭格式 -----BEGIN RSA PRIVATE KEY-----
  1. PKCS8的檔案頭格式 -----BEGIN PRIVATE KEY-----

生成用戶端證書

1.生成用戶端私鑰

openssl genrsa -out client.key 2048           

2.請求證書

openssl req -new -sha256 -key client.key -out client.csr -subj "/C=CN/ST=SZ/L=SZ/O=C.X.L/OU=C.X.L/CN=CLIENT/[email protected]"           

3.使用CA憑證簽署用戶端證書

openssl x509 -req -days 36500 -sha256 -extensions v3_req -CA ca.cer -CAkey ca.key -CAserial ca.srl -CAcreateserial -in client.csr -out client.crt           

4.驗證服務端證書

openssl verify -CAfile ca.crt client.crt           

5.檢視服務端證書

openssl x509 -noout -text -in client.crt           

證書轉換

CRT轉為PEM

#.key 轉換成 .pem:
openssl rsa -in server.key -out server-key.pem
#.crt 轉換成 .pem:
openssl x509 -in server.crt -out server.pem -outform PEM           

既然PEM與DER隻是編碼格式上的不同,那麼不管是證書還是密鑰,都可以随意轉換為想要的格式:

PEM轉DER

openssl x509 -outform der -in server.pem -out server.der           

DER專PEM

openssl x509 -inform der -in server.der -out server.crt           

注:也可以直接生成PEM格式的證書,生成方式和CRT一樣

差別:

// 生成CA憑證
openssl req -x509 -new -nodes -key ca.key -sha256 -days 3650 -out ca.pem
// 生成服務端證書
openssl x509 -req -in server.csr -CA ca.pem -CAkey ca.key -CAcreateserial -out server.pem -days 3650 -sha256 -extensions v3_req -extfile openssl.cnf
// 生成用戶端證書
openssl x509 -req -days 3650 -in client.csr -CA ca.pem -CAkey ca.key -CAcreateserial -out client.pem           

SSL/TLS 雙向連接配接的啟用及驗證

1.EMQX

在 EMQ X 中 mqtt:ssl 的預設監聽端口為 8883。

将前文中通過 OpenSSL 工具生成的 server.crt 、server.key 及 ca.crt 檔案拷貝到 EMQ X 的 etc/certs/ 目錄下,并參考如下配置修改 emqx.conf:

## listener.ssl.$name is the IP address and port that the MQTT/SSL
## Value: IP:Port | Port
listener.ssl.external = 8883
## Path to the file containing the user's private PEM-encoded key.
## Value: File
listener.ssl.external.keyfile = etc/certs/server.key
## Path to a file containing the user certificate.
## Value: File
listener.ssl.external.certfile = etc/certs/emqx.pem
## Path to the file containing PEM-encoded CA certificates. The CA certificates
## Value: File
listener.ssl.external.cacertfile = etc/certs/ca.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).
##
## Value: verify_peer | verify_none
listener.ssl.external.verify = verify_peer           

2.MQTT 連接配接測試

參照下圖在 MQTT X 中建立 MQTT 用戶端(Host 輸入框裡的 127.0.0.1 需替換為實際的 EMQ X 伺服器 IP)

此時 Certificate 一欄需要選擇 Self signed ,并攜帶自簽名證書中生成的 ca.pem 檔案, 用戶端證書 client.pem 和用戶端密鑰 client.key 檔案。

點選 Connect 按鈕,連接配接成功後,如果能正常執行 MQTT 釋出/訂閱 操作,則 SSL 雙向連接配接認證配置成功。

裝置CA憑證認證

2.SMQTT

雙向向認證配置

smqtt:
  tcp: # MQTT配置
    ssl: # ssl配置
      enable: true # 開關
      key: C:\Users\Administrator\Desktop\fsdownload\pkcs8_key.pem # 指定ssl檔案 預設系統生成
      crt: C:\Users\Administrator\Desktop\fsdownload\server.crt # 指定ssl檔案 預設系統生成
      ca: C:\Users\Administrator\Desktop\fsdownload\ca.crt           

MQTT 連接配接測試

此時 Certificate 一欄需要選擇 Self signed ,并攜帶自簽名證書中生成的 ca.pem 檔案, 用戶端證書 client.pem 和用戶端密鑰 client.key 檔案。

點選 Connect 按鈕,連接配接成功後,如果能正常執行 MQTT 釋出/訂閱 操作,則 SSL 雙向連接配接認證配置成功。

裝置CA憑證認證

MQTT Java 用戶端庫

Eclipse Paho Java Client(opens new window)是用 Java 編寫的 MQTT 用戶端庫(MQTT Java Client),可用于 JVM 或其他 Java 相容平台(例如Android)。

Eclipse Paho Java Client 提供了MqttAsyncClient 和 MqttClient 異步和同步 API。

通過 Maven 安裝 Paho Java

<dependency>
<groupId>org.eclipse.paho</groupId>
<artifactId>org.eclipse.paho.client.mqttv3</artifactId>
<version>1.2.2</version>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk15on</artifactId>
<version>1.70</version>
</dependency>           

Paho Java 使用示例

Java 體系中 Paho Java 是比較穩定、廣泛應用的 MQTT 用戶端庫,本示例包含 Java 語言的 Paho Java 連接配接SMQTT Broker ,并進行消息收發完整代碼:

MqttConnect

import lombok.Data;
/**
* @author C.X.L
* @date 2022/10/24 0024 16:29
* @description
*/
@Data
public class MqttConnect {
/**
* 根證書路徑
*/
private String CA_CRT_PATH="C:\\Users\\Administrator\\Desktop\\fsdownload\\ca.crt";
/**
* 裝置crt證書路徑
*/
private String DEVICE_CERT_PATH="C:\\Users\\Administrator\\Desktop\\fsdownload\\client.crt";
/**
* 裝置key證書路徑
*/
private String DEVICE_PEM_PATH="C:\\Users\\Administrator\\Desktop\\fsdownload\\client.key";
/**
* mqtt代理伺服器位址
*/
private String host="ssl://127.0.0.1:1883";
/**
* 裝置id
*/
private String clientId;
private boolean cleanSession = false;
/**
* 裝置密碼
*/
private String password="smqtt";
private String userName="smqtt";
}           

SSLUtils

import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.openssl.PEMKeyPair;
import org.bouncycastle.openssl.PEMParser;
import org.bouncycastle.openssl.jcajce.JcaPEMKeyConverter;
import java.io.*;
import java.security.KeyPair;
import java.security.KeyStore;
import java.security.Security;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import javax.net.ssl.KeyManagerFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManagerFactory;
/**
* @author Charley
* @date 2022/12/05
* @description
*/
public class SSLUtils {
public static SSLSocketFactory getSingleSocketFactory(InputStream caCrtFileInputStream) throws Exception {
Security.addProvider(new BouncyCastleProvider());
X509Certificate caCert = null;
BufferedInputStream bis = new BufferedInputStream(caCrtFileInputStream);
CertificateFactory cf = CertificateFactory.getInstance("X.509");
while (bis.available() > 0) {
caCert = (X509Certificate) cf.generateCertificate(bis);
}
KeyStore caKs = KeyStore.getInstance(KeyStore.getDefaultType());
caKs.load(null, null);
caKs.setCertificateEntry("cert-certificate", caCert);
TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm());
tmf.init(caKs);
SSLContext sslContext = SSLContext.getInstance("TLSv1.2");
sslContext.init(null, tmf.getTrustManagers(), null);
return sslContext.getSocketFactory();
}
public static SSLSocketFactory getSocketFactory(final String caCrtFile,
final String crtFile, final String keyFile, final String password)
throws Exception {
Security.addProvider(new BouncyCastleProvider());
// load CA certificate
X509Certificate caCert = null;
FileInputStream fis = new FileInputStream(caCrtFile);
BufferedInputStream bis = new BufferedInputStream(fis);
CertificateFactory cf = CertificateFactory.getInstance("X.509");
while (bis.available() > 0) {
caCert = (X509Certificate) cf.generateCertificate(bis);
}
// load client certificate
bis = new BufferedInputStream(new FileInputStream(crtFile));
X509Certificate cert = null;
while (bis.available() > 0) {
cert = (X509Certificate) cf.generateCertificate(bis);
}
// load client private key
PEMParser pemParser = new PEMParser(new FileReader(keyFile));
Object object = pemParser.readObject();
JcaPEMKeyConverter converter = new JcaPEMKeyConverter().setProvider("BC");
KeyPair key = converter.getKeyPair((PEMKeyPair) object);
pemParser.close();
// CA certificate is used to authenticate server
KeyStore caKs = KeyStore.getInstance(KeyStore.getDefaultType());
caKs.load(null, null);
caKs.setCertificateEntry("ca-certificate", caCert);
TrustManagerFactory tmf = TrustManagerFactory.getInstance("X509");
tmf.init(caKs);
// client key and certificates are sent to server, so it can authenticate
KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());
ks.load(null, null);
ks.setCertificateEntry("certificate", cert);
ks.setKeyEntry("private-key", key.getPrivate(), password.toCharArray(),
new java.security.cert.Certificate[]{cert});
KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory
.getDefaultAlgorithm());
kmf.init(ks, password.toCharArray());
// finally, create SSL socket factory
SSLContext context = SSLContext.getInstance("TLSv1.2");
context.init(kmf.getKeyManagers(), tmf.getTrustManagers(), null);
return context.getSocketFactory();
}
}           

MqttServiceTest

import com.demo.smqtt.vo.MqttConnect;
import com.demo.xl.utils.SSLUtils;
import org.eclipse.paho.client.mqttv3.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.net.ssl.SSLSocketFactory;
import static org.eclipse.paho.client.mqttv3.MqttConnectOptions.MQTT_VERSION_3_1_1;
/**
* @author Charley
* @date 2022/12/05
* @description
*/
public class MqttServiceTest {
private static Logger log = LoggerFactory.getLogger(MqttServiceTest.class);
/**
* 消息級别
*/
private final static int QOS = 0;
public static MqttClient createMqtt(MqttConnect mqttConnect) throws Exception {
MqttClient client = new MqttClient(mqttConnect.getHost(), mqttConnect.getClientId());
MqttConnectOptions connOpts = new MqttConnectOptions();
connOpts.setUserName(mqttConnect.getUserName());
connOpts.setPassword(mqttConnect.getPassword().toCharArray());
connOpts.setCleanSession(mqttConnect.isCleanSession());
connOpts.setKeepAliveInterval(90);
connOpts.setAutomaticReconnect(true);
connOpts.setMqttVersion(MQTT_VERSION_3_1_1);
SSLSocketFactory factory = SSLUtils.getSocketFactory(
mqttConnect.getCA_CRT_PATH(),
mqttConnect.getDEVICE_CERT_PATH(),
mqttConnect.getDEVICE_PEM_PATH(),"");
connOpts.setSocketFactory(factory);
client.connect(connOpts);
log.info("mqtt({}) connect success", mqttConnect.getClientId());
return client;
}
public static void subscribe(MqttClient client, String topic, String clientId) {
try {
// 建立MqttClient
if (!client.isConnected()) {
log.error("mqtt({}) sub is disconnect", clientId);
client.connect();
log.error("mqtt({}) is error");
}
client.setCallback(new MqttCallback() {
@Override
public void connectionLost(Throwable arg0) {
log.error("connectionLost : " + arg0.getMessage());
}
@Override
public void messageArrived(String topic, MqttMessage message) throws Exception {
log.info("recive message ->{} ", new String(message.getPayload()));
}
@Override
public void deliveryComplete(IMqttDeliveryToken token) {
log.info("delivery is Complete:" + token.isComplete() + " and delivery response:" + token.getResponse());
}
});
client.subscribe(topic, QOS);
} catch (Exception e) {
log.error(e.getMessage());
}
}
public static void publish(MqttClient client, String msg, String topic, String clientId) throws MqttException {
if (!client.isConnected()) {
log.error("mqtt({}) connect is error", clientId);
client.connect();
log.error("mqtt({}) connect reconnect", clientId);
}
MqttTopic mqttTopic = client.getTopic(topic);
MqttMessage message = new MqttMessage(msg.getBytes());
message.setQos(QOS);
mqttTopic.publish(message);
log.info("MQTTUtil({}) Send-> topic: " + topic + "\n Message: " + msg, clientId);
}
public static void main(String[] args) throws Exception {
MqttConnect mqttConnect = new MqttConnect();
mqttConnect.setClientId("123456");
MqttClient mqttClient = createMqtt(mqttConnect);
//訂閱消息
subscribe(mqttClient,"test/hello","123456");
//釋出消息
publish(mqttClient,"say hello~~~","test/hello","123456");
}
}           

測試結果:

裝置CA憑證認證

繼續閱讀