《全面解析:数字签名与加密在授权系统中的应用——盒子授权》

前言

对于客户端授权,我们提供客户端软件,并设定使用期限。商户在授权期满后需要重新购买才能继续使用。为此,我们会向客户端发送授权码,完成续期或重新授权的操作。
这边我们一起实现下,如何使用授权码,实现对软件的授权操作。

逻辑分析

1. 身份验证

在授权过程中,首先要验证客户端的身份,确保软件或用户是合法的。也就是当前我们这个授权码只能给特定的设备,也是就是授权码和设备绑定。这边和设备绑定的方式是通过设备ID或其他唯一标识符来确认客户端设备的合法性。也就是授权码里要包含设备ID,授权码设备ID和设备端的设备ID一致才能进行授权。这边为了防止被篡改还可以加上设备的MAC地址,也就是身份验证通过设备ID和MAC地址。

MAC地址很容易就被修改,如果只使用MAC那么用户直接修改MAC地址即可。这边就需要增加硬件信息。看了网上说的使用CPU序列号和硬盘序列号,这边执行命令都是固定的,没有意义。

cpu序列号
root@nld-master:~# cat /proc/cpuinfo | grep Serial | awk '{print $3}'
root@nld-master:~#

硬盘序列号
root@nld-master:~# lsblk -o SERIAL | tail -n 1
00000000000000000001

获取 MAC 地址
root@nld-virtual-machine:~# cat /sys/class/net/ens160/address
00-11-22-33-44-55


获取 BIOS UUID
root@nld-master:~# cat /sys/class/dmi/id/product_uuid
e1d2c3b4-5678-90ab-cdef-1234567890ab

通过上面可以看出CPU序列号和硬盘序列号没有实际意义。
DeviceFinger(设备指纹)=SHA256(MAC+BIOS UUID)

网卡MAC码是由全球惟一的一个固定组织来分配的,未经认证和授权的厂家无权生产网卡。
BIOS UUID(也称为 系统 UUID)是主板固件提供的唯一标识符,通常用于标识设备。在 Linux 或 Windows 上都可以获取 BIOS UUID。

在 Docker 容器环境 下,直接获取 MAC 地址 和 BIOS UUID 可能会受到限制,因此需要采取更稳妥的方案。
解决方案:

1.初始化时采集设备信息
	在盒子初始化阶段,获取设备的 MAC 地址关键信息。
	由于 Docker 容器内获取这些信息可能受限,应在 宿主机 运行时采集并存储。
2.加密存储 & 防篡改
	将采集到的设备信息 加密存入文件,确保数据安全。
	采用 数字签名 验证数据完整性,防止用户篡改信息绕过验证。
3.混淆文件路径 & 命名
	采用 sha256(mac + 密钥) 生成文件名,避免用户轻易定位或删除文件。
4.	验证授权码设备和当前运行设备,读取生成的盒子文件	

上面方案存在的问题
如果用户能够找到 MAC 地址文件的存储路径,他们可以通过替换成其他盒子的MAC地址文件,使系统识别为任意设备的 MAC 地址。这样,算法就能在其他设备(盒子)上运行,而不会受到原本的 MAC 地址绑定限制。尽管用户无法直接修改 MAC 地址文件,但如果可以替换它,就等同于绕过了原本的安全机制。因此,该方案存在安全漏洞,无法有效防止设备欺骗。

可行的解决方案
由于算法程序需要与内网摄像机通信,设备必须处于局域网环境才能正常运行。基于这一特性,我们可以利用 MAC 地址修改可能导致网络连接中断 的情况,增加一个 定时器机制:

10 分钟自动进行一次授权码校验,确保设备仍然符合授权条件。
如果用户尝试修改 MAC 地址,可能会导致网络断开,从而影响算法的正常运行。
频繁验证增加修改成本,使用户难以持续伪造 MAC 地址,最终放弃篡改尝试。

还有docker模式运行如何获取宿主机MAC地址,那么就是启动容器使用主机网络模式 --network host

docker run --rm -it --network host ubuntu /bin/bash

那么在容器内部算法程序,请求访问到就是宿主机mac地址,也就是盒子设备的地址。

2. 有效期控制

为了限制客户端的使用时间,授权码会被设定一个有效期,例如1个月、1年等。到期后,授权码将失效,客户端软件不能继续使用。商户或用户需要重新购买并获取新的授权码才能继续使用。也就是授权码里包含授权的结束时间,授权超过结束时间那么软件将无法使用。

为了防止用户通过修改系统时间绕过授权,我们可以采取以下策略:

时间记录与验证:每次软件运行时,记录当前系统时间,并将其存入文件。下次启动时,验证当前时间是否早于上次记录的时间,若检测到回退,则判定为异常。
数据加密与签名:对存储的时间数据进行加密,并使用数字签名防篡改,确保文件内容无法被恶意修改。
文件混淆与隐藏:文件名采用 sha256(mac + 算法ID + 密钥) 生成,确保难以被用户定位和删除。

还有一个问题就是用户找到了这个文件,把最新时间文件删除了。那么这该如何解决这个方案。

3.业务信息

有的软件会限制连接的设备数,比如当前软件只允许10个相机接入。也就是通过授权码限制相机接入数,这边就是授权码增加一个字段接入相机数。

4.服务端加密与签名

4.1 加密

为了防止用户看到授权码信息,如果用户看到授权码内容。那么会更容易被破解,那么我们这边需要将授权码加密。加密算法采用AES加密。

        System.out.println("==========Ecdsa 签名==================");
        final String signEcdsa = JiDigitUtil.signEcdsa(encryptAes, keyPair[1]);
        System.out.println("signEcdsa:" + signEcdsa);
        System.out.println("verifyEcdsa:" + JiDigitUtil.verifyEcdsa(encryptAes, signEcdsa, keyPair[0]));
        System.out.println("decryptAes:" + decryptAes);
待加密数据:{"algorithmId":"1897464647821430784","boxId":"67c8ff47e3db1ae95498567d","channelNum":10,"createTime":"2025-03-06 09:49:59","endTime":"2026-03-06 09:49:59","eventTypes":"[\"PedestrianIntrusionNotStaff\",\"PedestrianIntrusion\"]","mac":"ab754de0-2386-421b-b2b2-60f48349d6be","nonce":"KY2LUXxOu9DZQgZS","tenantId":"67c8ff47e3db1ae95498567c"}
aesKey:eNuZkRPgko75xhEOU++aPJpZW9iZjcOfdZKNpVXfmUU=,encryptAes:rPiroTmI7pCv6JGGjkUjur1vzxiNy63vE5Y2DG78OdsXZXUOlgXOJEa0CSeAPyMgUV8xnl4p7M5R03/dRwU81eCb3JD4e5Ce7pifrTVXwgJ1vGkYaRdfe91jA9hrWel/na3My36mpU6LKUITf4WT/6f9FvDS4iNFiYzhvPrvpvLIC5XIqsIZhQ5AXM44HH44uYn6mqB59dlwTAQQI9cxKaUhq0/GgZ78tfYFoUEV8kCrIvGCiomvuTWu4kUCOQYZh1HefMh/fKHm+7PbMx7eXTm+2H8JWk2NnwTB2cg3mOcmTUODkwHBUaqUmpbsZrftpgJ45hz9nWedAiDELQI/Yjv/wngRZXPOIKboMG+k8JJfxtLaXu6BXDdFeymhsy2lbDFXFvnk63LEI3Ee/sZJNmUP5YNUc4Bgv2PDVuHzaJ4b/SZe+QJEDGyxyLM3xcjtsfZt142VsJlMf0HsMCrQoA==


4.2 签名

我们发送一串授权码,为了防止被篡改。那么需要给授权码加上签名,同时为了密钥的安全。这边采用非对称加密(ECC)生成摘要。(这边没有使用RSA算法,因为同样的密钥长度ECC密码强度更强)

        System.out.println("==========Ecdsa 签名==================");
        final String signEcdsa = JiDigitUtil.signEcdsa(encryptAes, keyPair[1]);
        System.out.println("signEcdsa:" + signEcdsa);
        System.out.println("verifyEcdsa:" + JiDigitUtil.verifyEcdsa(encryptAes, signEcdsa, keyPair[0]));
        System.out.println("decryptAes:" + decryptAes);

signEcdsa:MIGIAkIBqYpOFyuZ6dVHExOnNyaC6rT52sJTrbtgQ98QMDQwXWv3tziRA4zix/9MalZ1UAGMpZaWMkFauuku5FOdMrejYVACQgHF7ODCic4Tvpz2449rlN/PJY1ZB6uVhD5Y4i1P1f1VZsDqDWGpgdfJ/55NAMkCW/lafMTWmKnCYk+H1+0VmhL2GA==
verifyEcdsa:true

4.3Base64编码

这边我们生成的授权码长这样:

        System.out.println("==========base64==================");
        final JSONObject userLicenseMap = new JSONObject();
        userLicenseMap.put("authString", "v2.0/" + nonce);
        userLicenseMap.put("encrypt", encryptAes);
        userLicenseMap.put("sign", signEcdsa);
        System.out.println("userLicenseMap看到的内容:" + userLicenseMap);

        System.out.println("userLicenseMap看到的内容转base64:" + Base64Utils.encodeToString(userLicenseMap.toJSONString().getBytes(StandardCharsets.UTF_8)));

这边 “authString”:"{授权码版本}/{nonce},是为了方便授权的扩展,支持多版本运行,可以适配多种密码算法。

authString": "2.0/um2s2wVITNTrJxPB
2.0:授权码版本
um2s2wVITNTrJxPB:nonce授权码唯一标识和AES初始变量

密码学原则:相同明文每次生成的密文都不一样

Base64编码前

 {"encrypt":"rPiroTmI7pCv6JGGjkUjur1vzxiNy63vE5Y2DG78OdsXZXUOlgXOJEa0CSeAPyMgUV8xnl4p7M5R03/dRwU81eCb3JD4e5Ce7pifrTVXwgJ1vGkYaRdfe91jA9hrWel/na3My36mpU6LKUITf4WT/6f9FvDS4iNFiYzhvPrvpvLIC5XIqsIZhQ5AXM44HH44uYn6mqB59dlwTAQQI9cxKaUhq0/GgZ78tfYFoUEV8kCrIvGCiomvuTWu4kUCOQYZh1HefMh/fKHm+7PbMx7eXTm+2H8JWk2NnwTB2cg3mOcmTUODkwHBUaqUmpbsZrftpgJ45hz9nWedAiDELQI/Yjv/wngRZXPOIKboMG+k8JJfxtLaXu6BXDdFeymhsy2lbDFXFvnk63LEI3Ee/sZJNmUP5YNUc4Bgv2PDVuHzaJ4b/SZe+QJEDGyxyLM3xcjtsfZt142VsJlMf0HsMCrQoA==","sign":"MIGIAkIBqYpOFyuZ6dVHExOnNyaC6rT52sJTrbtgQ98QMDQwXWv3tziRA4zix/9MalZ1UAGMpZaWMkFauuku5FOdMrejYVACQgHF7ODCic4Tvpz2449rlN/PJY1ZB6uVhD5Y4i1P1f1VZsDqDWGpgdfJ/55NAMkCW/lafMTWmKnCYk+H1+0VmhL2GA==","authString":"v2.0/KY2LUXxOu9DZQgZS"}

Base64编码后

eyJlbmNyeXB0IjoiclBpcm9UbUk3cEN2NkpHR2prVWp1cjF2enhpTnk2M3ZFNVkyREc3OE9kc1haWFVPbGdYT0pFYTBDU2VBUHlNZ1VWOHhubDRwN001UjAzL2RSd1U4MWVDYjNKRDRlNUNlN3BpZnJUVlh3Z0oxdkdrWWFSZGZlOTFqQTlocldlbC9uYTNNeTM2bXBVNkxLVUlUZjRXVC82ZjlGdkRTNGlORmlZemh2UHJ2cHZMSUM1WElxc0laaFE1QVhNNDRISDQ0dVluNm1xQjU5ZGx3VEFRUUk5Y3hLYVVocTAvR2daNzh0ZllGb1VFVjhrQ3JJdkdDaW9tdnVUV3U0a1VDT1FZWmgxSGVmTWgvZktIbSs3UGJNeDdlWFRtKzJIOEpXazJObndUQjJjZzNtT2NtVFVPRGt3SEJVYXFVbXBic1pyZnRwZ0o0NWh6OW5XZWRBaURFTFFJL1lqdi93bmdSWlhQT0lLYm9NRytrOEpKZnh0TGFYdTZCWERkRmV5bWhzeTJsYkRGWEZ2bms2M0xFSTNFZS9zWkpObVVQNVlOVWM0Qmd2MlBEVnVIemFKNGIvU1plK1FKRURHeXh5TE0zeGNqdHNmWnQxNDJWc0psTWYwSHNNQ3JRb0E9PSIsInNpZ24iOiJNSUdJQWtJQnFZcE9GeXVaNmRWSEV4T25OeWFDNnJUNTJzSlRyYnRnUTk4UU1EUXdYV3YzdHppUkE0eml4LzlNYWxaMVVBR01wWmFXTWtGYXV1a3U1Rk9kTXJlallWQUNRZ0hGN09EQ2ljNFR2cHoyNDQ5cmxOL1BKWTFaQjZ1VmhENVk0aTFQMWYxVlpzRHFEV0dwZ2RmSi81NU5BTWtDVy9sYWZNVFdtS25DWWsrSDErMFZtaEwyR0E9PSIsImF1dGhTdHJpbmciOiJ2Mi4wL0tZMkxVWHhPdTlEWlFnWlMifQ==

44.代码示例

以下加密算法工具【JiDigitUtil】可以参考笔者另一篇博客密码学之美——密码学总结和对外开放接口实现

    @Test
    public void license() {
        final String[] keyPair = JiDigitUtil.genKeyPair(JiDigitUtil.ECC_ALGORITHM);
        System.out.println("密钥对(公钥/私钥):" + JSON.toJSONString(keyPair));
        final HashMap<String, Object> data = new HashMap<>();
        final String nonce = JiDigitUtil.createNonce(16);
        //租户id
        data.put("tenantId", IdUtil.objectId());
//        盒子id 设备id
        data.put("boxId", IdUtil.objectId());
//        mac地址
        data.put("mac", IdUtil.fastUUID());
//        授权码创建时间
        data.put("createTime", new DateTime().toString());
        data.put("nonce", nonce);
//        算法结束时间
        data.put("endTime", DateUtil.offsetDay(new Date(), 365).toString());
//        算法id
        data.put("algorithmId", IdUtil.getSnowflake(1, 1).nextIdStr());
//        算法接入相机个数
        data.put("channelNum", 10);
        String[] eventTypes = {"PedestrianIntrusionNotStaff", "PedestrianIntrusion"};
        data.put("eventTypes", JSON.toJSONString(eventTypes));

        System.out.println("==========AES_ALGORITHM加密==================");

        String bodyDataStr = JiDigitUtil.getBodyDataStr(data);
        final String aesKey = JiDigitUtil.genSecretKey(JiDigitUtil.AES_ALGORITHM);
        final String encryptAes = JiDigitUtil.encryptAes(bodyDataStr, aesKey, nonce);
        final String decryptAes = JiDigitUtil.decryptAes(encryptAes, aesKey, nonce);
        System.out.println(String.format("aesKey:%s,encryptAes:%s", aesKey, encryptAes));

        System.out.println("==========Ecdsa 签名==================");
        final String signEcdsa = JiDigitUtil.signEcdsa(encryptAes, keyPair[1]);
        System.out.println("signEcdsa:" + signEcdsa);
        System.out.println("verifyEcdsa:" + JiDigitUtil.verifyEcdsa(encryptAes, signEcdsa, keyPair[0]));
        System.out.println("decryptAes:" + decryptAes);

        System.out.println("==========base64==================");
        final JSONObject userLicenseMap = new JSONObject();
        userLicenseMap.put("authString", "v2.0/" + nonce);
        userLicenseMap.put("encrypt", encryptAes);
        userLicenseMap.put("sign", signEcdsa);
        System.out.println("userLicenseMap看到的内容:" + userLicenseMap);

        System.out.println("userLicenseMap看到的内容转base64:" + Base64Utils.encodeToString(userLicenseMap.toJSONString().getBytes(StandardCharsets.UTF_8)));

    }

5.客户端授权

5.1密钥分发

这边签名用的ECC密钥需要,在设备端给用户时提前放入软件中。同时AES加密的密钥也需要,提前放入设备软件中。这边为了方便维护密钥,统一采用同一个租户使用的密钥是一样。同时设备端的程序,需要是C和C++这样的编译型语言才行。因为如果是解释型语言,那么很容易被反编译。如果密钥被替换了,或者验签的逻辑被更改。(任何验签都返回true)那么验签也就是毫无意义了,所以一般客户端的验签都没有用python,Java等解释型语言,而是都采用C和C++这样不容易被反编译和破解。

5.1授权码验证

客户端的授权码验证流程优化如下:

1.签名校验
	使用 公钥验签,确保授权码未被篡改,来源可信。
	若签名无效,则直接拒绝授权,防止伪造授权码绕过验证。
2.解密授权码
	只有通过验签的授权码才进行解密,提取业务信息(如设备标识、授权时长等)。
3.身份与时间验证
	身份验证:检查授权码中的设备信息是否与当前设备匹配。
	时间验证:校验授权码是否仍在有效期内,防止过期使用。

总结

为了确保授权系统的安全性,我们采用了 ECC 非对称加密 生成签名,并使用 AES 对称加密 保护授权码内容。服务器生成授权码并用 ECC 签名,而客户端通过公钥验证签名,确保数据完整性和来源可信。授权码内容则通过 AES 加密保护,确保敏感信息安全。由于客户端程序暴露在用户端,密钥需嵌入代码并进行编译,防止被替换或提取,同时可以通过代码混淆和反调试等技术增强密钥保护。此外,为防止用户通过修改系统时间绕过授权,系统加入了时间验证机制,同时支持多个设备使用授权码,确保授权管理的灵活性和安全性。此流程能有效防止伪造、篡改、密钥泄露和时间篡改,保障授权系统的安全。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值