乐于分享
好东西不私藏

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

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

一、引言

商业软件的 License(许可证)机制是软件保护的核心手段。一个设计糟糕的License系统形同虚设——要么容易被绕过,要么分发麻烦,要么密钥管理混乱。本文结合 RSA 非对称加密原理与实际工程经验,系统性地阐述如何设计一套难以伪造、难以篡改、易于分发、深度绑定业务 License体系,并辅以完整的演示代码。

二、RSA 非对称加密原理

2.1 对称加密的困境

传统对称加密(如 AES)使用同一把密钥加密和解密。用于 License 时有一个致命弱点:解密密钥必须内嵌在软件里,只要攻击者逆向分析软件,提取出密钥,就能自己伪造 License。RSA 非对称加密解决了这个问题。

2.2 RSA 的数学基础

RSA 基于一个数论事实:将两个大质数相乘很容易,但反过来对乘积做因数分解极其困难

密钥生成步骤:

第一步:选取两个大质数pq(各约 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)+1mod 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 加密的明文数据里。
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"14, fp);                    /* 魔数     */    ub4 ver = 0x01000000fwrite(&ver, 14, 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 解码失败,无法达到续期目的。

3License数据即业务数据,绕不过去

不要设计成“有 License就运行,没有就退出”。将授权数据与核心业务数据合一,软件正常运行依赖 License 解密结果,即使攻击者找到并绕过了启动检查,核心功能也因缺少业务数据而无法正常工作。

4分发模型简洁安全,无需复杂基础设施

License文件本身不含任何密钥,可通过邮件、HTTP 等普通渠道传输。整个体系的安全性集中于一点:License生产工具的访问控制。管好这一个工具,整套 License 体系就是安全的。