Web 应用身份验证的未来?WebAuth 介绍

818次阅读  |  发布于2年以前

你的密码安全吗

如何判断自己的密码已经泄露?

一些安全厂家会定期收集互联网上被拖库的相关数据账号信息,我使用了Avast提供的一个工具来查询自己的密码是否被泄露(https://www.avast.com/hackcheck/) ,这里只需要输入你的邮箱地址,就能查询到你关联地址的邮箱的登陆账号或者密码是否被泄漏。我在这里输入了我之前比较常用的QQ邮箱账号,可以发现我的密码已经遭到泄漏,泄露的来源分别是Adobe、CSDN和京东。其中Adobe和京东泄露的是加密后的密码,因此还算可控,CSDN泄露的是明文密码,所以非常危险。

如果你使用了Chrome浏览器保存密码,在Chrome的安全设置中,也有提醒你密码已经被泄露的选项。勾选这个选项,在你的密码被泄露时就会给你发出警告。

除此之外,还有一些黑灰产的途径可以查询到自己的密码和个人隐私信息是否被泄露(如Telegram社工库之类的途径),因为涉及到相关法律问题,因此不仔细分享。但是可以得到的结论是:我们的密码并不安全。

为什么你的密码可能不安全

密码是如何存储的

通常情况下,密码都应该在数据库中被加密存储,使用明文存储密码是开发信息系统的大忌,但是事实上,能够做到这一点的信息平台开发商都不多,对于一些“小作坊式”开发的厂家,密码很多情况下都是明文保存的。

对于密码的加密,通常会采用一些Hash算法,在注册账户时,对账号的密码进行Hash计算后,再进行存储,以MD5算法为例:

MD5 ("password") = 5f4dcc3b5aa765d61d8327deb882cf99

若用户的密码为password,在Hash计算后,得到的值为5f4dcc3b5aa765d61d8327deb882cf99,在进行存储时,将得到的Hash值存入数据库的密码字段。

在用户登陆时,只要将用户密码的输入再做一次MD5计算,将得到的Hash值与数据库存储的值进行比较,就可以判断用户的密码输入是否正确了。

这样做的好处是,即使数据库中用户密码的相关信息字段被泄露,用户真正的密码明文是不知道的。这其中利用到了Hash算法的不可逆性:在有限算力的情况下,Hash算法只能从输入得到输出,无法从输出反推得到输入。因为这样的特性,Hash算法通常用在保存密码,信息和文件完整性校验等地方。常用的Hash算法有MD5、SHA、HMAC等。

但是这样的算法也会存在一些问题,虽然无法从输出反推得到输入,但是可以建立一个密码到其哈希值的字典(即彩虹表),在破解时只需要查询彩虹表的值,即可得到对应的密码明文数据。

为了解决这样的问题,我们可以在计算哈希值时采用加盐的方式。对于每个用户,盐值都采用随机数。在计算哈希时,将用户输入的密码和盐值进行组合之后再进行计算,最后将加盐的Hash值存入密码字段。如使用用户的随机UID32142作为盐值,计算方法为:

MD5 ("password32142") = 970e6155f135079c9c1b9d3302b957b5

通过随机加盐的方式,可以有效地避免彩虹表攻击造成密码原文泄露。MD5是一种比较简单的Hash算法,因此在不推荐在实际使用的时候采用MD5算法存储密码,通常情况下我们会有一些更适合密码的哈希算法比如Bcrypt算法来对密码计算Hash,Bcrypt算法在生成时就会生成一段随机的盐值,并且将盐值保存在输出的结果中,这样的好处是即使是同样的输入,得到的输出也是不同的,能够有效避免密码原文泄露的风险。

Bcrypt("password")=10$7ioawRWnYEBZwGE7QAGL0.oa5suk8/NjdAS0RSlqy8Kehplft8CBy

密码过于简单

谈到密码的安全性,首先需要强调的是几条密码保护的基本规则:

但是实际上对于上述几点,能够完美坚持做到每一点的同学我相信一定不多。大家通常使用的密码都是与自己相关的规律组合,因此存在着一定的隐私泄露的风险。

业务系统存在漏洞

事实上,所有的业务系统都是由人来开发的。但是人并不是机器,因此总是会存在着犯错的可能性,这是无法避免的,我们只能从流程上来规范以尽量降低发生错误的概率。但是就如Meta/Twitter这样的大公司都存在着各种各样的密码泄露的问题,国内的BAT等公司也都曾爆出过密码被泄漏的安全问题。除此之外,还有着XCode Ghost/Docker Hub数据泄露等供应链攻击,就连流程详尽规范的大公司都防不胜防,就更不用说你手机上那些不知名小公司的小App了。

就连我们自己的业务系统也不例外:在某日排查线上bug的时候,我也发现在我们业务使用到的某上游服务中,也有把用户的密码直接明文打印在日志中的行为。根据公司的数据安全管理规定数据分类分级表,用户操作行为日志等属于L3级别数据 ,用户账号的密码等数据属于L4级别数据,因此在日志中,不可以出现用户明文密码相关的敏感字段。这样任何拥有L3级别日志查询权限的用户,都可以获取到L4级别的保密数据,这样的操作相当于把L4级别的数据范围扩大到了L3级别上,因此是违法相关数据保护规定的行为。出现这样的问题可能是无意为之,但是还是暴露了我们在代码安全上的意识还有需要加强的地方。

基础设施/硬件漏洞

对于业务系统产生的一些安全漏洞,我们可以通过一些流程和规范尽量保障,但是对于一些基础设施和硬件的漏洞,就很难能够通过这样方式来预防了,如以下的几个例子:

上述这样的一些漏洞都会造成服务端的风险,从而产生被黑灰产拖库导致用户密码泄漏的风险。

如何解决密码不安全的问题

总的来说,实现绝对的安全是不可能的,所以我们只能在有限的条件下,寻求相对的安全。为了能够实现这样相对的安全,现有的密码验证方式矛盾的地方在于:安全等于麻烦,为了实现账户的安全,你需要记忆各种复杂的密码和验证方式,要想省事的话就只能使用一些简单方便记忆的密码,并且在不同平台使用相同的密码,这样虽然省事了,但是可能会带来更多的麻烦:密码泄露导致账户被盗造成个人隐私和财产的损失。

怎么解决安全和麻烦这样的问题呢,FIDO(Fast IDentity Online)联盟想出了比较完美的解决方案:

既然密码不安全,那么没有密码不就可以解决密码不安全的问题了吗?

FIDO联盟和W3C为了解决这样的鱼与熊掌的问题,希望可以制定相关的技术规范,定义一套开放的、可扩展的、能互用的机制,减少用户 在认证时对密码的依赖,于是WebAuthn应运而生。

WebAuthn是什么

WebAuthn全称为Web Authentication,是一个使用非对称加密的方式,在Web应用上替代密码/短信验证码等方式在Web应用中进行注册、登陆、双重认证(2FA) 的API。能够解决钓鱼攻击、数据泄漏的安全问题,同时也能提高应用的用户体验(不必记忆复杂的密码)。

通过使用WebAuthn的API,我们可以方便实现指纹、人脸等生物信息的认证或者是加密硬件如USB Key(如网银使用的U盾)、蓝牙设备等来验证身份信息,在方便的同时能够保障安全和隐私。

兼容性

从CanIUse中可以看到,Web Authentication在桌面端浏览器的兼容性较好,绝大多数的桌面端浏览器都能支持,但是在移动端的兼容性较差:iOS仅在14.5以上的版本完全支持,安卓支持Android5以上版本,除此之外,由于依赖TPM模块和指纹/人脸识别等传感器,硬件配置也会影响WebAuthn的可用性。

需要注意的是,WebAuthn只能在HTTPS或者localhost中使用,不支持HTTP环境,即只有在保证当前的环境是安全的前提下,WebAuthn的这些实现才是有意义的。

一些概念

2FA

2FA的全称为2 Factor Authentication,即双因子认证,通常是结合密码以及其他标志(如OTP、USB Key、短信验证码、指纹等生物特征)进行认证的方式。现在2FA的使用已经比较普遍,许多厂家会结合风控系统在密码验证的同时结合其他认证方式,保证授权是可信的。比如说在新的设备上登录社交App时,通常除了密码之外,还会需要其他的认证方式。

OTP

OTP即一次性密码(One-time password),OTP的计算通常是基于时间戳的,密码的生成具有有效期(通常是30s)。使用OTP能够有效地避免“重放攻击”,一般的静态密码,在使用时如果设备上有可以监听键盘输入的木马等程序,很容易造成密码的泄露。OTP解决了这样的问题,即保证了泄露的密码也是没有用的(因为只能使用一次,且在极短时间内有效)。通常会在安全性要求比较高的情况下使用:如网游账号(左图,盛大密宝,我小时候玩盛大游戏时,使用到的OTP密码器)或企业认证(右图,Seal VPN)。OTP的缺点在于每次都要打开对应的软件或者相关的OTP设备来获取一次性密码,使用的便捷程度上存在一定的不便。

因为OTP的计算是基于时间戳的,之前我有遇到过一种情况,OTP怎么输入都不对,后来发现是我本地设备的NTP存在问题,没有和NTP Server进行同步导致客户端获取到的时间不对,因此计算得到的OTP也不对。如果下次大家有遇到类似的情况,记得先检查一下本地设备的时间是否正确。

对称加密

对称加密算法的特点是,在加密和解密的过程中,使用的密钥是相同的,因此通讯的双方需要交换并持有相同的密钥。常见的对称加密算法有AES/DES等。

优点:

缺点:

非对称加密

和对称加密不同的是,在非对称加密中,需要一组密钥对来进行加密和解密。这一组密钥对我们称为公钥(可以公开的密钥)和私钥(需要保密的密钥)。除了在WebAuthn中有用到之外,其他地方也非常常见,比如说JWT/HTTPS(HTTPS既有非对称加密,也有对称加密)/SSH等。非对称加密有这样的一些特点:

常用的非对称加密算法有RSA/ECC等。

优点

缺点

WebAuthn

术语

工作原理

FIDO协议原理

我们在这里所指的FIDO协议,通常指UAF/U2F/FIDO2这三种协议中的任意一种,因为这三种协议的工作原理都是类似的,区别只在于结构的不同。FIDO要实现在没有其他信息的情况下证明使用者的身份,需要引入一种零知识证明的方式来解决。FIDO基于一种挑战应答方式(Challenge-Response)机制的方式进行工作:每次认证时,服务端给客户端发送一个随机的挑战字符串,客户端接收到这个挑战后,做出相对应的应答。其工作流程如下:

依赖方(服务端)向客户端发送challenge和凭证 ID,客户端再附加依赖方的信息将其发送给身份认证器。身份认证器会首先检查用户是否存在,然后通过弹窗等请求用户按下按钮或者使用指纹传感器等进行完整的用户身份的认证。验证完成后,身份认证器使用凭证ID所标识的对应私钥对challenge进行加密,再将断言返回给客户端,客户端将相关身份认证器的提供信息转发给依赖方。然后依赖方会检查返回的信息,确保响应包含符合预期的来源和challenge,然后用服务端已经存储的公钥来验证签名。这个工作流中的任何一个环节失败,即说明存在钓鱼攻击等场景,认证就会失败。

Challenge-Response 的认证方式使用非常广泛,很多协议如Kerberos/SSH也采用了类似的方式。

过程

WebAuthn使用的工作流一般分为两种:注册(Registration)和验证(Authentication),注册用来给系统添加用户相关的身份认证信息,验证用来校验和检查用户的身份是否正确。

注册

注册的流程如下:

0⃣️ 浏览器向依赖方发送注册请求:

如点击一个Button开始注册用户,这里的协议和格式都不在WebAuthn 的标准范围之内,可以由客户端自行实现

1.依赖方向浏览器发送挑战、依赖方信息和用户信息:

这里的协议和格式也不在WebAuthn的标准范围内,可以是基于HTTPS的XMLHttpRequest/Fetch实现,也可以使用任意其他协议(如ProtoBuf),只要客户端能够和服务端发送对应的请求消息即可,但是需要保证环境是安全的(HTTPS)

2.浏览器向认证器发送用户信息、依赖方信息和客户端信息的Hash值

在浏览器内部,浏览器将验证参数并用默认值补全缺少的参数,然后这些参数会变为clientDataJSON。调用 create() 的参数与clientDataJSON 的 SHA-256 哈希一起传递到认证器(只有哈希被发送是因为与认证器的连接可能是低带宽的 NFC 或蓝牙连接,之后认证器只需对哈希签名以确保它不会被篡改)

3.认证器请求用户确认,然后创建一对公私钥,用私钥创建证明

在进行这一步时,认证器通常会以某种形式要求用户确认,如输入 PIN、使用指纹、人脸扫描等,以证明用户在场并同意注册。之后,认证器将创建一个新的非对称密钥对,并安全地存储私钥以供将来验证使用。公钥则将成为证明的一部分,被在制作过程中烧录于认证器内的私钥进行签名。这个私钥会具有可以被验证的证书链。

4.认证器把签名和公钥以及凭证ID发送给浏览器

新的公钥、全局唯一的凭证 ID 和其他的证明数据会被返回到浏览器,成为 attestationObject。

5.浏览器将证明数据和客户端信息发送给依赖方

这里和前面一样,可以使用任意的格式或协议,前提是需要保证环境安全

6.依赖方用公钥验证发送的challenge,如果成功则将对应的公钥与用户绑定存储在数据库中,完成注册流程

在这一步,服务器需要执行一系列检查以确保注册完成且数据未被篡改。步骤包括:

验证

在验证中,浏览器向依赖方发送某个用户的验证请求后的行为如下:

1. 依赖方向浏览器发送挑战

2. 浏览器向认证器发送依赖方ID、挑战和客户端信息的Hash值,

3. 认证器请求用户确认,然后通过依赖方ID找到对应的私钥,使用私钥签名挑战(断言)

4. 认证器将断言信息和签名后发送给浏览器

5. 浏览器将签名、验证器数据以及客户端信息发送给服务器

6.服务器使用用户绑定的公钥验证挑战是否与发送的一致,如果验证通过则表示认证成功

区别

注册和验证之间的主要区别在于:

浏览器API

要使用 WebAuthn,需要先了解Credentials Management API,因为Web Authn API继承自Credentials Management API。Credentials Management API允许网站与用户代理的密码系统进行交互,以便网站能够以统一的方式处理站点凭证,而用户代理为凭证管理提供更好的帮助。通常用来存储和检索用户、联合账户凭证(FederatedCredential,如OpenID Connect)、和非对称密钥对凭证(WebAuthn使用的就是非对称密钥对的凭证)。

注册:navigator.credentials.create(options)

通过这个API,使用publicKey选项时, 创建一个新的凭据,无论是用于注册新账号还是将新的非对称密钥凭据与已有的账号关联。其中options的结构如下:

const options = {
  publicKey: {
    rp: {
        id: "example.com",
        name: "example.com",
    },
    user: {
      name: "username@example.com",
      id: userIdBuffer,
      displayName: "User Name",
    },
    pubKeyCredParams: [
        { type: "public-key", alg: -7 }
    ],
    challenge: challengeBuffer, 
    authenticatorSelection: { authenticatorAttachment: "platform" },
  },
}

对于 pubKeyCredParams,通常我们只需添加 ES256 (alg: -7) 算法即可兼容大部分外部认证器,此外,再添加 RS256 (alg: -257) 算法即可兼容大部分平台内置认证器(如 Windows Hello/TouchID)。前端添加算法之后,后端也需要相应的算法支持进行签名校验。

const publicKeyCredential = await navigator.credentials.create(options);

调用完创建凭证的API后,浏览器会弹出一个认证器的选择弹窗(具体取决于authenticatorAttachment 这个字段的设置,如果指定了可能就不会有认证器选择弹窗的这个环节),在这个弹窗中,用户可以选择使用哪个认证器来生成凭证:

以选择“本设备”为例,会调用系统内置的认证器(在macOS上即表现为使用Touch ID/Windows上为Windows Hello)进行认证:

使用指纹认证成功后,WebAuthn API会通过Promise返回PublicKeyCredential对象,包含其对应的公钥凭证信息如下:

PublicKeyCredential的结构如下:

CBOR是一种提供良好压缩性,扩展性强,不需要进行版本协商的二进制数据交换形式。

其字段结构定义如下:- rpIdHash:依赖方ID的SHA-256哈希值,32bytes

认证器应实现签名计数器功能。这些计数器在概念上认证器为每个凭据存储,或为整个认证器全局存储。凭据的签名计数器的初始值在认证器注册返回的认证器数据的值中指定。对于每个成功的认证器验证操作,签名计数器都会递增一些正值,并且后续值将再次返回到认证器数据中的 WebAuthn 信赖方。签名计数器的目的是帮助信赖方检测克隆的认证器。克隆检测对于保护措施有限的认证器更为重要。

信赖方存储最新认证器验证操作的签名计数器。(或者来自认证器的计数器注册操作,如果没有认证器验证曾经对凭据执行过。在后续认证器验证操作中,信赖方将存储的签名计数器值与断言的认证器数据中返回的新值进行比较。如果任一值为非零,并且新值小于或等于存储的值,则可能存在克隆的认证器,或者认证器可能出现故障。

{
     "type": "webauthn.create" | "webauthn.get"
     "challenge": "把服务端传输过来的challenge字符串进行base64编码"
     "origon": "https://test.example.com", // 请求源,依赖方域名
     "crossOrigin":  false, //"是否为跨源调用的信息"
 }

其中最重要的参数之一是 origin,它是 clientData 的一部分,同时服务器将能在稍后验证它。

实际上在默认情况下,注册时认证器并不会对挑战进行签名(首次使用时信任模型),attestationObject 并不会包含签名后的挑战。只有依赖方明确要求证明且用户同意后认证器才会对挑战进行签名(具体实现据情况会有所不同)。

得到上面的数据后,我们需要把上面的数据回传给依赖方即后端进行校验,后端的操作至少如下:

1.校验rpIdHash

2. 检查 UV 和 UP

3. 存储证明的计数

4. 存储公钥

验证:**navigator.credentials.get(options)**

这个API也需要传入一个options对象,options的结构和注册的时候比较类似:

const options = {
  publicKey: {
      challenge: challengeBuffer, // 
      rpId: '',
      userVerification: '',
  }
}

和create方法一样,调用get这个方法也会返回一个Promise,可以得到认证器返回PublicKeyCredential的对象,结构如下:

认证器的信息和上方的attestedCredentialData的数据结构类似,只是这里不包含公钥的信息了。

得到这些后,我们只要把PublicKeyCredential对象的内容发送给依赖方即服务端验证即可,依赖方至少要做这样一些的校验:

1.验证 rpIdHash

2.检查 UV 和 UP

3.验证签名的计数,并更新数据库中签名的计数

4.公钥校验签名

可发现凭据

实现完上述的流程后,我们就可以使用指纹等进行登录了,但是这时我们还是会需要使用到用户名,然后才能进行身份认证。如何在不输入用户名的情况下进行认证呢?许多验证器提供了这个功能,我们称为可发现凭据(Discoverable Credentials)。

为什么普通的 WebAuthn 为什么不能实现无用户名登录?大部分认证器为了实现无限对公私钥,会将私钥通过加密后包含在凭证 ID 中发送给依赖方,这样认证器本身就不用存储任何信息。不过,这就导致需要身份认证时,依赖方必须通过用户名找到对应的凭证 ID,将其发送给认证器以供其算出私钥。要不输入用户名,则需要认证器将私钥在自己的存储中也存储一份。这样,依赖方无需提供凭证 ID,认证器就可以通过依赖方 ID 找到所需的私钥并签名。因此这个特性需要认证器能够储存用户ID,即上面的userHandle字段。

为了实现安全隔离,认证器的存储通常会和平台隔离,拥有独立的存储空间(如在Mac平台下,TouchID有独立的安全芯片T1/T2,运行独立的操作系统BridgeOS),一般情况下认证器能够永久存储的私钥数量是有限的,所以只有在真正需要需要无用户名登录时才启用这个特性

实现:

const options = {
    publicKey: {
        ...options.publicKey,
        authenticatorSelection: {
            ...options.publicKey.authenticatorSelection,
            residentKey: "required",     // 设置为required
            userVerification: "required" // 设置为required
        }
    }
}
const options = {
    publicKey: {
        ...options.publicKey,
        userVerification: "required", // 设置为required
        allowCredentials: [],         // 设置为空
    }
}

参考资料

[1] MDN: Web Authentication API

https://developer.mozilla.org/zh-CN/docs/Web/API/Web_Authentication_API

[2] W3C文档:Web Authentication API Spec

https://www.w3.org/TR/webauthn-3/

[3] WebKit 文档:Meet Face ID and Touch ID for the web

https://webkit.org/blog/11312/meet-face-id-and-touch-id-for-the-web/

[4] Medium: Introduction to WebAuthn API

https://medium.com/webauthnworks/introduction-to-webauthn-api-5fd1fb46c285

[5] 谈谈WebAuthn

https://flyhigher.top/develop/2160.html

[6] WebAuthn介绍与使用

https://obeta.me/posts/2019-03-01/WebAuthn%E4%BB%8B%E7%BB%8D%E4%B8%8E%E4%BD%BF%E7%94%A8

[7] WebAuthn Guide

https://webauthn.io/

[8] Apple 安全隔离介绍

https://support.apple.com/zh-cn/guide/security/sec59b0b31ff/web

Copyright© 2013-2020

All Rights Reserved 京ICP备2023019179号-8