Home

CipherShell v0.1.0 技术复盘:国密 SSH 双引擎、Web 终端与跨平台发布历程

一、这篇文章要解决什么问题

这篇不是产品宣传,而是我把 CipherShell v0.1.0 从“能连上”做成“可发布、可回归、可开源”的完整技术复盘。

这次我主要想回答 5 个问题:

  • 我们到底在做什么样的客户端,不是“另一个 SSH 工具”而已。
  • 国密场景里,为什么单引擎方案最终走不通。
  • 终端与 SFTP 为什么会反复出问题,根因在架构哪一层。
  • 我们最后采用了什么架构,关键实现是什么。
  • 这个版本为什么可以作为 v0.1.0 正式版,而不是“演示版”。

二、问题定义与约束:这不是标准 SSH 的单一场景

我们要同时覆盖三类链路:

  • 麒麟 V10 SP3 2403:偏 ecgm-sm2-sm3 的旧版国密服务端生态。
  • openEuler :2222:独立纯国密服务(sm2-sm3 族)。
  • openEuler :22:标准 SSH 兼容链路。

约束非常明确:

  • 不能只做“标准 SSH 正常”。
  • 不能只做“国密能协商但不可用”。
  • 不能把服务端配置问题误判成客户端算法实现问题。

这直接决定了客户端需要具备三种能力:

  • 算法策略切换:auto / gm_only / standard_only
  • 引擎能力分层:modern 国密引擎 + legacy ecgm 兼容引擎。
  • 自动回退与审计可解释:失败要有原因码,不允许黑盒行为。

三、架构演进:从“功能堆叠”到“策略驱动”

3.1 最终架构分层

flowchart LR A[Qt GUI MainWindow] --> B[SshEngineAdapter] B --> C[SshCommandBuilder] B --> D[OpenSSH modern ssh/sftp] B --> E[OpenSSH legacy ssh-legacy-ecgm/sftp-legacy-ecgm] A --> F[TerminalSessionWidget] F --> G[Web Terminal: Qt WebEngine + WebChannel + xterm.js] A --> H[SftpPanel] A --> I[AuditLogger] I --> J[audit.log JSON lines] A --> K[Credential Store AES-256-GCM]

我这版重点不是“新增功能”,而是把原来的耦合点拆开:

  • 连接策略决策集中在 SshEngineAdapter
  • 参数注入集中在 SshCommandBuilder
  • 终端渲染与 SSH IO 解耦到 Web Terminal 桥接。
  • SFTP 从“临时命令调用”改为“会话上下文驱动”。

3.2 为什么保留 gmssh-client 应用名

src/main.cpp 里保留:

  • app.setApplicationName("gmssh-client")

这是一个兼容性决策,不是命名疏漏。原因是我们已经有存量用户配置和本地凭据,如果直接改应用名,会导致旧配置目录失联。

同类兼容策略还包括配置目录回退路径:

  • QStandardPaths::AppConfigLocation / AppDataLocation
  • 兜底 ~/.gmssh-client

四、核心实现拆解

4.1 国密参数注入与探针判断

src/core/ssh_command_builder.cpp,国密参数注入是显式且固定的:

  • KexAlgorithms=ecgm-sm2-sm3,sm2-sm3
  • HostKeyAlgorithms=sm2,sm2-cert
  • PubkeyAcceptedAlgorithms=sm2,sm2-cert
  • Ciphers=sm4-ctr
  • MACs=hmac-sm3

探针分为三类错误信号:

  • 算法不匹配:no matching ... / Bad SSH2 ...
  • 本地引擎不支持参数:Unsupported KEX algorithm
  • 运行时不兼容:message authentication code incorrectverify KEX signature ... unexpected internal error

这让“协商失败”和“运行时失败”能被分开处理,不会被一个“连接失败”粗暴吞掉。

4.2 双引擎自动回退

src/core/ssh_engine_adapter.cpp 中,我把回退做成了可解释决策:

  • gm_probe_mac_incorrect_modern_to_legacy
  • gm_probe_verify_kex_internal_modern_to_legacy
  • gm_probe_internal_error_modern_to_legacy
  • gm_probe_verify_kex_internal_legacy_to_modern

同时把引擎偏好按主机维度缓存(含 host/port/user/策略),避免每个操作重复探针,减少“每点一次都像重连”。

4.3 SFTP 会话化改造

SFTP 不是简单调用 sftp 命令,而是和当前连接策略保持一致:

  • 强制注入 -S <selected ssh>,保证 sftp 跟随当前 SSH 引擎。
  • 使用 ControlMaster=auto + ControlPersist=300 + ControlPath=<hash> 复用连接。
  • ControlPath 包含主机、端口、用户、算法候选、签名策略的哈希,避免串线。
  • 遇到 host key changed,会走 ssh-keygen -R + 行过滤兜底修复。

这部分是 SFTP 从“能执行”到“可长期使用”的关键。

4.4 本地凭据加密

src/core/crypto_utils.cpp 的实现:

  • 密钥派生:PBKDF2-HMAC-SHA256150000 轮。
  • 对称算法:AES-256-GCM
  • 随机参数:salt(16B) + iv(12B) + tag(16B)
  • 设备绑定材料:machineUniqueId + hostName + USER/USERNAME,再做 SHA256

这不是绝对防护模型,但在桌面端“可用性与成本”之间是合理平衡。

4.5 终端子系统:WebEngine + WebChannel + xterm.js

TerminalSessionWidget 侧采用:

  • Qt QWebEngineView 加载 qrc:///terminal/terminal.html
  • QWebChannel 桥接输入输出
  • 前端通过 window.gmsshWriteBase64(...) 接收后端输出
  • PTY 尺寸变化回传后端,保持 top/vim 一致性

resources/terminal/terminal.html 内置 xterm.js + fit addon,并由桥接处理:

  • 输入 term.onData -> sendInputBase64
  • 输出 gmsshWriteBase64 -> term.write

这次真正解决的问题不是“能显示文本”,而是交互终端(top/vim)的屏幕模型一致性。

4.6 审计链路不是附属功能

审计事件是贯穿连接生命周期的,关键事件包括:

  • algorithm_fallback / engine_fallback / sftp_engine_fallback
  • terminal_input / terminal_output / terminal_control_input
  • web_terminal_bootstrap_failed

并且输入审计支持敏感提示场景脱敏(redacted=true),不是把所有键盘输入明文落盘。


五、最难的问题与根因

5.1 终端在 top/vim 场景异常

现象:

  • 输出错位、闪屏、光标行为异常。

根因:

  • 传统文本渲染路径对 alternate screen / 高频重绘场景支持不完整。

处理:

  • 切换到 Web Terminal 统一渲染模型。
  • 保留后端 PTY 语义,前端只负责终端协议展示。

5.2 SFTP 目录“自动回根目录”与操作卡顿

现象:

  • 输入路径后焦点和目录状态回退。
  • 每次操作都像重新建立会话。

根因:

  • SFTP 操作和会话状态耦合弱,策略与上下文没有被稳定持有。

处理:

  • 改成会话化执行,上下文绑定当前标签会话。
  • 连接复用 + 引擎偏好缓存,减少重复探测和重复握手。

5.3 国密联调里“看起来是客户端问题”

现象:

  • openEuler 系统 :22 纯国密失败。

根因:

  • 服务端配置/启动链路问题,不是客户端算法注入缺失。

处理:

  • 把纯国密验证固定到独立 :2222 服务端链路。
  • 客户端继续推进发布,不被服务端专项阻塞。

5.4 打包后运行异常

现象:

  • DMG 安装后出现空白页或启动失败。

根因:

  • Web terminal 资源加载、Qt 依赖闭包、签名流程是一个整体问题,不是单点命令。

处理:

  • 修正资源与引擎打包路径。
  • 强化签名与依赖校验流程。
  • 用安装态而不是构建态做最终回归。

六、这一版的“创新点”到底是什么

我认为 v0.1.0 真正的创新点不是算法本身,而是“工程化闭环”:

  • 把国密兼容做成“可解释策略引擎”,不是临时脚本。
  • 把终端体验统一到 Web 渲染,跨平台一致。
  • 把 SFTP 变成连接策略感知的会话组件。
  • 把审计作为系统内建能力,而不是外部附加。

换句话说,我们不是在“拼功能菜单”,而是在建立一个可持续迭代的底座。


七、验收证据链

这版能发布,是因为有完整证据链:

  • GUI 验收:docs/gui-acceptance-2026-04-29.md
  • 互通验收:docs/interop-acceptance-2026-05-07.md
  • 兼容矩阵:docs/gm-compatibility-matrix.md
  • 发布清单:docs/releases/v0.1.0.md

关键结果:

  • P1 矩阵 9/9 通过。
  • 麒麟链路:modern 遇 MAC incorrect 后自动回退 legacy,终端/SFTP 可用。
  • openEuler 纯国密 :2222sm2-sm3/sm2/sm4-ctr/hmac-sm3 链路通过。
  • macOS / Windows 发布产物均完成打包校验与安装态冒烟。

八、发布工程与开源治理

v0.1.0 对外产物:

  • ciphershell-0.1.0-Darwin.dmg
  • ciphershell-0.1.0-win64-setup.exe
  • ciphershell-0.1.0-win64-portable.zip
  • SHA256SUMS.txt

下载与项目地址

如果你想直接下载安装包或查看源码,可以从这里进入:

如果你在实际使用中有定制、改造或适配需求,也可以联系我,我会在能力范围内提供一些帮助。

治理动作包括:

  • 对外品牌统一为 CipherShell
  • 保留内部 GMSSH_* 兼容键,避免迁移时破坏存量数据。
  • 双语 README。
  • 许可证采用 AGPL-3.0-only,并补充 NOTICE

九、复盘结论:我这次最重要的三点经验

  • 先分清边界,再谈优化。
    • 客户端和服务端问题混在一起时,任何“修复”都会变慢。
  • 体验问题常常是架构问题。
    • top/vim 不是“小 bug”,它是终端模型是否正确的试金石。
  • 可发布性本身就是功能。
    • 签名、依赖闭包、安装态验证、审计可追溯,缺一项都不算“正式版”。

十、下一步

  • 继续收敛 openEuler :22 纯国密服务端链路专项。
  • 补齐更多认证形态回归(sm2_key / cert / x509)。
  • 在不破坏兼容的前提下,逐步收敛内部历史命名。

如果你也在做“安全连接类客户端”,这条线里最难的通常不是算法实现,而是跨环境兼容与工程交付闭环。

技术分享