核心目标:理解 TLS 如何实现加密、完整性保护和身份认证;掌握单向认证与双向认证的完整流程;能够生成实验用证书,在 TLS 1.2/1.3 下验证 Modbus、IEC 104、IEC 61850 MMS、DNP3 通信,并使用 Wireshark 留存验证证据。
前置知识:TCP/IP、客户端/服务器模型,以及对应工业协议的基础报文结构。
文档约定:示例运行于隔离实验环境;所有截图位置均为占位,尚未填入真实抓包。证书生成脚本随文档交付;通信验证统一通过 EMS Simulate 界面操作。以下“单向/双向”均指身份认证方向,两种模式都会加密双向业务数据。
阅读顺序:第 1~5 章理解原理 → 第 6~7 章生成证书并在界面中导入 → 第 8~12 章运行协议实验 → 第 13~15 章抓包、排障与验收。
🚀 配套实战项目:EMS Simulate(能源管理系统模拟器)
本文使用我开发并持续维护的 EMS Simulate 作为实战工具。它是一款开源工业协议仿真软件,支持 Modbus、IEC 104、IEC 61850、DNP3 等协议,可用于设备模拟、主从站联调和报文分析。本文涉及的四种协议均可在服务端与客户端导入 TLS 证书,通过界面配置单向或双向认证,再配合 Wireshark 观察握手和加密后的通信。
想跟着文章实际操作,可以先安装 EMS Simulate,创建一组服务端和客户端,按照后文的证书导入步骤完成实验。如果项目对你有帮助,欢迎 Star ⭐,也欢迎反馈使用问题。
- 📦 GitHub 开源仓库(欢迎 Star ⭐)
- 📖 在线技术文档
- 🏪 Microsoft Store 下载

1. TLS 保护什么
1.1 在通信栈中的位置
TLS(Transport Layer Security,传输层安全协议)建立在可靠、有序的字节流之上。本文讨论 TCP 上的 TLS,不讨论 UDP 上的 DTLS。
没有 TLS 时,抓包者通常能够直接解析功能码、点号、寄存器值和控制命令。启用 TLS 后,业务字节作为 TLS 记录的载荷传输;普通抓包仍能看到 IP、端口、流量大小和时序,但不能直接读取受保护的业务内容。
| 安全目标 | 解决的问题 | 主要机制 | 不能据此推导的结论 |
|---|---|---|---|
| 机密性 | 第三方偷看业务数据 | 会话密钥 + 对称加密 | 不隐藏所有流量特征 |
| 完整性 | 第三方修改、插入受保护记录 | AEAD 认证标签、记录序号等 | 不等于业务数据本身正确 |
| 身份认证 | 对端是否持有可信身份对应的私钥 | 证书验证 + 握手签名 | 不等于该身份可以操作所有点位 |
| 密钥协商 | 双方如何得到一致的会话密钥 | 本文使用 ECDHE 与密钥派生 | 私钥不会通过网络发送 |
TLS 握手负责协商和认证,记录层负责保护后续数据。它不自动提供业务操作授权、终端防入侵或完整的拒绝服务防护。TLS 与应用协议的身份绑定需由应用安全规范明确。TLS 1.3 规范说明
1.2 加密、认证与授权要分开理解
以主站读取 RTU 数据为例:
- 认证服务器:主站检查对端是不是预期的 RTU 或安全网关。
- 认证客户端:双向认证时,RTU 检查主站证书并验证其私钥持有证明。
- 授权业务:将主站身份映射到角色,判断能否读取某点位、修改定值或执行遥控。
- 保护传输:使用会话密钥保护两个方向的报文。
常见误区:设备只要持有同一个 CA 签发的证书,就应该能执行全部操作。正确做法是先校验证书,再按身份、角色、点位和功能码执行授权。
1.3 TLS 不改变业务协议语义
TLS 不会把 Modbus 功能码 03 改成另一个功能码,也不会代替 IEC 104 的 STARTDT、MMS 的关联建立或 DNP3 的应用层确认。它提供安全通道,业务协议仍按原有状态机工作。
一次业务报文可能分布在多个 TLS 记录中;一个记录也可能包含多条业务报文。TLS 记录、TCP 段和业务帧之间没有一一对应关系。抓包分析必须允许重组。
2. 单向认证与双向认证
2.1 单向认证:客户端认证服务器
服务器提供证书,客户端验证证书和服务器私钥持有证明。客户端无需提供客户端证书,但通常必须预先配置可信 CA 和预期服务器名称。
| 端点 | 必需配置 | 验证内容 |
|---|---|---|
| 客户端 | 信任 CA、预期服务器 DNS 名或 IP | 服务器证书链、有效期、用途、身份及握手证明 |
| 服务器 | 服务器证书及配套私钥 | 不通过客户端证书识别主站;业务层可另行认证 |
单向认证适合学习 TLS 基础,或已有独立客户端认证机制的系统。本文可在 EMS Simulate 中选择它进行四种协议的对照实验;不能据此宣称满足要求强制双向认证的工业安全配置文件。
2.2 双向认证:mTLS
在客户端验证服务器的基础上,服务器通过 CertificateRequest 请求客户端证书,客户端提交证书并用私钥完成握手签名。服务器验证后建立客户端的证书身份。
| 端点 | 自身身份材料 | 信任材料 | 额外检查 |
|---|---|---|---|
| 主站 | 客户端证书、客户端私钥 | 签发服务器证书的 CA | 预期服务器 SAN |
| 设备 | 服务器证书、服务器私钥 | 签发客户端证书的 CA | 客户端身份到角色/权限的映射 |
两端可以使用不同 CA。实验用同一 CA 只是为了简化部署。服务器必须设置“客户端证书必需”,不能仅设置为“有证书就验证”。
2.3 如何证明配置生效
| 实验 | 单向认证应有结果 | 强制双向认证应有结果 |
|---|---|---|
| 客户端不提供证书 | 握手与业务正常 | 拒绝连接或拒绝继续传输业务 |
| 客户端提供可信且用途正确的证书 | 服务器未请求时通常不会发送 | 握手与业务正常 |
| 客户端证书由陌生 CA 签发 | 不能据此证明服务端校验客户端 | 应拒绝 |
| 服务器身份与预期名称不匹配 | 客户端拒绝 | 客户端拒绝 |
注意:TLS 1.3 客户端可能先完成本地握手调用,随后在读写时才收到服务端拒绝客户端证书的告警。验收应同时检查服务端结果和业务是否成功,不只看客户端打印的版本号。
3. 证书、私钥与信任链
3.1 各类文件的职责
| 文件 | 内容 | 是否秘密 | 分发对象 |
|---|---|---|---|
ca.cert.pem | CA 公钥、身份和签名 | 否,但分发过程必须可信 | 需要信任此 CA 的验证端 |
ca.key.pem | CA 签名私钥 | 是,最高敏感级别 | 仅签发环境,不部署到主站或设备 |
server.cert.pem | 服务器身份、公钥和扩展 | 否 | 服务器,在握手时提供 |
server.key.pem | 服务器私钥 | 是 | 仅服务器 |
client.cert.pem | 客户端身份、公钥和扩展 | 否 | 客户端,按请求提供 |
client.key.pem | 客户端私钥 | 是 | 仅客户端 |
*.csr.pem | 证书签名请求 | 通常不是秘密 | 提交给 CA;不是最终证书 |
*.keys | 实验会话密钥日志 | 是,可解密对应会话 | 仅实验分析人员 |
PEM 是带 BEGIN/END 标记的文本编码,DER 是二进制编码,PKCS#12(常见 .p12/.pfx)可以打包证书和私钥。文件扩展名不决定 TLS 版本。
3.2 关键 X.509 扩展
| 字段/扩展 | 本文设置 | 用途 |
|---|---|---|
| Subject | 服务端名称或客户端名称 | 展示身份;不能只看 CN 判断服务器身份 |
| Issuer | 实验根 CA | 标识签发者,需配合签名验证 |
| Serial Number | 当前 CA 下的证书序列号 | 唯一定位证书,用于吊销和审计 |
| Validity | 叶子证书默认 365 天 | 验证端时钟必须可靠 |
| Basic Constraints | 根 CA 为 CA:TRUE,叶子为 CA:FALSE | 防止把叶子证书作为签发 CA |
| Key Usage | 叶子为 digitalSignature | 本文 RSA 身份签名,不使用静态 RSA 密钥交换 |
| Extended Key Usage | 服务器 serverAuth;客户端 clientAuth | 限定用途 |
| Subject Alternative Name | 服务器 DNS/IP;客户端 URI | 匹配预期身份或映射应用角色 |
通过 IP 连接并按 IP 验证时,证书必须包含相应的 IP 类型 SAN;把 192.0.2.10 写成 DNS 类型不等价。SNI 用于服务器选择证书,发送 SNI 不等于执行主机名验证。OpenSSL 分别提供 -servername、-verify_hostname 和 -verify_ip。OpenSSL 验证选项
3.3 实际验证顺序
图示表达检查项目,具体库的内部执行顺序可能不同。任一必要检查失败都应终止连接。
生产环境通常采用“根 CA → 中间 CA → 设备证书”。服务端发送叶子和所需中间证书,根 CA 通常通过可信渠道预置,无需随握手发送。本文使用根 CA 直接签发叶子,便于实验。
吊销不是自动完成的:配置 CA 信任并不表示库会自动联网下载 CRL 或查询 OCSP。需要明确离线站点的 CRL 更新方式、过期处理、失联策略和审计机制。本文证书生成脚本不实现吊销服务。
3.4 身份密钥与会话密钥不是同一把钥匙
证书公钥用于验证身份签名;长期私钥用于证明身份;ECDHE 临时私钥用于计算共享秘密;通过密钥派生得到握手和业务流量密钥。正常 ECDHE 握手不会把会话密钥用证书公钥“加密发过去”。
双向业务使用各自方向的流量密钥。采用正确的临时密钥交换并销毁临时秘密后,事后泄露长期证书私钥一般不能解密此前录制的会话,这称为前向保密。泄露会话密钥日志则会破坏对应会话的保密性。
4. TLS 1.2 与 TLS 1.3 对照
| 维度 | TLS 1.2(本文使用 ECDHE + RSA + AES-GCM) | TLS 1.3(本文使用证书 + 临时密钥交换) |
|---|---|---|
| 身份证书 | X.509,可与 1.3 共用 | X.509,可与 1.2 共用 |
| 完整握手典型时延 | 约 2 RTT,不含 TCP | 约 1 RTT,不含 TCP;重试会增加 |
| 套件例子 | ECDHE-RSA-AES128-GCM-SHA256 | TLS_AES_128_GCM_SHA256 |
| 套件名称含义 | 同时体现密钥交换、认证和对称保护 | 主要表示 AEAD 与 HKDF 哈希;签名/组独立协商 |
| 服务器证书可见性 | 完整初始握手中通常明文可见 | ServerHello 后通常已加密 |
| 服务器私钥证明 | ECDHE 的 ServerKeyExchange 签名 | CertificateVerify 签名 |
| ChangeCipherSpec | 切换到协商的记录保护状态 | 可能仅为兼容性消息,不是密钥切换依据 |
| 恢复会话 | 会话 ID / Ticket 等 | 通常使用 PSK/Ticket,可结合新的密钥交换 |
| 0-RTT | 非本文讨论方式 | 可选早期数据,存在重放风险;本实验不启用 |
证书不标记“TLS 1.2 证书”或“TLS 1.3 证书”。兼容性取决于证书公钥类型、签名算法、用途、TLS 库及设备安全配置。本文生成 RSA 3072 位、SHA-256 签发的实验身份,配合支持所需签名方案的实现进行两种版本测试。
运行时通过最小/最大版本或 -tls1_2、-tls1_3 控制协议;TLS 1.2 套件用 OpenSSL -cipher,TLS 1.3 用 -ciphersuites。只设置 -cipher 不会限制 TLS 1.3 套件。
TLS 1.2 的基本握手见 RFC 5246。IEC 62351-3:2023 的官方范围同时涉及 TLS 1.2 与 1.3;是否可以在某设备上启用 1.3,要检查其标准版本、固件和一致性声明。IEC 62351-3:2023
5. TLS 认证完整流程
5.1 连接前的准备
先配置证书、私钥、信任锚、身份匹配规则、版本范围和客户端证书策略,再建立 TCP。TCP 三次握手成功只能证明端口可达,不能证明 TLS 或业务成功。
以下时序是首次完整证书握手,不包含恢复会话、0-RTT 或握手后认证。时序图按 TLS 消息表示,不对应实际 TCP 包的数量。
5.2 TLS 1.2 单向/双向认证时序
逐步理解:
ClientHello列出客户端能力,并携带随机数及相关扩展。ServerHello确定双方共同支持的参数;没有共同参数则失败。Certificate提供身份与公钥。证书公开可复制,所以仅收到证书并不能证明对端持有私钥。- 本文 ECDHE 场景中,
ServerKeyExchange携带临时参数及服务器签名,绑定临时密钥交换和服务器身份。 - 双向认证增加客户端证书和
CertificateVerify,后者证明客户端持有配套私钥。 - 两端分别计算共享秘密并派生密钥,私钥和最终会话密钥不在网上直接传输。
Finished绑定握手上下文并确认密钥一致;篡改协商消息会导致校验失败。- 双方按策略接受握手后,业务状态机开始工作。
单向认证省去图中的两组“客户端证书认证”消息,不会省去客户端 Finished,也不会省去客户端方向的数据加密。上述消息语义按 TLS 1.2 握手规范 整理。
5.3 TLS 1.3 单向/双向认证时序
TLS 1.3 把临时公钥参数提前到 Hello 中,并在 ServerHello 后保护大部分握手内容。CertificateVerify 证明私钥持有,Finished 验证握手上下文和密钥一致。握手密钥与业务密钥分阶段派生。服务器在协议允许的时点可提前发送应用数据,但要求 mTLS 的业务必须等客户端身份确认后再开放权限。TLS 1.3 握手与认证消息
5.4 抓包中容易混淆的四种情况
| 现象 | 原因 | 判断方法 |
|---|---|---|
TLS 1.3 仍出现 0x0303 | legacy 兼容字段 | 看 ServerHello 的 supported_versions 选择值 0x0304 |
| 看不到服务器或客户端证书 | 1.3 握手已加密,或使用了恢复会话 | 加载会话密钥,并重新建立完整握手 |
| 多一个 HelloRetryRequest | 服务器需要客户端提供另一组参数 | 找后续 ClientHello 和最终 ServerHello |
| 看到了 ChangeCipherSpec | 可能是 1.3 兼容消息 | 不能单凭它判断实际版本或业务加密起点 |
5.5 恢复、重连、关闭与重放
恢复会话可能不再发送完整证书链,不能拿恢复握手截图证明首次证书验证流程。验收首次握手时重启客户端,避免显式导入旧会话;必要时关闭服务端会话缓存和 Ticket。
正常 TLS 关闭可发送 close_notify,直接 TCP RST 或异常 EOF 要结合应用日志判断。长连接重新握手/重连后,IEC 104、MMS 等仍应重建各自的业务会话状态。
正常受保护记录可检测连接内的非法修改和序列问题,但应用在重连后重发同一条合法业务命令,TLS 无法判断业务是否应去重。TLS 1.3 早期数据还有额外重放风险,因此本文不用于遥控、写寄存器或定值操作。
6. 生成可供 TLS 1.2/1.3 共用的证书
6.1 环境与目录
本文仓库主体是 Markdown,脚本使用 Python 3.10+。证书生成依赖 OpenSSL 命令行;生成后在 EMS Simulate 中导入,不需要编写客户端、服务端或验证程序。
TLS加密认证/
├── TLS加密认证完全指南.md
├── scripts/generate_certs.py # 仅用于生成证书
├── images/ # 软件界面及 Wireshark 图片占位
├── certs/ # 本地生成的证书,导入软件使用
└── captures/ # 自行保存真实抓包
以下证书生成命令从 TLS加密认证 目录执行,适用于 PowerShell 和常见 Linux shell。Linux 若只有 python3,将 python 替换为 python3。建议使用仍受维护的 OpenSSL 3.x;未加入 PATH 时通过 --openssl 指定安装路径。
6.2 一键生成
完整脚本:generate_certs.py。脚本直接调用 OpenSSL,不依赖第三方 Python 包。
# 本机抓包实验:证书包含 DNS:tls-server.lab 与 IP:127.0.0.1
python scripts/generate_certs.py --out certs --dns tls-server.lab --ip 127.0.0.1
# 两台实验机:IP 换成 TLS 服务端实际地址;使用独立输出目录
python scripts/generate_certs.py --out certs-remote --dns tls-server.lab --ip 192.0.2.10 --days 365
Windows 指定路径示例(路径以实际安装为准):
python scripts/generate_certs.py --openssl "C:/Program Files/Git/usr/bin/openssl.exe" --out certs --dns tls-server.lab --ip 127.0.0.1
192.0.2.10 是说明用地址,不能原样视为现场设备。若 certs 已存在,脚本拒绝覆盖,使用新的目录名即可。
6.3 脚本具体做了什么
- 校验 DNS、IP、有效期和 OpenSSL 可执行文件。
- 创建全新目录,生成 RSA 3072 位根 CA 私钥。
- 自签根 CA,配置
CA:TRUE,pathlen:0和签发/吊销用途。 - 分别生成服务器、客户端的独立私钥和 CSR。
- 服务器加入 DNS/IP SAN 与
serverAuth;客户端加入 URI SAN 与clientAuth。 - 用根 CA 签发叶子证书,并验证链、用途、服务器 DNS 和 IP。
根 CA 有效期 3650 天,叶子默认 365 天。pathlen:0 表示此根 CA 不允许再签发下级非自颁发中间 CA,适合本文的直接签发结构。
脚本每次创建新 CA,并在该 CA 下使用两个固定且不同的叶子序列号;它不是可持续签发的 CA 数据库。生产签发应使用证书管理系统保证同一 CA 下的序列号唯一、留存签发记录并支持续期和吊销。
实验密钥说明:为无人值守运行示例,输出私钥未加口令。POSIX 下脚本限制目录和私钥权限;Windows 需检查 NTFS ACL。CA 私钥只应保留在签发侧,不能复制整套
certs到每个设备。.gitignore已忽略私钥、证书实验目录和抓包密钥日志。
6.4 等价的核心 OpenSSL 命令
下面展示脚本内部关键命令,便于审计;ca.cnf、server.ext、client.ext 由脚本生成。在全新的证书输出目录内理解/执行这些命令,不要对已有私钥重复执行。
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:3072 -out ca.key.pem
openssl req -new -x509 -sha256 -days 3650 -key ca.key.pem -out ca.cert.pem -config ca.cnf
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:3072 -out server.key.pem
openssl req -new -sha256 -key server.key.pem -subj /O=TLS-Lab/CN=tls-server.lab -out server.csr.pem
openssl x509 -req -sha256 -days 365 -in server.csr.pem -CA ca.cert.pem -CAkey ca.key.pem -set_serial 0x1001 -extfile server.ext -out server.cert.pem
服务器扩展文件内容:
basicConstraints = critical,CA:FALSE
keyUsage = critical,digitalSignature
extendedKeyUsage = serverAuth
subjectAltName = DNS:tls-server.lab,IP:127.0.0.1
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid,issuer
客户端使用独立密钥、序列号 0x1002、clientAuth 和 URI:urn:tls-lab:master01;完整流程以随附脚本为准。这个 URI 只是实验身份标签,不自动赋予任何业务权限,也不是 Modbus Security 的角色扩展。
6.5 生成后检查与材料分发
脚本结束时会显示服务器、客户端证书的验证结果。也可以使用系统证书查看器打开 .cert.pem,检查签发者、有效期、SAN 和用途;不要打开私钥内容截图。
| 准备用途 | 需要的文件 |
|---|---|
| 服务端单向认证 | server.cert.pem、server.key.pem |
| 客户端单向认证 | ca.cert.pem |
| 服务端双向认证 | server.cert.pem、server.key.pem、ca.cert.pem |
| 客户端双向认证 | client.cert.pem、client.key.pem、ca.cert.pem |
ca.key.pem 仅用于签发,不导入通信设备。实验可以复用一套身份材料比较四种协议,实际部署应为不同设备签发独立身份。

7. 在 EMS Simulate 中导入 TLS 证书
7.1 操作入口
在 EMS Simulate 中新增或编辑实验设备,先选择对应协议和服务端/客户端角色,再进入 “加密配置” 页签。打开 “启用 TLS”,在 “TLS 模式” 中选择 “单向认证 TLS” 或 “双向认证 TLS”。
界面根据角色和认证模式显示需要的材料,通过 “上传证书”、“上传私钥”、“上传 CA 证书” 分别选择文件。保存后重新打开配置,核对“已保存”文件名;若设备已运行,停止后重新启动,确保本次连接使用新配置。
界面约定:字段名称依据本地 EMS Simulate 当前界面整理。后续截图请注明实际安装版本;不同版本的布局可能不同。本文网络实验使用 TCP,IEC 61850 选择 MMS 通信。
7.2 单向认证:两端分别导入什么
| 操作位置 | 服务端 | 客户端 |
|---|---|---|
| 启用 TLS | 开启 | 开启 |
| TLS 模式 | 单向认证 TLS | 单向认证 TLS |
| 本端证书 → 上传证书 | server.cert.pem | 不需要,通常不显示此项 |
| 私钥 → 上传私钥 | server.key.pem | 不需要,通常不显示此项 |
| CA 证书 → 上传 CA 证书 | 此模式不要求 | ca.cert.pem |
| 作用 | 向客户端提供服务器身份 | 使用 CA 验证服务器证书链 |
服务端先启动监听,客户端再连接。单向认证下客户端没有上传自己的证书仍能完成通信,这是预期行为;客户端方向的请求同样经过加密。

完整展示服务端“加密配置”,包含“启用 TLS”“单向认证 TLS”、服务器证书和私钥的已保存文件名。

展示客户端“单向认证 TLS”和 CA 证书已保存状态。此模式不提交客户端证书。
7.3 双向认证:两端都导入三项材料
| 操作位置 | 服务端 | 客户端 |
|---|---|---|
| 启用 TLS | 开启 | 开启 |
| TLS 模式 | 双向认证 TLS | 双向认证 TLS |
| 本端证书 | server.cert.pem | client.cert.pem |
| 私钥 | server.key.pem | client.key.pem |
| CA 证书 | 信任客户端签发 CA,本例为 ca.cert.pem | 信任服务器签发 CA,本例为 ca.cert.pem |
客户端和服务端使用自己的证书及配套私钥,不能为了省事把同一个服务器身份同时导入两端。双方保存配置后重新建立连接。

服务端“加密配置”,展示“双向认证 TLS”和本端证书、私钥、CA 三项材料。

客户端“加密配置”,展示
client.cert.pem、client.key.pem、ca.cert.pem,避免与服务端文件混淆。
7.4 TLS 1.2/1.3 怎样确认
证书生成一次即可用于两个版本;导入证书时无需选择“1.2 证书”或“1.3 证书”。实际版本由双方 TLS 实现和配置协商决定,在连接详情或 Wireshark 的 ServerHello 中记录结果。
当前检查到的通用“加密配置”界面包含 TLS 开关、认证模式和证书上传,没有单独的 TLS 版本选择项。因此本文不要求点击不存在的版本下拉框。若实际安装版本提供版本范围配置,可分别将两端限定为 1.2 和 1.3;否则使用明确支持相应版本限制的测试对端与 EMS Simulate 联调,或者将尚未实际协商到的版本标为“待测”。
不能把两次自动协商到 TLS 1.3 的连接分别填成“1.2 已通过”和“1.3 已通过”。某个协议是否支持目标版本,还需结合其实际使用的 TLS 库和设备版本确认。
7.5 证书链验证与服务器名称验证
第 3 章介绍了完整身份验证需要考虑的项目。当前 EMS Simulate 的 TLS 模式说明明确采用 CA 证书链校验,不校验主机名或 IP。因此本文界面实验能够观察证书链认证及加密通信,不能宣称已经验证了服务器 SAN 与目标地址的匹配。
证书脚本仍生成 SAN,以便复用到执行严格身份匹配的设备上。正式部署应根据项目要求补齐预期对端身份约束;不要把所有同 CA 签发的设备都视作同一个目标身份。
8. 四种协议共用的实验准备
8.1 实验拓扑
在 EMS Simulate 中创建一组协议服务端和客户端,分别配置证书,客户端连接服务端的 TLS 监听地址。无需额外编写 TLS 代理或测试程序。
可在本机运行两个角色,也可在两台实验机上分别运行。以下使用 127.0.0.1 演示;两台机器时换成服务端的实验网卡地址,并开放对应端口。
8.2 端口与协议选择
本文统一使用各协议安全服务的标准 TCP 端口。服务端在该端口启用 TLS 监听,客户端连接相同端口;TLS 1.2 与 TLS 1.3 共用该端口,不按版本分配不同端口。
| 协议 | TLS 标准端口(TCP) | IANA 服务名 | 实验业务 |
|---|---|---|---|
| Modbus TCP Security | 802 | mbap-s | FC03 读取两个保持寄存器 |
| IEC 104 over TLS | 19998 | iec-104-sec | STARTDT 后总召唤 |
| IEC 61850 MMS over TLS | 3782 | iso-tp0s(Secure ISO TP0) | 建立关联并读取状态值 |
| DNP3 over TLS | 19999 | dnp-sec | Class 0 完整性读取 |
端口依据 IANA 登记:Modbus Security、IEC 104 安全服务、Secure ISO TP0 / MMS、DNP3 安全服务。
若 EMS Simulate 初始显示明文协议默认端口,启用 TLS 后将监听端口与客户端目标端口改为上表数值。后文连接地址、截图图注和 Wireshark 过滤器均按此表填写。使用标准端口仍需正确开启 TLS、导入证书并验证认证结果。
8.3 每次实验的操作顺序
- 准备一套证书,在两端配置相同协议、对应角色和业务点表。
- 按第 7 章导入证书,核对认证模式和文件名,保存配置。
- 启动 Wireshark,选择实验流量实际经过的接口。
- 启动服务端监听,再启动客户端连接。
- 在软件中执行该协议的读取操作,查看连接状态、报文记录和点值。
- 在 Wireshark 核对协商版本与 TLS 记录,保存抓包及截图。
- 切换认证模式时停止两端、修改材料并保存,再重新连接,避免继续使用旧连接。
截图保留设备名称、协议、服务端/客户端角色和端口,便于把配置与抓包对应起来。证书上传成功、TCP 已连接、TLS 成功、业务读取成功是四个不同层次的结果。
8.4 每个协议统一预留四张图
| 图片 | 内容 | 用途 |
|---|---|---|
| 服务端证书导入 | 协议/角色、TLS 模式、本端证书/私钥/CA | 证明服务端配置 |
| 客户端证书导入 | 协议/角色、TLS 模式、客户端材料 | 证明客户端配置 |
| 软件通信结果 | 连接记录、请求响应、点值 | 证明业务实际成功 |
| Wireshark 抓包 | 同一连接的 Hello、版本、受保护记录 | 证明线上使用 TLS |
后面各协议默认以双向认证说明。单向对照按第 7.2 节替换材料,重复相同业务操作即可。
9. Modbus TCP:证书导入与通信验证
9.1 实验目标
Modbus TCP 服务端配置 Unit ID=1,协议地址 0、1 的两个保持寄存器分别设为 0x0000、0x0001。客户端选择保持寄存器读取(功能码 03),起始地址 0、数量 2。
部分工具把协议地址 0 展示为 40001,应按软件实际地址基准设置;若采用十进制显示,对应值为 0、1。
9.2 服务端导入证书
在 EMS Simulate 新增或编辑 Modbus 设备,选择网络 TCP 模式和服务端角色,监听地址填 127.0.0.1、端口填 802。配置 Unit ID 和两个寄存器点,进入“加密配置”启用双向认证,上传服务器证书、私钥和 CA,保存后启动。

9.3 客户端导入证书
创建 Modbus TCP 客户端,目标地址为 127.0.0.1:802,Unit ID 与服务端一致。进入“加密配置”启用双向认证,上传客户端证书、客户端私钥及 CA,保存后连接。使用协议操作界面发起 FC03 读取。

9.4 操作结果与抓包
软件中应显示两个寄存器的预置值。检查请求和响应的事务 ID 对应,不能仅凭“已连接”判断成功。
预期请求示例:00 00 00 00 00 06 01 03 00 00 00 02
预期响应示例:00 00 00 00 00 07 01 03 04 00 00 00 00
事务 ID 由客户端实际生成,上述 0001 仅作说明。该十六进制示例用于读图,不要求用代码发送。Modbus Security 还涉及双向 X.509 认证、证书角色扩展等要求;成功导入普通实验 TLS 证书不等于通过该安全规范的完整一致性验证。Modbus Security 规范

上图是EMS Simulate 实际运行界面,保留Unit ID、FC03、起始地址、返回的两个寄存器值。图上显示是软件内部报文记录,并非网络明文。

使用
tcp.port == 802定位连接,再选定实际tcp.stream;展示 Hello、协商版本和受保护数据。若没有会话密钥,保留密文状态即可。
单向认证对照:停止两端,按第 7.2 节改为单向认证,服务端保留自己的证书和私钥,客户端仅导入 CA,重新连接并重复上述读取。每次实验分别保存抓包,不混用旧连接的结果。
10. IEC 104:证书导入与通信验证
10.1 实验目标
服务端公共地址 CA=1,配置至少一个遥信和一个遥测点。客户端在连接成功后完成 STARTDT,再发起总召唤,核对点值。
TLS 握手成功之后,IEC 104 仍需建立数据传输状态,不能直接把 TCP/TLS 连接状态当作总召唤完成。
10.2 服务端导入证书
新增或编辑 IEC 104 服务端设备,监听 127.0.0.1:19998,配置公共地址 1 与实验点表。打开“加密配置”,选择双向认证,分别上传服务器证书、服务器私钥、CA,保存后启动监听。

IEC 104 服务端“加密配置”,展示协议/角色、双向认证、本端证书、私钥、CA 的已保存状态;
10.3 客户端导入证书
新增或编辑 IEC 104 客户端,目标为 127.0.0.1:19998,公共地址与从站一致。在“加密配置”上传客户端证书、私钥、CA,选择双向认证并保存。连接后在协议操作界面执行总召唤,观察点值刷新及报文记录。

IEC 104 客户端“加密配置”,展示客户端证书、私钥、CA;
10.4 操作结果与抓包
预期业务顺序如下,部分初始化操作可能由客户端自动完成:
STARTDT act 的业务字节为 68 04 07 00 00 00,STARTDT con 为 68 04 0B 00 00 00。检查总召唤对应的公共地址、点号、原因码和数据值。TLS 不取代 IEC 104 的发送序号、确认窗口与 t0/t1/t2/t3 定时器。相关字段参见IEC 104 通信机制。

EMS Simulate 实际运行界面,保留公共地址、STARTDT、总召唤 、遥信遥测值。
单向认证对照:停止两端,按第 7.2 节改为单向认证,服务端保留自己的证书和私钥,客户端仅导入 CA,重新连接并重复上述读取。每次实验分别保存抓包,不混用旧连接的结果。
11. IEC 61850 MMS:证书导入与通信验证
11.1 实验目标
在服务端导入实验 SCL/ICD 模型,客户端浏览相同模型并读取一个实际存在的状态值。例如 simpleIOGenericIO/GGIO1.Ind1.stVal、功能约束 ST;实际对象名以导入模型为准。
本例只讨论 MMS 客户端/服务器通信。传统二层 GOOSE、SV 不通过本例 TCP/TLS 连接,其安全机制需按适用的 IEC 62351-6 等规范处理。IEC 62351-6 范围
11.2 服务端导入证书
创建 IEC 61850 服务端,导入 SCL/ICD 模型并准备一个可读点,配置 MMS 监听地址 127.0.0.1:3782。在“加密配置”选择双向认证,上传服务器证书、私钥和 CA。保存后启动服务端,确认模型已加载。

IEC 61850 MMS 服务端“加密配置”,展示协议/角色、双向认证、本端证书、私钥、CA 的已保存状态;
11.3 客户端导入证书
创建 IEC 61850 客户端,目标 MMS 地址设为 127.0.0.1:3782,关联参数与服务端配置一致。进入“加密配置”上传客户端证书、私钥、CA,选择双向认证并保存。建立连接后浏览逻辑设备和逻辑节点,选取实际存在的 stVal 进行读取。

IEC 61850 MMS 客户端“加密配置”,展示客户端证书、私钥、CA;图注注明目标地址与认证模式。
11.4 操作结果与抓包
先观察 MMS 关联建立,再核对 Read 请求和响应的 invokeID、变量引用和返回值。若 TLS 成功但关联失败,检查 AP-title、AE-qualifier、P/S/T-selector 等实际使用的关联参数。
MMS 对象名和 BER 编码长度依模型而变,使用软件界面生成请求即可,不需要拼接固定报文。关于模型参见IEC 61850 信息模型。安全 MMS 工程还需核对 IEC 62351-3/-4 等适用规范,不能用一次 TLS 读取成功替代全部安全功能验证。协议库维护方说明

EMS Simulate 实际运行界面,显示已加载模型、实际对象引用、MMS 关联、Read 返回值。

使用
tcp.port == 3782定位连接,再选定实际tcp.stream;展示 Hello、协商版本和受保护数据。若没有会话密钥,保留密文状态即可。
单向认证对照:停止两端,按第 7.2 节改为单向认证,服务端保留自己的证书和私钥,客户端仅导入 CA,重新连接并重复上述读取。每次实验分别保存抓包,不混用旧连接的结果。
12. DNP3 TCP:证书导入与通信验证
12.1 实验目标
创建 DNP3 TCP Outstation 与主站。示例主站链路地址为 1、从站为 10,配置一个二进制输入和一个模拟量输入点,发起 Class 0 完整性读取。
实验只读取模拟点,不执行真实输出控制。
12.2 服务端导入证书
创建 DNP3 服务端/Outstation,监听 127.0.0.1:19999,设置本端链路地址 10、对端主站地址 1 和实验点表。进入“加密配置”,选择双向认证,导入服务器证书、私钥和 CA,保存后启动。

DNP3 TCP 服务端“加密配置”,展示协议/角色、双向认证、本端证书、私钥、CA 的已保存状态;
12.3 客户端导入证书
创建 DNP3 客户端/主站,目标为 127.0.0.1:19999,主站链路地址 1、从站地址 10。启用双向认证,导入客户端证书、私钥及 CA,保存并连接。通过协议操作界面执行 Class 0/完整性读取,操作名称以实际版本为准。

DNP3 TCP 客户端“加密配置”,展示客户端证书、私钥、CA;
12.4 操作结果与抓包
预期读请求为 READ,对象 Group 60 Variation 1(Class 0);响应包含 IIN 及配置的静态点。核对 READ=0x01、RESPONSE=0x81、应用序号、链路地址和实际点值。若响应设置 CON,应按 DNP3 规则确认,通常由协议栈处理。
返回对象的 Variation 取决于点表和服务端配置。TLS 负责通道保护,不替代 DNP3 的应用确认、事件机制或 Secure Authentication。不要把“TLS 已连接”写成“已经启用 SAv5/SAv6”。DNP 用户组说明

EMS Simulate 实际运行界面,保留主从链路地址、Class 0 请求、IIN、静态点值。

图 21 待补:使用
tcp.port == 19999定位连接,再选定实际tcp.stream;展示 Hello、协商版本和受保护数据。若没有会话密钥,保留密文状态即可。
单向认证对照:停止两端,按第 7.2 节改为单向认证,服务端保留自己的证书和私钥,客户端仅导入 CA,重新连接并重复上述读取。每次实验分别保存抓包,不混用旧连接的结果。
13. Wireshark 抓包:把软件配置与网络结果对应起来
13.1 选择接口并开始抓包
本机实验选择能捕获回环流量的接口;Windows 通常使用 Npcap 回环接口,Linux 通常为 lo。跨机器选择实际传输 TLS 流量的网卡。先开始抓包,再在 EMS Simulate 中连接客户端,否则可能漏掉握手。
可以使用以下显示过滤器,不需要运行命令:
| 实验 | Wireshark 显示过滤器 |
|---|---|
| Modbus | tcp.port == 802 |
| IEC 104 | tcp.port == 19998 |
| MMS | tcp.port == 3782 |
| DNP3 | tcp.port == 19999 |
| 单个连接 | tcp.stream eq 3,3 替换为实际编号 |
| ClientHello / ServerHello | tls.handshake.type == 1 / tls.handshake.type == 2 |
| Certificate / CertificateRequest | tls.handshake.type == 11 / tls.handshake.type == 13 |
| CertificateVerify / Finished | tls.handshake.type == 15 / tls.handshake.type == 20 |
| TLS 告警 | tls.alert_message |
若该连接未自动识别 TLS,在 Wireshark 的“Decode As / 解码为”中将对应 TCP 连接分派为 TLS。不要把加密流直接强制解码为 Modbus、MMS 或 DNP3。
13.2 TLS 1.2 的认证证据
首次完整 TLS 1.2 握手通常能直接看到服务器证书;双向认证时还可看到 CertificateRequest 和客户端 Certificate/CertificateVerify。将这些消息与两端导入的证书对应,记录序列号或 Subject,避免只凭一行 Application Data 判断认证方式。

13.3 TLS 1.3 的认证证据
TLS 1.3 在 ServerHello 后加密大部分握手消息。没有会话密钥时,不能要求截图中直接展开客户端证书;应结合 ServerHello 选择版本、软件两端证书配置、连接记录和失败对照来判断。
在 ServerHello 中检查 supported_versions 的选择值 0x0304。兼容字段中的 0x0303 不表示实际协商成 TLS 1.2。

13.4 可选解密:以实际导出能力为前提
普通抓包不需要解密也能记录实际 TLS 版本和受保护数据。如果实验版本或 TLS 后端提供会话密钥日志导出能力,可将对应文件填入 Wireshark“编辑 → 首选项 → Protocols → TLS → (Pre)-Master-Secret log filename”,再重新加载抓包。
当前检查到的 EMS Simulate 通用 TLS 配置界面没有会话密钥导出入口,本文不将此项作为必做步骤,也不要求运行代码补出密钥。没有密钥时,使用 EMS 软件中的请求/响应及点值截图 + Wireshark TLS 抓包 作为两类对应证据。
ECDHE/TLS 1.3 一般不能只靠服务器私钥解密;SSLKEYLOGFILE 也不是对所有程序自动生效的开关。Wireshark TLS 说明
14. 在软件界面中做失败对照
14.1 客户端没有提供证书
先确认双向认证下正常读取成功。停止客户端,把客户端改为“单向认证 TLS”,仅保留 CA;服务端保持“双向认证 TLS”不变。重新连接,观察服务端拒绝和业务读取失败。
如果直接在双向模式删除必需文件,界面可能在保存时就拦截,这只能证明配置检查,不能证明发生过失败握手。通过客户端单向、服务端双向的组合,才能观察“客户端没有提交证书”的实际连接处理。

14.2 使用不受信任的证书
使用另一套实验 CA 签发的客户端证书及配套私钥,在客户端双向模式下替换“本端证书”和“私钥”;客户端验证服务器的 CA 仍保留原 CA,服务端信任的 CA 也不变。
重新连接应被服务端拒绝。这样能够把失败定位为“服务端不信任客户端身份”,避免同时替换双方 CA 导致原因不清。另一套证书可重复使用第 6 章脚本,指定新的输出目录生成。

14.3 其他可通过界面观察的场景
| 操作 | 应观察的结果 | 结论范围 |
|---|---|---|
| 证书与私钥不配套 | 保存或启动失败,具体位置依版本 | 身份材料加载检查 |
| TLS 开关一开一关 | 无法完成预期 TLS 业务通信 | 通信模式不一致 |
| 客户端导入错误服务器 CA | 连接失败 | 服务器链不可信 |
| 使用已过期测试证书 | 加载或握手按策略拒绝 | 有效期检查;不要更改生产设备时钟 |
| 两端版本无交集 | 不应建立 TLS | 仅在实际版本配置/对端能力可控时执行 |
| 改变目标地址但仍用同 CA 证书 | 当前链校验模式不保证拒绝 | 不能作为 SAN/主机名校验通过的证据 |
失败文本随实现可能不同,结合配置、连接记录和业务结果判断,不要求出现固定英文错误。TLS 1.3 拒绝可能在客户端开始读写时才体现,不只看初始连接提示。
14.4 按层排障
| 现象 | 首先检查 |
|---|---|
| 服务端无法启动 | 监听端口冲突、证书私钥配套、文件保存状态 |
| TCP 不可达 | 地址、监听、网卡、防火墙 |
| TCP 可达但 TLS 失败 | 双端 TLS 开关、模式、CA、身份材料、有效期 |
| TLS 成功但 Modbus 读失败 | Unit ID、地址基准、寄存器类型和数量 |
| TLS 成功但 IEC 104 无点值 | STARTDT、公共地址、总召唤与点表 |
| TLS 成功但 MMS 读取失败 | 关联参数、模型、对象引用和访问权限 |
| TLS 成功但 DNP3 无点值 | 主从链路地址、Class 0、对象配置和 IIN |
| 看不到完整握手 | 是否在客户端连接前开始抓包、是否抓到恢复会话 |
| 1.3 看不到证书 | 握手受保护,未提供对应会话密钥 |
15. EMS Simulate 实测记录与验收
15.1 四协议记录表
下表均为待实测,不代表已运行软件或抓包。每个通过项都应包含正确业务请求/响应,并在抓包中确认实际 TLS 版本。
| 协议 | 1.2 单向 | 1.2 双向 | 1.3 单向 | 1.3 双向 | 业务证据 |
|---|---|---|---|---|---|
| Modbus TCP | 待测 | 待测 | 待测 | 待测 | FC03 两个寄存器 |
| IEC 104 | 待测 | 待测 | 待测 | 待测 | STARTDT + 总召唤 |
| IEC 61850 MMS | 待测 | 待测 | 待测 | 待测 | 关联 + Read |
| DNP3 TCP | 待测 | 待测 | 待测 | 待测 | Class 0 READ/RESPONSE |
无法限定版本时填写实际协商版本,未覆盖项保留待测。单向模式成功不能替代双向模式验收;双向认证成功也不等于完成角色或点位授权。
15.2 单次实验记录模板
| 项目 | 实际填写 |
|---|---|
| EMS Simulate 版本 / 操作系统 | 待填写 |
| 协议、服务端与客户端设备名称 | 待填写 |
| 监听与目标 IP/端口 | 待填写 |
| 单向 / 双向认证 | 待填写 |
| 两端证书与 CA 标识,不记录私钥内容 | 待填写 |
| 实际 TLS 版本、可观察到的套件 | 待填写 |
| 业务请求与响应结果 | 待填写 |
| pcapng 名称、tcp.stream、关键帧号 | 待填写 |
| 对应截图编号 | 待填写 |
| 失败对照结果 | 待填写 |
15.3 通过标准
- 两端证书已按角色正确导入,并使用保存后的新连接。
- 软件连接结果与抓包对应,实际协商版本记录准确。
- 正确业务请求得到正确响应,软件报文/点值与实验配置一致。
- 双向认证时,无客户端证书、不受信任客户端证书等实际握手被拒绝。
- 截图明确区分软件内部业务显示与线上 TLS 记录;未解密数据不伪装成明文分析。
15.4 从实验到工程部署
实验使用简化 CA 和普通身份材料。正式部署仍需确认设备独立身份、私钥保护、预期对端身份约束、证书续期与吊销、协议角色/点位授权、时间同步和对应标准版本的一致性要求。
EMS Simulate 的证书导入和协议联调帮助观察 TLS 行为;验收结论应写清已验证的机制及范围。本文不把界面说明或预期报文写成真实实测结果。
16. 参考资料与延伸阅读
以下来源用于核对协议与工具行为,检索日期为 2026-09-06。标准条款和设备支持范围以项目采用的正式版本为准。
| 资料 | 用途 |
|---|---|
| RFC 5246 | TLS 1.2 消息与握手背景 |
| RFC 8446 | TLS 1.3 早期发布规范,便于对照设备资料 |
| RFC 9846 | TLS 1.3 更新规范,取代 RFC 8446;不是 TLS 1.4 |
| OpenSSL s_client | 客户端验证、版本和密钥日志 |
| OpenSSL s_server | 强制客户端证书及调试参数 |
| EMS Simulate 在线文档 | 软件配置与协议操作 |
| Wireshark TLS | 会话密钥与解密限制 |
| Modbus Security | 802、mTLS、角色扩展与 MBAP |
| IEC 62351-3:2023 | TCP/IP 协议使用 TLS 的安全配置范围 |
| IEC 62351-6:2020 | IEC 61850 安全机制范围 |
| DNP 用户组资料 | 区分 TLS 与 DNP3 认证 |
本仓库关联文档:IEC 104 报文结构、IEC 61850 通信协议栈、DNP3 应用层框架。
附录 A:完整证书生成脚本
以下内容与随附 scripts/generate_certs.py 一致,可直接保存为 Python 文件运行。脚本生成 X.509 身份证书;TLS 1.2/1.3 的选择在握手配置中完成。
#!/usr/bin/env python3
"""Generate a LAB CA and RSA certificates shared by TLS 1.2 and TLS 1.3.
Requires Python 3.10+ and OpenSSL 1.1.1+ (prefer a supported 3.x release).
Private keys are unencrypted: lab use only. Never overwrite an existing output.
"""
import argparse
import ipaddress
import os
from pathlib import Path
import re
import shutil
import subprocess
def generate(out, openssl, dns, ip, days):
if not re.fullmatch(r"[A-Za-z0-9](?:[A-Za-z0-9.-]*[A-Za-z0-9])?", dns):
raise ValueError("DNS must be a plain ASCII hostname")
ip = str(ipaddress.ip_address(ip))
if not 1 <= days <= 825:
raise ValueError("days must be between 1 and 825")
exe = shutil.which(openssl)
if exe is None:
raise ValueError("OpenSSL not found; use --openssl with its executable path")
subprocess.run([exe, "version"], check=True)
out = Path(out).resolve()
out.mkdir(parents=True, exist_ok=False)
if os.name != "nt":
out.chmod(0o700)
def run(*args):
subprocess.run([exe, *args], cwd=out, check=True)
(out / "ca.cnf").write_text("""[req]
prompt = no
distinguished_name = dn
x509_extensions = v3_ca
[dn]
CN = Industrial TLS Lab Root CA
O = TLS Lab
[v3_ca]
basicConstraints = critical,CA:TRUE,pathlen:0
keyUsage = critical,keyCertSign,cRLSign
subjectKeyIdentifier = hash
authorityKeyIdentifier = keyid:always
""", encoding="ascii")
run("genpkey", "-algorithm", "RSA", "-pkeyopt", "rsa_keygen_bits:3072",
"-out", "ca.key.pem")
run("req", "-new", "-x509", "-sha256", "-days", "3650",
"-key", "ca.key.pem", "-out", "ca.cert.pem", "-config", "ca.cnf")
for role, cn, eku, san, serial in (
("server", dns, "serverAuth", f"DNS:{dns},IP:{ip}", "0x1001"),
("client", "master01", "clientAuth", "URI:urn:tls-lab:master01", "0x1002"),
):
(out / f"{role}.ext").write_text(
"basicConstraints = critical,CA:FALSE\n"
"keyUsage = critical,digitalSignature\n"
f"extendedKeyUsage = {eku}\nsubjectAltName = {san}\n"
"subjectKeyIdentifier = hash\nauthorityKeyIdentifier = keyid,issuer\n",
encoding="ascii")
run("genpkey", "-algorithm", "RSA", "-pkeyopt", "rsa_keygen_bits:3072",
"-out", f"{role}.key.pem")
run("req", "-new", "-sha256", "-key", f"{role}.key.pem",
"-subj", f"/O=TLS Lab/CN={cn}", "-out", f"{role}.csr.pem")
run("x509", "-req", "-sha256", "-days", str(days),
"-in", f"{role}.csr.pem", "-CA", "ca.cert.pem", "-CAkey", "ca.key.pem",
"-set_serial", serial, "-extfile", f"{role}.ext", "-out", f"{role}.cert.pem")
run("verify", "-CAfile", "ca.cert.pem", "-purpose",
"sslserver" if role == "server" else "sslclient", f"{role}.cert.pem")
run("verify", "-CAfile", "ca.cert.pem", "-verify_hostname", dns, "server.cert.pem")
run("verify", "-CAfile", "ca.cert.pem", "-verify_ip", ip, "server.cert.pem")
if os.name != "nt":
for key in out.glob("*.key.pem"):
key.chmod(0o600)
print(f"Certificates ready: {out}")
if __name__ == "__main__":
p = argparse.ArgumentParser(description=__doc__)
p.add_argument("--out", default="certs")
p.add_argument("--openssl", default="openssl")
p.add_argument("--dns", default="tls-server.lab")
p.add_argument("--ip", default="127.0.0.1")
p.add_argument("--days", type=int, default=365)
a = p.parse_args()
try:
generate(a.out, a.openssl, a.dns, a.ip, a.days)
except (ValueError, OSError, subprocess.CalledProcessError) as exc:
p.exit(1, f"Generation failed: {exc}\n")

314

被折叠的 条评论
为什么被折叠?



