什么时候「匿名」才是合理的?| 匿名评教平台复盘

Spencer Woo
SpencerWeekly
Published in
9 min readAug 19, 2019

我在 8 月初,和三位大佬同学一起参加了在南京举办的「信息安全作品赛」的决赛。时隔半个月,我们再来看看当初的系统设计。我会尽量利用通俗易懂的语言,来复盘一下为什么我们没有获得「一等奖」。🥇

我们获得了二等奖

🎯 目标

今年我们拿去参加「信息安全作品赛」的作品是「匿名课程评价系统」。我们需要通过我们设计的系统,利用密码学理论和算法,来保证:

  • 用户进入评教平台以及之后的行为是匿名的
  • 提交给评教平台后台的数据(评教结果)是匿名的
  • 即使是管理员在比对登录平台和评教平台的后台数据后,也无法溯源用户的真实身份(学号)
  • 但是,需要能够区分用户的班级,并限制登录用户的重复评价、越权评教等行为

这些是我们「匿名课程评价系统」需要客观保证的功能。我们是基于接下来的原理,来对这些功能和特性进行实现。

🧩 原理

事实上,上面讲到的前三部分的论证就是 OPAAK(Open Anonymous Authentication Framework — — 开放匿名认证架构)的基本理论证明。根据 OPAAK 中的陈述,我们的「匿名课程评价系统」中设计有三个基本实体:

  1. 学生用户客户端 — — Client
  2. 匿名登录服务端 — — AIP (Anonymous Identity Provider)
  3. 课程评价服务端 — — TES (Teaching Evaluation System)
大致的系统实体架构

可以看到,基本的「评教」生命周期大概是这样的:

  • 【准备阶段】学生用户利用「账户 — — 密码」登录至 AIP,进入「教务处」等待进入「评教平台」。
  • 【请求进行评教】学生用户点击「开始评教」,客户端随机生成用户秘钥 uk 并秘密保存于本地。AIP 利用「CL 签名」算法,在不知道 uk 的前提下对 uk 进行签名,生成匿名身份凭证 (s,e,v),并将其秘密保存于本地。同时,客户端此时会进入评教平台。
  • 【评教过程】在这一过程,客户端拥有用户秘钥 uk,匿名身份凭证 (s,e,v) 以及三个实体公认的「公钥」。利用这些参数,客户端可以在「零知识证明」的理论支持之上,在不向评教后端透露秘钥与匿名身份凭证的基础之上,证明自己身份的合法性。同时,客户端会计算生成 rnym 作为身份识别令牌,用以限制用户多次与越权评教。
CL 签名的基本数学依赖

从原理上来说,我们利用了 CL 签名的算法来签发用户的匿名身份,利用了零知识证明的理论支持来认证用户的匿名身份。在 OPAAK 的基础之上,我们为每个班级设计了公钥,这样就能区分不同班级用户的评教行为。由于用户秘钥 uk、匿名身份凭证 (s,e,v) 都只保存于客户端,上述签名过程都是不可逆的,rnym 的计算也是唯一且不可回溯的,因此我们可以认为整个系统是匿名的。

不难发现,在「请求进行评教」阶段,我们保证了身份签发的匿名性;在「评教过程」阶段,我们保证了身份认证的匿名性;同时,利用 rnym 我们保证了用户身份的可区分性。

📦 架构设计

在工程方面,我们采用了前后端分离的架构设计。其中前端采用了基于组件化设计的框架 Vue.js 以及开源 CSS 框架 Vuetify,后端采用了基于 Python 的 Django 框架。

根据原理所述的「生命周期」,我们开发了:

  • 前端:登录页面、教务处页面(实际上是假页面)、评教页面,以及各种附属页面(比如 404 错误页面等)
  • 后端:AIP 身份认证服务端、TES 匿名评教服务端

其中,前端的「登录页面」和「教务处页面」配合 AIP 身份认证服务端,「评教页面」配合 TES 匿名评教服务端。AIP 身份认证后端能够查看已注册学生用户的班级、学号、已选课程;TES 匿名评教后端能够查看评教详情、每条评教结果的对应 rnym

前端

事实上,前端主要由两部分构成:登录、评教。

登录

在登录部分,我们利用 JWT — — JSON Web Token 来认证、维护利用「账号 — — 密码」登录的用户 session。登录用户会进入「教务处」,点击「开始评教」即可进入「签名 — 身份匿名化 — 进入评教阶段」的过程。

教务处

在点击「开始评教」之后,客户端也会随即开始 uk 的生成,以及匿名身份凭证 (s,e,v) 的签名请求。之后,客户端会将得到的 uk、(s,e,v) 以及一个公有的公钥 Public Key 一并持久化存于本地(浏览器 local storage)。

存储于 localStorage 的 uk 和 (s,e,v)

用户接下来会被导向「评教页面」,这也就是前端的评教部分。在这里,客户端会根据「班级」和「年级」请求一份「评教任务」,此时用户就可以正常的开始评教。

评教界面

进行一门课的评教之后,用户可以点击「下一步」来提交本门课程的评教任务。此时:

  • 客户端首先会计算一系列零知识证明相关的盲化参数以及 rnym,通过接口 /auth 进行身份认证:
认证接口
  • 每次 /auth 认证时,如果遇到身份认证失败,那么用户会被直接踢出评教过程,显示「身份认证失败」的页面:
身份认证失败
  • 认证成功之后,客户端紧接着会将 rnym 以及评教任务以 JSON 的形式提交给接口 /result 进行评教结果的提交:
提交评教结果

这就是一门课一次完整的评教过程。接下来用户会继续对剩下的课程进行评价,直到全部评教任务结束。

全部评教任务结束,评教成功

前端实现中的两个难点在于:

  1. localStorage 的传递。由于 AIP 和 TES 事实上并不在一个域名之下,因此 localStorage 并不共享。所以我们需要利用 <iframe> 将数据 hack 过去,不太优雅,不过应该没有别的解决方法了。
  2. 「零知识证明」盲化参数的计算。这部分计算涉及到 50 余个 1000 位以上大整数的计算,即使利用非常优化的大整数库进行「指数幂」算法的计算,认证部分参数的计算仍需要大概 1–2 秒左右,因此这里我们利用 Web Worker 将计算「放在另一个线程进行」,实现「JS 多线程」。

Web Worker 部分的原理我在 Medium 上有更为详细的介绍:利用 Web Worker 在浏览器里让 JavaScript「多线程」。PS:后来学弟利用 Rust 和 Web Assembly 将这部分计算优化到 30ms 左右,🐂🍺。

后端

事实上,服务端由唐大佬主导开发的,因此我不详细介绍了。只说明一下功能。

对于 AIP,管理员可以查看已注册用户的信息,课程信息以及学期信息。

后台用户注册信息

对于 TES,管理员可以查看用户评教信息,包括评教结果以及 rnym。但是并不知道具体的评教人。

后台评教结果信息

我们的算法能够确保即使管理员将 AIP 后端以及 TES 后端两者的数据合在一起,也无法知晓用户的真实身份。

🔎 问题所在

到这里,你可能会感觉:「诶,这么好的作品怎么没一等奖呢?」😁

事实上,我们经过不到半个月设计、不到一个月开发的作品,有诸多漏洞。当然并非安全性、匿名性的漏洞,而是系统功能或实用价值上的漏洞。比如:

  • 我们只能对一个班级的用户提供评教服务,对于不同班级选修不同课程的学生用户,我们无法对他们提供评教任务。这部分是由于我们设计了「一个班级一枚公钥」的限制,所以无法区分同班级用户选修不同课程的情况。
  • 我们只能让用户在一个浏览器上面进行评教,如果用户中途切换其他浏览器,由于 localStorage 数据并不共享,因此用户也会随之失去「匿名身份凭证」,也就无法提交评教结果。事实上,如果我们不将系统客户端部署于网页,而是提供一个可下载的客户端,那么这一问题会被更好解决。

当然,这些都不构成我们最终同一等奖擦肩而过的最大问题所在。在我们决赛答辩的过程中,答辩导师询问我们的最大问题在于:

「如果学生在评教过程中是恶意的,你们系统由于匿名性过高,是完全没有办法追溯恶意用户的评教结果。那么如何将这一结果从最终的总评教结果中剔除呢?」

简单来说,我们系统没有 Fail-safe。

Fail-safe

对于恶意的评价行为,虽然这样行为是符合规范且完全合法的,但是其结果是有偏差且不能合并到正式的统计数据之中的。我们无法将这种数据进行筛选、剔除,因此我们系统的实用性方面有非常重大的缺陷,也就是:「高校是不会将我们的系统完整的交付于正式评教工作中去的。」

🎈 尾巴

最后,我想说的是,在设计系统的时候一定要考虑全面的情况,需要从所有可能使用这一系统的用户角度去考虑系统功能。像我们这次的问题,是完全可以通过设定结果判定阈值去触发「假数据的主动丢弃」,或者通过「群签名」来花费较大代价去追溯用户身份等方法来改进的。

还有最重要的一点,就是答辩的时候要顺着老师的思路去说,不要怼老师。 (/ω\)

这篇文章到这里就基本结束了,感谢大家阅读。

--

--