Table of Contents
TLS1.2 VS TLS1.3
要想将透明的mqtt通信设置tls,首先需要服务端设置证书,因为在非对称加密通信中,公钥一般存在证书文件中,服务器会将证书发送给客户端,客户端验证证书有效后,根据使用的tls版本选择不同的加密通信方式。
具体来说,如果使用tls1.2,则客户端会在本地生成RSA对称密钥,然后使用证书公钥加密这个对称密钥,然后将这个密钥发送给服务端,后续所有的通信使用这个对称密钥进行加密,也就是本次会话密钥。
如果使用tls1.3,则客户端在验证证书有效后,会强制使用 ECDHE 来进行密钥交换,具体的流程为:
- 客户端和服务端随机选取一个数a,b
- 客户端 用一个公开的数学方法(椭圆曲线运算)算出 A = a * G,然后把 A 发送给 服务端。
- 服务端 也用同样的方法算出 B = b * G,然后把 B 发送给 客户端。
- 客户端 用她的秘密 a 计算 S = a * B = a * (b * G);服务端 用他的秘密 b 计算 S = b * A = b * (a * G)。
S即为会话密钥,后续使用S加密后续消息。
ECDHE的核心在于,虽然G是公开的,椭圆曲线运算也是公开的,但是椭圆曲线运算在数学上是 单向不可逆 的,反推出a和b的概率很低。
单向认证
不管使用RSA对称密钥还是ECDHE方式的密钥,前提是都需要证书来进行身份认证。可以复用nginx的证书作为服务器证书。
mosquitto服务端设置证书
listener 8883
cafile /etc/letsencrypt/xxx.pem
certfile /etc/letsencrypt/xxx.pem
keyfile /etc/letsencrypt/xxx.pem
require_certificate false//为后面双向认证做准备
app设置
//由于使用的是受信任的CA机构签发的证书,因此使用默认的系统认证机制
val sslContext = SSLContext.getInstance("TLSv1.3")
sslContext.init(null, null, null) // 默认的 TrustManager 和 KeyManager 会被使用
options.socketFactory = sslContext.socketFactory // 设置 socketFactory 为系统默认
注意修改url地址,就可以正常进行单向认证通信。
测试结果:
作为对比,在使用TLS加密之前,用户名密码等信息将会在网络上裸奔:
双向认证(mTLS)
双向认证和单向认证的区别,就是在客户端验证服务器证书的基础上,服务器也需要验证客户端的身份,这种情况一般使用在物联网设备上,例如车载通信等。
在双向认证中,每个设备都有一个唯一代表本设备的证书,这个证书由自建CA签发,部署在内网中,整个身份认证流程都在私有环境下进行,服务器和客户端都只信任自建 CA 颁发的证书,实现双向 TLS 认证(mTLS),也就是一机一密。
1. 在服务器中创建自建CA目录
//推荐的自建 CA 目录结构
/etc/ca/
├── ca.key # CA 私钥
├── ca.crt # CA 根证书(用于 MQTT 服务器 & 设备端验证)
├── serial # 证书序列号记录
├── index.txt # 证书签发记录
├── certs/ # 存放签发的客户端证书
│ ├── device_001.crt
│ ├── device_002.crt
│ ├── ...
├── crl/ # 证书吊销列表(可选)
│ ├── ca.crl
mkdir -p /etc/ca/{certs,crl,newcerts}
cd /etc/ca/
touch index
echo 1000 | tee serial
echo 1000 | tee crlnumber
2. 添加cnf配置文件
该文件作为证书签发和管理操作的核心配置文件,用于定义CA在签发证书时的相关参数和行为。指定了如何生成证书、如何管理签发的证书、以及签发证书时所需要的各种策略和规则。
示例:通用证书配置文件示例
注意:cnf配置文件可以设置很多参数,并且还可以指定扩展文件和扩展参数。为了方便签发,我们可以设置一个通用配置文件,用于基本签发参数配置,使用-config进行指定,其次针对服务端证书,客户端证书,以及不同场景的设备,例如物联网设备证书,可以使用-extensions 和-extfile指定,其中-extensions为扩展节,-extfile为扩展文件。例如,对于服务器证书签发,必须含有SAN,但是在客户端中不应该含有此参数,因此我们可以将这个参数设置到扩展文件server_ext.cnf中:
示例:服务端证书配置文件示例
3. 使用openssl工具创建密钥,签发证书
#生成CA私钥
openssl genpkey -algorithm RSA -out /etc/ca/ca.key -pkeyopt rsa_keygen_bits:2048
#生成CA根证书
openssl req -new -x509 -days 3650 -key /etc/ca/ca.key -out /etc/ca/ca.crt -config /etc/ca/ca.cnf
#生成设备私钥
openssl genpkey -algorithm RSA -out /etc/ca/certs/xxx.key -pkeyopt rsa_keygen_bits:2048
#生成设备证书签发请求csr
openssl req -new -key /etc/ca/certs/xxx.key -out /etc/ca/certs/xxx.csr -config /etc/ca/ca.cnf -subj "/C=CN/ST=Beijing/L=Beijing/O=Wakamda/CN=device01"
#生成设备证书
openssl ca -in /etc/ca/certs/xxx.csr -out /etc/ca/certs/xxx.crt -config /etc/ca/ca.cnf
#更新crl文件
openssl ca -gencrl -out /etc/ca/crl/crl.pem -config /etc/ca/ca.cnf
后期可以使用脚本自动化处理
当需要扩展文件配置特定参数,例如配置服务端证书时:
#生成特定设备证书 openssl ca -in certs/Server.csr -out certs/Server.crt -config ca.cnf -extensions server_ext -extfile server_ext.cnf
当需要吊销设备证书时:
#吊销证书 openssl ca -revoke /etc/ca/newcerts/1000.pem -config /etc/ca/ca.cnf #更新crl文件 openssl ca -gencrl -out /etc/ca/crl/crl.pem -config /etc/ca/ca.cnf
4. 设置mqtt broker配置
listener 8883
cafile /etc/ca/ca.crt
certfile /etc/ca/certs/Server.crt
keyfile /etc/ca/certs/Server.key
require_certificate true
tls_version tlsv1.3
此时使用单向通信已经连接失败:
5. 客户端绑定证书
#生成客户端所需要的p12文件
openssl pkcs12 -export -out xxx.p12 -inkey certs/xxx.key -in certs/xxx.crt
6. 修改客户端代码
// 导入p12文件
val p12File = applicationContext.resources.openRawResource(R.raw.client)
val p12Password = "111111"
// 加载 KeyStore,包含客户端证书和私钥
val keyStore = KeyStore.getInstance("PKCS12")
keyStore.load(p12File, p12Password.toCharArray())
// 创建 KeyManagerFactory 来管理客户端证书和私钥
val keyManagerFactory = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm())
keyManagerFactory.init(keyStore, p12Password.toCharArray())
// 创建 TrustManagerFactory 来验证服务器证书
val trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
// 默认使用系统信任的证书
//trustManagerFactory.init(null as KeyStore?)
// 加载 CA 根证书
val caInput: InputStream = resources.openRawResource(R.raw.ca)
val cf = CertificateFactory.getInstance("X.509")
val ca = cf.generateCertificate(caInput)
// 创建一个包含 CA 证书的 KeyStore
val caKeyStore = KeyStore.getInstance(KeyStore.getDefaultType())
caKeyStore.load(null, null)
caKeyStore.setCertificateEntry("ca", ca)
// 初始化 TrustManagerFactory
trustManagerFactory.init(caKeyStore)
// 创建 SSLContext,并设置 KeyManager 和 TrustManager
val sslContext = SSLContext.getInstance("TLSv1.2")
sslContext.init(keyManagerFactory.keyManagers, trustManagerFactory.trustManagers, null)
//应用
options.socketFactory = sslContext.socketFactory
此时使用双向通信连接成功:(注意此时使用的是TLSv1.2)
TLSv1.3相较于TLSv1.2来说,在双向认证过程中,简化了证书的交换流程,因此,在握手期间,使用wireshark抓包时仅能抓取类似于单向认证的包,也就是客户端请求包和服务端回复包。
val sslContext = SSLContext.getInstance("TLSv1.3")
双向认证中客户端私钥和CA证书的安全性问题
在双向认证中,客户端必须保存私钥(一机一密)和CA根证书(自建CA),因此私钥和证书的安全性保存至关重要。
在许多物联网设备中,一般会采用硬件安全模块(HSM),加密存储,固件加密和安全启动等形式进行安全加固。但是安卓app使用硬件方式不现实,一般可以使用其他方式,例如对私钥和根证书进行加密。
p12格式的文件保存着设备私钥以及证书链,在生成时默认有密码保护,为了安全起见,使用自定义密钥和aes加密算法对p12文件和CA根证书进行二次加密。
由于要求不能硬编码且不能使用网络传输密钥,因此该密钥在服务端进行加密,设备端进行解密,需要双方有共同的点来进行密钥生成,可以根据安卓设备指纹或其他指纹再加以变化,最终生成AES密钥。
具体措施
客户端的通信密钥和证书使用其他密钥进行二次加密后,可以直接放入资源文件res/raw中,此时文件是加密状态,然后在程序运行时在内存中解密,且解密后不保存文件,而是直接在内存中导入密钥和证书,保证密钥安全。
// 生成解密密钥
val key = generateKey()
// 加载加密文件
val encryptedP12File = resources.openRawResource(R.raw.client)
val encryptedCaFile = resources.openRawResource(R.raw.ca)
//解密
val p12Bytes = aesDecryptInMemory(encryptedP12File, key)
val caBytes = aesDecryptInMemory(encryptedCaFile, key)
//正常绑定options
...
其他措施
p12文件进行第一次加密时,也是用二次加密使用的密钥相同的方案生成加密密钥,以防止在app代码中硬编码被逆向。
一机一密的缺点
书接上回,设置为双向认证之后,客户端总需要绑定自己的证书,并且为了防止被攻击,证书需要加密,这样一来,每安装到一个设备,就需要重新编译一个app,这样显然是很行不通的。
我们可以将客户端证书的获取,设置为网络获取,这样编译好的app中就不会有CA文件,程序运行时才会下载到运行文件夹中,无法被其他程序获取;而且,程序的使用体验会更好,因为用户并不关心也不想去设置什么加密证书(听起来就很头疼😐)
使用网络传输时保证安全,使用https协议
服务端设置
要想从服务端https下载文件,需要进行nginx设置:
# 不要加 ^~,让正则匹配优先生效,如果加^~,就不会匹配正则了
location /certs/ {
deny all;
}
location ~ "\.en$" {
root /etc/ca/;
autoindex off;
default_type application/octet-stream;
limit_except GET {
deny all;
}
add_header Content-Disposition "attachment";
}
伪代码
- 程序启动时,初始化页面
- 进行网络状态,网络如果正常且没有证书存在,则进入ca下载程序,下载后进行验证(解密);如果网络不正常,重试或其他
- ca导入成功后,弹窗提示填入其他信息
- 必要信息填入后,再进行广播,服务初始化。
文件下载
安卓对于网络下载的文件存储有严格的分类,大致分为:
- 内部存储:适用于存储私密数据,只能应用访问。
- 外部存储:适用于存储较大文件,其他应用可以访问。
- 应用的外部文件目录:私有的外部存储,只有当前应用可以访问。
针对客户端证书来说,属于私密文件,应该存储在内部。