缘起
电力监控系统里,104 规约(IEC 60870-5-104)过去是明文跑的,谁都能在链路上看,也能往里塞东西。IEC 62351-3 就是来解决这个问题的,它把 TLS 套在 TCP 之上,让 104 的报文有了加密和双向认证。
但"支持 TLS"这四个字太笼统了。一个设备说自己支持 62351-3,到底是真按规矩做的,还是只是能握手成功?比如它允许不允许 TLS 1.0 接进来?收到一张过期的客户端证书,它是报警还是装没看见?对方一直不响应重协商,它会不会傻等下去?
这些问题,IEC 62351-3 本身没有逐条给出负面场景的预期行为,于是就有了 IEC TS 62351-100-3。它是 62351-3 的一致性测试用例集,其中第 6.3 节专门管"异常情况下设备该怎么做",也就是 resiliency test。
我们这轮要做的事,就是把这 39 条测例在一台真实设备上跑一遍。
被测对象和测试拓扑
被测设备(DUT)是服务端角色,也就是变电站里的装置或者主站前置机,跑 104 服务,监听安全端口 19998。测试设备是客户端角色,由我们自己控制,可以任意构造握手参数和证书。
拓扑很朴素,就两台:一台 DUT,一台测试机。中间隔着交换机或者直连都行。真正的工作量不在网络,而在客户端能造出多刁钻的输入。
证书体系
整个测试的基石是一套自建的 PKI。根 CA 自己签,服务器证书和正常客户端证书都由它签发。光有正常证书不够,绝大部分测例要的是"一张有问题但看起来很正常的证书",所以我们准备了这么一批:
| 证书 | 怎么造的 | 用来测什么 |
|---|---|---|
| client | 正常签发 | 基线,证明环境本身是通的 |
| big | 塞 200 个 subjectAltName,把体积撑大 | 证书超尺寸 |
| other_client | 另一个 CA(other_ca)签发 | 签发 CA 没装 |
| client_diff | 同一个 CA,换个 CN | 个体证书不在白名单 |
| expired | 有效期设成 2020-01-01 到 2020-01-02 | 证书过期 |
| revoked | 签发后写进 CRL | 证书被吊销 |
| min1024 | RSA 1024 位 | 密钥长度踩在 legacy 下限 |
| short | RSA 512 位 | 密钥长度不够 |
| md5 | 用 MD5 签名 | 签名算法不被接受 |
| tampered | 把 PEM 里的一个字节翻转 | 签名对不上 |
生成脚本是纯 Python,用 cryptography 库直接写 X.509,没依赖 openssl 命令行。这样跨平台稳,也不用担心不同版本 openssl 的参数差异。
CRL 也得准备两份:一份正常的ca.crl,里面有 revoked 那张证书的序列号;一份故意做旧的ca_expired.crl,nextUpdate 时间设在过去,用来测"CRL 过期"。
服务端该有的配置
为了让测试有意义,DUT 必须按 PIXIT 声明的那套参数来配。我们这边的口径是:
- TLS 版本只开 1.2,1.0、1.1、1.3 全关;
- 密码套件只留 AES128-GCM-SHA256 和 AES256-GCM-SHA384;
- 强制双向认证,客户端必须出示证书;
- 信任锚是
ca.crt,吊销检查用ca.crl; - 最大证书尺寸声明为 4096 字节;
- 重协商间隔声明 60 秒,测试时临时调到 10 秒。
只要有一条对不上,测出来的结果就没法往标准上套。所以每次换设备,第一件事都是先核对这套参数。
三种客户端工装
初始握手组的测例,大多数用 openssl 命令行就够了,s_client换几个参数就能造出对应场景。但重协商组不行,那些场景要控制"第一次正常、第二次变坏"的时序,命令行做不到,只能自己写 C 程序。我们前后攒了三类工装:
第一类,openssl s_client。换证书、换版本、换密码套件,一行命令的事。初始握手组里凡是不涉及状态变化的,都用它。
第二类,cert_callback 切证书。客户端注册一个证书回调,初始握手和第一次重协商加载正常证书,等到设定的那次重协商再换成异常证书。6.3.29 之后那一批基本都套这个模板,改的只是喂进去的异常证书路径。
第三类,BIO 过滤器。这个稍微绕一点。重协商时 TLS 记录头的版本字段是明文的,我们可以挂一个自定义 BIO,在写出去之前把03 03(TLS 1.2)改成03 01(TLS 1.1),这样协商出来的版本就和初始握手不一致了,6.3.21 和 6.3.23 靠它。6.3.22 则反过来,在读方向拦截并丢弃服务端发来的 HelloRequest,让服务端等不到回应。
这三类工装对应的客户端程序命名都是c6-3-XX,交叉编译后丢到目标板上跑。
M、PICS、PIXIT 是什么意思
标准里每条测例都标了 Required,取值有三种:
M是强制,跟 62351-3 里的强制条款挂钩,跑不了。
PICS是有条件的,只有当设备的 PICS 声明里勾了对应功能,这条才适用。比如 6.3.1 要求设备支持 TLS 1.1,可我们这台设备压根不支持,这条对我们就是 NA,不用测。系列里带 PICS 的那几篇我会说清楚条件是什么、什么情况下才需要动手。
PIXIT也是有条件,但取决于 PIXIT 里声明的具体参数值,比如重协商间隔、CRL 检查方式、最大证书尺寸。设备声明的值不同,测法会跟着变。
怎么读这个系列
39 条测例,一条一篇,按编号顺序排。每篇大致是这个路子:先说这个场景在现实里意味着什么,再给标准要求的预期行为,然后讲我们怎么把这个场景造出来,最后是怎么判定设备做对了。
如果你只想看个大概,读 README 里的目录表,每篇都有一句话摘要。如果你要照着复现,建议从 6.3.3 开始看,那是第一条不依赖额外条件的 M 测例,跑通了再往下。
有一点得提前说明:这套东西测的是设备的"行为对不对",不是"加密强不强"。TLS 本身的实现正确性不在这个标准的范围内,那是 RFC 的事。所以别指望用这套用例去发现 TLS 库的漏洞,它找的是设备有没有按 62351-3 的规矩办事。