为了保障政府主体的数据通信安全,涉及到敏感数据在公网上的传输时,需要对请求入参和响应进行非对称加密,且保证以下两点以确认安全性:
- 客户端不留存私钥(存在反编译和暴力解包风险)
- **私钥在任何时候都不经过响应体进行返回(**无私钥无法解密,私钥只能明文传输,不安全)
鉴于此,结合互联网上流行的加解密策略,我们针对请求和响应两个阶段进行了不同的加解密策略设计。请求加密后,是由服务端验证 + 解密,是相对安全的。但是反过来,响应报文需要由客户端解密,客户端是不安全的,app和web都可以被反编译,密钥即使加密缓存也可能被破解。
需要讨论的点是:
- 使用高位数的非对称加密算法已经足够安全,黑客除非得到私钥,否则不可能篡改请求(防篡改和加密二选一?结合使用?)
防篡改机制在一定程度上存在安全隐患,比较适用于后端与后端对接。按照签名key的特殊性,应该只留存于服务端。目前三晋先锋使用场景主要在开放平台,后端对接,没什么问题,但是放在app上就存在一定隐患。已经使用改进方案,基于用户登录请求分发签名key,加密缓存在Jwt Token中,后端校验token的同时校验签名key匹配。下面的方案主要基于结合使用的方案,安全系数非常高,但是在高并发场景下,对性能有一定损耗。
无论是从性能还是从安全性角度上,实际使用中我们用国家非对称安全加密算法(SM2,简称国密2算法),对RSA进行了完全替代。
文章目录
- 一、签名算法
- 2.1.1 签名报文的解析验证流程
- 2.1.2 签名规则
- 2.1.3 签名key的生成与验证
- 二、请求报文加密(~~RSA-256~~ SM2 国密2算法)
- 2.1 加密算法
- 2.2 加解密工作模式
- 三、响应报文加密(KEY-MAP + clientKey + SM2-ECC)
- 3.1 注册密钥 + 服务端加密策略
- 3.2 注册设备逻辑
- 3.3 服务端加密和密钥缓存
- 3.4 客户端解密请求体
- 3.5 安全性加固和时效策略考虑
- 四、 全局渠道标识
一、签名算法
签名算法是一种防止篡改的机制,但是本质上并不安全。黑客得知了签名算法,并得到约定的key时(很容易在客户端反编译取得),就会变得畅通无阻。不过,签名算法 + 非对称加密算法,让黑客从根本上无法得到真正的请求体。本小节默认所有的请求体和响应体都是加密状态,加密算法下小节提。使用更安全的方案,参考序章
目前app服务端的验签算法已经完全能满足要求,这里结合沃支付接口和党建接口,对于请求和响应都进行验签,以保证安全性。进行整合后设计如下:
2.1.1 签名报文的解析验证流程
服务端:
客户端:
2.1.2 签名规则
签名的生成规则在服务端和客户端需要保持一致。额外的,为了通信的对称性,还约定了一个key额外的拼接到请求体后面,再进行md5编码运算,最终比对md5值是否匹配。
我们约定了重要的请求必须以POST请求发送,所以将整个请求体按照字符串的形式进行处理,拼接约定的签名key作为额外的内容,然后进行md5-hex32加密字符串。
例如:
用户请求POST http://api.ht-travel.gov.cn/api/scenes,约定的key为6fg3c7e5b2z0f03h1
, 请求体为:
{"name": "大槐树","page": 0,"size": 10,"order": "asc"}
则将上述内容作为整体字符串,直接拼接key,得到
{"name": "大槐树","page": 0,"size": 10,"order": "asc"}6fg3c7e5b2z0f03h1
然后使用HEX32的格式对上面的字符串进行md5编码,结果为3e40a7c47fd31e80ed8a8074bff5087b
,该值就是签名内容。
最终,用户访问这个接口的正确内容为:
{"headers": {"Authorization": "用户token","Signature": "3e40a7c47fd31e80ed8a8074bff5087b","Channel": "ios",},"body": "{\"name\": \"大槐树\",\"page\": 0,\"size\": 10,\"order\": \"asc\"}",
}
响应的签名算法也一致。需要客户端进行验签确认。
签名key在客户端和服务端都必须留存,且内容一致。
2.1.3 签名key的生成与验证
签名key由于其客户端留存性,对于安全性而言是一个隐患。在安全性的保证方式面前,我们优先选择用户绑定的方式,这种方式安全、明确、且较为容易实现。
分发时机:用户登录,返回token的同时返回签名key,客户端缓存key和token。**注意,返回的token和key的报文都是经过加密的,无法破译。**在token有效期内,签名key一直有效,如果用户token被撤回或过期,签名key失效,无法验证通过,最大程度保证了安全性和可逆性。
**验证时机:**用户请求接口,携带token以及使用签名key进行签名后,服务端从token信息中解析出key,使用签名算法进行计算,比对。安全性在于:
- token永远都是由服务器发放,客户端永远无法伪造,因为不知道密钥。
- 签名key和token严格绑定,如果伪造签名key,绝对无法通过验证
- 签名key无法被拦截,不会明文交互于互联网,仅存在于用户手机缓存,且每个用户都不一样。token经过加密,即使截获token,也无法成功发送任何请求。
综上所述,该方案的安全性基本上可以达到90%以上(排除暴力破解和客户端设备被偷盗)
二、请求报文加密(RSA-256 SM2 国密2算法)
请求报文加密的基础条件是:
- 仅允许使用POST请求发送参数或请求数据,以保证参数的key和value都不被泄露
- 客户端需要通过签名算法,保证请求体不被篡改,然后再使用公钥加密。(公钥是公开透明,缓存在客户端,所以必须加签名算法防止恶意篡改伪造)
- 加密的目的和签名算法不同,主要是为了:保证传输过程中内容的保密性,让参数和值均不透明
2.1 加密算法
这两种和咱们现在登录接口的加密方式都基本一致,请求报文加解密基本上就可以定这种了,现在主要是响应报文加解密的安全性需要讨论。党建那个也没涉及到响应报文的解密场景。
2.2 加解密工作模式
由服务端生成唯一的一对SM2-ECC密钥对(椭圆曲线算法),公钥提供给客户端留存一份,私钥在服务端留存。
客户端发送请求前,统一使用SM2算法,用公钥对请求体进行加密,然后才发送。服务端接收后,先对报文用私钥解密后,再进行签名验证或者后续操作。
三、响应报文加密(KEY-MAP + clientKey + SM2-ECC)
上述请求报文加密的方案非常常见,且着实有效,在市场和生产环境中已经得到了充分的认证。与请求报文不同的是,响应报文必须在客户端实现解密,才可保障在传输过程中全程密文的安全性。三晋先锋的响应报文加解密仍然使用RSA,且客户端和服务端使用同一套私钥,虽然每个对接方都有独立的密钥对, 但是仍然有着较大的安全隐患。不过由于三晋先锋是面向服务端对接(不直接请求自客户端,我们是),所以这部分风险相当小。当然这些不在我们讨论的范畴内,我们的目的是为了保障传输过程中的安全性。
安全性设计不存在绝对的安全。但是我们会尽可能的加大其安全系数。为此,我们采用端到端加密的方案,深度保障:
- 客户端不留存密钥,密钥和硬件+用户关联,实现端到端的通信。每台手机的密钥都不一样,除非手机被偷,否则是安全的。具体的,我们要做到:
(1)同一个设备,不同的用户,有着不同的密钥
(2)不同设备,同一个用户,有着不同的密钥
(3)不用设备,不同用户,有着不同的密钥
(4)同一设备,同一用户,退出登录后再登录,有着不同的密钥
- 密钥基于注册的方式进行单次通信,在生命周期内仅存在一对一的映射,再次注册之后之前的密钥会失效。注册时机埋点在用户成功后,客户端的回调处理里,类似于微信,钉钉异地登录顶掉目前登录的设备一样。
3.1 注册密钥 + 服务端加密策略
为了尽可能的规避客户端和服务端之间发生密钥交换的情况,我们设计了一组密钥生成策略。以RSA密钥生成原理而言,密钥的生成需要一组随机值,随机值的获取,一种是UUID,基于硬件生成的唯一ID,一种就是机器UDID(设备唯一识别码)。方案甄选时,由于UDID在iOS设备上不再受到支持,故放弃。
最终的实现方案是:**用户登录换取token成功后,客户端基于RSA256算法,基于一个UUID生成一套密钥对。然后通过签名和加密算法,将公钥注册到服务端。**密钥对一个用户同时只有一对可用。
使用国密2椭圆曲线算法后,不再需要随机值,方案便简化为:
用户登录换取token成功后,客户端基于js版本的国密2算法库,当下直接生成一套密钥对,然后通过签名和加密算法,将公钥注册到服务器,并与当前token进行归属绑定。token有效期间,仅允许存在一套绑定。
3.2 注册设备逻辑
根据我们上面的设计,注册设备的时机是用户登录,和token进行绑定,这里我们描述一下登录认证接口(注册接口同理,理论上,注册即登录,最终也会对key进行绑定)
POST http:// s e r v e r : {server}: server:{port}/api/users/authenticate
请求头:
{"Sign": "用户签名"
}
请求报文如下(加密的):
{"username": "","password": "",...,"publicKey": "ALSDFJLASJDFASLJKFSADF7SDF78AS876FSA98DF9SA8F7S89ADF798==="
}
不返回任何响应。
该接口要求登录。原则上,将用户ID作为最小单元进行映射。
流程图如下:
3.3 服务端加密和密钥缓存
服务端接收到绑定请求后,会将该密钥对进行缓存,放入redis用户缓存区,以便后续使用。放入redis,也能保障分布式环境下的密钥原子性问题,保证一个用户同一时刻只有一对密钥可用。
流程图如下:
3.4 客户端解密请求体
注册密钥后,客户端已经可以正常发送请求。服务端返回的报文为密文,客户端使用当前缓存的密钥对,得到私钥进行解密。解密只有成功和失败两种结果,解密成功,正常处理后续逻辑;解密失败,客户端触发退出登录动作,弹出用户的非法请求。
3.5 安全性加固和时效策略考虑
本方案虽然能够严格保障通信安全,但是存在操作漏洞(不存在传输过程漏洞)。如果黑客已经取得了用户token,并且截获了我们的公钥,得到了签名算法,就可以自由的生成UUID强行换取密钥对进行恶意注册,显然这是不被允许的。
为了规避这一点,我们要求设备在用户登录时就提供公钥信息,这样一来,黑客不光得截获token,还得熟知用户的账号,密码或验证码等信息,而且没有请求公钥无法解密,最终黑客将束手无策。
我们将上述方案改造为和登录接口结合的策略。用户输入账号密码(验证码)后,由app端执行密钥生成,并拼接到登录请求体内。
请求报文将变为:
{"phone": "152123121231","code": "215331","key": "ALSDFJLASJDFASLJKFSADF7SDF78AS876FSA98DF9SA8F7S89ADF798==="
}
响应仅返回token
四、 全局渠道标识
app在接口调用过程中,需要明确标记调用渠道,以方便之后的埋点、终端统计等功能。
需要全局的在请求头中添加Channel
,可选值为ios
, android
, web
, h5
等