API接口签名和敏感信息加密使用国密SM方案

发布于:2025-07-22 ⋅ 阅读:(23) ⋅ 点赞:(0)

这或许是一个讨论文,因为我也不确定哪种方案更好。

签名方式使用国密sm,用到的有sm2、sm3、sm4。因为国家对这方面的安全检查越来越严格了,有要求使用国密。我们项目最近也被密码安全部门检查过,如果不想以后要整改,建议加密相关的需求最好使用国密算法。

sm2是类似于RSA的非对称加密算法,sm3是类似于md5的摘要算法,sm4是类似AES的对称加密算法。

接口中签名使用的参数

appId:appId

nonce:随机数

timestamp:时间戳

message:报文json字符串

这些参数的含义就不在此阐述了。

国密的用法

使用sm3对请求参数进行摘要签名,防止数据篡改。

使用sm4对敏感信息字段加密,比如登录接口有username,password。就可以对password字段加密。如果没有敏感信息则无需加密。

使用sm2对密钥或者摘要数据加密。

sm2的用法我是比较纠结的一点,因为非对称加密算法很复杂,需要占用一部分性能。

sm4的密钥的使用有两种方式,1是客户端每次请求生成一个新的sm4密钥,对敏感信息字段加密后,把密钥放入待签名字符串中一起进行签名,然后用sm2公钥加密此密钥,放入请求头中传给后端。2是固定使用一个sm4密钥,前后端各自保存,不需要放入带签名字符串中和请求头中。

下面说方案。

方案1:

客户端保存appId、sm2公钥。

客户端每次请求生成sm4密钥,仅对敏感信息字段加密,然后放入待签名字符串中一起进行sm3摘要,再使用sm2公钥对sm4密钥加密放入请求头。后端先使用sm2私钥对sm4密钥解密,然后拼接参数进行sm3摘要,最后和前端传来的sm3摘要做对比。

前端代码如下,使用js模拟

/**
 *
 * @param data 请求参数
 * @param encryptKey sm4密钥
 */
export const apiSign = (data: any, encryptKey: string) => {
    const appId: string = "appId";
    const publicKey: string = "sm2公钥";
    const nonce: string = randomStr(32, true);
    const timestamp: string = new Date().getTime().toString();
    const message: string = JSON.stringify(data);
    const signStr: string = `appId=${appId}&nonce=${nonce}&timestamp=${timestamp}&message=${message}&encryptKey=${encryptKey}`;
    const signature = SmUtils.sm3(signStr);
    const encryptKeyResult = SmUtils.sm2Encrypt(encryptKey, publicKey);
    const header: Map<string, string> = new Map();
    header.set("appId", appId);
    header.set("nonce", nonce);
    header.set("timestamp", timestamp);
    header.set("signature", signature);
    header.set("encryptKey", encryptKeyResult);
    return header;
};

响应的时候后端使用前端传来的sm4密钥加密敏感信息字段,然后签名方式同请求时一致,只是不需要再传回来sm4密文了,前端还是用当前请求生成的sm4密钥解密敏感信息字段。

方案2:

客户端保存appId、sm2公钥。

客户端每次请求生成sm4密钥,仅对敏感信息字段加密,然后放入待签名字符串中一起进行sm3摘要,再使用sm2公钥对摘要进行加密,再使用sm2公钥对sm4密钥加密放入请求头。后端先使用sm2私钥对sm4密钥解密,再使用sm2私钥对签名字段解密得出摘要,然后拼接参数进行sm3摘要,最后和前端传来的sm3摘要做对比。

此方案对比方案1多进行了一次sm2,势必会占用一些性能资源。

/**
 *
 * @param data 请求参数
 * @param encryptKey sm4密钥
 */
export const apiSign = (data: any, encryptKey: string) => {
    const appId: string = "appId";
    const publicKey: string = "sm2公钥";
    const nonce: string = randomStr(32, true);
    const timestamp: string = new Date().getTime().toString();
    const message: string = JSON.stringify(data);
    const signStr: string = `appId=${appId}&nonce=${nonce}&timestamp=${timestamp}&message=${message}&encryptKey=${encryptKey}`;
    const sm3Str = SmUtils.sm3(signStr);
    const signature = SmUtils.sm2Encrypt(sm3Str, publicKey);
    const encryptKeyResult = SmUtils.sm2Encrypt(encryptKey, publicKey);
    const header: Map<string, string> = new Map();
    header.set("appId", appId);
    header.set("nonce", nonce);
    header.set("timestamp", timestamp);
    header.set("signature", signature);
    header.set("encryptKey", encryptKeyResult);
    return header;
};

响应的时候后端使用前端传来的sm4密钥加密敏感信息字段,然后签名方式同请求时一致,只是不需要再传回来sm4密文了,前端还是用当前请求生成的sm4密钥解密敏感信息字段。

方案3:

客户端保存appId、sm2公钥 、sm4密钥。

使用固定sm4密钥,仅对敏感信息字段加密,前后端各自保存,不需要一起进行sm3摘要,也不需要放入请求头。拼接带签名字符串进行sm3摘要,再使用sm2公钥对摘要进行加密。后端使用sm2私钥对签名字段解密得出摘要,拼接参数进行sm3摘要,最后和前端传来的sm3摘要做对比。

此方案的缺点是使用固定sm4密钥,安全性不如每次生成。

/**
 *
 * @param data 请求参数
 */
export const apiSign = (data: any) => {
    const appId: string = "appId";
    const publicKey: string = "sm2公钥";
    const nonce: string = randomStr(32, true);
    const timestamp: string = new Date().getTime().toString();
    const message: string = JSON.stringify(data);
    const signStr: string = `appId=${appId}&nonce=${nonce}&timestamp=${timestamp}&message=${message}`;
    const sm3Str = SmUtils.sm3(signStr);
    const signature = SmUtils.sm2Encrypt(sm3Str, publicKey);
    const header: Map<string, string> = new Map();
    header.set("appId", appId);
    header.set("nonce", nonce);
    header.set("timestamp", timestamp);
    header.set("signature", signature);
    return header;
};

响应的时候后端使用固定sm4密钥加密敏感信息字段,然后签名方式同请求时一致。

这三种方案我倾向于方案2,但方案2使用了2次sm2,对性能有一定的影响。也希望和各位大佬讨论一下,是否有更合适的方案?


网站公告

今日签到

点亮在社区的每一天
去签到