前言
在一般意义上的后台服务中,身份认证可以保证数据源没有问题,完整性校验可以保证数据没有经过窃听者的篡改,但我们还要防止窃听者知道数据的内容,这就还需要加解密来帮助我们守住最后一道围墙
而在私有云交付的环境中,我们无法用现有的公司平台加解密服务,并且按照国家、金融行业等要求,需要用国密算法实现的加解密方案
国密存在哪些问题
使用不便
最大问题是使用不便。这是由于国密不在IETF国际标准中,不同于ECDSA、ECDH、RSA等国际算法,系统中往往包含相关标准加解密方式,业务数据包通过HTTPS传输时完全不用考虑如何交换公钥,如何加解密数据。
因此现阶段使用国密必须在业务层手动进行数据加解密,相当于对数据进行一步额外的操作。
无最佳实践
确定业务层进行加解密后,应该使用哪一种国密实现、该如何进行加解密是另一个难点,且暂不存在一个最优解。国密的实现方案很多,包括TencentSM、GmSSL和KMS服务器,这也需要进一步的调研和测试来决定最终方案。
国密落地过程
国密落地分为调研、制定、实现与测试四个阶段。
调研阶段主要目的有两个:
- 找到性能高效、使用便捷并且有足够保障(维护活跃度、大厂背书、有变现渠道保证不会暴死等)的国密算法实现
- 调研公司内国密改造案例
国密算法选择
我们通过benchmarks测试评估多种国密库算法性能,最终结果如下:
可以看到TencentSM的benchmarks测试结果较为突出,也是作为我们的最终选择
国密实现方案选择
对于国密的实现,我们收集到的可行方案如下:
- XX实验室建议方案:引入额外KMS系统,独立部署管理证书、私钥,分发公钥。
- 优点:保密性好,完整的加解密方案(包括证书、信任链等待)
- 缺点:需要在业务侧缓存大量SM4密钥,对性能有影响
- XXX网关团队数据加密方案:一次链接一个SM4密钥,发送SM4加密的数据同时发送SM2公钥加密后的SM4密钥。
- 优点:过程简单
- 缺点:服务端每次数据处理都需要双解密(解密密钥后解密数据),对性能有影响
- 无侵入数据加密方案:使用双SSL证书(RSA/SM2)的HTTPS前置机作为网络入口,完全无侵入性地替换。
- 优点:完全无缝地替换国际加密协议,业务侧无感知
- 缺点:SDK侧同样需要双证书支持,但目前普遍缺乏实现;需要添加额外组件HTTPS前置机
在制定方案之前,我们总结了对解密方案的需求:
- 简单无依赖
- 性能好
- 成熟
- 支持服务器热更新SM2公钥
- 支持上报失败后数据加密落地重传
可以接受的缺点:
- 对业务侵入
于是在制定方案时我们充分考量了HTTPS加解密方式,设计了类似的加密上报方式
基于对称密钥加密公钥的非对称加密方案,时序图如下:
代码语言:javascript复制 sequenceDiagram
participant S as SDK
participant E as Entrance
participant A as AppConfig
Note over E: 获取或生成SM2钥匙对
E->> E: SM2钥匙对
S-->>E: requestForPubKey()
E->>S: 返回SM2公钥
Note over S: 若错误,停止后续
S-->>E: 请求token
E->>S: token
S-->>A: requestForConfig()
A->>S: 返回配置
Note over S: 若错误,停止后续
loop 上报数据
S->> S: 生成临时SM4钥匙
S->>E: uploadEncryptedJson()/uploadEncryptedFile()
alt 上报成功
E->>S: 成功
Note over S: 啥也不干
else 上报成功但公钥需更新(1507)
E->>S: 返回最新SM2公钥
Note over S: 更新本地公钥
else 解码失败(1508)
E->>S: 返回最新SM2公钥
Note over S: 抛弃所有数据,更新公钥
else Check-Code校验失败(1509)
E->>S: 无
Note over S: 不应该在正式SDK发生
else 上报失败
E->>S: 失败
Note over S: 缓存数据、落盘(md5、加密后钥匙(16进制字符串)、iv、加密后数据)
end
S->>-S: 销毁临时SM4钥匙
end
E->>-E: SM2钥匙对
值得注意的是:
- 为了确保解密后数据无误,同时上报原始数据MD5用以比对
- 为了确保服务器更新SM2公钥后上报仍然可以进行,我们设计了主从密钥方式,被换下的密钥并不删除,而是作为从钥继续用以解密,并且通知终端更新公钥
- 为了确保上报失败后可以重试,我们将加密数据以及meta信息落盘
该方案经过云鼎实验室同事确认可靠性,我们最终采取了该方案
重难点解决
高并发下的解密实现
由于解密过程需要用到线程相关的变量,若每次解密都去生成对应的上下文将非常耗时。同时由于在QAPM的国密方案下公钥是定期更新的,所以这里为了保证解密流程的性能,在初始化时使用SM2InitCtxWithPubKey
为每一个Worker创建了一个上下文。
/**
* @brief 使用SM2获取公私钥或加解密之前,必须调用SM2InitCtx或者SM2InitCtxWithPubKey函数.如果使用固定公钥加密,可调用SM2InitCtxWithPubKey,将获得较大性能提升
* @param ctx 函数出参 - 上下文
* @param pubkey 函数入参 - 公钥
*/
func SM2InitCtxWithPubKey(ctx *SM2_ctx_t, pubkey []byte) {
if ctx == nil || pubkey == nil {
panic("invalid parameter")
}
if len(pubkey) < 130 {
panic("memory len is too small")
}
C.SM2InitCtxWithPubKey(&ctx.Context, (*C.char)(unsafe.Pointer(&pubkey[0])))
}
再通过自行实现的协程池管理并发解密任务,兼顾解密服务的稳定性和吞吐量。
代码语言:javascript复制type Worker struct {
name string
ctx *sm.SM2_ctx_t // ThreadLocal ctx
handler WorkerHandler
}
避免国密接口对原有架构的侵入性
由于实现的国密方案是在项目原有的接入层微服务代码中拓展实现,为保证原有架构的完整性,避免国密接口侵入导致的额外开发量以及额外的维护成本,我们对接入层的架构进行了微调,最终通过重写fasthttp的部分方法(如BodyGunzip, MultipartFormBoundary, MultipartForm, Body等),细化对request的处理步骤,完全解耦了解密和接口具体逻辑
耗时监控
最终上线符合国密标准的接入层系统后,构建耗时监控如下:
可以看到耗时在可控范围内,且除文件上报外耗时无明显变化
总结
安全无小事,这是我第一次参与大型的加解密服务的设计与实现工作中,希望后续还会有机会丰富我浅薄的网络安全知识