前言
在汽车出行愈加智能化的今天,我们可以实现手机远程操控车辆解锁、启动通风、查看车辆周围影像,也可以通过 OTA(空中下载技术)完成升级车机固件、更新地图包等操作,自动驾驶技术更是可以让车辆根据路面状况自动辅助实施转向、加速和制动。
然而,每项提升我们使用体验的功能,都有可能成为致命的安全漏洞。腾讯安全科恩实验室曾向外界披露并演示过如何凭借 3/4G 网络或者 WiFi 网络,在远程无物理接触的情况下入侵智能汽车,实现对车辆信号灯、显示屏、门锁甚至是刹车的远程控制。不仅如此,“有心者”甚至可以利用某个已知漏洞获取智能汽车的 Autopilot 控制权,对车辆行驶方向进行操控。
因此,我们在车联网平台构建时也应充分认识到通信安全、身份认证、数据安全的重要性,正确使用相关加密认证等技术手段来提供保障。
本篇文章我们将全面介绍 SSL/TLS 协议在车联网通信安全中的应用,希望能让大家对 SSL/TLS 的作用有更清晰直观的认识。此外,我们还将详细讲解 SSL/TLS 的配置方式,确保大家能正确使用 SSL/TLS,实现安全性保障。
车联网安全通信 MQTTS 协议
MQTTS 协议是在 MQTT 协议的基础上,封装了一层基于 SSL/TLS(传输层安全)的加密协议, 它确保车机端和车联网平台通信是加密的。但如果没有正确配置 SSL/TLS,依然会存在很多安全隐患。想要真正运用好 SSL/TLS,我们必须了解 SSL/TLS 解决了哪些问题,以及对 SSL/TLS 用到的密码技术有初步的认知。
通常情况下,通信过程需要具备以下四个特性,才能被认为是安全的,分别是:机密性、完整性、身份认证和不可否认。
机密性
机密性是安全通信的基础,缺少机密性任何窃听通信的人都可以轻而易举获取到你的诸如登录密码、支付密码等关键隐私信息。实现机密性最常用的手段就是加密,这样窃听者只能得到加密后的毫无意义的一串数据,只有持有密钥的人才能将密文恢复成正确的原始信息。根据密钥的使用方法,加密方式可以分为对称加密和非对称加密两种。对称加密是指加密和解密使用相同的密钥,非对称加密则是指加密和解密时使用不同的密钥。
对称加密由于通信双方要使用相同的密钥来进行加解密,所以必然会遇到密钥配送问题,即我需要对方能够解密我发送过去的密文,我就必须把我加密时使用的密钥告诉对方,但是我如何保证将密钥与对方同步的过程中密钥不会泄漏?这就是对称加密的密钥配送问题。
目前常用的解决方案是使用非对称加密和使用 Diffie-Hellman 密钥交换算法。非对称加密的核心是生成一对密钥,一个是公钥,一个是私钥,公钥用于加密,它是公开的,可以派发给任何人使用,私钥用于解密,不参与通信过程,需要被妥善保管,这样就解决了密钥配送问题。Diffie-Hellman 密钥交换算法的核心思想则是通信双方交换一些公开的信息就能够计算出相同的共享密钥,而窃听者获得这些公开信息却无法计算出相同的密钥。Diffie-Hellman 算法的一个好处是没有非对称加密的性能问题,非对称加密虽然解决了密钥配送问题,但非对称加密算法的运算速度远远不及对称加密算法,它们甚至能有几百倍的差距。虽然保障了安全,但严重影响了通信的效率,丧失了实用性。因此实际应用时通常会将对称加密和非对称加密结合使用,即使用伪随机数生成器生成会话密钥后,用公钥进行加密并发送给对方,对方收到密文后使用私钥解密取出会话密钥,后续通信将完全使用该会话密钥。这样既解决了密钥配送问题,又解决了非对称加密带来的性能问题,这种方式通常又被称为混合加密。
完整性
仅仅具备机密性还不足以实现安全的通信,“有心者”依旧可以篡改、伪造密文内容,而接收者既无法判断密文是否来自正确的发送者,也无法判断解密后的明文是否是未经篡改的。尽管对加密之后的密文进行针对性篡改的难度有所上升,例如篡改之后明文的数据结构很有可能会遭到破坏,这种情况下接收者能够很轻易地拒绝这个明文。但依然存在篡改之后正好使得解密得到的明文消息中某些本身就具备随机属性的字段的值发生变化的概率,例如电机转速字段的值从 500 变为了 718,无非是几个比特位的变化,如果接收者正常接受这些消息,就可能带来意想不到的隐患。
因此,我们还需要在机密性的基础上进一步保证信息的完整性。常见的做法就是使用单向散列函数计算消息的散列值,然后将消息和散列值一起发送给接收者。单向散列函数能够确保消息中哪怕只有 1 比特的改变,也有很高的概率产生不同的散列值。这样接收者就可以计算消息的散列值,然后对比收到的散列值来判断数据是否被人篡改。
身份认证
但可惜的是,当“有心者”同时伪造消息和对应的散列值时,接收者依然无法识破这个伪装。因此我们不仅需要确认消息的完整性,还需要确认消息是否来自合法的发送者,也就是说还需要对身份进行认证。这个时候我们就需要用到消息认证码,消息认证码依然基于单向散列函数,但它的输入除了原本的消息以外,还包括了一个发送者与接收者之间共享的密钥。由于消息认证码本身并不提供消息机密性的保证,所以在实际使用中,通常会将对称加密与消息认证码结合使用,以同时满足机密性、完整性和认证的要求,这种机制也被称作认证加密(AEAD)。具体怎么使用上,产生了以下几种方案:
- Encrypt and MAC:先用对称密码将明文加密,再计算明文的 MAC 值,最后把二者拼接起来发给接收方。
- MAC then Encrypt:先计算明文的 MAC 值,然后将明文和 MAC 值同时用对称密码加密,加密后的密文发送给接收方。
- Encrypt then MAC:先用对称密码将明文加密,再后计算密文的 MAC 值,最后把二者拼接起来发给接收方。
在很长一段时间内,SSL/TLS 都采用了第二种方案,但事实上以上三种方案都已经陆续被验证为存在安全漏洞。SSL/TLS 历史上的 POODLE 和 Lucky 13 attack 都是针对 MAC then Encrypt 方案中的漏洞实现的。目前业界推荐的安全方案是采用 AEAD 算法,SSL/TLS 1.3 版本中也正式废除了其他加密方式,仅支持 AEAD 加密。
不可否认
现在,我们已经保证了消息的机密性,同时也能识别出伪装和篡改,但是由于消息认证码的核心是需要通信双方共享密钥,因此又引发了新的问题,即无法对第三方证明以及无法防止否认。假设 Bob 接收了来自 Alice 的消息,想要向第三方证明这条消息的确是 Alice 发送的,就需要将原本只有两个人知道的密钥告诉给第三方,这显然会增加后续继续使用这个密钥通信的安全风险。同时,即便第三方拿到了密钥,也无法得出有效的结论,例如 Bob 可以宣称这条消息是由 Alice 构造的,因为 Alice 也持有相同的密钥。
因此,我们还需要引入数字签名机制,它的原理与非对称机密很像,又正好相反。数字签名需要发送者用私钥对消息施加签名,然后将消息与签名一并发送给接收者,接收者则使用对应的公钥验证签名,确认签名来自合法的发送者。由于只有持有私钥的人才能施加正确的签名,这样发送者就无从否认了。而公钥只是用来验证签名,所以可以随意派发给任何人。可能敏感的读者到这里心中已经有些疑问了,是的,取到公钥的人如何确认这个公钥的确来自自己期望的通信对象呢?如果“有心者”伪装成发送者,并把自己的公钥给了接收者,那么就能在无需破解数字签名算法的前提下完成攻击。
我们已经陷入了一个死循环,数字签名是用来用识别消息篡改、伪装以及否认的,但在此之前我们又必须从没有被伪装的发送者得到没有被篡改的公钥才行。到了这一步,我们只能借助外力的帮助了,委托公认的可信第三方,也就是我们现在常说的认证机构或 CA,由它来给各个公钥施加签名,形成公钥证书。显而易见的是,认证机构需要努力确保自己的私钥不被窃取,以保证数字签名的有效性。虽然认证机构的私钥依然有泄漏的概率,甚至认证机构本身也可能被“有心者”伪装,我们依然无法获得绝对的安全,但提前信任几个已知的认证机构,总是比从全新的通信对象获取并信任他的公钥要可靠的多。
以上这些密码技术,共同构成了现代安全通信领域的基石。而 SSL/TLS 作为目前世界上应用最广泛的密码通信方法,综合运用了前面提到的对称加密、非对称加密、消息认证码、数字签名、伪随机数生成器等密码技术,来提供通信安全保障。考虑到密码学技术是不断进步发展的,或者说目前看似可靠的加密算法,可能在第二天就会被宣告攻破,所以 SSL/TLS 并没有强制使用某一种密码技术,而是提供了密码套件(Cipher Suite)这一机制,当某项密码技术被发现存在弱点,可以随时像零件一样替换它,当然前提是客户端和服务端使用相同的密码技术。这也延伸出了 SSL/TLS 的握手协议,协商使用的密码套件就是这一部分协议的主要工作之一。
想要 SSL/TLS 具备良好的安全性,就需要避免使用已经被攻破或者已经被验证为弱安全性的加密算法,要避免使用容易被预测的伪随机数生成器,要尽量保证各个算法具有近似的安全性(短板效应)。
因此,如何正确选择密码套件,也成为了保障安全性的一个重要环节。这里我也会对目前推荐的密码技术和加密算法进行一个简单的整理,希望可以帮助各位读者查漏补缺:
- 对称加密算法中 RC4、DES、3DES 都已经被认为是不安全的了,目前推荐使用的只有 AES 和 ChaCha20。ChaCha20 是 Google 设计的一种加密算法,如果 CPU 或软件不支持 AES 指令集,ChaCha20 可提供比 AES 更好的性能。
- AES 这类对称加密算法只能加密固定长度的明文,想要加密任意长度的明文,还需要用到分组模式。早期的 ECB、CBC、CFB、OFB 等分组模式已经被认定为存在安全漏洞,目前更推荐使用 GCM、CCM 和 Poly1305。
- 常用的非对称加密算法有 DH、RSA、ECC 这几种。由于 DH 和 RSA 都不具备前向安全性,目前已经不推荐使用,TLS 1.3 中更是直接废除了 DH 和 RSA 算法,取而代之的是安全强度和性能都明显优于 RSA 的 ECC 算法,它有两个子算法,ECDHE 用于密钥交换,ECDSA 用于数字签名。但需要注意的是,由于 ECDHE/DHE 不提供身份验证,因此服务端应当启用对客户端证书的验证。
- 散列算法方面,我们熟知的 MD5 和 SHA-1 都已经被认定为不再可靠,不推荐继续使用。目前通常建议使用 SHA256 或更高版本。
在了解推荐使用的密码技术以后,也许我们想要修改客户端或服务端的密码套件配置,但此时我们可能会发现这些密码套件的名称还有点难以理解。例如 TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384,其中 TLS 只是表示协议,ECDHE 表示密钥交换算法,ECDSA 表示身份认证算法,AES_256_CBC 表示批量加密算法,SHA384 表示消息认证码 MAC 算法。这通常是 TLS 1.2 中密码套件的命名格式,而到了 TLS 1.3 则又发生了一些变化。由于 TLS 1.3 只接受使用 ECDHE 算法进行密钥交换,并且使用 ECDSA 进行身份认证,因此它的密码套件名称可以精简成 TLS_AES_256_GCM_SHA384 这种格式。
如果仅从安全性角度出发,个人建议使用 TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA384 和 TLS_AES_256_GCM_SHA384。但考虑到目前仍有很多以 RSA 方式签发的证书正在使用,因此我们还需要根据自身情况来选择是否要继续使用 TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA384。
构建安全认证体系典型架构
采用基于 PKI/CA 的数字证书体系是解决车联网安全关键的一步,也是大多数车企典型安全管理体系。其主要的设计思路如下:
- 基于数字证书的身份标识:通过 PKI/CA 系统建立严谨的证书管理和使用规范,为车联网的应用和终端颁发数字证书,虚拟身份和真实身份进行绑定,解决身份标识和唯一性问题(可实现一机一密或一型一密);
- 所有数据交互时通过终端的身份唯一标识证明身份的真实性,防止第三方恶意入侵;
- 基于数字证书安全功能,提供身份鉴别、身份认证、数据加解密、数字签名与验签等多种功能,满足车联网中 TSP、OTA 等多业务安全需求。
车联网平台安全通信交互流程,一般是将车机端申请终端证书,下载并完整安装后通过 MQTTS 安全协议与云端平台请求建立安全连接。在云端我们可以选择在云厂商的负载均衡产品、基于 Nginx/HAProxy 自行搭建的 LB 层或是 MQTT Broker 层进行认证鉴权,同时通过 proxy_protocol v2 将车机端的 ID 信息、用户名密码及证书的 CN/SN 等信息通过调用 PKI/CA 统一认证接口进行唯一性认证,实现一机一密或一型一密的安全认证。
MQTTS 通信中单、双向认证的配置方式
SSL/TLS 连接认证认证的是对方身份,是否是可信的通信对象,认证的依据则是通信对象提供的证书。通常情况下是由客户端对服务端的身份进行认证,也就是所谓的单向认证。那么双向认证顾名思义就是在单向认证的基础上,服务端对客户端的身份进行认证。
认证的原理其实非常简单,以单向认证为例,最简单的情况就是服务端在 SSL/TLS 握手阶段发送服务端证书,客户端验证该证书是否由受信任的 CA 机构签发,也就是使用受信任的 CA 证书中的公钥来验证服务端证书中的数字签名是否合法。当然大部分情况会比这个稍微复杂一些,即服务端的证书不是由最顶层的 CA 机构直接签发的,而是由根 CA 机构对下层 CA 机构的公钥施加数字签名,形成中间 CA 证书,这样的关系可能会多达几层,以尽可能保护根证书的安全。大部分情况下常见 CA 机构的根 CA 证书和中间 CA 证书都已经内置在我们的操作系统中了,只有少数情况下需要自行添加信任的 CA 证书。
多级证书或者说证书链的认证过程会稍微复杂一些,但如果我们搞明白了前面说的证书签发逻辑,其实理解起来也很简单。还是以单向认证为例,如果客户端只信任了根 CA 证书,那么服务端在握手阶段就需要发送服务端证书和根 CA 证书到服务端证书之间的所有中间 CA 证书。只有客户端拿到了完整的证书链,才能通过自己持有的根 CA 证书一层一层往下验证,缺少中间 CA 导致证书链不完整或者包含了错误的中间 CA,都会导致信任链中断而无法通过认证。
如果客户端除根 CA 证书以外,还持有一部分中间 CA 证书,那么在认证过程中,服务端还可以省略这些中间 CA 证书的发送,来提高握手效率。
因此,当我们配置单向认证时,需要在服务端指定服务端证书和中间 CA 证书(可选),以及服务端私钥文件。客户端则需要信任相应的根 CA 证书,信任的方式可以是在连接时指定或者通过证书管理工具将该根 CA 证书添加到信任列表。通常客户端库还提供了对端验证选项允许选择是否验证证书,关闭对端验证将在不验证证书的情况下直接创建加密的 TLS 连接。但这会带来中间人攻击的安全风险,因此强烈建议启用对端验证。
在启用对端验证后,客户端通常还会检查服务器证书中的域名(SAN 字段或 CN 字段)与自己连接的服务器域名是否匹配。如果域名不匹配,则客户端将拒绝对服务器进行身份验证或建立连接。
双向认证的配置方式只需要在单向认证的基础上,在服务端启用对端验证即表示启用双向认证以外,再参考服务端证书的配置方式正确配置客户端证书即可。
常见 TLS 选项介绍
当使用 EMQX 配置 SSL/TLS 连接时,通常会有 certfile、keyfile 等选项,为了帮助大家更好地了解这些选项的配置方式,接下来我们会对这些常见的 TLS 选项做一个简单的梳理和介绍:
- certfile,用于指定服务端或客户端证书和中间 CA 证书,需要指定多个证书时通常将它们简单地合并到一个证书文件中即可。
- keyfile,用于指定服务端或客户端私钥文件。
- cacertfile,用于指定 Root CA 证书,单向认证时客户端需要配置此选项以校验服务端证书,双向认证时服务端也需要配置此选项以校验客户端证书。
- verify,用于指定是否启用对端验证。客户端启用对端验证后通常还会去检查连接的服务器域名与服务器证书中的域名是否匹配。客户端与服务端同时启用则意味着这将是一个双向认证。
- fail_if_no_peer_cert,这是一个服务端的选项,通常在服务端启用对端验证时使用,设置为 false 表示允许客户端不发送证书或发送空的证书,相当于同时开启单向认证和双向认证,这会增加中间人攻击的风险。
- versions,指定支持的 TLS 版本。通信双方会在握手过程中,将 versions 选项中指定的版本发送给对方,然后切换至双方都支持的最高版本。同时也会基于该协议版本来协商密码套件。
- ciphers,指定支持的密码套件。注意事项:避免使用前文提到的或其他被认定为弱安全性的密码套件,以及当使用包含 ECDSA 签名算法的密码套件时,需要额外注意自己的证书是否为 ECC 类型。
- server_name_indication,服务器名称指示,这是一个客户端的选项。通常在客户端启用对端验证且连接的服务器域名与服务器证书中的域名不匹配时使用。例如服务器证书中的域名为 abc.com,而客户端连接的是 123.com,那么就需要客户端在连接时指定 server_name_indication 为 abc.com 表示自己信任该域名以通过证书检查。又或者将 server_name_indication 设置为 disable 来关闭此项检查,但这会增加中间人攻击的风险,通常并不建议这样做。
示例
为了便于演示,我们会使用 EMQX (4.3.0 版本及以上)作为服务端,在 EMQX 的控制台中使用 Erlang 的 ssl:connect/3 函数作为客户端。
(1) 单向认证
修改 EMQ X 配置如下:
# 监听端口我们使用默认的 8883
listener.ssl.external = 8883
# 服务端私钥文件
listener.ssl.external.keyfile = etc/certs/zhouzb.club/Nginx/2_zhouzb.club.key
# 证书捆绑包文件,包含了服务端证书和中间 CA 证书
listener.ssl.external.certfile = etc/certs/zhouzb.club/Nginx/1_zhouzb.club_bundle.crt
# 不开启对端验证
listener.ssl.external.verify = verify_none
# 支持 TLS 1.2 和 TLS 1.3
listener.ssl.tls_versions = tlsv1.3,tlsv1.2
# 服务端支持的密码套件
listener.ssl.external.ciphers = TLS_AES_256_GCM_SHA384,TLS_AES_128_GCM_SHA256,TLS_CHACHA20_POLY1305_SHA256,TLS_AES_128_CCM_SHA256,TLS_AES_128_CCM_8_SHA256,TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
启动 EMQ X 并进入控制台:
$ ./emqx console
使用 ssl:connect/3 函数连接:
%% 1. 指定用于验证服务端证书的 Root CA 证书
%% 2. 启用对端验证
%% 3. 仅支持 TLS 1.2
%% 4. 仅支持 TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 这一个密码套件
ssl:connect("zhouzb.club", 8883, [{cacertfile, "etc/certs/zhouzb.club/DigiCertGlobalRootCA.crt.pem"},
{verify, verify_peer},
{versions, ['tlsv1.2']},
{ciphers, ["TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384"]}]).
(2) 双向认证
为了尽快进入正题,这里将继续使用服务端证书来充当客户端证书,这会有严重的安全隐患,在生产环境中请禁止这样使用!
修改 EMQ X 配置如下:
# 监听端口我们使用默认的 8883
listener.ssl.external = 8883
# 服务端私钥文件
listener.ssl.external.keyfile = etc/certs/zhouzb.club/Nginx/2_zhouzb.club.key
# 证书捆绑包文件,包含了服务端证书和中间 CA 证书
listener.ssl.external.certfile = etc/certs/zhouzb.club/Nginx/1_zhouzb.club_bundle.crt
# 指定用于验证客户端证书的 Root CA 证书
listener.ssl.external.cacertfile = etc/certs/zhouzb.club/DigiCertGlobalRootCA.crt.pem
# 启用对端验证
listener.ssl.external.verify = verify_peer
# 要求客户端必须提供证书
listener.ssl.external.fail_if_no_peer_cert = true
# 支持 TLS 1.2 和 TLS 1.3
listener.ssl.tls_versions = tlsv1.3,tlsv1.2
# 服务端支持的密码套件
listener.ssl.external.ciphers = TLS_AES_256_GCM_SHA384,TLS_AES_128_GCM_SHA256,TLS_CHACHA20_POLY1305_SHA256,TLS_AES_128_CCM_SHA256,TLS_AES_128_CCM_8_SHA256,TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
启动 EMQ X 并进入控制台,然后使用 ssl:connect/3 函数连接:
%% 1. 指定用于验证服务端证书的 Root CA 证书
%% 2. 启用对端验证
%% 3. 仅支持 TLS 1.2
%% 4. 仅支持 TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384 这一个密码套件
ssl:connect("zhouzb.club", 8883, [{cacertfile, "etc/certs/zhouzb.club/DigiCertGlobalRootCA.crt.pem"},
{certfile, "etc/certs/zhouzb.club/Nginx/1_zhouzb.club_bundle.crt"},
{keyfile, "etc/certs/zhouzb.club/Nginx/2_zhouzb.club.key"},
{verify, verify_peer},
{versions, ['tlsv1.2']},
{ciphers, ["TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384"]}]).
结语
工业和信息化部印发的《车联网网络安全和数据安全标准系统建设指南》中明确指出,要构建车联网网络安全和数据安全的标准体系。车联网领域的网络通信安全和数据安全将受到越来越多的关注,是车联网企业提高竞争力的关键影响因素之一。希望通过本文,读者可以掌握 SSL/TLS 协议的使用方式,在实际业务场景中正确应用,实现车联网通信安全保障。