API接口签名和敏感信息加密使用国密SM方案
这或许是一个讨论文,因为我也不确定哪种方案更好。
签名方式使用国密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}×tamp=${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}×tamp=${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}×tamp=${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,对性能有一定的影响。也希望和各位大佬讨论一下,是否有更合适的方案?