作者简介 : Henryye,叶轩,来自腾讯微信事业群,主要负责腾讯开源项目TENCENT SOTER(GitHub地址:https://github.com/Tencent/soter, 生物认证平台的开发、维护与运营。

提到指纹支付,你会怎么做?

假如有一天,产品经理安排你做指纹支付,并且要下版本就上,你会怎么做?

如果是产品大哥,就从工位下面抽出一把指甲刀架在他脖子上,让他跪在墙角唱征服;

如果是产品妹子,就让她请你喝咖啡,然后谈天说地,趁此机会告诉她“还是选择世界和平吧,比做指纹支付简单多了。”

当然,想象还是太温柔了。真正做过指纹支付项目的在下,经常会在半夜三更回忆起当年做指纹支付需求时候的噩梦,在梦里,我就给自己加戏,手撕产品经理。

也许产品大大们会发出抗议:“指纹支付而已,客户端现成的接口,有何难?”

系统接口行不行?

从2013年iPhone 5s第一款带有指纹识别功能的iPhone上市以来,“指纹支付”这个词就开始频繁出现在各个产品的PM列表排期中。但是,Android 6.0以前的设备,并没有一个统一的指纹认证接口。这也就意味着如果你是一个苦逼的程序猿,那么你就要一家家适配各自的指纹方案,并且还要维护厂商的接口升级。如果结合Android市场的碎片化来看,想要全机型覆盖,简直就是Impossible Mission。实际上,在项目初期,微信便尝试了一家家接入,结果仅仅接入华为Mate 7和荣耀7,便用了整整三个月!这种投入显然是得不偿失的。

好在,从Android 6.0开始,系统提供了标准的FingerprintManager。这一重大利好,让做着类似需求的程序猿们仿佛在黑暗中看到一丝光明,因为这个接口看上去是那么简单易用。无论你是什么品牌的手机,只要是Android 6.0或更新的系统,按照下面的写法,就可以实现指纹认证功能:

FingeprintManager mFingeprintManager = ...
mFingeprintManager.authenticate(null, mCancellationSignal, 0 , new AuthenticationCallback(){...}, null);

设计本身也很简单:

系统认证接口

看上去很完美,仿佛实现指纹支付根本不用开发1个版本,只用1小时,对不对!

但是仔细看下这个接口,感觉哪里不太对:接口仅仅返回认证成功/失败,如果直接信任这个结果,手机被root了,岂不是随时可以将认证结果从false改为true?

那我们换一种思路:root的手机不让用指纹支付行不行?

傻孩子,那你怎么判断手机是不是root呢?不也是通过Android接口获取的值么?这个值一样可以被改掉。

支付安全不可儿戏,一旦出问题,就是重大事故。所幸的是,Google也意识到了这个问题,所以在发布指纹认证接口的同时,增强了原本的KeyStore接口,和Fingeprprint接口联动(代码实现可参考Google官方Sample,链接:https://github.com/googlesamples/android-FingerprintDialog):

Fingerprint安全架构

这张图看上去不明觉厉,原理其实并不难:Google在Android 6.0之后,允许用户在应用中生成一对非对称密钥,将私钥存储在TEE中(什么是TEE?稍后会讲),任何人,包括应用自己甚至Android系统都无法获取私钥,除非用户使用指纹授权才能使用,签名或者加密传入的数据,然后输出密文。这样的话,就可以利用密钥的签名-验签机制(小白不懂什么是签名验签?Google下咯~或者看这篇文章解释签名的部分),只有用户使用指纹签名之后,才能产生正确的签名,后台验签即可,这样就能保证链路安全。

这个设计非常巧妙,但是Google百密一疏:如果黑客在密钥生成的时候就拦截了请求,替换为自己的密钥,那么后面签名和加密,用的也是黑客的密钥,那么整套系统的设计也就崩塌了。

Hack示意

另外还有一个问题,如果仅仅返回true/false,那么只要是录入到设备内的指纹,就可以假冒你的身份支付。这对于家里有熊孩子的家长来说,简直就是银行卡噩梦。雪上加霜的是,对于Android设备而言(其实iOS也是一样),只要知道了锁屏密码就可以录入新的指纹。如果支付后台直接信任指纹认证结果,就相当于将原本非常秘密的支付密码,退化到了锁屏密码的级别。这样,无论支付后台做了多么严密的风控策略,按照木桶原理,从根本上整个系统就是不符合支付安全的。

当然,当时也有类似于FIDO(链接:https://fidoalliance.org/) 之类的认证联盟,但是整个流程过于复杂,甚至还要求在应用后台植入sdk。而且,类似方案的中心服务权限过高,会导致如支付笔数、开通用户数等关键指标为人所知,因此也就无法使用。并且支持设备数实在太少,也并无接入动力。

研究过这些之后,发现并不可直接使用任何一个方案,场面一度尴尬。没有合适的轮子,怎么办?

没有轮子,能造轮子么?

让我们回头看看Android系统的指纹接口设计:

  • 方便的指纹接口,完美!
  • 创造性得将指纹模块与密钥模块结合起来,使得用户授权即签名变得可能,完美!

那Google没有做到什么呢?

  • 由于没有一个可信的信任根,导致密钥很容易被替换
  • 无法从认证结果中获取到底是哪一个用户授权本次认证请求;

同时,我们意识到,在生物认证领域这个千亿级市场中,缺乏一个统一、安全、易接入的认证标准,微信有这样的需求,其他应用也必然如此。微信有能力解决这些问题,实现自己的业务需求,也希望将成功经验复制。既然这样,借此机会制定一个生物认证标准,提供一个生物认证平台,微信责无旁贷,这就是SOTER的起源。

如果以做标准的要求来实现SOTER,那么除了刚刚所述的系统接口缺陷之外,系统设计时还需要考虑:

  • 后台不存储任何敏感信息,包括对称密钥、非对称密钥私钥,更不能将指图案以任何形式传输或存储,防止应用后台被脱库;
  • 如果有后台交互,不暴露应用方核心商业隐私,如认证次数、业务开通次数;
  • 应用接入门槛低,客户端无须集成重量级sdk,后台无须集成sdk;
  • 简单易用,第三方应用只需要操作上层接口,无须进行复杂的底层开发。

如何产生一个可信的信任根(设备根密钥)?

信任根的重要性之前已经说明。如果一个系统依赖密钥签名,有一个可以信任的根密钥,才有可能构建安全的信任模型。但是,如果一台手机出厂之后才产生根密钥(ATTK),那么中间有足够时间窗口给到黑产从业者替换掉它。因此,常规方式产生根密钥一定是不行的。所以,我们有了一个大胆的想法:直接与厂商合作,在设备出厂之前,产线上生成设备根密钥,公钥通过厂商服务传输给微信密钥服务——TAM。这个想法虽然对厂商改造比较大,但是由于我们直接通产业链上游(高通、MTK等)合作,研发出了一套统一的解决方案,以及产线工具,成功说服厂商对产线做了最小化的改造。It’s tough, but we did it!

设备根密钥流程

  1. 厂商在产线上对设备下发生成设备根密钥命令;
  2. TEE中生成一对设备唯一的RSA-2048非对称密钥,私钥存储在设备RPMB区域,没有任何厂商或者应用方可以读取(包括微信与设备厂商),公钥以及设备ID导出;
  3. 公钥和设备ID上传到厂商服务器,之后通过公众平台安全接口,传输到微信公众平台;
  4. 微信公众平台将公钥与设备ID传输到微信TAM服务器。
  5. 这里,我们又遇到了我们的老朋友:TEE(Trusted Execution Environment),后面我们也会多次与这个名词打交道。如果想要看详细的介绍,可以参考这里(链接:https://en.wikipedia.org/wiki/Trusted_execution_environment)。 当然了,我相信大部分同学都跟我一样,只想要一个形象的解释。简单地说,你的手机中,除了类似Android这样的操作系统之外,还有一个独立的环境。这个环境目前并无行之有效的破解方法,也就是说即使Root了Android系统,都无法破解TEE中的数据。如果将整部手机比作房子的话,Android操作环境就是客厅,TEE就是你的保险箱。可想而知,如果将所有的数据都存储在TEE,关键操作也在TEE内进行,岂不美哉!当然了,这样的话,所有从TEE中出来的敏感数据,就一定要添加上使用可信密钥对其的签名了。

有了设备根密钥之后,认证链的构造逻辑就清晰了很多:采用密钥链的形式,用已认证的密钥来认证未认证的密钥就可以了!

如何构造完整认证流程?

方法论有了,实施就变得简单。

但是,依然有一个问题需要思考:到底需要多少层密钥呢?密钥层数少,那么每一次都需要前往中心服务(微信TAM)验签,对第三方应用而言,会更担心泄露自己的商业逻辑;密钥层数越多,会增加了传输复杂度和失败率。经过多方讨论,SOTER决定使用三级密钥,除了产线预制的设备根密钥之外,增加定义应用密钥(每一个应用生命周期内只需要存在一对)和业务密钥(每一个业务需要一对)。事实证明,这是一个明智的选择:这样既保证了流程的流畅度,又保证应用的关键商业隐私不暴露。为什么?往下看。

架构

SOTER架构图

我们十分欣赏Google的指纹和密钥模块接口设计,因此,我们与厂商合作,在此基础上添加patch包,即可迅速实现整个上层架构。

准备应用密钥(ASK)

准备应用密钥流程示意图

  1. 应用第一次启动时,或者在第一次使用业务之前,请求生成设备根密钥;
  2. 密钥生成之后,私钥在被TEE保护,加密存储;
  3. 公钥和设备ID等相关信息,在TEE内直接被设备密钥私钥签名之后,返回给应用;
  4. 应用将公钥相关信息和签名传输至应用后台;
  5. 应用后台通过微信公众平台后台接口,请求验签;
  6. TAM使用对应的设备密钥公钥验签,通过之后返回给应用;
  7. 应用后台存储对应的应用公钥。

传输给后台的原串示例:

注1:自设备出厂即在TEE中存储。每一次SOTER相关操作都会使该值自增,后台存储该放重放因子。如果后台发现本次请求防重放因子比已记录的值小,则可认为是非法请求。

注2:本意为Linux系统中用户ID,在Android系统中,一般而言每一个应用都有一个uid,可用于区分应用以及权限控制。注意,uid不同,对应的应用密钥与业务密钥均不同,后台应将uid与cpu_id一起区分密钥。

准备业务密钥(Auth Key)

准备业务密钥流程示意图

  1. 应用在开通业务时(如指纹支付),请求生成业务密钥。同时,在生成时声明该密钥除非用户指纹授权,否则私钥不可使用。
  2. 密钥生成之后,私钥在被TEE保护,加密存储;
  3. 公钥和设备ID等相关信息,在TEE内直接被应用密钥私钥签名之后,返回给应用;
  4. 应用将公钥相关信息和签名传输至应用后台;
  5. 应用后台使用对应的应用密钥公钥验签,无须请求微信TAM中心服务;
  6. 验签成功之后,应用后台存储对应的业务密钥。

传输数据与含义与应用密钥相同,不再赘述。

认证流程

认证流程示意图

  1. 客户端请求后台,获取挑战因子;
  2. 获取挑战因子之后,将挑战因子送往TEE,准备签名结构体,准备签名;
  3. 应用请求用户指纹授权;
  4. 用户指纹授权后,直接将本次认证使用指纹在本设备内的索引传输给密钥模块,在TEE内使用业务密钥私钥签名挑战因子以及该索引。

应用获取原串与签名串后,传输至应用后台。应用后台使用对应的业务密钥公钥验签,如果成功,则此次认证或者开通请求合法。

传输给后台原串示例

流程是否符合要求?

轮子造好了,我们在自我欣赏的路上越走越远。回过头来看,SOTER是否满足了我们的要求呢?

  • 添加信任根:SOTER在工厂环境中传输设备根密钥,保证信任根安全;
  • 可区分指纹:认证之后,TEE内部直接传输本次使用的指纹ID,可使应用自由选择是否区分指纹;
  • 后台不存储敏感信息:后台仅存储设备ID和公钥,从此即使后台设计再烂,也不再害怕脱库;
  • 后台交互不暴露隐私:独创应用密钥,保证业务开通、业务使用量等不需要经过应用服务器;
  • 后台不需要sdk:后台使用成熟的公众平台接口,文档丰富,学习成本低,更无须sdk。

当然了,我们的方案得到了各大厂商、芯片上的认可,在短时间内,已经拥有了数亿设备的支持,覆盖几乎所有的主流手机品牌,因此应用接入完全无须考虑是否需要多设备适配,或者质疑适配不足。顺便,我们支持了vivo和OPPO的5.x指纹机型,即使系统本身不具有统一的指纹接口。

然而,还有最后两点没有做到:

  • 客户端接入门槛低,客户端sdk轻量,甚至不需要sdk;
  • 简单易用,客户端无须进行深度开发即可使用。

解决这两个问题的方法只有:开源!

我们开源了什么?

为了满足不同应用的不同场景,我们开源了:

  • SOTER客户端核心接口soter-core:SOTER内部操作密钥、调用指纹的直接接口。sdk大小约40KB,对安装包大小增量可忽略不计;
  • SOTER客户端过程封装接口soter-wrapper:封装了SOTER具体流程以及适配问题机型。sdk大小约70KB,安装包大小增量更少;
  • 快速上手的客户端demo,从零开始,帮你短短几行代码实现指纹支付;
  • 完整的客户端文档和后台接口文档;
  • 完整的原理剖析和快速入手。

这一切,尽在TENCENT SOTER(GitHub地址:https://github.com/Tencent/soter)。

使用SOTER最快能多块?如果你只需要做锁屏之类对安全性要求不高的需求,只需要:

  • 添加gradle依赖
    在项目的build.gradle中,添加 SOTER依赖
    dependencies { ... compile 'com.tencent.soter:soter-wrapper:1.3.8' ... }
  • 声明权限
    在 AndroidManifest.xml中添加使用指纹权限
    <uses-permission android:name="android.permission.USE_FINGERPRINT"/>
  • 初始化
    初始化过程整个应用声明周期内只需要进行一次,用于生成基本配置和检查设备支持情况。你可以选择在Application的onCreate()中,或者在使用SOTER之前进行初始化。
    InitializeParam param = new InitializeParam.InitializeParamBuilder() .setScenes(0) // 场景值常量,后续使用该常量进行密钥生成或指纹认证 .build(); SoterWrapperApi.init(context, new SoterProcessCallback<SoterProcessNoExtResult>() {...}, param);
  • 准备密钥
    需要在使用指纹认证之前生成相关密钥
    SoterWrapperApi.prepareAuthKey(new SoterProcessCallback<SoterProcessKeyPreparationResult>() {...},false, true, 0, null, null);
  • 进行指纹认证
    密钥生成完毕之后,可以使用封装接口调用指纹传感器进行认证。
AuthenticationParam param = new AuthenticationParam.AuthenticationParamBuilder()
  .setScene(0)                                    
  .setContext(MainActivity.this)                                    
  .setFingerprintCanceller(mSoterFingerprintCanceller)
  .setPrefilledChallenge("test challenge")                                    
  .setSoterFingerprintStateCallback(new SoterFingerprintStateCallback() {...}).build();
SoterWrapperApi.requestAuthorizeAndSign(new SoterProcessCallback<SoterProcessAuthenticationResult>() {...}, param);

当然了,如果你想要实现指纹支付、指纹登录等高安全性场景,还有一些其他工作要做,具体可以参考我们的示例代码(链接:https://github.com/Tencent/soter/tree/master/soter-client-demo)和安全接入文档(链接:https://github.com/Tencent/soter/wiki/%E5%AE%89%E5%85%A8%E6%8E%A5%E5%85%A5)。

SOTER(GitHub地址:https://github.com/Tencent/soter
开源之后,已经有包括微众银行在内的多个应用已经接入,这些应用接入的时间均不超过一个版本。使用的场景也从指纹支付,到指纹登录、指纹解锁。用过的,都说好。

那么,让我们再回顾下开头的场景:“我们要做指纹支付,下个版本上…”,想必你已经知道怎么做了,括弧逃~

原文来自:腾讯开源

文章来源于腾讯云开发者社区,点击查看原文