一
背景
Key Management
随着互联网应用的发展,作为一个服务普通用户的应用,整个流程很难完全由一个厂商全部完成,不可避免的会需要与其他厂家进行对接,比如应用的支付需要对接支付宝或者微信支付。而作为平台型应用,为了给用户提供统一的体验,也需要对接多个厂商进行服务的聚合,比如地图类的APP聚合打车一样。而且公司内部也会购买一些第三方服务。这些服务的接入一般是通过调用厂商提供的OpenAPI接口实现,通常不同的厂商会提供相应的加签加密规则,同时提供授权的加签密钥。而不同的厂商技术能力参差不齐,导致接口的安全性上有很大的差别。
图1 第三方服务校验条件及安全性对比
二
问题
Key Management
2.1
问题的发现
《通过窃取与银联结算密钥,编写非法结算程序转账》
罗某某在工作期间发现任职公司与某支付机构之间的资金结算交易、对账系统存在管控漏洞,遂通过其窃取的任职公司与某支付机构的资金结算密钥,编写非法结算程序,冒充公司身份,向某支付机构发送资金结算请求,使某支付机构将资金转入其指定的银行账户。
公司内部员工清楚业务流程,再加上可以比较轻易的接触到密钥信息,而且由于审计审查的滞后性,所以经常可以在新闻上看到一些内部员工泄密内部信息,对企业造成重大损失的案例。
2.1.1 代码风险
在发展初期,这些密钥由业务开发人员管理,可能会硬编码在代码或者配置文件中,提交到代码仓库,变更密钥也需要上线代码或者配置,有代码阅读权限的人均可查看密钥数据。
图2 密钥通过配置文件存储并使用
2.1.2 人员风险
为了对密钥进行统一管理,引入了KMS系统,密钥存储在KMS系统中,业务开发人员不直接接触密钥而是在程序中调用KMS系统获取密钥并进行后续的业务处理。通过KMS系统统一了密钥管理的流程,降低了密钥泄露的风险。
但是人员始终是不可控因素,开发人员完全可以通过调试程序等手段获取对应的密钥数据,并可以私下使用,而在整个过程中很难被发现。例如:可以调用接口私自查询犯罪记录等等。
图3 密钥通过KMS存储并使用
2.2
传统的解决方案
图4 业务程序获取密钥并访问第三方接口
密钥由KMS系统管理之后,用户在发起请求之后,业务方程序会从KMS中获取对应的密钥信息,对请求进行签名加密,然后访问对应的第三方服务获取数据。
业务方仍然需要在提供服务时获取密钥信息,也就无法杜绝业务方研发人员接触密钥。
例如:在程序的调试和运行过程中经常会打印各种日志,研发人员完全可以通过日志形式输出密钥信息,而且后续通过日志审计等操作也很难发现密钥泄露的问题。即使最终发现也只能事后补偿,密钥可能已经被泄露和利用。
2.3
理想的解决方案
在以上介绍的场景中有三个角色:服务提供方、服务调用方、密钥存储方。
图5 密钥存储及业务调用理想模型
-
密钥存储方:知道密钥,不知道业务逻辑,无法利用密钥
-
服务调用方:知道业务逻辑,可以利用密钥,但是无法获取密钥
-
服务提供方:第三方服务完全无感知
理想情况下业务调用方和密钥存储方逻辑上是相互独立的两个服务,需要相互配合完成OpenAPI调用,并且对于第三方服务来讲是无感知的。
实际情况下业务调用方和密钥存储有数据上的依赖关系,系统间的调用就会带来密钥泄露的风险。
计算机领域的任何问题都可以通过引入一个中间层解决。所以在解决OpenAPI调用中密钥泄露的问题上,也采用了引入一个中间层的方式解决。
图6 插入密钥网关层的调用示意图
三
密钥网关
Key Management
图7 密钥网关功能示意图
-
密钥网关:密钥管理+出口网关的结合,产生了1+1>2的效果。
-
密钥管理:主要功能是对密钥进行加密存储,并且对密钥的更新提供了版本控制。
-
出口网关:对访问第三方OpenAPI的请求进行转发,并且提供超时控制、限流等功能。
-
密钥网关:结合了密钥管理和出口网关两者的功能,转发请求的同时可以获取密钥生成签名,并且对传输的数据进行加解密等处理。
图8 密钥网关功能模块图
3.1
签名模块
签名模块是密钥网关的核心模块,负责对请求数据进行签名及加密处理,并且对响应数据进行校验和解密。
签名生成又分为简单签名和二次验证两种情况。
3.1.1 插件
最开始的时候统一由插件形式实现,每接入一个第三方就单独开发一个插件应用,功能灵活定制,需要单独开发,单独测试上线,整体的工期流程比较长。
-
简单签名:
简单签名是最常见的模式,一般情况下由第三方服务提供AppKey和AppSecure两个Key,一个作为接入标识传输,一个作为必须参数参与签名计算。例如:
{
"appKey":"appKey",
"data":"123456",
"sign":sha256('data=xxxx×tamp=xxxxx'+AppSecure)
}
-
二次验证
在二次验证模式下,除了像简单签名那样对请求参数进行排序和签名之外,还需要单独从第三方服务获取token,相当于多了一步登录的过程,并且后续调用中需要保存及更新token,维护登录状态。
图9 二次验证模式访问流程
3.1.2 自定义模式
随着接入的增多,发现第三方签名规则都大同小异,主要处理过程总结为以下几个步骤:
-
获取token
-
请求参数填充或者变更
-
请求参数排序
-
生成待签名字符串
-
计算签名
-
回填签名信息
-
处理返回值(解密、格式变更、清理Token缓存等)
其中第1步为二次验证模式下需要的,并且大部分第三方接入也只是对参数做签名,所以第7步也不是必要的。简单签名基本上只需要2~6步。为了避免重复开发->测试->上线等一系列重复操作引入了自定义模式。用户可以用代码方式实现以上步骤的逻辑,实现第三方的签名和加密过程。
自定义模式引入了一个JavaScript代码解释器,选择JavaScript主要是考虑普及度比较高,方便使用者上手。并且在解释器初始化过程中内置了常用的排序及签名加密算法。用户可以根据第三方签名文档,以JavaScript表达式方式直接在后台编辑签名方法,可实时生效。具体的编辑界面如下:
图10 密钥网关后台签名配置界面
密钥网关在收到请求之后,拆分请求数据,生成对应的变量数据
-
reqHeaders : 请求头结构体
-
reqParams URL : 链接参数结构体
-
reqBody POST : 请求body参数结构体
上图中AppKey、AesKey、AesIV、PrefixUrl、TokenUrl几个参数代表参与计算时需要的第三方密钥及相关参数,这些需要先配置,然后在编辑界面引用。
图11 密钥网关后台密钥存储界面
-
签名
将所需参数传入到自定义签名方法中进行计算,用户根据具体的第三方签名规则编写签名及加密方法,对请求数据进行重新赋值。包括重写请求体、请求头、URL参数以及变更URL路径等,然后发送请求至对应的第三方服务。
-
返回值处理
在收到返回值之后,会调用返回处理方法中的代码进行解密处理,对调用方来讲无需关心解密操作,拿到返回值可以直接使用。
-
token管理
在以上功能的基础上,又增加了token获取和缓存的功能,这样自定义模式就能统一实现简单签名和二次验证的签名模式。
-
请求案例
下面用一个实际的案例进行举例,这是一个需要先获取token然后进行签名、加密操作的调用,现实中能碰到最复杂的情况基本就是这样了。
图12 请求案例处理流程图
① 初始化及密钥加载
图中第1行可以看到业务方传入的数据只有时间戳和页码信息,并且在最下面的日志中可以看到已经获取到相关的密钥信息,只展示了密钥名称,具体的值由密钥网关单独管理并缓存在进程中。
图13 初始化及密钥加载日志
以上日志就是调用签名方法前的操作。
② 签名方法
function (reqHeaders, reqParams, reqBody, AppKey, AesKey, AesIV, PrefixUrl, TokenUrl) {
var ts = reqBody.get('timestamp');
var token = token_load();
//判断token是否存在,不存在就刷新
if (!token) {
var reqData = {
"appKey": AppKey,
"data": base64_encode(aes_cbc_pkcs5_encrypt(AesKey, '{"appKey": "'+AppKey+'"}', AesIV)),
"timestamp": ts
};
debug_info("token_str", "appKey="+AppKey+"&data="+reqData["data"]+"×tamp="+ts);//调试输出
reqData["sign"] = sha256("appKey="+AppKey+"&data="+reqData["data"]+"×tamp="+ts).toUpperCase();
debug_info("token_data", reqData);//调试输出
var respObj = JSON.parse(http_post_json(TokenUrl, reqData, {}));
if (respObj["data"]["accessToken"]) {
token = "Bearer " + respObj["data"]["accessToken"];
token_save(token, respObj["data"]["expireTime"]);
}
}
reqHeaders.set("Authorization", token);
reqHeaders.setURI(PrefixUrl + reqHeaders.getURI());
var originData = reqBody.get('data');
var enData = base64_encode(aes_cbc_pkcs5_encrypt(AesKey, originData, AesIV));
verify(enData);//校验错误
reqBody.set("appKey", AppKey);
reqBody.set('data', enData);
debug_info("body", reqBody.buildSignStr());//调试输出
var sign = sha256(reqBody.buildSignStr());
verify(sign);//校验错误
reqBody.set('sign', sign.toUpperCase());
}
上面签名方法代码运行的日志如下图所示,可以对比代码及日志相互印证。
图14 签名处理及请求转发日志
日志最后一行就是实际发出请求,可以看到已经将传入的data参数进行加密,并且添加了appKey及sign参数。
③ 返回处理方法
在接收到第三方返回之后,将body体转为JSON对象赋值给respBody变量,然后调用自定义方法解析数据。
function (respBody, AppKey, AesKey, AesIV, PrefixUrl, TokenUrl) {
if (401 == respBody["code"]) {//code 401 认为是第三方服务端返回token异常,这时候删除本地缓存,下次请求会重新获取token
token_delete();
}
if (respBody["data"]) {
respBody["data"] = aes_cbc_pkcs5_decrypt(AesKey, base64_decode(respBody["data"]), AesIV);
}
}
返回处理的日志如下,返回为加密数据,如上面代码展示,对data字段进行解密处理,业务方无需关心加解密方式,可以直接进行后续业务逻辑处理。
图15 响应处理日志
3.2
权限管理
在密钥网关正式运营一段时间之后,第三方的签名接入完全由密钥网关研发进行维护,而业务方只关注业务流程的接入。
随着业务的发展,接入量的增加,统一由密钥网关研发人员进行维护已经不能应对业务快速发展的需要了。
3.2.1 多租户
自定义模式的接入方式完全可以由业务方开发人员自行操作,而密钥网关研发人员只需负责操作后台密钥管理,无需深度参与第三方接入的整体联调、测试、上线过程。这样就大大提高了业务方接入效率,同时也降低了密钥网关研发的工作量。
在此基础上,提出了多租户的概念。每个业务方就是一个租户,由密钥网关提供对应的AppKey,业务方需要在发起请求的时候携带对应的签名,密钥网关根据签名判断业务方访问的合法性。
每个租户可以通过后台管理自己接入的第三方服务,可以配置超时时间、绑定所需密钥、编辑签名规则、以及上下线服务等操作。
3.2.2 多版本
在多租户功能上线后,为了方便租户对于密钥以及配置的变更进行回溯,又加入了多版本功能。
每个第三方接入的配置都以版本形式提交,每次提交变更都会生成新的版本,待相关人员确认无误后以新版本替换原有版本。多次变更会形成一个完整的版本链,方便配置的变更和回滚。
3.2.3 多角色
在多租户和多版本的基础上,后续会引入了多角色的概念。有网关管理员、租户管理员、租户研发等几个角色。
网关管理员:拥有超管权限
租户方研发:可以新增、修改、上下线自己租户下的第三方配置,对于修改及上下线等敏感操作,只会在配置版本链上生成对应的节点,没有真实的变更。
租户管理员:负责租户下第三方配置的上下线及回滚操作。
这样在租户操作部分也加入了审核相关的操作,在double check的基础上降低操作风险。
3.3
异常处理
作为一个网关中间层,需要考虑各种失败的情况,保证服务的可用性和稳定性。常见的情况包括第三方访问超时、自身配置获取失败、密钥获取失败等。
3.3.1 第三方服务超时
每个第三方的服务质量、接口返回时长参差不齐。并且由于是外网请求,可能会碰到各种情况,所以要假设失败的可能性。
对于失败的请求要尽快返回,节省资源的同时尽快通知上游业务方。这样就不能设置一个统一的超时时间,需要根据不同第三方的特点设置对应的超时时间,使业务方尽可能的成功访问。
为了解决这个问题,在后台创建接入配置的时候首先会对TCP链接超时和HTTP返回超时设置一个默认的超时时间,后续根据第三方的具体表现可以准实时更改线上配置,延长或者减少超时时间,尽快返回失败,而不是大量的等待影响处理。
3.3.2 配置及密钥获取失败
密钥网关运行必须的配置和密钥会存储在数据库及KMS中,通过后台界面进行管理。这些数据是重要的基础数据,获取失败的情况下会造成服务异常。所以作为一个中间层做了相应的异常设计,具体逻辑分为部署和重启两种情况。
部署:在服务部署后第一次启动时,需要拉取全量配置及密钥保存在服务器节点中,如果拉取失败则服务启动失败。由于服务是滚动部署和升级的,如果节点重启失败,该节点转为摘除状态,由集群其他节点继续提供服务。
重启:服务重启会尝试读取数据库、KMS以及本地缓存数据,以远程数据为准,远程获取失败则以本地缓存配置重启服务,保证一定的可用性。
四
总结
Key Management
密钥网关服务作为一个中间层和业务密切相关,并且涉及多个业务线,多种签名及加解密算法。在服务过程中又要保证稳定性。该方案只是对密钥管理的一次尝试,后续在保证稳定性的基础上,持续对接入方式进行优化,降低接入工作量,方便使用。
引用:
窃取与银联结算密钥,编写非法结算程序,15 个月窃取 608 万:
https://baijiahao.baidu.com/s?id=1707630477206709907&wfr=spider&for=pc
原文始发于微信公众号(货拉拉安全应急响应中心):货拉拉第三方密钥管理实践
- 左青龙
- 微信扫一扫
-
- 右白虎
- 微信扫一扫
-
评论