商业软件License设计与实现指南

一、引言
商业软件的 License(许可证)机制是软件保护的核心手段。一个设计糟糕的License系统形同虚设——要么容易被绕过,要么分发麻烦,要么密钥管理混乱。本文结合 RSA 非对称加密原理与实际工程经验,系统性地阐述如何设计一套难以伪造、难以篡改、易于分发、深度绑定业务的 License体系,并辅以完整的演示代码。
二、RSA 非对称加密原理
2.1 对称加密的困境
传统对称加密(如 AES)使用同一把密钥加密和解密。用于 License 时有一个致命弱点:解密密钥必须内嵌在软件里,只要攻击者逆向分析软件,提取出密钥,就能自己伪造 License。RSA 非对称加密解决了这个问题。
2.2 RSA 的数学基础
RSA 基于一个数论事实:将两个大质数相乘很容易,但反过来对乘积做因数分解极其困难。
密钥生成步骤:
第一步:选取两个大质数p和q(各约 512 位),计算模数:
n = p × q
φ(n) = (p-1) × (q-1)
第三步:选取公钥指数e,要求gcd(e, φ(n)) = 1(即 e 与 φ(n) 互质)。实践中常取 e = 65537,因其二进制形式(10000000000000001)只有两个 1,模幂运算效率极高。
第四步:计算私钥指数d,满足e × d ≡ 1 (mod φ(n)),即在模φ(n) 的运算体系中,d 是 e 的乘法逆元,通过扩展欧几里得算法求得。
公钥:(e, n),可公开;私钥:(d, n),严格保密。
加密与解密:
加密:c = m^e mod n解密:m = c^d mod n
为什么解密能还原明文?因为e × d = k × φ(n) + 1,代入欧拉定理m^φ(n) ≡ 1 (mod n):
c^d mod n = (m^e)^d mod n= m^(e×d) mod n= m^(k×φ(n)+1) mod n= (m^φ(n))^k × m mod n= 1^k × m mod n= m
2.3 RSA 的“反向”用法:签名模式
标准 RSA 是用公钥 e加密、私钥d解密。License 场景中通常反向使用:
License 生产方(持有私钥 d):用 d 加密 → 生成 License 文件软件运行方(持有公钥 e): 用 e 解密 → 验证 License 文件
这样,客户手里的软件只能验证,无法伪造。因为伪造需要私钥d,而d只存在于 License 生产工具中,从不分发给客户。
2.4 PKCS#1 v1.5 填充的必要性
原始 RSA(”教科书式 RSA”)不能直接用于实际加密,存在三类攻击:
|
漏洞 |
攻击方式 |
|
确定性加密(同明文→同密文) |
字典攻击、频率分析 |
|
明文数值过小 |
直接对密文开 e 次方根,无需破解密钥 |
|
乘法同态性 |
选择密文攻击,可操控解密结果 |
PKCS#1 v1.5 规定加密前必须先添加填充:
[0x00][0x02][随机非零字节 ≥ 8 个][0x00][真正的明文]↑ ↑ ↑ ↑固定 类型标志 引入随机性 分隔符
填充的作用:① 让每次加密结果不同,② 把明文”撑大”成接近 n 的数值,③ 提供格式校验,任何篡改都会导致解码失败。对于 2048-bit RSA(n 为 256 字节),每次加密:
-
输出固定 256 字节密文
-
最多可加密 256 – 11 = 245 字节明文
-
超出 245 字节的数据需要分块加密
三、为什么 RSA 1024 位以上极难被破解
3.1 破解 RSA 等价于因数分解
要从公钥(e, n)推出私钥d,必须先分解n = p × q,求出φ(n) = (p-1)(q-1),才能计算d。所以破解 RSA 的难度 = 因数分解 n 的难度。
3.2 因数分解的计算复杂度
目前最快的因数分解算法是通用数域筛法(GNFS),其时间复杂度为:
exp( (1.923 + o(1)) × (ln n)^(1/3) × (ln ln n)^(2/3) )
这是亚指数级复杂度——随着 n 的位数增长,计算量呈爆炸式增长:
|
RSA 密钥位数 |
模数 n 的十进制位数 |
相对破解难度(以 RSA-512 为基准) |
|
512 bit |
≈ 155 位 |
1×(2009 年已实际破解) |
|
768 bit |
≈ 232 位 |
≈ 1,000×(2009 年已实际破解) |
|
1024 bit |
≈ 309 位 |
≈ 1,000,000×(至今未公开破解) |
|
2048 bit |
≈ 617 位 |
≈ 10²⁰×(当前绝对安全) |
破解 RSA-2048 的估算计算量,相当于全球所有计算机不间断运行数十亿年。
3.3 密钥长度选择建议
|
密钥长度 |
安全状态 |
适用场景 |
|
512-bit |
❌ 已被破解 |
不可用 |
|
1024-bit |
⚠️ NIST 2013 年已弃用,暂未公开破解 |
低安全需求/遗留系统 |
|
2048-bit |
✅ 当前推荐最低标准 |
通用商业软件 |
|
4096-bit |
✅ 高安全标准 |
金融/政务/长期保密场景 |
结论:对于商业软件 License,使用 2048-bit RSA 在可预见的未来(30 年以上)均无法通过暴力手段破解。
四、License 设计原则
4.1 嵌入时间戳,防止篡改续期
License 文件中必须包含有效期时间戳,但时间戳放在哪里、如何保护,直接决定了这道防线是否有效。
❌ 错误做法:时间戳放在明文文件头
许多实现将时间戳直接写在文件头的明文区域:
文件头(明文):[魔数][版本][创建时间戳][有效天数][校验和]↑明文,任何人都可以直接修改
这种做法的致命弱点:攻击者用十六进制编辑器打开文件,找到时间戳字节,直接改成当前时间,License 就被”续期”了。即使文件有校验和,只要校验和没有覆盖时间戳字段,或者校验和算法简单到攻击者可以重新计算,都无法阻止这种攻击。
✅ 正确做法:时间戳放入 RSA 加密的数据内
RSA 加密的明文内容(攻击者无法在不知道私钥的情况下修改):┌─────────────────────────────────────────────────────┐│ created_time(8字节) ← 创建时间戳 ││ expire_days (4字节) ← 有效天数 ││ business_data[] ← 核心业务数据(授权范围) │└─────────────────────────────────────────────────────┘↓ RSA 私钥加密(分块)[密文块1][密文块2]...[密文块N] → 写入License文件
攻击者现在面临的困境:
-
修改密文中的任意字节→ RSA 解密时 PKCS#1 填充验证失败 → 报错
-
想生成包含新时间戳的合法密文→ 必须用私钥 d 重新加密 → 没有私钥,无法完成
-
结论:时间戳与业务数据同等强度地受 RSA 保护
为什么修改密文后 PKCS#1 验证一定失败?
RSA 解密是一个精确的数学运算。合法密文解密后,明文具有严格的格式:
[0x00][0x02][至少8个随机非零字节][0x00][真正的数据]
攻击者随意修改密文字节后,解密出的是一个伪随机的 256 字节序列,以 [00][02]开头的概率约为 1/65536,且后续还必须满足”至少 8 个非零字节后跟一个 0x00″的条件,综合概率极低。因此,任何对密文的改动,几乎必然导致 PKCS#1 解码失败。
4.2 多层防篡改机制
单一防护容易被针对性破解,应设计多层:
第一层:RSA + PKCS#1 完整性→ 密文任何字节被改动 → PKCS#1 解码失败第二层:解密后的结构化验证→ 解密数据长度必须精确等于预期值→ 时间戳必须合法(不能在未来,不能超过有效期)→ 格式不符即报错终止第三层:文件级 XOR-32 校验和(可选,快速预检)→ 覆盖所有密文块,防止文件传输损坏或粗糙篡改→ 注意:校验和必须覆盖所有密文数据,且本身也要被保护
任何一层失败都会导致软件拒绝启动,攻击者必须同时绕过所有层次。
五、将 License 融入核心业务流程
5.1 简单判断”是否有 License”的致命弱点
许多商业软件的保护方式是:
if (!check_license()) {printf("License invalid, exiting.\n");exit(1);}// 继续运行核心功能...run_core_logic();
这种方式极易被破解:攻击者只需用调试器找到check_license()的调用点,将返回值改为true,或者直接跳过这一行,License 验证就形同虚无。核心功能依然能完整运行。
5.2 正确做法:License 数据即业务数据
设计的精髓在于:License 文件里存的不是”是否授权”的布尔标志,而是软件运行所必需的核心业务数据本身。
以一款数据处理工具为例,该软件需要读取特定来源的数据,将授权的数据源标识列表(客户特有,他人无法猜测)放入 RSA 加密的 License 文件:
License 文件中 RSA 加密的数据:[时间戳][有效天数][数据源ID_1][数据源ID_2][数据源ID_3]...
软件的业务流程绑定示意:
软件启动│├─── 解密 License 文件│ ├── RSA 解密 → 得到时间戳 + 业务数据│ ├── 验证时间戳(有效期检查)│ └── 验证结构完整性│└─── 用解密出的业务数据构建运行时数据结构│└─── 后续所有核心操作都依赖这个数据结构(没有这些数据 = 软件无法完成任何有意义的操作)
攻击者面临的困境:
-
不能跳过 License 解密步骤,因为解密结果是业务必需的数据
-
不能伪造 License,因为没有私钥 d 无法生成合法密文
-
不能篡改时间戳,因为时间戳已在 RSA 加密数据内,改动密文会导致解密失败
-
无法“补全”业务数据,因为这是客户特定的配置,没有 License 生产方无法预知
5.3 通用设计原则:选择什么数据放入 License
|
原则 |
说明 |
|
业务必需性 |
软件核心功能不可或缺的数据,而非可选配置 |
|
客户特异性 |
数据因客户而异(如机器码、数据源标识、授权模块列表),无法猜测 |
|
不可绕过性 |
软件每次执行关键操作都必须使用该数据,而非仅在启动时检查一次 |
|
体积合适 |
能装入 RSA 加密的数据量范围(可分块,总量无上限) |
不同类型软件的参考:
|
软件类型 |
可放入 License 的核心业务数据 |
|
数据库工具 |
授权连接的数据库标识、授权操作的表/文件列表 |
|
CAD 软件 |
授权使用的功能模块代码(渲染引擎、有限元分析等) |
|
加密通信软件 |
会话密钥的派生参数、授权的通信节点地址 |
|
工业控制软件 |
授权的设备序列号、控制点 ID |
|
报表软件 |
授权的数据源连接字符串、授权报表模板 ID |
六、License 的获取与分发模型
6.1 三角分发模型
一套简洁安全的 License 分发体系只需要三个角色:
┌──────────────────────────────┐│ License 生产方(软件商) ││ 持有 License生产工具(私钥d) │└──────────────┬───────────────┘│┌─────────────────────┼──────────────────────┐│ 客户发来: │ 软件商签发后返回: ││ license_request.txt │ license.dat ││ (明文,可公开传输) │ (加密后,可公开传输) │└─────────────────────┼──────────────────────┘│┌──────────────▼───────────────┐│ 客户 ││ 持有商业软件(内嵌公钥 e) ││ 只能验证,无法伪造 │└──────────────────────────────┘
安全性来源:
-
License 生产工具包含私钥,绝不分发给客户,是整个体系的安全核心
-
商业软件包含公钥,可以自由分发,即使被逆向分析也无法推出私钥
-
license_request.txt 和 license.dat 均不含任何密钥信息,可通过邮件/HTTP 等任何渠道传输
6.2 标准化分发流程
第一步:客户填写申请信息└── 收集软件运行所需的核心配置(数据源信息、机器码等)→ 生成 license_request.txt(明文,可公开传输)第二步:软件商生成 License└── 接收 license_request.txt→ 用 License 生产工具(含私钥)签发→ 生成 license.dat(加密后,含时间戳在 RSA 数据内)→ 返回给客户(可公开传输)第三步:客户使用 License└── 将 license.dat 放入软件指定目录→ 软件启动时自动加载、验证→ 验证通过 → 解密出业务数据 → 正常运行→ 验证失败 → 报错退出第四步:License 续期└── 客户重新发送 license_request.txt(内容不变)→ 软件商重新签发(创建时间戳更新为当前时间)→ 返回新的 license.dat
6.3 密钥管理要点
|
项目 |
管理要求 |
|
私钥 d |
只存在于 License 生产工具中,不存储在服务器,不通过网络传输 |
|
License生产工具 |
严格控制访问权限,只有授权人员可以运行 |
|
公钥 e |
编译进软件二进制,无需保密 |
|
License文件 |
无需保密,可通过任何渠道传输 |
|
私钥备份 |
必须做加密备份,一旦丢失无法再签发 License |
七、完整实现参考
7.1 License 文件格式规范
偏移 长度 字段名 说明+0 4 magic 魔数,如 "LCNS",快速识别文件类型+4 4 version 版本号,软件拒绝不兼容的版本+8 4 line_count 业务数据条目数+12 4 block_size RSA 输出块大小(= 密钥字节数,2048位为256)+16 4 block_count RSA 加密块数量+20 4 other_data_size XOR 混淆区字节数+24 4 checksum 全密文区 XOR-32 校验和+28 484 padding 随机填充(凑满512字节文件头)↑ 以上为明文文件头(512字节)━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━↓ 以下为加密数据区+512 block_count×256 RSA 加密块(包含时间戳+有效期+核心业务数据)+512+enc_size 256 RSA 加密的 XOR 种子块(245字节随机密钥)+后续 other_data_size XOR 混淆的辅助业务数据(文件名/路径等)
关键设计:时间戳不在明文文件头,而在 RSA 加密块内:
RSA 分块加密的明文(245字节/块):┌──────────────────────────────────────────────────┐│ [0..7] created_time :8字节 Unix timestamp ││ [8..11] expire_days :4字节 有效天数 ││ [12..] business_data[]:剩余空间存核心业务数据 │└──────────────────────────────────────────────────┘
7.2 License 生产端演示代码
/* ============================================================* License 生产工具核心逻辑(含私钥,严禁分发)* ============================================================ */#include<stdio.h>#include<stdlib.h>#include<string.h>#include<time.h>#include"mpi.h"#include"mprsa.h"typedef unsigned int ub4;typedef unsigned long long ub8;/* RSA 2048-bit 私钥(d)和模数(n),十六进制硬编码 */static const char *PRIV_D_HEX = "...(私钥d的十六进制,仅存在于生产工具中)...";static const char *MOD_N_HEX = "...(模数n的十六进制)...";#define RSA_BLOCK_SIZE 256 /* 2048-bit RSA 输出固定256字节 */#define RSA_PLAIN_MAX 245 /* 256 - 11(PKCS#1 v1.5 开销)*/#define HEADER_SIZE 512/* License 明文头部结构(将被 RSA 加密,时间戳在此) */typedef struct {ub8 created_time; /* Unix epoch,创建时间 */ub4 expire_days; /* 有效天数(如30) *//* 后续紧跟 business_data[],内容因软件而异 */} LIC_PLAINTEXT_HEADER;/** produce_license()* request_data : 从 license_request.txt 解析出的业务数据数组* request_count : 业务数据条目数* expire_days : 授权有效天数* out_path : 输出的 license.dat 路径*/intproduce_license(ub4 *request_data, int request_count,ub4 expire_days, const char *out_path){mp_int priv_d, mod_n;mp_err err;FILE *fp;/* 1. 加载 RSA 私钥 */mp_init(&priv_d);mp_init(&mod_n);mp_read_radix(&priv_d, PRIV_D_HEX, 16);mp_read_radix(&mod_n, MOD_N_HEX, 16);/* 2. 构建明文数据:[时间戳(8)][有效天数(4)][业务数据(N×4)] */int plain_total = sizeof(LIC_PLAINTEXT_HEADER) + request_count * 4;unsigned char *plainbuf = malloc(plain_total);LIC_PLAINTEXT_HEADER *hdr = (LIC_PLAINTEXT_HEADER *)plainbuf;hdr->created_time = (ub8)time(NULL); /* 当前时间写入明文 */hdr->expire_days = expire_days;memcpy(plainbuf + sizeof(LIC_PLAINTEXT_HEADER),request_data, request_count * 4);/* 3. 打开输出文件,预写512字节占位头 */fp = fopen(out_path, "w+b");unsigned char pad[HEADER_SIZE];memset(pad, 0, HEADER_SIZE);fwrite(pad, 1, HEADER_SIZE, fp); /* 占位,后面回写 *//* 4. RSA 分块加密明文(每块最多245字节 → 输出256字节) */unsigned char *p = plainbuf;int remain = plain_total;int block_count = 0;ub4 checksum = 0;while (remain > 0) {int chunk = (remain >= RSA_PLAIN_MAX) ? RSA_PLAIN_MAX : remain;char *enc_buf;int enc_len;err = mp_pkcs1v15_encrypt((char *)p, chunk,&priv_d, &mod_n,&enc_buf, &enc_len, enc_rand);if (err != MP_OKAY || enc_len != RSA_BLOCK_SIZE) goto fail;fwrite(enc_buf, 1, enc_len, fp);/* 计算校验和(覆盖所有密文块) */ub4 *w = (ub4 *)enc_buf;for (int i = 0; i < enc_len / 4; i++) checksum ^= w[i];free(enc_buf);p += chunk;remain -= chunk;block_count++;}/* 5. 生成 XOR 随机种子,RSA 加密后写入 */unsigned char xor_seed[RSA_PLAIN_MAX];enc_rand((char *)xor_seed, RSA_PLAIN_MAX);char *seed_enc; int seed_enc_len;mp_pkcs1v15_encrypt((char *)xor_seed, RSA_PLAIN_MAX,&priv_d, &mod_n, &seed_enc, &seed_enc_len, enc_rand);fwrite(seed_enc, 1, seed_enc_len, fp);ub4 *w = (ub4 *)seed_enc;for (int i = 0; i < seed_enc_len / 4; i++) checksum ^= w[i];free(seed_enc);/* 6. XOR 混淆辅助文本(文件名、路径等非关键数据)后写入 *//* (此处略,原理:辅助数据与 xor_seed 循环异或后写入文件) *//* 7. 回到文件头,写入真实头部字段 */rewind(fp);fwrite("LCNS", 1, 4, fp); /* 魔数 */ub4 ver = 0x01000000; fwrite(&ver, 1, 4, fp); /* 版本 *//* 注意:line_count、block_count、checksum 等写入明文头 *//* 时间戳和有效期已经在第2步写入 RSA 加密明文中,此处不重复 */fclose(fp);free(plainbuf);mp_clear(&priv_d);mp_clear(&mod_n);return 0;fail:fclose(fp);free(plainbuf);mp_clear(&priv_d);mp_clear(&mod_n);return -1;}
7.3 License 验证端演示代码
/* ============================================================* 商业软件内部的 License 验证逻辑(含公钥,可分发)* ============================================================ *//* RSA 2048-bit 公钥(e = 65537)和模数(n) */static const char *PUB_E_HEX = "010001"; /* 65537 */static const char *MOD_N_HEX = "...(与生产工具相同的模数n)...";#define LICENSE_EXPIRE_DAYS 30 /* 最长有效天数 */intload_license(constchar *path, BUSINESS_DATA *out_data){mp_int pub_e, mod_n;mp_init(&pub_e); mp_read_radix(&pub_e, PUB_E_HEX, 16);mp_init(&mod_n); mp_read_radix(&mod_n, MOD_N_HEX, 16);FILE *fp = fopen(path, "rb");if (!fp) FAIL("cannot open license file");/* 1. 读取并验证明文文件头 */unsigned char header[HEADER_SIZE];fread(header, 1, HEADER_SIZE, fp);if (memcmp(header, "LCNS", 4) != 0) FAIL("invalid license file");/* version、block_count 等从 header 中解析... *//* 2. RSA 解密所有块,拼接还原明文 */unsigned char plainbuf[MAX_PLAIN_SIZE];int plain_total = 0;ub4 checksum = 0;for (int i = 0; i < block_count; i++) {unsigned char cipherblock[RSA_BLOCK_SIZE];fread(cipherblock, 1, RSA_BLOCK_SIZE, fp);/* 校验和验证 */ub4 *w = (ub4 *)cipherblock;for (int j = 0; j < RSA_BLOCK_SIZE / 4; j++) checksum ^= w[j];/* RSA 解密 + PKCS#1 填充验证(任何密文改动都会在此失败) */char *dec_buf; int dec_len;mp_err err = mp_pkcs1v15_decrypt((char *)cipherblock, RSA_BLOCK_SIZE,&pub_e, &mod_n, &dec_buf, &dec_len);if (err != MP_OKAY)FAIL("RSA decryption failed: license tampered or invalid");memcpy(plainbuf + plain_total, dec_buf, dec_len);plain_total += dec_len;free(dec_buf);}/* 3. 从解密后的明文中提取时间戳(此处才是真正受保护的时间戳) */LIC_PLAINTEXT_HEADER *lhdr = (LIC_PLAINTEXT_HEADER *)plainbuf;time_t now = time(NULL);if ((ub8)now < lhdr->created_time)FAIL("license timestamp is in the future");ub8 elapsed_days = ((ub8)now - lhdr->created_time) / 86400;if (elapsed_days > lhdr->expire_days)FAIL("license expired");/* 4. 验证解密数据长度是否符合预期(结构化完整性检查) */int expected_len = sizeof(LIC_PLAINTEXT_HEADER) + line_count * 4;if (plain_total != expected_len)FAIL("decrypted data length mismatch: license corrupted");/* 5. 验证文件级校验和 *//* (读取 XOR 种子块、混淆数据并纳入校验和计算,与头部存储值比对) */if (computed_checksum != stored_checksum)FAIL("checksum error: license file corrupted");/* 6. 将业务数据注入核心运行时结构(而非设置"已授权"布尔标志) */ub4 *biz = (ub4 *)(plainbuf + sizeof(LIC_PLAINTEXT_HEADER));build_runtime_objects(biz, line_count, out_data);mp_clear(&pub_e);mp_clear(&mod_n);fclose(fp);return 0;}
7.4 关键验证失败模式对照表
|
失败原因 |
触发条件 |
阻止了什么攻击 |
|
invalid license file |
魔数不匹配 |
伪造文件 |
|
RSA decryption failed |
PKCS#1 填充验证失败 |
修改密文字节 |
|
license timestamp is in the future |
时间戳 > 当前时间 |
预签未来 License |
|
license expired |
超过有效天数 |
过期续用 |
|
decrypted data length mismatch |
解密后长度不符 |
截断/拼接攻击 |
|
checksum error |
文件级校验和不匹配 |
修改密文块或混淆数据 |
八、总结
一个优秀的商业软件 License 系统,核心设计思想可以归纳为四点:
1、用非对称加密分离签发权与验证权
私钥(签发能力)永远不离开软件商;公钥(验证能力)内嵌在软件中。客户无论如何分析软件,都无法获得伪造 License的能力。使用 2048-bit 以上的 RSA 密钥,在数学层面保证无法暴力破解。
2、时间戳放入RSA加密数据内,而非明文文件头
时间戳和有效期必须是 RSA 加密明文的一部分,与业务数据一起受私钥保护。任何续期行为都必须经过软件商用私钥重新签发,在没有私钥的情况下,修改密文只会导致 PKCS#1 解码失败,无法达到续期目的。
3、License数据即业务数据,绕不过去
不要设计成“有 License就运行,没有就退出”。将授权数据与核心业务数据合一,软件正常运行依赖 License 解密结果,即使攻击者找到并绕过了启动检查,核心功能也因缺少业务数据而无法正常工作。
4、分发模型简洁安全,无需复杂基础设施
License文件本身不含任何密钥,可通过邮件、HTTP 等普通渠道传输。整个体系的安全性集中于一点:License生产工具的访问控制。管好这一个工具,整套 License 体系就是安全的。
夜雨聆风