截至 2025 年 12 月 31 日这个节点,全国 ETC 正在完成国产密码算法迁移的收尾——高速路口的 RSU(路侧单元)从跑 3DES 改成跑 SM4,2025 年 5 月 1 日起新发行的 OBU/ETC 卡已全部是国密设备,存量 3DES 设备靠"增量停止、存量自然淘汰"逐步退场。切换不是因为 3DES 不够用,而是因为老算法把密钥体系的一个结构性弱点顶到了明面上:全网的 OBU、RSU 用同一套密钥逻辑,一旦某省的密钥体系被攻破,泄漏会顺着"部—省—应用"逐级放大,谁也没法从根上证明"这张卡是我发的、这笔通行费没被动过"。
这篇文章把 ETC 的密钥链路从原理层拆开:从部级根密钥怎么产生,到省级怎么领、怎么把"一车一钥"分散出来,再到 OBU 在产线上怎么把密钥烧进去、车道 RSU 每次交易怎么做到"一次一密",最后收在 3DES 到 SM4 的迁移为什么不是换算法那么简单。整条链上,密钥是唯一从头贯到尾的东西。
学习地图:ETC 密钥链路在这个坐标系里
先看 ETC 系统放平之后长什么样:
部级密钥管理中心
│ 部级ETC主密钥(根,SM4)
▼
省级密钥管理系统(领母卡/传输卡,在线认证+脱机保管)
│ 按省级标识分散 → 省级业务主密钥
▼
OBU 产线(一次发行母卡)──▶ OBU(车载单元,内置 ESAM 芯片)
车道 RSU(内置/插 PSAM 卡)──▶ CPU用户卡(记账卡/储值卡)
│
└─▶ 收费数据上传省中心 / 部路网中心
对应到硬件,密钥分散体系的落点分成三类:
- OBU(车载单元):装在车上的应答器,内含 ESAM/OBE-SAM 安全模块,保存应用密钥,负责应答车道的认证和扣费;
- RSU + PSAM(路侧/车道):收费车道的读写器,装载 PSAM 卡(终端安全模块),用来认证用户卡、完成扣费交易里的密码运算;
- CPU 用户卡 / CPC 卡:卡里的密钥体系独立于 OBU 又同源,是"另一条卡链"。
为什么值得把这条链拆到底:① ETC 是覆盖数亿用户、连接全国收费车道的基础设施,密钥体系设计错了,一次泄漏就是全网风险;② 密评(GB/T 39786-2021)把"密钥管理"作为独立层面,收费系统的密钥生成、存储、分发、轮换逐项要查;③ 眼下正在收尾的国密算法迁移(2025 年底前完成),本质是把"3DES + 分散"整链换成"SM4 + 分散",不改懂这条链,迁移就是在照抄老架构。
本系列在交通密钥主线上已拆过地铁 AFC 票卡(《地铁AFC密钥管理:票卡密钥从发卡到清分的全链路分析》),本篇是它的公路姊妹篇——AFC 是"卡—闸机—清分",ETC 是"OBU—RSU—部省结算",底层是同一种多级分散密钥结构。下一篇将回到信号侧讲《轨道交通信号系统安全规范解读》。
环节一:根密钥的诞生——部级主密钥怎么来、为什么只能躺在加密机里
ETC 的密钥体系是全国一张网、部省两级管,支撑它的那套软件+硬件合起来,行业里通常就叫 ETC 密钥管理系统——它管的是从部级根密钥到每台 OBU/PSAM 设备密钥的全生命周期,而不只是"发几张卡"。这一层的核心事实是:根密钥不在任何省份手里,只存在部级加密机里,全国的密钥从这一颗根逐级长出来。
部级ETC主密钥(全网唯一根,SM4,只存在部级加密机)
├── 按"省代码"分散 → 省级ETC业务主密钥
│ ├── 按"区域代码"再分散 → 市级/区域母密钥
│ └── 按"运营商标识+密钥用途"分散 → 各类应用母密钥
│ └── 按"OBU/PSAM 唯一标识"最终分散 → 一车一钥 / 一设备一钥
└── 认证签名密钥(SM2,签发证书、验证数据来源)
为什么根密钥必须硬件化?因为"分散"这个动作天然是单向放大的:用根密钥对"省代码 + 分散因子"做一次加密,就能派生出某一省的全部业务密钥。根密钥一旦以明文出现在任何一台服务器或任何一个人手里,谁拿着它就能离线派生全国任何一省、任何一张卡、任何一台 OBU 的密钥——这就是为什么各级密钥系统都有"领导卡/A、B 卡"机制:根密钥的装载需要多张领导卡按特定顺序协作,防止个别人私下生成密钥。
先把"谁管哪一级"分清楚:部级根密钥中心由交通运输部统管,是专用体系,根不出交通部系统——这个位置不在任何厂商产品的承接范围。真正落到省中心、运营单位头上、需要厂商帮一把的是自己这一级的同类问题:省级业务主密钥同样不能以明文躺在业务服务器上。做法与部级同构:本级配一台符合 GM/T 0030-2014 的服务器密码机(HSM)承接母钥——省级主密钥在加密机内部生成、内部参与分散运算,外部只有接口调用,没有明文读取;业务侧通过密钥管理平台申请,拿到的从来不是母钥本身。用密码学术语讲,这叫密钥不出硬件,只有使用权没有拥有权,部级如此,省级也一样。
逐句注解:为什么"分散"要放在加密机里而不是放在业务系统里?
分散的数学本质是"用母钥加密子因子"。如果分散放在业务服务器上,那台服务器上就必须有母钥明文——等于把整棵密钥树的根暴露在攻击面最大的地方。放在加密机里,母钥以硬件形态存在,业务侧每要一把子钥就调一次接口,中间没有明文落地的瞬间。密评现场评审员会直接问"你的主密钥在哪、谁能调用、调用有没有审计",这三个问题的答案都指向同一件事:本级母钥必须硬件化。
这一层还有一个容易漏的细节:领导卡与密钥恢复。部级主密钥的备份通常以"加密分片 + 多人分持"的形式存在,恢复时要多人到场、按流程拼接,不是某个人拿张 U 盘就能拷走。这在密评里对应"密钥备份与恢复"的检查项,答不上"备份在哪、谁有权限恢复"就是一个不符合项。
环节二:分散的机制——"一车一钥"是怎么算出来的
这是整条链最核心的机制,值得贴代码。OBU 产线烧录时,并不会把省级主密钥甚至部级根密钥写进车载单元(那样车丢了整棵树就漏了),而是用上层密钥分散出一把该车专用的应用密钥,只把这一把注入 ESAM。分散原理和地铁 AFC 同源:拿上层密钥对"分散标识 + 因子"做一次加密,输出就是下一层密钥。
package main
import (
"encoding/hex"
"fmt"
"github.com/tjfoc/gmsm/sm4"
)
// deriveKey 用上层密钥对"分散输入"做一次 SM4 加密,得到下一层密钥。
// master 与 in 都必须是 16 字节,否则直接报错,不做静默截断。
func deriveKey(master, in []byte) ([]byte, error) {
if len(master) != 16 {
return nil, fmt.Errorf("master key must be 16 bytes, got %d", len(master))
}
if len(in) != 16 {
return nil, fmt.Errorf("factor must be 16 bytes, got %d", len(in))
}
block, err := sm4.NewCipher(master) // SM4 分组固定 16 字节
if err != nil {
return nil, err
}
out := make([]byte, 16)
block.Encrypt(out, in) // 一次加密输出即分散密钥
return out, nil
}
// buildFactor 拼 16 字节分散输入:发行方标识(区域4B+运营商2B+保留1B+分散标识1B)
// 拼设备唯一编号,不足 16 字节的部分以 0x00 补位。
func buildFactor(region, operator, divFlag byte, devID []byte) []byte {
f := []byte{region, 0x00, 0x00, 0x00, operator, 0x00, 0x00, divFlag}
f = append(f, devID…) // 设备内部编号(OBU/PSAM 唯一标识)
return append(f, make([]byte, 16–len(f))…)[:16] // 补位到 16 字节
}
func main() {
// 省级主密钥(真实工程中由部级主密钥按省代码分散而来)
provMaster, _ := hex.DecodeString("0123456789ABCDEF0123456789ABCDEF")
// 分散输入:区域代码 0x31(沪) + 运营商 + 分散标识 0x01(应用主密钥)
// 拼这台 OBU 的设备编号,同一台车不同用途因子派生不同密钥
factor := buildFactor(0x31, 0x01, 0x01, []byte{0x00, 0x0A, 0x0B, 0x0C})
obuKey, err := deriveKey(provMaster, factor)
if err != nil {
panic(err)
}
fmt.Printf("OBU application key: %X\\n", obuKey)
}
拆开逐行看:
- sm4.NewCipher(master):SM4 是国密对称算法,分组固定 16 字节,密钥也是 16 字节。上层密钥作为主密钥传入;
- buildFactor(0x31, 0x01, 0x01, …):这里用一个示意构造来说明分层思路——发行方标识由 4 字节区域代码 + 2 字节运营商标识 + 1 字节保留 + 1 字节密钥分散标识构成,再拼设备唯一编号(真实 ETC 的字段编码规则以交通运输部现行密钥规范为准,不在这里展开)。分层要保证的是这件事:不同区域、不同运营商、不同用途(分散标识)、不同设备,派生出的密钥互不相同;
- block.Encrypt(out, in):一次 SM4 加密,输入是分散因子,输出就是这把设备的密钥。分散是不可逆的——即使一把子钥被攻破,反推母钥在计算上不可行(SM4 128 位密钥空间无可行捷径),这就是"一车一钥"能把泄漏半径锁死在单台设备上的原因。
这段代码用标准国密 SM4 库实测过(Go gmsm 与 Python gmssl 同为 GB/T 32907 实现,结果一致),输入输出为:
buildFactor(0x31,0x01,0x01,[00 0A 0B 0C]) = 3100000001000001000A0B0C00000000
OBU application key = 60B83CCDB9BF874E6321BFB5A2256932
测试向量已用 GB/T 32907 附录标准向量(0123…→681E…)校验过,密钥确实生效。把分散标识从 0x01 改成 0x02(扣费密钥),同一母钥下输出立即变为 84B3E7F2196740096EFE8A27E531961E——"同车不同用途、不同车同用途"都得到不同密钥,这就是一车一钥的密码学来源。
这个机制解决的正是 ETC 早期最怕的问题:共钥。 如果全网 OBU 用同一把应用密钥,攻击者拆一台设备读出密钥,就能伪造任意一辆车的认证应答——伪造通行、篡改扣费。分散之后变成"一车一钥":每台 OBU 的 ESAM 里只存自己的那把应用密钥,不存上层母钥;攻击者拆开一台车,最多拿到这一把,用它反推省级母钥、进而伪造全省车辆,计算上不可行。
分散的编排权也不该在 OBU 产线手里。产线只负责"把分散好的密钥注入 ESAM",而**"分散输入怎么拼、用哪一级密钥分散、密钥版本是多少"应该由密钥管理平台统一编排**——这就是省级密钥管理系统 / KSP 这类平台在这一层的位置:省级业务主密钥的领用、分散标识的注册、密钥版本的递增和归档全部集中管理,产线通过接口取用,拿不到母钥明文。
环节三:OBU 产线注入——密钥是怎么安全烧进车载单元的
分散算好了,接下来是真正的工程难点:全国每年上千万台 OBU 要过产线,每一台都要注入一把不同的密钥,注入过程还不能让密钥明文暴露在产线工人或普通 PC 能碰到的地方。
产线注入的标准做法是"一次发行 + 安全环境":
- 一次发行母卡:省级密钥系统先产生一批"一次发行母卡"(内含可派生产线的分散能力),在安全环境(专用工位、专用读卡器、禁网)中由产线安全终端使用;
- 产线注入:OBU 空卡上电 → 安全终端用母卡里的母钥,对"该台 OBU 的唯一标识 + 用途因子"现场分散 → 把结果写入 ESAM 的对应密钥区 → 校验写入成功 → 该台 OBU 密钥区置为"已发行",不可再写;
- 防导出:ESAM 的密钥区从设计上就不支持外部读出——产线只能"写进去、校验、使用",不能"读出来"。写入的密钥离开安全终端后,在 OBU 内部参与认证,但永远不会以明文形式回到外部总线。
这一层真正的风险不在"写入"这个动作,而在母卡管理。一张一次发行母卡如果流到产线外,等于拿到一把"能派生一批 OBU 密钥"的母钥。所以产线密钥管理有几条硬规矩:母卡领用登记到人、用完回收销毁、产线安全终端离线禁网、注入日志全程留痕。丢一张母卡的后果,远比丢一台 OBU 严重——前者能派生一批,后者只泄漏一把。
逐句注解:为什么是"安全终端"而不是产线电脑直接烧?
分散注入需要母钥参与运算。如果母钥进了产线 Windows 工控机,那台机器就成了全网 OBU 密钥体系的单点——被攻破等于批量泄漏。安全终端的意义是把母钥放在硬件安全模块形态(读卡器/加密芯片)里,产线电脑只能发指令、看结果,中间没有母钥明文经过。这和环节一"根不出硬件"是同一个原则,只是从部级下沉到了产线工位。
产线这一环,厂商真正要解决的是三件事:母钥不能进产线电脑、每台设备注入的密钥不能对不上账、注入完了还要能跟上级密钥体系对得上。车企侧解决过同款问题——ECU 安全芯片的批量密钥注入。安当在产线注入场景能承接的,是把批量密钥注入做进汽车密钥管理系统(CAS)这条产品线:CAS 管的就是"把车载/嵌入式安全芯片的密钥按批次注入进去"这件事——密钥的派生编排、母卡生命周期、注入批次与设备标识的对应关系全部在平台侧管,产线安全终端通过受控接口取用分散能力,母钥不进产线。平台记录"哪一批、哪些设备标识、用了哪一级密钥、哪个版本",密评密钥管理层面要的可追溯性就落在这张账上——产线侧要交的"注入台账",平台直接导得出。
环节四:车道交易——RSU 怎么用 PSAM 做"一次一密"
密钥注入之后,真正的高频场景来了:车以 60km/h 通过收费车道,车载 OBU 和路侧 RSU 要在几百毫秒内完成一次认证 + 扣费。
车道交易的密码动作分两段:
- 认证:RSU 发起交易,OBU 用卡内 ESAM 的应用密钥应答,证明"我是合法 OBU";RSU 侧由 PSAM 卡完成对 OBU 的认证和后续密码运算——PSAM 相当于车道侧的安全执行器,密钥和运算都在 PSAM 内部完成,RSU 主机接触不到密钥明文;
- 扣费与防重放:确认身份后,交易里要防止两件事——改金额(完整性)和重放(把上一次的合法交易报文截下来重发,白嫖通行)。
"一次一密"解决的就是重放。每次交易,车道侧生成一个随机挑战(challenge),参与会话密钥的生成或 MAC 计算,让每一次交易的密码报文都不相同:
// sessionKey 用应用密钥,对"随机挑战(前12字节)+ 交易计数器(后4字节)"做 SM4 运算,
// 得到本次交易的会话密钥。挑战每次交易不同 → 会话密钥每次不同 → 旧报文重放必失败。
func sessionKey(appKey, challenge []byte, counter uint32) ([]byte, error) {
if len(appKey) != 16 {
return nil, fmt.Errorf("app key must be 16 bytes, got %d", len(appKey))
}
if len(challenge) < 12 {
return nil, fmt.Errorf("challenge must be >= 12 bytes, got %d", len(challenge))
}
in := make([]byte, 16)
copy(in, challenge[:12]) // 车道下发的随机挑战
in[12] = byte(counter >> 24) // 交易计数器(防重放:序号单调递增)
in[13] = byte(counter >> 16)
in[14] = byte(counter >> 8)
in[15] = byte(counter)
block, err := sm4.NewCipher(appKey)
if err != nil {
return nil, err
}
out := make([]byte, 16)
block.Encrypt(out, in) // 输出即本次交易的会话密钥
return out, nil
}
拆开逐行看:
- copy(in, challenge[:12]):会话密钥的前 12 字节来自车道下发的随机挑战——挑战每次交易不同,这是"一次一密"的随机来源;
- in[12..15] = counter:后 4 字节放交易计数器(防重放:序号单调递增,同一挑战下旧报文重放会被识破)。把 32 位计数器按大端序拆成 4 个字节逐位写入;
- block.Encrypt(out, in):把拼好的 16 字节(挑战 12 + 计数 4)当输入,用应用密钥做一次 SM4 加密,输出即本次交易的会话密钥。输入任何一个字节不同,输出 16 字节全部不同,这是分组密码的雪崩效应,也是"挑战变 → 密钥变"能成立的根本原因。
用上面 60B83CCD… 那把应用密钥,同一挑战、计数器从 1 变 2,实测两次会话密钥完全不同:
挑战 = 000102030405060708090A0B
计数1 = 99ED60B0445D5F6C7C54B9BD771020C5
计数2 = 7990FB77D6FBB30F7109F2996F9650E9 ← 计数+1,会话密钥立即变化
这段的逻辑就两件事:挑战让每次会话密钥不同,计数器让交易序号单调递增。重放攻击者手里有一段合法报文,但那段报文对应的挑战和计数器已经"用过"了——车道侧再收到相同序号或无法匹配挑战的报文,直接判失败。这就是 ETC 里"一次一密"能落地的原因,也是它和地铁 AFC 闸机防重放完全同源的地方。
车道侧还有一个常被忽略的关键:PSAM 卡本身也是要"发"的。一台车道 RSU 配一张或多张 PSAM,PSAM 里的密钥同样由省级密钥系统按车道设备标识分散产生、通过安全渠道发放。车道数量大、分布广(全国几十万条车道),PSAM 的发放、启用、注销、回收要按设备逐张可追溯——哪条车道在用哪张 PSAM、它对应哪一级密钥、何时启用,都要能在密钥管理平台查得到。PSAM 卡的密钥生命周期管理,是这一层工程上真正难的地方。
环节五:国密迁移——为什么换算法不是换个加密函数那么简单
回到开头那个现场。这场 ETC 国密算法迁移由交通部在 2024 年正式定调(《关于推动全面发行国产密码算法ETC车载装置的通知》,交办公路函〔2024〕347号,配套技术方案与运营要求见公交办路函〔2024〕1052号),明确 2025 年 12 月 31 日前完成软硬件系统升级改造。表面是"把 3DES 换成 SM4",但真正动的是整条密钥链:
- 算法:对称加密 3DES → SM4(GB/T 32907-2016),摘要/完整性 3DES MAC → SM3(GB/T 32905-2016),认证签名补齐 SM2(GB/T 32918);
- 密钥体系:老体系的"部—省—应用"分级结构保留,但各级密钥都用新算法重新生成、重新分散,不是把老密钥换个算法继续用;
- 分散标识:发行方标识、密钥分散标识(两级/三级分散)的编码规则在国密体系下重新约定,新老设备按不同标识区分;
- 迁移节奏:过渡期采用双兼容——车道 RSU 和车载 OBU 同时支持 3DES 与 SM4。政策定的是"增量完全停止、存量自然淘汰":2025 年 5 月 1 日起新发行设备全部跑 SM4,存量双算法 OBU 回收到一次发行系统逐步关闭 3DES,尚未发行的先关 3DES 再按纯国密发行。数亿存量用户不可能"明天全部换新机",版本兼容窗口是迁移里最现实的工程约束——这也是为什么直到 2025 年底,河南、安徽等省仍在陆续招标车道改造与密钥系统建设。
双兼容窗口期有个绕不开的安全边界:窗口期内,攻击面是"老算法 + 新算法"两张网的并集。老 OBU 的 3DES 弱点在窗口期内依然存在,所以迁移不是"切完 SM4 就完事",而是要有明确的老算法下线计划:什么时候停止发行 3DES OBU、什么时候车道不再接受 3DES 认证、存量设备怎么分批回收。密钥管理平台在这一层的作用,是把"新老算法、新老密钥版本"的并行与下线节奏管起来——新版本密钥递增、旧版本归档但不立即清除,双兼容窗口一过,老版本密钥彻底下线。
这一段同时回应密评(GB/T 39786-2021):“应用和数据安全"层面查的就是密码应用是否合规,而国密迁移的意义不只是"用了国密算法”,而是让整条收费链的密码应用处在可被检验、可被审计的状态——用哪个算法、密钥存在哪、谁能调用、多久轮换,全部有据可查。落到系统上,这就是 ETC 密钥管理系统存在的意义:它把"密钥状态可查"这件事从制度承诺变成平台能力,密评专家要什么证据,平台上直接导得出记录。
回到现实:这条链上,用户要解决的问题安当能帮上哪些
把上面五个环节,按"用户要解决的问题 → 安当能帮上什么"摊开,就是一张选型对照图:
| 部级/省级根密钥 | 部级根由交通运输部统管;省级母钥不能明文落服务器 | 省级/运营侧配符合 GM/T 0030-2014 的 HSM 承接本级母钥 |
| 省级密钥派生与生命周期 | 分散标识注册、密钥版本、轮换、审计,不靠人工台账 | KSP(密钥全生命周期 + 三级密钥体系)把账管起来 |
| OBU 产线批量注入 | 母钥不进产线、注入批次与设备对得上账 | CAS 汽车密钥管理系统(车载芯片批量密钥注入) |
| 车道 PSAM / 交易密码服务 | PSAM 发放回收、会话密钥编排、防重放 | KSP(按设备标识分散)+ HSM(签名验签算力) |
| 收费数据落库与防篡改 | 落盘泄露、日志被改无法追责 | TDE(透明加密)+ KSP(日志数字签名) |
注意一个贯穿五层的共性:这条链上没有任何一个环节依赖 OBU 产线、车道主机或应用服务器保存母钥明文。根在部级加密机,分散在平台编排,产线和车道只通过接口/安全模块取用——这是密评"密钥管理"层面和《交通运输领域数据安全管理办法》都在反复强调的结构,也是 ETC 从 3DES 到国密迁移真正要守住的东西。
验收:当场怎么证明链路是通的
分散与防重放两段 Go 代码都能本地跑,几分钟出上面那组测试向量。落到真实系统上,省级密钥中心采购的密钥管理系统通常对外暴露 RESTful 接口,可以拿一条命令验证"接口能调、密钥服务在线":
# 省级系统向密钥管理平台请求一次 SM4 分散密钥(演示接口,字段以平台文档为准)
# factor_hex 即上文的分散输入;这里用实测值做演示,response 由本机按同逻辑算得
curl -s -X POST https://ksp.local/api/v1/key/derive \\
-H "Authorization: Bearer $TOKEN" \\
-H "Content-Type: application/json" \\
-d '{"master_key_id":"prov-2016-01","factor_hex":"3100000001000001000A0B0C00000000"}'
# → {"code":0,"data":{"key_hex":"60B83CCDB9BF874E6321BFB5A2256932","key_version":7}}
# 母钥 ID 带版本号 → 分散出的密钥版本可追溯;换 factor 立即得不同 key
对着这张单子逐条验:
- 密钥分散验证:拿一个 OBU 设备标识 + 用途因子,用对应母钥独立重算一遍分散密钥(上面那段 Go 代码几分钟跑完),与接口返回的 key_hex 比对一致,才说明分散编排是对的;
- 母钥不可导出:确认省级/运营侧加密机的密钥导出权限为 0,任何调用都拿不到母钥明文;领导卡恢复流程需多人到场;
- PSAM 台账可查:在密钥管理平台查任意一条车道的 PSAM,应能定位到"设备标识—密钥版本—启用时间—状态",注销/回收记录完整;
- 一次一密可验:在车道侧抓两次不同车通过时的认证报文,确认两次会话密钥/MAC 不同(挑战不同);重放上一次报文,应被拒绝;
- 迁移双兼容有下线计划:确认平台上有新老算法/密钥版本的并行台账,且有明确的老算法下线时间点与存量 OBU 回收计划。
系列导航
- 前文:《地铁AFC密钥管理:票卡密钥从发卡到清分的全链路分析》——拆过 AFC"卡—闸机—清分"的分散密钥链,本篇是公路 ETC 的同源姊妹篇;
- 再前文:《CBTC信号系统安全:一文讲透车载ATP认证与信号系统访问控制》与《铁路CTC调度系统安全实战》——信号侧两条线;
- 本篇定位:ETC"OBU—RSU—部省结算"密钥链路全貌;下一篇回到信号侧,讲《轨道交通信号系统安全规范解读:ATS 认证与国密改造合规落地》;
- 本系列主线:交通密码安全(信号 → AFC → 调度 → ETC),同一套分散密钥逻辑在不同交通子系统里的落地与差异。
上面的分散、会话密钥代码都能拿本地跑一遍,几分钟验证"一车一钥、一次一密"的机制。觉得有用就收藏 + 关注,交通密钥线接着拆。
文章作者:安当加密-焱垚
