民航AOC系统安全实战:签名验签与等保合规落地
某航司运行控制中心的一次内部复盘,争论了很久一个问题:那天夜里航班改航备降,到底是谁下的指令。运行控制系统里有一条改航记录,时间对得上;签派值班日志里写着「按指挥室要求」;指挥室的即时消息群里有一句「先备降」;机组收到的是电话通知。四条记录,四个版本,没有一条能证明自己是原始指令。复盘最后不了了之——不是因为查不清,而是因为这套指令体系从来没打算让指令自己证明自己。
坐标先立:交通运输线第 8 篇。这条线前几篇分别立了车载信号认证与访问控制、票卡密钥全链路、调度员认证与合规、不停车收费密钥派生、车站系统认证与国密改造、交通运输重要数据分类分级,上一篇回到机场侧讲了离港系统的事务库加密。轨道侧的 CBTC 与 CTC 已在早期几篇里完整承接,本文只作定位引用,不重讲轨道信号机制。这一篇的对象是航空公司运行控制中心(AOC)的运行控制指令——签派放行、改航备降、配载调整、油量调整、除冰、延误处置这些「一句话改变一个航班」的动作。与上一篇的边界很清楚:离港是机场旅客服务侧,AOC 是航司运行控制侧;离港关心数据在磁盘上安不安全,AOC 关心指令在流转中算不算数。
01 | 运行指令的这件事,难在哪儿
五条难点,每一条都出在「指令是一句话,责任是一条链」之间的落差上:
- 指令有签发主体,系统里却常常没有主体概念。放行、改航、备降这些决定,很多时候先发生在电话、对讲和即时消息里,系统里的那条记录是事后补录的。补录的人不一定是签发的人,补录的时间不一定是签发的时间。系统侧看到的是「有一条记录」,看不到「这条记录是谁的意志」。
- 指令有时序,而重放和乱序都有实际后果。一条过期的放行指令如果被系统重新受理,等于让航班按一份旧的运行条件起飞;一条新的改航指令如果被旧指令覆盖,机组拿到的就是过期信息。时钟和序号在运行控制里不是元数据,是安全性的一部分。
- 角色与指令类型的授权关系复杂,系统里却常常是「能登录就能发」。签派管放行与油量,机长管油量确认与最终决断,配载管载重平衡,机务管除冰与适航,运行指挥管协调与全局调整。这五类角色各管一段,交叉的地方有明确的规章要求。但落到系统里,很多运行控制系统给的是「岗位权限」而不是「指令权限」——进了这个岗位,系统里所有指令都能发。
- 可用性优先,安全动作不能拖慢运行。运行控制是典型的实时业务:一个航班在天上,几十个航班在地面等着放,指令系统的任何延迟都会直接传导到航班正常性。这决定了安全动作必须内嵌在指令流程里,不能做成一道额外的手工环节——要求签派员多敲一次密码、多等一次验证,最后的结果一定是有人绕过它。
- 事后调查要还原完整指令链,而指令是分散的。放行在签派系统、改航在运行控制、配载在载重平衡系统、除冰在机务系统。各签各的、各存各的,链断在系统边界上。真要复盘一个航班的完整决策过程,靠的是把四套系统的日志人工拼起来,而拼起来的东西本身不能自证。
这五条合起来,指向一个落地形态:给每条运行指令配一个可验证的签发主体、一个单调的时序、一张角色授权表,并把同一航班的指令串成一条链。
02 | 机制拆解:运行指令的签发链
先说清楚「签名验签」在 AOC 场景里到底解决什么。它不是给报文加个签名那么简单,它是把「一句话」变成「一份有主体、有时序、有上下文的证据」。三种形态的差别很大:
| 形态 | 主体可追溯 | 内容可自证 | 越权可拦 | 事后可还原 | 对运行的打扰 |
|---|---|---|---|---|---|
| 电话/对讲口头指令 | 弱,依赖在场人记忆 | 无 | 无 | 依赖人工记录 | 无,但代价在事后 |
| 系统录入留痕 | 中,能追到账号 | 弱,记录可改 | 弱,岗位权限粗 | 中,跨系统要拼接 | 低 |
| 签名签发 | 强,绑到密钥与角色 | 强,改一个字节即失败 | 强,按指令类型授权 | 强,链内自证 | 低,随指令动作完成 |
三种形态不是替代关系。口头指令在紧急处置里永远存在,问题在于它必须在事后可回溯的时间窗内落到签名链上——先执行、后补签,补签动作带时限、带原因、带告警,而不是让口头指令永远停留在口头。签名签发的价值不在消灭口头指令,在让每一条口头指令都必须付出一次「可被验证的确认」。
┌──────────── AOC 运行控制指令链(同一航班) ────────────┐
│ 放行(签派) ──▶ 改航(运行指挥) ──▶ 配载调整(配载) │
│ seq=1 seq=2 seq=3 │
│ │ │ │ │
│ prev="" prev=h(cmd1) prev=h(cmd2) │
│ │ │ │ │
│ 签名(私钥) 签名(私钥) 签名(私钥) │
└───────┼──────────────┼──────────────────┼────────────┘
▼ ▼ ▼
角色授权表 序号单调+时效 归档包整体签名
放行→签派 重放/超期即拒 摘要被换即失败
为什么运行控制侧讲「可用性优先」而不是把安全放一边
运行控制里有个绕不开的现实:验签失败时,航班不能停在空中。这常常被理解成「安全要给运行让路」,其实是个误解。正确的做法是把失效处置设计成流程的一部分,而不是把校验开关做成可跳过的选项:
- 正常路径上,验签失败即拒。这一条没有商量余地——指令不可信就是不可信,系统不能受理。
- 应急路径上,允许先执行后补签,但补签收三样东西:时限(比如多少分钟内必须补上)、原因(为什么走了应急)、告警(应急通道被使用要进值班台账并复核)。
- 应急通道的使用率是被监控的指标。一个长期高使用率的应急通道,说明正常路径设计得不合适——要么太慢,要么太繁琐,要改的是正常路径,不是放宽应急通道。
这个设计的关键在于:安全动作内嵌在指令动作里,签派员不需要额外操作——他在系统里点「放行」的那一刻,签名已经随指令完成。可用性优先说的是「不能让校验变成额外的一道手工环节」,不是「校验可以不做」。
为什么指令要串成链而不是各签各的
各签各的能解决「这条指令是谁发的、有没有被改」,解决不了「这条指令在决策序列里的位置」。运行控制的复盘问的从来不是单条指令,而是序列:先放行了什么,之后改了什么,谁覆盖了谁,最后一次有效指令是什么。串成链之后,每条指令带上一条指令的摘要,链本身就成了序列的证据——抽掉中间任何一条,后面的引用就对不上;替换任何一条的内容,它自己的签名先崩。这个特性把「拼日志」变成「验一条链」。
03 | 先跑通:七个环节
下面这份演示把 AOC 运行指令的签发链拆成七个环节跑一遍:指令分级与角色授权 → 签发主体绑定与内容未改 → 越权与冒名拦截 → 防重放与时效 → 指令链串联 → 密钥轮换后历史指令可验 → 归档包不可篡改。用国密 SM2/SM3,SM2 签名用固定随机数保证结果可复现。
# -*- coding: utf-8 -*-
# 民航AOC运行控制指令签名验签演示(国密 SM2/SM3)
# 场景:指令分级与签发角色授权 → 签发主体绑定 → 内容未改 → 越权拦截 →
# 防重放与时效 → 指令链串联 → 密钥轮换后历史指令可验 → 归档包不可篡改
# 说明:同一 SM2 私钥配固定随机数 K 仅为结果可复现;真实系统必须使用随机 K,否则会泄露私钥。
# 演示数据全部为合成数据,与任何真实航班无关。
import json
from gmssl import sm2, sm3, func
def sm3h(b: bytes) -> str:
return sm3.sm3_hash(func.bytes_to_list(b))
def digest(b: bytes) -> bytes:
return bytes.fromhex(sm3h(b))
def pub_of(p: str) -> str:
return sm2.CryptSM2(private_key=p, public_key="00")._kg(int(p, 16), sm2.default_ecc_table["g"])
def line(t: str, ok: bool):
print(f"[{t}] = {'True' if ok else 'False'}")
K = "20260701123456789abcdef0123456789abcdef0123456789abcdef012345678" # 固定K,仅演示用
# 签发主体:签派、机长、机务、配载、运行指挥,以及一个冒名者
DIS_P = ("4d5e6f708192a3b4" "c5d6e7f8091a2b3c" "4d5e6f708192a3b4" "c5d6e7f8091a2b3c")
CAP_P = ("5e6f708192a3b4c5" "d6e7f8091a2b3c4d" "5e6f708192a3b4c5" "d6e7f8091a2b3c4d")
MNT_P = ("6f708192a3b4c5d6" "e7f8091a2b3c4d5e" "6f708192a3b4c5d6" "e7f8091a2b3c4d5e")
LOD_P = ("708192a3b4c5d6e7" "f8091a2b3c4d5e6f" "708192a3b4c5d6e7" "f8091a2b3c4d5e6f")
OPS_P = ("8192a3b4c5d6e7f8" "091a2b3c4d5e6f70" "8192a3b4c5d6e7f8" "091a2b3c4d5e6f70")
ROG_P = ("92a3b4c5d6e7f809" "1a2b3c4d5e6f7081" "92a3b4c5d6e7f809" "1a2b3c4d5e6f7081")
DIS2_P = ("a3b4c5d6e7f8091a" "2b3c4d5e6f708192" "a3b4c5d6e7f8091a" "2b3c4d5e6f708193")
DIS = sm2.CryptSM2(private_key=DIS_P, public_key=pub_of(DIS_P))
CAP = sm2.CryptSM2(private_key=CAP_P, public_key=pub_of(CAP_P))
MNT = sm2.CryptSM2(private_key=MNT_P, public_key=pub_of(MNT_P))
LOD = sm2.CryptSM2(private_key=LOD_P, public_key=pub_of(LOD_P))
OPS = sm2.CryptSM2(private_key=OPS_P, public_key=pub_of(OPS_P))
ROG = sm2.CryptSM2(private_key=ROG_P, public_key=pub_of(ROG_P))
DIS2 = sm2.CryptSM2(private_key=DIS2_P, public_key=pub_of(DIS2_P))
RING = {"dispatch-v1": sm2.CryptSM2(private_key=None, public_key=pub_of(DIS_P)),
"captain-v1": sm2.CryptSM2(private_key=None, public_key=pub_of(CAP_P)),
"maint-v1": sm2.CryptSM2(private_key=None, public_key=pub_of(MNT_P)),
"load-v1": sm2.CryptSM2(private_key=None, public_key=pub_of(LOD_P)),
"opsctrl-v1": sm2.CryptSM2(private_key=None, public_key=pub_of(OPS_P))}
print("== 环节0:运行指令分级与签发角色授权 ==")
# 指令类型 → 允许签发角色;放行这类指令绑死签派,配载调整绑死配载
AUTH = {"放行": {"dispatch"}, "改航": {"dispatch", "opsctrl"},
"配载调整": {"load"}, "除冰": {"maint", "opsctrl"},
"油量调整": {"dispatch", "captain"}} # 按角色授权,不绑具体密钥版本
line("五类运行指令均有签发角色授权且放行类只授权签派",
len(AUTH) == 5 and AUTH["放行"] == {"dispatch"})
NOW = 1790000000
WINDOW = 900 # 运行指令时效窗口 15 分钟
def body_of(typ: str, no: str, field: str, val: str, seq: int, prev: str) -> dict:
return {"type": typ, "flight": no, "field": field, "val": val,
"seq": seq, "ts": NOW, "prev": prev}
def issue(signer_id: str, sk, typ: str, no: str, field: str, val: str, seq: int, prev: str = ""):
b = body_of(typ, no, field, val, seq, prev)
raw = json.dumps(b, sort_keys=True, ensure_ascii=False).encode()
return {"signer": signer_id, "body": b, "raw": raw, "sig": sk.sign(digest(raw), K)}
def accept(cmd: dict, last_seq: int, now: int = NOW) -> bool:
b = cmd["body"]
role = cmd["signer"].rsplit("-", 1)[0] # dispatch-v2 → dispatch
if b["type"] not in AUTH or role not in AUTH[b["type"]]:
return False # 角色与指令类型不匹配
if b["seq"] <= last_seq or now - b["ts"] > WINDOW:
return False # 重放或超期
verifier = RING.get(cmd["signer"])
return verifier is not None and verifier.verify(cmd["sig"], digest(cmd["raw"]))
print("== 环节1:签发主体绑定与内容未改 ==")
c1 = issue("dispatch-v1", DIS, "放行", "CA1501", "route", "PVG-CTU", 1)
line("签派签发的放行指令验签通过并被受理", accept(c1, 0))
tampered_raw = c1["raw"].replace(b"PVG-CTU", b"PVG-PEK")
line("航段被改(PVG-CTU→PVG-PEK)后验签失败",
not RING["dispatch-v1"].verify(c1["sig"], digest(tampered_raw)))
print("== 环节2:越权与冒名拦截 ==")
c_mnt = issue("maint-v1", MNT, "放行", "CA1501", "route", "PVG-CTU", 2)
line("机务角色签发的放行指令被拒(不在授权表内)", not accept(c_mnt, 1))
c_rog = issue("dispatch-v1", ROG, "放行", "CA1501", "route", "PVG-CTU", 2)
line("冒名主体(换私钥)签发的放行指令被拒", not accept(c_rog, 1))
print("== 环节3:防重放与时效 ==")
line("重放同一序号的旧指令被拒(序号不单调)", not accept(c1, 1))
line("超出时效窗口的指令被拒", not accept(c1, 0, NOW + 1200))
print("== 环节4:指令链串联 ==")
c2 = issue("dispatch-v1", DIS, "改航", "CA1501", "alt", "CTU→CKG", 2, sm3h(c1["raw"]))
line("链上第二条指令引用第一条摘要,串联成立",
c2["body"]["prev"] == sm3h(c1["raw"]) and accept(c2, 1))
broken = {"signer": c1["signer"], "body": dict(c1["body"]), "sig": c1["sig"]}
broken["body"]["val"] = "PVG-PEK"
broken["raw"] = json.dumps(broken["body"], sort_keys=True, ensure_ascii=False).encode()
line("替换链上任一指令但保留原签名,验签失败",
not RING["dispatch-v1"].verify(broken["sig"], digest(broken["raw"])))
print("== 环节5:密钥轮换后历史指令可验 ==")
RING["dispatch-v2"] = sm2.CryptSM2(private_key=None, public_key=pub_of(DIS2_P))
c3 = issue("dispatch-v2", DIS2, "放行", "CA1502", "route", "CTU-PVG", 3)
line("轮换后新指令由新密钥签发并验签通过", accept(c3, 2))
line("保留旧公钥,轮换前的历史指令仍可验", RING["dispatch-v1"].verify(c1["sig"], digest(c1["raw"])))
line("新密钥指令用旧公钥验签失败(确认密钥确已轮换)",
not RING["dispatch-v1"].verify(c3["sig"], digest(c3["raw"])))
print("== 环节6:运行指令归档包不可篡改 ==")
pack = json.dumps({"flight": "CA1501", "cmds": [sm3h(c1["raw"]), sm3h(c2["raw"])],
"ts": NOW}, sort_keys=True, ensure_ascii=False).encode()
pack_sig = OPS.sign(digest(pack), K)
line("归档包整体签名可验", RING["opsctrl-v1"].verify(pack_sig, digest(pack)))
line("归档包内指令摘要被替换后验签失败",
not RING["opsctrl-v1"].verify(pack_sig, digest(pack.replace(b"CA1501", b"CA1502"))))
== 环节0:运行指令分级与签发角色授权 ==
[五类运行指令均有签发角色授权且放行类只授权签派] = True
== 环节1:签发主体绑定与内容未改 ==
[签派签发的放行指令验签通过并被受理] = True
[航段被改(PVG-CTU→PVG-PEK)后验签失败] = True
== 环节2:越权与冒名拦截 ==
[机务角色签发的放行指令被拒(不在授权表内)] = True
[冒名主体(换私钥)签发的放行指令被拒] = True
== 环节3:防重放与时效 ==
[重放同一序号的旧指令被拒(序号不单调)] = True
[超出时效窗口的指令被拒] = True
== 环节4:指令链串联 ==
[链上第二条指令引用第一条摘要,串联成立] = True
[替换链上任一指令但保留原签名,验签失败] = True
== 环节5:密钥轮换后历史指令可验 ==
[轮换后新指令由新密钥签发并验签通过] = True
[保留旧公钥,轮换前的历史指令仍可验] = True
[新密钥指令用旧公钥验签失败(确认密钥确已轮换)] = True
== 环节6:运行指令归档包不可篡改 ==
[归档包整体签名可验] = True
[归档包内指令摘要被替换后验签失败] = True
逐段注解:
- 环节0 是指令分级与角色授权。五类运行指令各配一张允许签发的角色表:放行只授权签派,改航授权签派与运行指挥,配载调整授权配载,除冰授权机务与运行指挥,油量调整授权签派与机长。这张表是整条链路的起点——系统里先有「什么角色能发什么指令」,才有后面「这条指令该不该受理」的判断。注意授权绑的是角色而不是某一个密钥版本,否则密钥一轮换,整张表就要跟着改。
- 环节1 是签发主体绑定与内容未改。签派用自己的私钥签发放行指令,系统验签通过并受理;把航段从 PVG-CTU 改成 PVG-PEK 而保留原签名,验签失败。这一步解决开篇那个问题:指令自带主体,不需要事后靠人回忆是谁说的。
- 环节2 是越权与冒名拦截。机务角色签发放行指令,不在授权表内,被拒;冒名者用自己的私钥签发但自称签派,验签失败被拒。两条分别对应「有权限的人做了超出权限的事」和「没有权限的人冒充有权限的人」,是两种性质不同的风险,都要单独拦。
- 环节3 是防重放与时效。同一序号的旧指令重新提交,因为序号不单调被拒;超出时效窗口的指令被拒。运行指令的时序不是记账需求,是安全需求——过期指令被受理,后果落在航班上。
- 环节4 是指令链串联。链上第二条指令引用第一条的摘要,串联成立;替换链上任一指令但保留原签名,验签失败。有了这条链,「最后一次有效指令是什么」这个问题不需要人工拼接,验一遍链就知道。
- 环节5 是密钥轮换。轮换后新指令由新密钥签发并验签通过;保留旧公钥,轮换前的历史指令仍可验;新密钥签发的指令用旧公钥验签失败——这一条是反向确认:如果新旧公钥都能验,说明密钥根本没换。运行指令的保存期通常长于密钥的轮换周期,所以旧公钥必须在确认没有指令引用之后才能退役。
- 环节6 是归档。一个航班的完整指令包整体签名,签名可验;包内指令摘要被替换后验签失败。归档包是事后调查与合规举证时真正被调出来的东西,它的完整性要独立于任何单条指令来验证。
验证点:现场演示时按「角色授权表 → 签发验签 → 越权冒名 → 重放超期 → 指令链 → 轮换可验 → 归档包」的顺序调出来。最有说服力的三个画面:航段被改一个字符验签立刻失败(回应「记录怎么证明没被改过」)、机务签发放行指令被拒(回应「越权怎么拦」)、重放旧放行指令被拒(回应「过期指令为什么会害事」)。演示用固定随机数保证结果可复现;正式签发时随机数必须由安全模块内部生成,同一私钥配相同随机数会泄露私钥。
04 | 落地动作:分三条线怎么做
AOC 运行指令的安全改造按三条线推进,每条线配一个可对照的整改样本。
线一:指令资产化与角色授权(对应授权底账)
- 动作:把运行控制里真正影响航班的指令类型全部列出来——放行、改航、备降、返航、配载调整、油量调整、除冰、延误与取消处置——逐类标注允许签发的角色,形成「指令类型—角色—系统动作」授权底账;系统权限从岗位权限改成指令权限,一个账号能进系统不等于能发所有指令;交叉授权的部分(如油量调整涉及签派与机长)明确主签与确认关系。
- 整改样本(背景→动作→结果):某航司运行控制系统按岗位授权,签派岗在系统内可发全部类型指令 → 按指令类型重建授权表,放行与改航分离、配载调整独立、除冰归机务 → 系统内的越权动作从「技术上可行」变成「提交即被拒并留痕」。
- 注意:授权底账要跟着运行规章和岗位调整走。新增一类运行处置流程、调整一次值班架构,都要回头改表;表不改,新流程就是默认无授权。
线二:指令签发与链路闭环(对应签名验签)
- 动作:所有影响航班的指令在签发时完成签名,签名动作内嵌在提交动作里,操作人无额外感知;指令带单调序号与时间戳,重放与超期在受理侧拦截;同一航班的指令按序串联,前序摘要进后序指令;口头指令设置补签时限,逾期未补的进值班台账并告警。
- 整改样本:某企业(非民航)的关键调度指令靠即时消息下达,事后无法确认主体 → 指令进系统签发并签名,消息群只作通知通道 → 一次事后复盘从「翻聊天记录」变成「调指令链验签」。
- 注意:补签时限不要设得太长。时限越长,口头指令停留在口头的时间越久,链上的空白就越多;一般按班次或一个运行阶段为界,跨班次的补签要升级审批。
线三:密钥体系与归档举证(对应 KSP 与 HSM)
- 动作:签发密钥按角色与主体分发,私钥在安全模块或硬件密码机内生成与使用、不导出;公钥按版本管理,轮换后旧公钥在确认无指令引用前保留,保证历史指令长期可验;密钥的生成、分发、轮换、退役全程留痕;一个航班的指令包整体签名归档,归档包独立于单条指令验证完整性。
- 整改样本:某单位指令库加密归档但没有整体签名,一次举证时无法证明归档包是否被替换 → 归档环节加整体签名并单独留存签名值 → 举证时先验包再验链,材料的证明力明显提升。
- 注意:归档包的签名要单独留存一份。签名与数据放在同一个可写目录里,等于把锁和钥匙放在一起。
三条线的推进顺序:先建指令授权底账(线一),它是判断依据;再做签发与链路闭环(线二),它把每一条指令变成证据;最后收密钥与归档(线三),它决定这套东西在半年后、一年后还验得了吗。最常见的反向坑是先上签名功能不建授权表——签是签了,但机务也能签放行,签名只证明了「确实是机务本人发的」,没有拦住越权本身。
05 | 避坑清单:8 条最容易踩的坑
| # | 坑 | 后果 | 怎么验证避开了 |
|---|---|---|---|
| 1 | 只上签名不建指令授权表 | 越权照样发生,只是留下了证据 | 用非授权角色发指令,应被拒 |
| 2 | 授权绑到密钥版本而非角色 | 密钥一轮换授权全失效 | 轮换后用新密钥发同类指令,应受理 |
| 3 | 指令不带序号与时间戳 | 重放与过期指令被受理 | 重放旧序号指令,应被拒 |
| 4 | 口头指令无补签时限 | 链上长期留空白 | 抽查口头指令是否在时限内补齐 |
| 5 | 应急通道无监控 | 绕过校验成为常态 | 查应急通道使用率是否异常偏高 |
| 6 | 密钥轮换后旧公钥立即退役 | 历史指令验不了 | 随机抽历史指令验签,应通过 |
| 7 | 归档包无整体签名 | 无法证明归档被替换 | 替换包内摘要,验签应失败 |
| 8 | 签名动作做成额外手工环节 | 运行中被绕过 | 观察高峰期是否出现绕过行为 |
挑第 5 条展开:应急通道是最容易被设计成后门的东西。出发点是好的——总有系统故障、网络中断、密钥服务不可用的时刻,必须留一条能走通的路。问题在于应急通道一旦存在,被使用的概率取决于正常路径好不好用,而不取决于应急场景多不多。如果正常路径慢半秒,运行人员就会养成「反正有应急」的习惯,应急通道从兜底变成主路,前面所有的签名与授权都成了摆设。治理办法只有一条:把应急通道的使用当作一个运营指标来盯——按周统计使用次数、使用原因、补签耗时,异常升高就回头改正常路径,而不是放宽应急条件。
06 | 合规视角:要对上哪些要求
- 网络安全等级保护(GB/T 22239-2019):运行控制系统通常按三级定级。涉及本文的控制点集中在身份鉴别(一人一账号、双因素)、访问控制(按角色与指令类型授权)、数据完整性(指令与归档包签名)、数据保密性(运行数据落盘加密)、安全审计(四要素齐全、记录不可改)几处。本文不重复展开等级保护的框架本身。
- 民航运行控制相关的规章要求:民航对航班签派放行、运行控制的职责划分、记录保存与签派员资质有明确的行业规章口径。这类要求按现行版本执行,本文不引具体文号——行业文件更新较快,引文号容易失真,实施时以主管部门现行有效版本为准。方向上是一致的:放行主体要有资质、指令过程要有记录、记录要能追溯到人。
- 密码法与商用密码应用安全性评估:真实性靠签名、密钥受控靠密钥管理体系。运行指令链路上的密码应用主要是这两件事,评估时按应用与数据层面的要求逐项对应。
- 生产安全与事件调查的举证口径:运行控制指令在事件调查中是关键证据。证据链的基本要求是原始、完整、可追溯——签名与指令链正好对应这三条:签名保证原始,串联保证完整,角色授权保证可追溯。
审查实操上,AOC 侧被问得最多的是三件事:某条指令是谁签发的、越权能不能发生、事后的指令序列能不能复原。这三问分别对应签发主体绑定、指令类型授权表、指令链与归档包。把「指令授权底账 + 指令链验签记录 + 归档包签名」一起调出来,举证会顺畅很多。证据留存统一四要素:时间、账号、数据对象、结论。
一句话边界:AOC 运行指令的安全是「证据工程」不是「加密工程」——它的重心不是让指令看不见,而是让指令在流转的每一步都能证明自己是谁发的、什么时候发的、有没有被改过。
07 | 落地答案:产品怎么承接
把三条线的动作翻译成落地组件,产品以「答案」身份出现,能力作主语:
- 统一身份与角色授权(ASP):签派、机长、机务、配载、运行指挥的账号收敛到统一身份平台,一人一账号、双因素登录、按角色与指令类型授权;岗位变动与值班交接时权限随角色自动调整,临时授权带时限、带审批、带回收;所有签发动作审计到人。这一层把「能登录」和「能发指令」彻底分开。
- 签名密钥与验签服务(KSP + HSM):签发私钥在硬件密码机内生成与使用、不导出;公钥按版本管理,轮换后旧版本在确认无指令引用前保留,历史指令长期可验;验签服务与指令受理流程内嵌对接,指令提交即完成验签,不额外占用运行时间;密钥的生成、分发、轮换、退役全程留痕。
- 运行数据落盘加密(TDE):飞行计划、运行控制数据、指令链与归档包在数据库侧密文落盘,运维侧只见密文,防止「能改系统的人同时能改指令记录」。这一层与签名是互补的:签名保证改了能被发现,落盘加密保证直接拖库也拿不到明文。
- 审计与归档:指令签发、受理、拒绝、应急通道使用、补签动作全部进审计台账并签名入链;一个航班的指令包整体签名归档,签名值单独留存;审计记录与密钥操作记录可交叉核对,回溯时既能查「谁发了指令」,也能查「谁用了哪把密钥」。
这些组件的关系:统一身份管「谁能发」,密钥底座管「发了能作数」,落盘加密管「记录改不了也搬不走」,审计归档管「事后查得清」。航司侧按三条线(指令资产化与角色授权 / 指令签发与链路闭环 / 密钥体系与归档举证)逐项对上,即为运行控制指令从口头到可验证的落地全路径。
08 | 验收清单与下一步
AOC 运行指令安全 验收清单
| 检查项 | 对应要求 | 验证点 | 是否落地 |
|---|---|---|---|
| 指令类型—角色授权底账 | 访问控制 | 五类以上指令均有明确签发角色 | □ |
| 一人一账号与双因素 | 身份鉴别 | 无共用账号,登录需口令与动态码两要素 | □ |
| 越权签发被拒 | 访问控制 | 非授权角色发指令被拒并留痕 | □ |
| 冒名签发被拒 | 身份鉴别 | 换私钥自称他角色的指令被拒 | □ |
| 指令内容不可改 | 数据完整性 | 改一个字段保留原签名验签失败 | □ |
| 重放与超期拦截 | 运行安全 | 旧序号指令被拒 | □ |
| 指令链串联完整 | 数据完整性 | 抽掉中间一条后链断裂被检出 | □ |
| 应急通道受控 | 运行连续性 | 使用率与补签耗时有周统计 | □ |
| 轮换后历史指令可验 | 密钥生命周期 | 随机抽历史指令验签通过 | □ |
| 归档包整体签名 | 数据完整性 | 替换包内摘要验签失败 | □ |
趋势上,运行控制的指令管理会从「记录留存」走向「链上举证」:随着运行数据化程度提高,航班每一次决策都会被要求给出可验证的序列,而不是一段人工整理的时间线。另一个方向是安全动作会进一步隐形——签名、验签、授权判断全部内嵌到指令动作里,运行人员不需要知道背后发生了什么,只知道「提交成功」或「提交被拒,原因是越权」。安全做得越不像安全,越不容易被绕过。
下一篇预告:Day 11 最后一篇回到收费数据——《ETC与轨道交通AFC安全合规解读:国密合规要求与收费数据防泄露》,讲不停车收费与自动售检票这两类收费数据的密钥派生、交易防篡改与国密合规对位,把交通运输线前半段的票卡与收费主题收口。
文章作者:安当加密-焱垚,安全工程师,专注身份认证、数据加密领域。

107

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



