开启两步验证的时候,网站会给我们一张二维码,用 Google 身份验证器(Google Authenticator)扫一下,App 里就多出一行六位数,每 30 秒换一个。登录时把它填进去,网站就放行。
用久了难免会好奇:这个六位数到底是个什么东西?手机切到飞行模式,它照样在变,服务器又是怎么认出它的?扫码时存进手机的那串东西,是不是一张证书?整套做法和银行的 U盾 是不是差不多?
本文就从这几个疑问出发,先弄清六位数是怎么算出来的,再回头看它和证书、和 U盾 到底是不是一回事。文末附一段 30 行左右的 Python,可以自己算一遍。
一、一次性密码的由来
现象:密码一旦泄露,就能被无限次使用
普通密码的问题在于它是静态的。只要被人看到一次,无论是在网络上被窃听、被键盘记录器记下,还是从别的网站的泄露库里撞出来,对方就可以在任何时候、任何地点反复使用它。服务器分不清这次输入是我们本人,还是一份录下来的副本。
所以很早就有人想:能不能让每次登录用的密码都不一样,用过一次就作废? 这样即使被看到,也只是一张过期的票。
第一次尝试:Lamport 的哈希链
1981 年,Leslie Lamport 提出了一个很漂亮的办法1:拿一个秘密 反复做哈希,得到一条链 。服务器只保存链的最末端 。第一次登录时,用户出示 ,服务器把它再哈希一次,看是否等于自己存的值;对上了就放行,并把存的值换成 。下一次用户出示 ,依此类推。
这个方案的好处是服务器只存一个“往后推”的值,就算服务器被攻破,攻击者也算不出下一个密码,因为哈希不能倒推。贝尔实验室后来把它做成了 S/KEY,并在 1995 年写成 RFC 1760,再演化成 RFC 2289 的 OTP 标准2。
它的不足在于链是有限的:用完 次就得重新初始化。而且每次要算出第 个值,要么事先打印一张密码表随身带着,要么在终端上输入口令现场计算,都谈不上方便。
第二次尝试:硬件令牌
另一条路是 RSA 的 SecurID,从 1980 年代后期开始卖。它是一个钥匙扣大小的硬件,屏幕上的数字每 60 秒变一次,看上去已经和今天的 App 很像了。
它的不足是封闭:算法是专有的,令牌里的种子(seed)由厂商生成,厂商自己也保留一份。企业要买硬件、买授权,还得信任厂商永远不出事。2011 年 3 月,RSA 公司被入侵,与 SecurID 种子相关的数据被窃取,随后美国军工企业洛克希德·马丁遭到了利用这批数据的攻击,RSA 不得不为客户更换大批令牌3。“种子在别人手里”从理论风险变成了真实事故。
解决:一个开放的标准
到了 2004 年,VeriSign 等公司发起了 OATH(Initiative for Open Authentication),目标是做一个任何人都能免费实现的一次性密码算法:不靠专有硬件,不靠厂商保管种子,用户和服务器之间直接共享一个密钥就行。它的第一个成果就是 2005 年发布的 HOTP。
下面这张图把这几代方案和它们各自留下的问题串在一起,后面的每一节,基本上都是在回答图右边那一列。
那么 HOTP 是怎么做到“两边不通信,却算出同一个数”的?
二、HOTP:从密钥和计数器到六位数字
分析:两边共享什么
HOTP 的思路很直接。用户的令牌和服务器在一开始共享两样东西:一个密钥 (通常 20 字节随机数),和一个计数器 (初始为 0)。每次需要密码时,就用 对 算一个 HMAC-SHA1:
其中 编码成 8 字节大端整数。算完一次,两边的计数器各加一。
为什么用 HMAC?因为它有两个性质正好对口:没有 的人算不出下一个值;而看到再多个历史码,也推不回 。这正是一次性密码需要的。顺带一提,HMAC 的安全性并不依赖 SHA-1 的抗碰撞性,所以 SHA-1 在 2017 年被找到碰撞,并不影响 HOTP4。
现象:20 个字节,怎么变成 6 位数?
HMAC-SHA1 的输出是 20 字节、160 位。人没法输入 160 位,只能输入几位十进制数字。那该从这 20 个字节里取哪一部分?
最直觉的办法是取前 4 个字节,转成整数再取后 6 位。这样做没错,但 RFC 4226 选了一个更巧的办法,叫动态截断(dynamic truncation):让 HMAC 的最后一个字节来决定从哪里取。具体是:
- 取最后一个字节的低 4 位,得到一个 0 到 15 的偏移量
offset; - 从第
offset个字节开始连取 4 个字节,拼成一个 32 位整数; - 把最高位清零,只留 31 位;
- 对 取余,不足 6 位前面补 0。
下图用 RFC 的测试密钥 12345678901234567890(20 个 ASCII 字符)和计数器 走一遍,每一个字节都是真实算出来的。
为什么要清掉最高位? 不同语言对 32 位整数的处理不一样,有的当有符号数,有的当无符号数。把最高位清零以后,结果在任何实现里都是同一个非负整数,避免了 Java 和 C 算出不一样的码。
六位数够安全吗? 一次猜中的概率是百万分之一。单看这个数字不算大,所以 RFC 4226 明确要求服务器做限速:连续输错几次就延迟或锁定5。六位数的安全性,有一半是靠服务器的限速来保证的。
新问题:计数器失步
HOTP 的令牌每按一次按钮,计数器就加一。可是用户按了按钮却没去登录,或者小孩拿着令牌按着玩,令牌的计数器就会跑到服务器前面去。
RFC 的办法是让服务器往前多试几个:不只比较 ,而是依次比较 ,在这个“前瞻窗口”里对上了,就把服务器的计数器直接推进到那个位置。超出窗口,就得让用户连续输入两个码重新同步。
这能用,但不优雅:两边都得保存并维护一个会变化的状态。能不能找一个两边不用商量、天然就一致的计数器?
三、TOTP:用时间代替计数器
解决:时间本身就是计数器
答案是时间。手机和服务器各自都有时钟,只要都对准了世界时间,就不需要再同步任何东西。TOTP 把 Unix 时间(从 1970 年 1 月 1 日起的秒数)按 30 秒一段切开,用段号代替 HOTP 的计数器:
算法的其余部分和 HOTP 一模一样。一个有趣的验证是: 秒时,,所以 TOTP 在第 59 秒的 8 位码 94287082,正好就是图 2 里 那个 31 位整数 1094287082 的后 8 位。RFC 6238 附录里的第一个测试向量就是它6。
这也回答了开头的问题:飞行模式下照样出码,是因为算码只需要两样东西,一个扫码时就存进手机的密钥,一个手机本来就有的时钟。 服务器那边拿着同一个密钥、看着同一个时间,自然算出同一个数。
为什么是 30 秒
时间步越短,被偷看到的码越快作废,但用户可能还没输完就过期了;越长则反过来。RFC 6238 推荐 30 秒,原文的说法就是“在安全性和可用性之间取一个平衡”6。
现象:手机时钟慢了几秒
时间当计数器,就带来了新的问题:两边的时钟不可能分毫不差,网络传输也要时间。用户在一个时间步的最后一秒看到码,等它提交到服务器,服务器那边已经是下一步了。
RFC 6238 的建议是,服务器除了当前步 ,最多再往回多查一步 ,用来容忍网络延迟;也可以记录每个客户端的时钟偏差,下次自动校正6。下图是一个具体的例子。
窗口也带来一个副作用:同一个码在 30 秒甚至 60 秒内都有效。如果有人在我们身后看到了码,马上抢先输入,服务器是分不出来的。所以 RFC 6238 要求服务器记住每个用户最近一次成功使用的时间步,同一步的码只认一次6。
到这里,算法本身已经讲完了。它是怎么从一份 RFC,变成几乎每个网站都支持的东西的?
四、Google 的推广:两步验证与二维码
经过:从企业版到所有用户
2010 年 9 月,Google 先给付费版、政府版和教育版的 Google Apps 推出了两步验证;2011 年 2 月,开放给所有 Google 账号,由用户自愿开启7。第二步的验证码有两种拿法:用短信或语音接收,或者装一个 Google 身份验证器 App 自己算。这个 App 用的就是 TOTP。
为什么是 TOTP,而不是自己发明一套,或者像银行那样发硬件令牌?Google 没有专门解释过,以下是根据当时的情况做的推断:TOTP 是开放标准,不用付授权费;智能手机已经普及,而且手机时钟由运营商或 NTP 校准,正好满足 TOTP 对时钟的要求;离线可用,出国没有信号也不影响;而且任何网站都能照着 RFC 实现,不需要和 Google 签任何协议。
Google 还把 App 连同一个 Linux PAM 模块一起开源了。PAM 模块至今还在维护,装上以后,SSH 登录服务器也能要求输入身份验证器上的六位数8。
那张二维码里装了什么
扫码那一步,其实就是把共享密钥 交给手机。二维码里只编码了一行文本,格式是 Google 定义的 Key URI9:
otpauth://totp/Example:alice@google.com?secret=JBSWY3DPEHPK3PXP&issuer=Exampletotp表示基于时间(换成hotp就是计数器模式,此时还要带counter参数);Example:alice@google.com是显示在 App 里的名字;secret就是密钥 ,用 Base32 编码;- 另外还可以带
algorithm、digits、period,默认值分别是SHA1、6、30。
为什么用 Base32,而不是更常见的 Base64? 因为二维码扫不了的时候,用户要能手工输入密钥。Base32 只用 A–Z 和 2–7,不分大小写,也没有 0/O、1/l 这种容易看错的字符。
这个格式的影响比算法本身还大。Google 身份验证器在相当长的时间里会忽略 algorithm、digits、period 的非默认值,于是网站为了兼容,几乎都用 SHA1、6 位、30 秒。RFC 6238 里写的 SHA-256、SHA-512 很少有人用,“SHA1 + 6 位 + 30 秒”就这样成了事实标准。GitHub、AWS、各大交易所,以及 Authy、1Password、微软身份验证器这些后来的 App,都按这个格式生成和读取二维码。
App 本身后来走了另一条路:Android 版在 2013 年前后转为闭源,开源社区随后做了 FreeOTP 等替代品10。但由于格式是公开的,换 App 不影响任何网站。
TOTP 普及以后,它的短板也一个个暴露出来。最先被用户感受到的,是换手机。
五、共享密钥的代价
现象:换了手机,码全没了
TOTP 是对称的:手机和服务器手里是同一个密钥 。早期的 Google 身份验证器把密钥只存在手机本地,不备份。手机丢了、摔坏了、或者换新手机前忘了迁移,所有账号的第二步就都进不去了,只能靠开启时保存的恢复码,或者走各个网站的人工申诉。
尝试:导出与云同步
Google 先在 2020 年加了“转移账号”功能,在旧手机上生成二维码,用新手机扫。这个二维码的内容是 otpauth-migration://offline?data=...,里面是一串没有加密的 protobuf,装着所有账号的密钥。
2023 年 4 月,App 又加入了随 Google 账号云同步。这解决了丢码的问题,却引出了一个新问题:安全研究者 Mysk 抓包发现,同步的数据不是端到端加密的,Google 的服务器能读到所有密钥。换句话说,一旦 Google 账号本身被盗,攻击者就能拿到这个账号保护的所有其他账号的第二因素。Google 随后表示会加入端到端加密11。
这件事背后是 TOTP 的一个根本性质:密钥必须同时存在于两端,所以无论放在哪里,都多了一个可能泄露的地方。 服务器那边也一样,网站的数据库要是泄露了 TOTP 密钥,攻击者就能为所有用户生成正确的码,所以密钥必须加密存放,不能像密码那样只存一个哈希。
两端共享同一个秘密,这是对称密码的天性。说到这里,常有人问:扫码导入的这串密钥,有的 App 和教程管它叫“令牌证书”;网银的 U盾 也叫“数字证书”;浏览器地址栏的小锁背后,同样是一张“证书”。它们是一回事吗?
六、数字证书:另一种信任方式
分析:对称和非对称
不是一回事。二维码里装的不是证书,而是前面一直在说的共享密钥 。两者的区别,在于双方手里各有什么:
- TOTP 是对称的。 手机和服务器拿着同一串字节,能验证码的一方,也一定能生成码。所以 必须对所有其他人保密。
- 证书背后是非对称密码。 一对密钥里,私钥只在持有者手里,公钥可以公开给任何人。私钥用来签名,公钥用来验证;能验证签名的人,造不出签名。
- 证书本身,就是把一把公钥、“这把公钥属于谁”和一个有效期写在一起,再由一个上级机构(CA,证书颁发机构)用它自己的私钥签名作保。
拿 Google 登录页自己的证书来看。在证书透明日志 crt.sh 上可以查到,accounts.google.com 的一张证书由 Google Trust Services 的中间证书 WR2 签发,有效期从 2026-09-02 到 2026-11-25,只有 84 天12。WR2 又由根证书 GTS Root R1 签发,而这张根证书预装在操作系统和浏览器里。浏览器顺着这条链一级一级验证签名,一直验到自己信任的根,就能确认一件事:对面这台服务器,握有 accounts.google.com 这把公钥对应的私钥。
下图把两种方式并排放在一起,标出了每一边手里的秘密。
所以二维码里装的不是证书。 它没有公钥,不需要 CA,也绝不能公开。U盾 才是真正的证书:里面存着用户自己的私钥,以及由 CFCA 等机构签发的证书,转账时用私钥对交易内容签名。银行同时代发的另一种小硬件“动态令牌”,一按就显示六位数,那才是一次性密码,和 Google 身份验证器是同一类东西。
不过,把两者放在一起,也引出了一个更尖锐的问题。
七、HOTP 为什么不用非对称密码
问题:平台也握着密钥
回头看图 4 左边,会发现一个让人不安的事实:服务器手里也有一份 。网站数据库一旦被攻破,攻击者就能为每个用户算出正确的码,第二因素等于没有。RFC 4226 自己也承认这一点,专门要求服务器把共享密钥放在隔离的安全区域,用防篡改硬件加密保存,只在验证的那一刻解密5。但这只是把 藏得更深, 仍然在平台上。
照着证书的思路,似乎有一个更好的设计:私钥只留在用户的令牌里,平台只存公钥,公钥泄露了也无所谓。HOTP 为什么不这么做?
第一次尝试:把 HMAC 换成签名
设想令牌不再算 HMAC,而是用私钥对计数器签名,服务器用公钥验证。问题马上出现:签名太长了。用 Ed25519 这种目前最短的常用签名算法之一,对计数器 1 签名,得到 64 字节;写成十进制有 154 位,写成 Base64 也要 88 个字符,没有人能把它抄进登录框。
那么像 HOTP 一样,截断成 6 位行不行?不行,这正是关键所在。
- HMAC 能截断,是因为验证就是重算。 服务器有 ,自己能算出完整的 20 字节,按同样的规则截出 6 位,比较一下就行。
- 签名不能截断,是因为验证不是重算。 只有公钥的一方算不出签名,算得出就等于能伪造了。它只能拿到完整的签名,用公钥去检查一个等式是否成立。少了任何一个字节,这个等式都没法检查。
所以,“人看一眼、手抄六位数”这个通道,本身就把非对称排除掉了。 只要中间是人,验证的一方就必须能自己重算出这几位数,也就必须拥有和令牌一样的秘密。
原因:当年的设计约束
RFC 4226 开头列出了六条设计要求5,几乎每一条都指向同一个方向:
- 算法应该能便宜地做进硬件,尽量少占电池、按键、处理能力和屏幕(R2);
- 令牌可以没有任何输入键(R3),所以服务器没法先发一个挑战给它;
- 显示的值必须容易读、容易输入,至少 6 位,最好全是数字(R4);
- 使用一个足够强的共享密钥,至少 128 位(R6)。
R6 直接写着 shared secret,可见对称是设计的起点,而不是疏忽。当年的目标设备是钥匙扣令牌、智能卡和 SIM 卡。这类芯片算一次 HMAC-SHA1 几乎不费电,做一次公钥签名则慢得多,也贵得多。这一点是推断,RFC 没有给出具体数字。
曾经做到过的“平台不持有秘密”
有意思的是,第一节的 Lamport 哈希链其实做到了这个性质:服务器只存链的末端 ,数据库泄露了,攻击者也算不出下一个密码,因为哈希不能倒推。它靠的不是公钥,而是单向函数;验证依然是“重算一次哈希再比较”,所以能让人输入。代价是链会用完,而且每个值有 64 位,S/KEY 只能把它编成 6 个英文短词让人敲进去。HOTP 用这个性质,换来了 6 位数字和不会用完的计数器。
解决:去掉中间的人
所以,要让平台不再持有秘密,前提是把“人抄写”这一环拿掉,让用户的设备和浏览器直接通信。USB、NFC、蓝牙,或者手机自带的安全芯片,都能把服务器给的随机挑战原样送进去,再把 64 字节的签名原样送回来。
这就是 2014 年 FIDO U2F 做的事,后来的 WebAuthn 和 passkey 也是一样:每个网站一对密钥,私钥只在用户设备里,平台只存公钥,数据库泄露了也算不出任何东西。它正是上面设想的那种架构。它没有出现在 HOTP 里,是因为 2005 年的令牌和服务器之间,只有一个靠人抄写的六位数通道。
换成签名以后,还多出了一个 HOTP 做不到的好处,这要从钓鱼说起。
八、钓鱼:证书和六位数之间缺的一环
现象:钓鱼站也有合法证书
地址栏有证书,手里有六位数,为什么还会被钓鱼?
钓鱼网站会做一个和 Google 登录页一模一样的页面,地址栏同样有一把小锁。申请证书只需要证明自己控制着这个域名,Let’s Encrypt 这样的机构可以免费、自动地签发。在 crt.sh 上搜索 accounts.google.com,就能看到 Let’s Encrypt 签给某个以 www.accounts.google.com------- 开头、实际挂在别人域名下的证书12。用户输入密码,再输入身份验证器上的六位数;钓鱼站在后台把这两样东西原样转交给真正的 Google,在 30 秒内完成登录。今天这类“实时中继”已经有现成的工具包。
原因:两者各管一半,中间断了一根线
证书证明的是“对面是证书上写的那个域名”,它不知道这是不是用户想去的网站。TOTP 证明的是“输入的人手里有密钥”,码里不包含任何域名信息:用户在 g00gle-login.example 上看到的码,和在 accounts.google.com 上看到的一模一样。
两者之间缺的那根线,是“我把码交给了哪个域名”。在 TOTP 里,这根线只能靠用户自己核对地址栏,而这恰恰是人最容易出错的地方。
解决:把证书验证过的域名签进去
上一节说的 U2F / WebAuthn(Google 和 Yubico 是 U2F 的主要推动者)把这根线也接上了。设备和浏览器直接通信以后,签名的内容不再受六位数的限制,可以多装一样东西:
- 登录时服务器发一个随机挑战,设备对它签名。关键在于,被签名的数据里有浏览器自己填写的当前网址(origin),也就是浏览器刚刚通过证书验证确认过的那个域名,网页的脚本改不了它。
- 浏览器还只会把某个网站的凭据交给那个网站:
accounts.google.com的凭据,在g00gle-login.example上根本不会被调出来。
到这里,证书和用户凭据终于在同一个签名里会合了:证书负责让浏览器确认“对面是哪个域名”,用户的私钥负责证明“我是我”,而签名把这两件事绑在一起,钓鱼站拿走签名也用不了。
有意思的是,passkey 用了公私钥,却不需要 CA 给用户签发证书。注册时网站直接记下这把公钥,信任在第一次注册时建立,有点像 SSH 第一次连接时记下服务器指纹。WebAuthn 里倒是有一种证书,叫认证证书(attestation certificate),由安全密钥的厂商签发,用来证明“这是某个型号的真硬件”。为了不让它变成追踪用户的标识,FIDO 要求同一张认证证书由一大批设备共用(通常至少 10 万个);同步到云端的 passkey 一般干脆不提供它13。
效果是很实在的。Google 从 2017 年初开始要求全体员工使用物理安全密钥,据 Google 对外的说法,此后八万五千多名员工中没有一个账号被钓鱼接管14。
passkey 是这条路的延续:把 FIDO 凭据存在手机或密码管理器里,并且可以跨设备同步,不再需要单独买一个 U 盘大小的硬件。Google 在 2023 年 5 月支持用 passkey 登录 Google 账号,同年 10 月把它设为默认选项15。
TOTP 还会存在下去吗
会存在很长时间。WebAuthn 需要浏览器和操作系统配合,网站要接入的东西也更多;而 TOTP 只需要服务器端几十行代码,不依赖任何平台,甚至可以在命令行和 SSH 里用。对大多数网站来说,它仍然是成本最低的第二因素。只是我们应该清楚它能防什么:它能防住密码被盗后的远程登录,防不住实时钓鱼。
九、动手:自己算一遍
下面这段 Python 只用了标准库,hotp 是图 2 的四步,totp 是在它外面套上图 3 的时间步。运行它会依次打印 RFC 4226 附录 D 的 10 个测试码、RFC 6238 附录 B 的 SHA1 测试码,以及当前时刻的 TOTP:
import base64, hashlib, hmac, struct, time
def hotp(key: bytes, counter: int, digits: int = 6, algo=hashlib.sha1) -> str: mac = hmac.new(key, struct.pack(">Q", counter), algo).digest() offset = mac[-1] & 0x0F # 动态截断:最后 4 位当下标 code = struct.unpack(">I", mac[offset:offset + 4])[0] & 0x7FFFFFFF return str(code % 10 ** digits).zfill(digits)
def totp(secret_b32: str, t: float | None = None, step: int = 30, digits: int = 6) -> str: key = base64.b32decode(secret_b32.upper() + "=" * (-len(secret_b32) % 8)) t = time.time() if t is None else t return hotp(key, int(t // step), digits)
if __name__ == "__main__": key = b"12345678901234567890" # RFC 4226 附录 D print([hotp(key, c) for c in range(10)]) # RFC 6238 附录 B(SHA1,8 位) for t in (59, 1111111109, 1111111111, 1234567890, 2000000000, 20000000000): print(t, hotp(key, t // 30, 8)) print(totp(base64.b32encode(key).decode()))输出的前几行和 RFC 里的值逐个对得上:
['755224', '287082', '359152', '969429', '338314', '254676', '287922', '162583', '399871', '520489']59 942870821111111109 070818041111111111 140504711234567890 890059242000000000 6927903720000000000 65353130最后一行 20000000000 是公元 2603 年,RFC 专门放进这个测试向量,是为了提醒实现者时间步要用 64 位整数,别在 2038 年溢出。
想和手机对一下,可以把密钥的 Base32 形式 GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ 手动加进身份验证器,再运行下面这一行,两边会显示同一个数:
print(totp("GEZDGNBVGY3TQOJQGEZDGNBVGY3TQOJQ"))小结
回到开头那个飞行模式下还在跳的六位数,它背后的问题链是这样的:
- 为什么需要一次性密码? 静态密码泄露一次就能被无限重放。Lamport 哈希链和 SecurID 都试过解决,但一个会用完,一个把种子握在厂商手里。
- 两边不通信,怎么算出同一个数? HOTP 让两边共享密钥 和计数器 ,算 HMAC-SHA1,再用动态截断从 20 个字节里取出 6 位数。
- 计数器失步怎么办? TOTP 用 30 秒一段的时间代替计数器,两边只要时钟对准就行;服务器多查一步来吸收时钟偏差,并且同一步的码只认一次。
- 它怎么普及的? Google 两步验证和身份验证器把它带给了普通用户,
otpauth://二维码让任何网站都能接入,也让“SHA1 + 6 位 + 30 秒”成了事实标准。 - 扫码导入的是证书吗? 不是。那是两端共享的对称密钥,两端都可能泄露;证书背后是非对称密码,私钥只有一份,公开的是公钥加上 CA 的签名,U盾 和 HTTPS 的小锁用的都是它。
- HOTP 为什么不用非对称? 因为中间是一个靠人抄写的六位数通道。HMAC 的验证是重算,可以截断成 6 位;签名的验证要检查完整的 64 字节,截断就没法验证。再加上 2005 年令牌硬件的限制,RFC 4226 从一开始就选了共享密钥。
- 平台不持有秘密要怎么做到? 去掉中间的人,让设备和浏览器直接传签名,这就是 FIDO U2F、WebAuthn 和 passkey:私钥只在用户设备里,平台只存公钥。
- 为什么有证书还挡不住钓鱼? 证书证明“对面是哪个域名”,六位数证明“我手里有密钥”,中间缺了“我把码交给了哪个域名”。WebAuthn 把浏览器经证书验证过的域名签进签名里,补上了这一环。
参考资料
Footnotes
-
Leslie Lamport. Password authentication with insecure communication. Communications of the ACM 24(11), 1981. ↩
-
N. Haller. RFC 1760: The S/KEY One-Time Password System, 1995;N. Haller et al. RFC 2289: A One-Time Password System, 1998. ↩
-
RSA 2011 年入侵事件及洛克希德·马丁攻击的报道,见 Wikipedia RSA SecurID 条目及其引用的新闻。 ↩
-
M. Bellare, R. Canetti, H. Krawczyk. Keying hash functions for message authentication. CRYPTO 1996;M. Bellare. New proofs for NMAC and HMAC: security without collision-resistance. CRYPTO 2006. ↩
-
D. M’Raihi, M. Bellare, F. Hoornaert, D. Naccache, O. Ranen. RFC 4226: HOTP: An HMAC-Based One-Time Password Algorithm, 2005。动态截断见 §5.3,限速见 §7.3,测试向量见附录 D。 ↩ ↩2 ↩3
-
D. M’Raihi, S. Machani, M. Pei, J. Rydell. RFC 6238: TOTP: Time-Based One-Time Password Algorithm, 2011。时间步见 §4、§5.2,重放见 §5.2,测试向量见附录 B。 ↩ ↩2 ↩3 ↩4
-
Google Security Blog. Advanced sign-in security for your Google account, 2011-02-10;Dark Reading. Google Bolsters Apps With Two-Step Verification, 2010-09-20. ↩
-
Key Uri Format, google-authenticator wiki. ↩
-
LWN. FreeOTP multi-factor authentication, 2014;Wikipedia Google Authenticator. ↩
-
BleepingComputer. Google will add end-to-end encryption to Google Authenticator, 2023-04. ↩
-
crt.sh 证书透明日志查询:accounts.google.com,2026-10-16 查询。文中引用的是 Not Before 为 2026-09-02、签发者为
C=US, O=Google Trust Services, CN=WR2的那张。 ↩ ↩2 -
W3C. Web Authentication: An API for accessing Public Key Credentials,attestation 相关章节;FIDO Alliance, FIDO Security Reference(关于认证证书由至少 10 万台设备共用的建议)。 ↩
-
Krebs on Security. Google: Security Keys Neutralized Employee Phishing, 2018-07-23. ↩
-
Google Blog. Passkeys 相关公告,2023-05-03 与 2023-10-10. ↩