ARTICLE · 1243851
RSA + 机器码:自研视觉软件怎么做到"数学上不可伪造"?
一、场景:交付现场的第一道坎
视觉项目做到第二版,甲方设备科把东西拷走之前问了一句:
"软件我拷一份,装我自己电脑上能用吗?"
这个问题躲不开。自研工业软件的商业模式就建立在一个朴素的事实上:你卖的是"这台机器上的这个能力",不是一份文件。文件一旦能被无限复制,后面所有的交付、维护、升级都失去了载体。
所以要做授权。但"授权"这两个字,在工控场景里有个特殊要求:它不能成为产线上的新故障点。产线一天跑一万件,授权校验哪怕抖一下,后果都是整批产品被判 NG。
这两句话放在一起,就是这套授权体系的全部设计约束:
- 对破解者,要"数学上不可伪造";
- 对现场,要"失效时安全、恢复时无感"。
下面按顺序说:签什么、绑什么、怎么防绕过、现场实测、以及一个我们踩过的坑。
二、签名什么?——一个字符串,两行校验
授权文件长这样,`license.xml`,和 exe 放同一个目录:
```xml
<License>
<MachineCode>047C-D353-33EC-79E1</MachineCode>
<Expiry>2099-12-31</Expiry>
<Signature>PZyjEpyLh/LEM9pvRNUsCagIYzN7...</Signature>
</License>
```
三个字段,干的活很直接。软件拿到文件后做三件事:
1. **验签**:把 `MachineCode + "|" + Expiry` 这串文本用 RSA 公钥验 `Signature`;
2. **对机器**:`MachineCode` 必须等于本机算出来的机器码;
3. **看日期**:`Expiry` 必须晚于当前时间。
顺序不能颠倒。先验签,是因为"改日期"这种最朴素的手法,在验签面前直接失效——签名的内容包含了有效期本身,你把 2026 改成 2099,签名立刻对不上。
// 1. RSA 签名校验(私钥签发,公钥验证,无法伪造)
byte[] data = Encoding.UTF8.GetBytes(code + "|" + expiry);
using var rsa = RSA.Create();
rsa.FromXmlString(PublicKeyXml);
bool sigOk = rsa.VerifyData(data, Convert.FromBase64String(signature),
HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
```
参数是 RSA2048 + SHA256 + PKCS1 填充,.NET 原生支持,没有引入任何第三方库。

(覆盖 MachineCode+Expiry 两个字段)→ 机器码匹配 → 有效期;红色叉标注"只改日期"会被验签拦下】
三、为什么"不可伪造"是数学结论,不是工程结论
值得单独说一句,因为这是整套体系的地基。
私钥在签发方手里,公钥嵌在软件里。*软件只有验证能力,没有签发能力。破解者拿到软件,能拿到公钥,能读到全部校验代码,能改任何一行——但他造不出一份能过验签的 license.xml,因为那需要私钥签名。
这不是"我们代码写得巧",这是 RSA 的数学性质:知道公钥和全部密文,推不出私钥。
所以"破解授权"这件事,在密码学意义上是不成立的。真正要防的是另一件事——绕过校验。这也是下一节的重点。
四、机器码怎么算:为什么不用 MAC 地址
机器码要满足三个条件:换机器就变、同机器稳定、不能被用户轻易伪装。
最初的想法肯定是 MAC 地址,但它有三个坑:
- 虚拟网卡、VPN、USB 网卡一多,一台机器能报出五六个 MAC,取哪个?
- 工厂批量部署用镜像克隆,网卡 MAC 会被写死成同一个,一批机器共用一个机器码;
- 用户自己就能改。
我们最后用的是两个系统级、用户级不可见、重装系统也不变的标识:
```csharp
// 1. Windows 安装唯一标识(注册表)
string guid = Registry.GetValue(
@"HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Cryptography",
"MachineGuid", "") as string ?? "";
// 2. C 盘卷序列号
uint serial = GetVolumeSerial(@"C:\");
// 3. SHA256 前 8 字节 → 16 个十六进制字符
string raw = guid + "|" + serial;
byte[] hash = SHA256.HashData(Encoding.UTF8.GetBytes(raw));
```
拼起来过一次 SHA256,取前 8 字节转 16 个十六进制字符,每 4 位加一个短横,就是现场看到的 `047C-D353-33EC-79E1`。
两个细节值得说:
- 取哈希前 8 字节而不是全文。机器码是要人在电话里、微信里念来念去的,16 位刚刚好。安全性一点没降——它本来就要跟 RSA 签名绑定使用,不是拿它当密钥。
- 故意不碰 CPU 序列号、主板序列号。那些要调 WMI,工控机上 WMI 服务经常被安全策略关着,为了一个机器码引入新故障点不值得。
换硬盘、加内存、换网卡都不影响机器码;重装 Windows 会变(MachineGuid 重建),现场重装后重新签发一份就行。
五、签发流程:一分钟的事,做成一个工具
现场拿机器码换授权,这个动作一天可能发生几十次,必须快到不用思考。
流程是这样:客户在软件启动失败的提示框里看到本机机器码 → 复制发过来 → 我们打开签发工具 → 粘机器码、选有效期(1 年 / 3 年 / 永久)→ 点签发 → 得到 `license.xml` 发回去 → 客户放到 exe 同目录 → 重启。
签发工具做成了图形版(LicenseIssuer),私钥放签发工具自己的目录里,不随软件发布:
// LicenseIssuer —— 授权签发工具(图形版)
// 输入客户机器码 → 选有效期 → 一键生成 license.xml
// 私钥放本 exe 同目录(私钥-妥善保管.xml),不随软件发布
// 签名格式与 LicenseGen sign 命令行完全一致(RSA2048 SHA256 PKCS1, code+"|"+expiry)
```
顺手加了个小兼容:现场经常忘改文件名,直接发 `license-047C-D353-33EC-79E1.xml` 过来,软件会主动去匹配 `license-*.xml`,认得出。这种"现场一定会忘"的事,靠制度不如靠代码兜。
六、最关键的一节:只在启动时校验,等于没校验
前面所有设计,都有一个隐含假设:校验代码会被执行。
这个假设一碰反编译就碎。把程序丢到反编译器里,找到启动时那个 `if (!License.Validate()) return;`,改成 `if (false)`,编译回去——破解版诞生,全程不需要伪造任何签名。
这是自研软件授权最容易踩的坑:把全部安全押在一个 if 上。
我们的解法是把授权从"启动时判断一次"改成"运行期持续约束",在四个互相独立的位置都要过:
// 运行期守卫,四个位置:
// ① Program.Main 启动硬闸
// ② 开 TCP 触发服务前(两个入口:自动启动 / 手动启动)
// ③ 每帧判定入口(RunProcess)
// ④ 30 秒周期巡检
//
// 关键安全属性:失败方向永远是"判 NG",
// 也就是说破解者即便绕过启动闸,拿到的也是一个全判 NG 的空壳。
```
拆开看每一层防的是什么:
**① 启动硬闸**:防"没授权就能用"。无授权文件、签名无效,直接不启动,TCP 端口都不开、日志都不建。
**② 开服务前**:防"绕过启动闸之后开始生产"。授权不过就不开触发服务——PLC 连不上视觉,破解版根本没法投产。这里做了 `Guard(true)`,忽略缓存做深校验。
**③ 每帧判定入口**:这是兜底层,也是整套设计的心跳。授权失效的这一刻,本帧不检测,直接按兜底策略回 NG:
```csharp
if (!License.Guard())
{
_licenseDenied++;
Log.Warn($"授权校验未通过,第 {_licenseDenied} 帧按 NG 回复 PLC({sn})");
ReplyLicenseNg(sn); // 无条件回 NG,不依赖任何配方开关
**④ 30 秒巡检**:把"授权刚刚失效"尽早暴露出来。注意这里故意不主动停机——现场不能因为授权抖动把产线卡死。巡检只告警,判定的安全性由 ③ 保证。

四点守卫——启动闸 / 开服务前 / 每帧入口 / 30 秒巡检,四个独立关卡;标注③为兜底层,失败方向恒为 NG】有个工程细节:守卫加了 2 秒缓存。
private const int CacheMs = 2000; // 2 秒内复用上次结论,避免每帧读文件 + RSA
产线节拍 133ms 一拍,如果每帧都读文件 + 做一次 RSA 验签,等于给每件产品白加几毫秒。2 秒缓存让正常帧只付一次 `Interlocked` 读的代价,授权状态变化最多延迟 2 秒——对"失效"这个事件来说完全可接受。
七、实测:三种情况,三种结果
这套东西不上手打几枪是不知道好不好用的。实测三种场景:

撤掉授权文件*→ 不停机、不弹窗、不报错,从下一帧开始逐帧判 NG,日志里逐条写明"授权校验未通过,第 N 帧按 NG 回复 PLC"。
放回授权文件 → 下一帧自动恢复,不用重启、不用重新连接相机。巡检那一层会打一条"授权守卫通过:授权有效,有效期至 2099-12-31"。
删文件 + 篡改签名*→ 停在启动闸,端口不开、日志不建,提示框里带着本机机器码。
第一种行为值得特别说明:授权失效时,程序看起来像"视觉坏了"——不报错、不弹窗,就是每件都判 NG、剔除器全开。这是刻意选择的方向:宁可全剔光,不放行未检测品。但它确实是个现场排障陷阱,所以第八节专门讲。
八、加固:混淆只能开一档,多了会崩
授权校验代码本身是可反编译的,所以发布包还做了混淆加固(ConfuserEx)。这里踩过一个很硬的坑,值得单独记:
`ref proxy`(引用代理)不能开。*这档保护会把方法调用改写为代理跳转,对纯托管代码没事,但我们是重度原生互操作——OpenCvSharp 的 P/Invoke、ONNX Runtime 的导出函数封送全被它破坏。实测完整版、去 anti-tamper、轻量三组配置,全部在跑到第 27~28 帧时抛 `AccessViolationException` 直接崩。只有关掉它才跑得满全程。
最后落地的档位是只开 `constants`(字符串常量加密):
- CT 代价 +2~3ms(44→47ms),判定逐帧零翻转;
- 混淆后中文串静态扫不到("最低置信度""模拟相机启动"这些全都进了加密段);
- DLL 399KB → 418KB。
另外两条铁律:
- `rename`(重命名)永久禁用。它会把 JSON 配置里的键名也改成不可见字符——配方文件直接不可读,现场等于全部配方报废。
- self-contained 包里不要补共享框架。net6 产物补了 8.0 的 `System.*.dll`,ConfuserEx 解析依赖会拿错版本。
这三条都不是"配错了",是重度原生互操作代码的必然结果。
九、现场唯一的风险:失效长得像故障
整套体系在技术上没什么可挑剔的,但它有一个产品层面的副作用,必须讲清楚:
授权失效时,程序不报错、不弹窗,而是每件产品判 NG。
现场操作员看到的现象是:剔除器狂喷、NG 计数暴涨、视觉"好像坏了"。他会去查相机、查光源、查 PLC,很难第一时间想到授权——因为屏幕上没有任何异常提示。
所以上机第一件事是确认日志里有这一行:
授权守卫通过:授权有效,有效期至 2099-12-31
有这行,说明授权链路是通的;没有这行,或者看到"授权巡检未通过",就直接找软件方重新签发,别再折腾硬件了。这条已经写进交付说明的第一页。
十、效果
最终形态:
- 防拷贝:RSA2048 签名绑定机器码,换机器即失效,改内容签名即失效;
- 防绕过:四点运行期约束,且失败方向恒为 NG——绕过启动闸拿到的是"全判 NG 的空壳",投产无意义;
- 防崩溃:授权失效不停机、不卡产线,放回后下一帧自动恢复;
- 防误判:2 秒缓存让每帧守卫只付一次原子读的代价,CT 代价实测 +2~3ms;
- 签发快:机器码粘进去、选有效期、点一下,一分钟内出文件。
写在最后
授权体系的设计可以浓缩成两句话:
第一句是密码学的——把"信任"放在只有你有的私钥里,软件只保留验证能力,伪造在数学上就不成立。
第二句是工程的——不要相信校验代码会被执行。把它从"启动时的一个 if"展开成"贯穿运行期的四道约束",并且永远让失败方向指向安全侧。
这两句话换到任何自研工业软件上都成立。授权做得好的标准,不是"没人破解过",而是破解者拿到东西也造不出价值。
下一篇回到性能主线:视觉节拍是怎么从 113ms 往下抠的——我们最近实测发现,慢的根本不是模型推理(纯推理只要 15ms),而是"低置信度 → 反复复核"这条链路,以及现场 80ms 硬指标下该怎么取舍。
如果你的产线也有自研软件的授权、防拷贝难题,或者正在为"接 PLC 就要改程序"发愁,欢迎留言或私信交流。关注后回复【视觉】,送你一份《产线视觉检测改造自查清单》。















































































