记一次国密落地的经历

2022-12-01 19:57:23 浏览数 (1)

前言

在一般意义上的后台服务中,身份认证可以保证数据源没有问题,完整性校验可以保证数据没有经过窃听者的篡改,但我们还要防止窃听者知道数据的内容,这就还需要加解密来帮助我们守住最后一道围墙

而在私有云交付的环境中,我们无法用现有的公司平台加解密服务,并且按照国家、金融行业等要求,需要用国密算法实现的加解密方案

国密存在哪些问题

使用不便

最大问题是使用不便。这是由于国密不在IETF国际标准中,不同于ECDSA、ECDH、RSA等国际算法,系统中往往包含相关标准加解密方式,业务数据包通过HTTPS传输时完全不用考虑如何交换公钥,如何加解密数据。

因此现阶段使用国密必须在业务层手动进行数据加解密,相当于对数据进行一步额外的操作。

无最佳实践

确定业务层进行加解密后,应该使用哪一种国密实现、该如何进行加解密是另一个难点,且暂不存在一个最优解。国密的实现方案很多,包括TencentSM、GmSSL和KMS服务器,这也需要进一步的调研和测试来决定最终方案。

国密落地过程

国密落地分为调研、制定、实现与测试四个阶段。

调研阶段主要目的有两个:

  • 找到性能高效、使用便捷并且有足够保障(维护活跃度、大厂背书、有变现渠道保证不会暴死等)的国密算法实现
  • 调研公司内国密改造案例

国密算法选择

我们通过benchmarks测试评估多种国密库算法性能,最终结果如下:

可以看到TencentSM的benchmarks测试结果较为突出,也是作为我们的最终选择

国密实现方案选择

对于国密的实现,我们收集到的可行方案如下:

  1. XX实验室建议方案:引入额外KMS系统,独立部署管理证书、私钥,分发公钥。
    • 优点:保密性好,完整的加解密方案(包括证书、信任链等待)
    • 缺点:需要在业务侧缓存大量SM4密钥,对性能有影响
  2. XXX网关团队数据加密方案:一次链接一个SM4密钥,发送SM4加密的数据同时发送SM2公钥加密后的SM4密钥。
    • 优点:过程简单
    • 缺点:服务端每次数据处理都需要双解密(解密密钥后解密数据),对性能有影响
  3. 无侵入数据加密方案:使用双SSL证书(RSA/SM2)的HTTPS前置机作为网络入口,完全无侵入性地替换。
    • 优点:完全无缝地替换国际加密协议,业务侧无感知
    • 缺点:SDK侧同样需要双证书支持,但目前普遍缺乏实现;需要添加额外组件HTTPS前置机

在制定方案之前,我们总结了对解密方案的需求:

  1. 简单无依赖
  2. 性能好
  3. 成熟
  4. 支持服务器热更新SM2公钥
  5. 支持上报失败后数据加密落地重传

可以接受的缺点:

  1. 对业务侵入

于是在制定方案时我们充分考量了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钥匙对

值得注意的是:

  1. 为了确保解密后数据无误,同时上报原始数据MD5用以比对
  2. 为了确保服务器更新SM2公钥后上报仍然可以进行,我们设计了主从密钥方式,被换下的密钥并不删除,而是作为从钥继续用以解密,并且通知终端更新公钥
  3. 为了确保上报失败后可以重试,我们将加密数据以及meta信息落盘

该方案经过云鼎实验室同事确认可靠性,我们最终采取了该方案

重难点解决

高并发下的解密实现

由于解密过程需要用到线程相关的变量,若每次解密都去生成对应的上下文将非常耗时。同时由于在QAPM的国密方案下公钥是定期更新的,所以这里为了保证解密流程的性能,在初始化时使用SM2InitCtxWithPubKey为每一个Worker创建了一个上下文。

代码语言:javascript复制
/**
 * @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的处理步骤,完全解耦了解密和接口具体逻辑

耗时监控

最终上线符合国密标准的接入层系统后,构建耗时监控如下:

可以看到耗时在可控范围内,且除文件上报外耗时无明显变化

总结

安全无小事,这是我第一次参与大型的加解密服务的设计与实现工作中,希望后续还会有机会丰富我浅薄的网络安全知识

0 人点赞