2020 年大多数基于位置的加密项目之所以悄无声息地消亡,是因为它们信任 GPS。GPS 是一种广播信号,任何接收器都能收听,而接收器也可以被指使去谎报它听到的内容。能做到这一点的工具在亚马逊上大约三十美元,从 2015 年起就是这个价。任何为“站在某个坐标上”向钱包付钱、又只用 GPS 来验证是否站在那里的协议,都会把钱输给那些躺在沙发上的假钱包。这种攻击既不难,也谈不上高明。
位置证明就是修补这个漏洞的那一层。这个词被使用的次数远多于被定义的次数,所以我们先给出定义。位置证明是一种主张:某台特定设备在某个特定时间窗口内位于某个特定坐标,并且这一主张可以由不在场的一方来验证。它是关于“在场”的主张,而不是关于坐标的主张。大部分工程工作都集中在这个区别上。
威胁模型。我们防御的不是国家级对手;那种极端情况放到最后再说。我们防御的是典型的攻击者:一部手机、一台笔记本电脑、一个下午。这样的攻击者可以用现成工具伪造 GPS,在模拟器里运行应用,一行代码就能生成一个钱包。任何单一的控制措施都能被这样的攻击者绕过。工程上的目标,是让同时绕过所有措施的成本高于空投里的奖励。
本文用浅显的语言介绍 Seek 目前在做什么,以及接下来会做什么。我们有意少谈营销,多讲机制。
第一层,捕捉时:设备认证。每一次捕捉请求都带有一份由操作系统签名的声明。在 iOS 上是 Apple App Attest,在 Android 上是 Google Play Integrity。Seek 在服务端验证这两者。认证用一个签名说明三件事:这是一台真实的设备;应用二进制文件没有被篡改;调用方持有一把在这台设备上为我们的应用包生成的密钥。已 root 的手机无法通过。模拟器无法通过。跳过位置检查的重新打包应用也无法通过。认证不能证明手机在哪里;它证明的是这部手机没有在“自己是一部手机”这件事上撒谎。正因为如此,其他信号才值得一读。
第二层,捕捉时:留有宽容区间的距离检查。只有当你位于刷新点的收集半径内时,客户端才会让你点击捕捉按钮。服务端会独立地把你发送的坐标与刷新点坐标进行比对,并接受最多七十五米的偏差。按钮半径和服务端半径之间的差距是有意为之的。手机靠在墙边时,GPS 抖动可能会漂移几米,这不是你的错,在这种抖动范围内拒绝捕捉会让人觉得莫名其妙。超过七十五米的请求会被直接拒绝,在这一层不打分,也不能申诉。
第二层(续):频率限制与单个刷新点上限。两次收集请求间隔不到五秒,就会收到一个 429,这道闸门由 Postgres 支撑,在各个无服务器实例之间都有效。同一个刷新点最多接受同一账号的两次尝试,第二次的价值为第一次的 65%。这些上限不是为了检测作弊;而是为了让一个已经突破其他各层的机器人,刷这个接口的速度不会比真人玩得更快。
第三层,捕捉时:环境信号,只记录不强制。每次捕捉还会带上一些标记:请求是否来自模拟器,Android 是否报告定位来自模拟位置提供程序,以及上报的 GPS 精度(以米为单位)。这些信息会被记录下来,但从不用于拒绝捕捉本身。一个会在这些信息上撒谎的客户端,恰恰是你最不希望由它来做决定的客户端,所以决定在别处做出。它们在第四层才发挥作用。
第四层,提现时:反作弊闸门。提现会经过一个服务端函数,它会读取该账号过去九十天的捕捉记录。只要其中有任何一次捕捉来自模拟器,或者 Android 报告了模拟位置提供程序,或者该账号有两次成功捕捉间隔不到十秒,或者该账号在两个捕捉坐标之间的移动速度超过了人类可能的速度,又或者另一个账号曾从同一设备指纹提现过,这次提现就会被转交人工审核,而不是自动批准。用户不会被告知是哪根警戒线被触发了,账号也照常可以玩。检测是一项关于付款的业务决策,而不是惩罚。
之所以把捕捉和提现分开,是因为它们防守的是不同的东西。捕捉层保护的是游戏。如果一次捕捉是作弊得来的,它会变成永远离不开应用的游戏货币,不会造成任何损失。提现层保护的是真金白银。每一块离开系统的钱,都要经过对余额来源的九十天审查,而这个时间窗口里只要出现一个可疑信号,就足以把决定交给人工。两层结合起来,意味着正常玩家在捕捉的那一刻不会遇到任何阻碍,而侥幸让一次作弊捕捉溜过去的攻击者,永远拿不到钱。
第五层,捕捉时:运动轨迹。每部手机都有加速度计和陀螺仪,与 GPS 在同一块芯片上通信。Seek 现在会记录捕捉前几秒的一小段运动数据,计算一小组统计量(幅度方差、旋转活动、静止帧数),并把这份摘要随收集请求一起发送。一部走到刷新点的手机,和一部放在桌上、开着 GPS 模拟器的手机,运动特征是不同的。目前这一特征在捕捉层并不强制执行,原因和环境标记一样:被修改过的客户端可以撒谎。它会进入九十天反作弊闸门,与模拟器和模拟位置提供程序信号放在一起,记录上一条平坦的运动轨迹,就是又一根能把提现转交人工审核的警戒线。
来看一个完整的例子。一名测试者运行 GPS 伪造工具,把坐标设置到城市另一头的一个 Seek 空投上,然后点击捕捉。距离检查通过了,因为伪造的坐标是匹配的。认证也通过了,因为应用没有被修改。捕捉成功登记。两小时后,同一名测试者又完成了三次捕捉,其中一次来自十公里外的另一个坐标。四次捕捉的运动轨迹都是平的(手机从没动过),而那两个坐标之间两小时内不可能步行到达,于是“不可能的移动”检查被触发。账号照常可以玩,照常收集,完全不知道哪里出了问题。当测试者申请提现时,这笔提现被送到人工审核台,附带着“不可能的移动”标记和平坦的运动轨迹。审核员拒绝了它。测试者收集到的四十美元余额,在应用之外永远一文不值。
从经济角度看。重要的不是伪造捕捉本身不可能做到;而是提现闸门会回溯账号为了积累这笔余额所做的一切。一个由认证和距离检查保护、价值两欧元的空投,攻击一次的成本很低,反复攻击却毫无意义,因为提现检查会在这种模式变成钱之前就把它识别出来。一个价值一万欧元的空投,则需要在捕捉层有更强的防护框架,这正是下面路线图要解决的问题。
路线图上有什么。下一个落地的控制措施是用于室内空投的信标备选方案。对于放置在建筑物内、GPS 毫无意义的刷新点,一个登记到该空投的小型蓝牙信标能让应用证明到场,完全不依赖卫星定位。第二条线是强制认证:目前未经认证的请求会被记录但仍然被接受,这样我们就能先测量对正常设备的误拒率,再拨动开关、直接拒绝它们。这两项都会作为小而朴素、可验证的补充,加入到现有的捕捉与提现模式中。
仍然做得不够好的地方。在购物中心里,室内 GPS 会偏差十到十五米,再聪明的服务端算法也解决不了;信标才是真正的答案,所以它在路线图上。亚秒级延迟目前做不到,大概以后也做不到,因为反作弊闸门读取的是历史记录,而不是单个请求。可穿戴设备的传感器比手机少,所以为手机调校的位置证明设计,没法原封不动地搬到眼镜上。还有,一个拥有一卡车射频设备的国家级对手,今天可以击败任何手机端验证器;任何声称并非如此的协议都没有对你说实话。我们不会那样说。
有一条设计原则值得明确说出来。验证器应该诚实地说明它能证明什么、不能证明什么。把一个概率分数包装成绝对的“是”或“否”,比依据严格规则给出绝对的“是”或“否”更糟糕,因为分数会带来一个滑块,而每当参与度目标落后时,任何滑块都会被推向接受更多的申请。我们的捕捉层没有滑块。要么通过检查,要么不通过。反作弊层有一个滑块,而这个滑块掌握在人手里,而不是写在代码里。
正是 Seek 底层的这一部分,让锚定资产和经过位置验证的空投变得值得去做。发布方之所以愿意在某个坐标放置一枚具有真实价值的代币,是因为最终得到它的人确实用一台经过认证的设备到过那个坐标,也因为在钱转出之前,提现闸门会回溯该账号的历史。白皮书里有完整的技术规范。本文是应用内部所发生之事的通俗版本。
简单来说:在场不等于坐标。单一信号无法证明在场。Seek 分两层验证在场:捕捉时的认证、距离和运动轨迹,以及提现时的九十天反作弊闸门。还剩下的一项,也就是室内空投的信标备选方案,将补上 GPS 单靠自己无法解决的最后一种坐标情形。这就是如今 Seek 内部真正的位置证明,也是为什么这一层比它所支撑的那些更炫目的功能都更重要。


