字数:约 4200 字
标签:AI 编程 / MCP / 国密 / 密评 / Rust
开始之前,先讲一个现编的故事
前段时间,一个做金融系统的朋友找我抱怨。
他们公司接到密评整改通知,要把一套核心交易系统里的 RSA-2048 签名全部换成 SM2。领导给了三个月,他自己搞了一个月没搞定,急了,就去问 AI。
他把 RSA 签名的代码贴给 LLM,说:"帮我改成 SM2。"
LLM 确实给出了代码,洋洋洒洒写了好几十行,用的是 BouncyCastle,API 也对。他满怀期待地跑起来,报错了:
SM2Engine cannot operate on empty data
他又问 AI,AI 给出了修复方案。再跑,又报错。来来回回折腾了三轮,最后还是没通——因为 SM2 签名有个 Z 值预处理步骤,那段代码根本没处理,而 AI 对这个细节语焉不详,每次给的答案都各说各话。
他最后的结论是:"AI 不懂国密。"
我跟他说:你冤枉它了。
不是 AI 不懂国密——大模型背过 GM/T 0009,知道 Z 值是怎么算的,知道 SM2 用的是 256 位素域,知道签名算法标识是 0x00020201。
问题是:AI 没手。
它只能说"应该这样写",但它没法自己跑起来验证一下生成的代码到底能不能签名、签出来的签名格式对不对、验签时另一端能不能认。
就像一个没有化验室的医生——知识到位,但只能靠经验下药,没法当场化验。
给 LLM 装上手:MCP Server 是什么
MCP(Model Context Protocol)是 Anthropic 在 2024 年提出的开放协议,核心思想很简单:
让 LLM 可以调用外部工具,获取实时数据,操作真实系统。
LLM 调用 MCP tool 就像人用手机里的 App——你告诉手机"帮我打车",手机调用滴滴 App,而不是光嘴上告诉你"你应该去路边打车"。
国密场景天然适合这个模式:
- 操作可离散化:SM2 签名、SM3 摘要、SM4 加解密、证书管理——每个都是一次输入→输出的操作,完美映射为 MCP tool
- 结果可验证:签出来的签名,同一个工具链可以立刻验签,闭环不需要真实设备
- 上下文连续:LLM 可以一步步"签名→验签→打包 CMS→验证证书链",像流水线一样串起来
我做了什么:gm-agent-stack
我把三个国密设备模拟器(Mock)各自包了一层 MCP Server,暴露为 LLM 可调用的工具:
┌──────────────────────────┐
│ LLM │ ← 你的 AI 助手
└──────────┬───────────────┘
│ MCP Tool Use
┌───────────────┼───────────────┐
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ skf-mock │ │ sdf-mock │ │ svs-mock │
│ GM/T 0016│ │ GM/T 0018│ │ GM/T 0029│
│ USB Key │ │ 加密机 │ │ 签名服务 │
│ :16000 │ │ :18000 │ │ :9000 │
└──────────┘ └──────────┘ └──────────┘
- skf-mock:模拟 GM/T 0016-2012 智能密码钥匙(USB Key),12 个 MCP tool
- sdf-mock:模拟 GM/T 0018-2023 服务端加密机(HSM),10 个 MCP tool
- svs-mock:模拟 GM/T 0029-2014 签名验签服务器,6 个 MCP tool
加起来 28 个 tool,覆盖了密评中最核心的三类密码设备。
实际效果
启动 svs-mock 后,把它加到 LLM:
docker run -d -p 9000:9000 svs-mock
LLM mcp add svs-mock --url http://localhost:9000/mcp
然后在 LLM 里对话:
我:用 SM2 对 "Hello World" 签名,然后验签,把全过程告诉我
LLM:
我用
svs_digest先计算 SM3 摘要(含 Z 值前缀),再用svs_sign进行 SM2 P1 格式签名...// svs_digest 结果 {"hex":"b94d27b9934d3e08a52e52d7da7dabfac484efe04294e576...","length":32} // svs_sign 结果 {"signed_data_hex":"3045022100f4a3b...","mode":"data"} // svs_verify 结果 {"valid":true}签名有效!整个流程:
- SM3(Z || data) → 摘要
- SM2 签名摘要 → DER SEQUENCE{r, s}
- 用公钥验签 → valid: true
对应的 Java BouncyCastle 代码:...
这就是"有手"和"没手"的区别:LLM 不只是告诉你"应该这样写",它自己跑了一遍,结果是真的。
技术实现
选型:为什么用 rmcp + Streamable HTTP
MCP 有三种传输层:stdio、SSE、Streamable HTTP。
- stdio:只适合本地进程,无法在网络上暴露
- SSE(Server-Sent Events):2025 年 3 月已被 MCP spec 标记为 deprecated
- Streamable HTTP:当前主推,单端口即可,LLM 都支持
Rust SDK 选 rmcp(modelcontextprotocol/rust-sdk,3300+ stars,官方背书)。一个 binary 同时跑 REST API 和 MCP:
// src/main.rs — 双模启动,共享业务层
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let cli = Cli::parse(); // --mode rest|mcp|both
let store = Arc::new(CertStore::from_config(&config)?);
let mut router = axum::Router::new();
match cli.mode {
Mode::Both => {
router = router
.merge(routes::build_router(store.clone())) // REST
.nest_service("/mcp", mcp::build_mcp_service(store)); // MCP
}
// ...
}
axum::serve(listener, router).await?;
}
关键:REST 和 MCP 共享同一个业务层(service/),不重复实现任何密码逻辑。
tool 粒度:场景化聚合,不做 C API 直映射
GM/T 标准里有 130+ 个 C 函数(SKF_xxx、SDF_xxx)。如果 1:1 映射为 MCP tool,LLM 会淹死在函数列表里。
错误做法(130 个 tool):
skf_init_dev
skf_connect_dev
skf_disconnect_dev
skf_get_dev_state
skf_open_dev
...
正确做法(场景聚合,12 个 tool):
skf_device_info // 枚举 + 连接 + 获取信息,一步到位
skf_open_app // 打开应用 + 验 PIN + 打开容器
skf_sm2_sign // 含 Z 值计算的完整签名流程
...
Init/Update/Final 三步流式接口全部内化到单个 tool 里,LLM 只需要传入完整数据。
tool description:每个字段必须说清楚
Anthropic 内部数据显示,tool description 是影响 LLM 调用准确率最关键的因素。字段描述漏写,LLM 经常乱猜。
踩过的坑:
// 错误:没说输入格式
pub data: String,
// 正确:明确 hex,举例子,说清楚语义
#[schemars(description = "待摘要的原始数据,hex 编码,例如 \"48656c6c6f\" = \"Hello\")"]
pub data_hex: String,
我的实践:所有二进制字段统一用 hex 编码,字段名加 _hex 后缀,description 里必须写清楚格式和含义。
#[tool_handler] 不能漏
这个坑我踩了很久。
rmcp 的 #[tool_router] 宏负责收集 tool 定义,#[tool_handler] 宏负责把 tool router 接入 MCP 协议处理链。缺少后者,服务启动正常,但 tools/list 返回空数组,LLM 看不到任何 tool。
#[tool_router] // ← 定义 tool
impl SvsMcpServer {
#[tool(description = "...")]
async fn svs_digest(&self, ...) -> String { ... }
}
#[tool_handler] // ← 接入协议,缺了这个 tools/list 返回空
impl ServerHandler for SvsMcpServer {
fn get_info(&self) -> ServerInfo { ... }
}
两个宏都要有,缺一不可。
StateLess 模式:每次 tool call 独立
加密机(SDF)和 USB Key(SKF)的真实 C API 是有状态的:打开设备 → 打开会话 → 操作 → 关闭会话。
MCP 是无状态的(每次 tool call 相互独立)。解法:
// MCP tool 内部自动打开/关闭会话
async fn sdf_sm2_sign(&self, Parameters(p): Parameters<SignParams>) -> String {
let mut session = 0u32;
sdf_impl::open_session(&mut session); // 开
let result = sdf_impl::sign(session, ...);
sdf_impl::close_session(session); // 关
// ...
}
每个 tool call 开头打开 session,结束关闭。对 mock 来说性能完全够用,对 LLM 来说这些 session 细节完全透明。
StateFul 模式的坑:StreamableHttpService::new 的类型推断
// 错误:类型参数 M 无法推断
let mcp_svc = StreamableHttpService::new(...);
// 正确:显式标注类型
let mcp_svc: StreamableHttpService<SvsMcpServer, LocalSessionManager> =
StreamableHttpService::new(...);
rmcp 的 StreamableHttpService 有两个泛型参数,Rust 编译器推断不出来,必须显式写。文档里没提,要么看源码,要么踩坑。
sdf-mock 和 skf-mock 的特殊之处
svs-mock 是一个 HTTP binary,直接加 MCP 层很自然。
但 sdf-mock 和 skf-mock 是动态链接库(cdylib),原本设计是给应用程序 FFI 调用,没有 HTTP server。
解法:Cargo.toml 保留 [lib](动态库),同时新增 [[bin]](MCP Server 可执行文件):
[lib]
name = "sdf_mock"
crate-type = ["cdylib", "rlib"] # 保留动态库不动
[[bin]]
name = "sdf-mcp"
path = "src/bin/mcp_server.rs" # 新增 MCP binary
MCP binary 直接 use sdf_mock::sdf_impl::* 复用所有业务逻辑,零重复实现。
三个真实踩坑
坑 1:target 目录权限问题
error: could not write to target/debug/.cargo-lock: permission denied
原因:之前用 root 跑过一次 cargo,锁文件被 root 持有。解法:所有 cargo 命令加 --target-dir /tmp/xxx,避开被 root 占用的目录。
坑 2:tools/list 返回空数组
服务跑通了,initialize 也正常,但 tool 列表是空的。查了半天 rmcp 源码才发现,#[tool_handler] 缺少。上文提到的坑,这里再次强调:两个宏都要有。
坑 3:SM2 密钥格式不统一
不同模块对 SM2 公钥的表示不一样:
- libsmx 接受
04||x||y(65 字节) - SDF C API 接受右对齐的
x(64字节)||y(64字节)(ECCrefPublicKey 结构) - X.509 证书里的 SubjectPublicKeyInfo 是带 OID 的 DER 结构
MCP tool 的输入输出统一用 04||x||y hex,内部根据目标 API 做转换。LLM 只需要知道一种格式。
现在就可以跑起来
# 克隆
git clone https://github.com/kintaiW/mp-mock
cd mp-mock/0029-svs-mock
# 启动(REST + MCP 双模,默认 9000 端口)
cargo run -- --mode both
# 添加到 LLM
LLM mcp add svs-mock --url http://localhost:9000/mcp
或者用 Docker:
docker run -d -p 9000:9000 \
-v $(pwd)/mock_certs.toml:/app/mock_certs.toml \
svs-mock
然后在 LLM 里问任何 SM2/SM3 相关的问题,它都可以自己跑起来验证。
后续计划
- GM-Bench:用这套工具链评测各大 LLM 的国密能力(DeepSeek、Qwen、GLM 谁更懂 SM2?)
- CCASA Agent/Harness:把三个 MCP Server 接入密评咨询 AI,实现"咨询→生成代码→验证→报告"全自动流程
- meta-repo:
gm-agent-stack一键docker compose up起全栈
最后
国密合规不是一个纯粹的技术问题,它还夹带着标准文件读不懂、真机买不起、测试环境搭不好的现实困境。
这套工具链的目标很简单:让 LLM 有手,让密评整改有一个可以跑通的沙箱。
不是替代工程师,是让工程师有个不知疲倦的助手在旁边帮你跑、帮你验、帮你说清楚 Z 值到底是什么。
代码在这里:gm-agent-stack
警告:本项目仅供学习、开发测试和密评模拟使用,严禁用于生产环境。Mock 实现不提供真实安全保证。