在 Linux 上,最常见的 OpenPGP 工具是 GnuPG,但是这个工具一直有一些问题为人(我)诟病,比如:

  1. GnuPG 似乎主要面对人而非程序。需要使用 PGP 功能的程序,很难调用它的各种功能。
    • 即使可以,GnuPG 也会试图自己来完成一些复杂的幕后工作,比如管理本地密钥环。
    • 还比如用pinentry询问用户密码。即使调用它的程序本身并非设计成用于桌面环境, 也不容易通过标准的进程间通讯来完成这个步骤。
  2. GnuPG 在新技术上过于保守,比如 gpg2 在支持 curve25519 曲线方面, 到现在也非常顽固,不在默认调用方式中提供这类选项。
  3. GnuPG 包含很多复杂的功能,比如密钥互相签名、智能卡管理,这些在日常操作中比较小众。

本实验室的设想是,用其他办法开发一套遵循 OpenPGP 标准的替代工具,提供 PGP 的基本功能, 但要足够简单,可以应付日常工作,也要标准化,以便别的软件(例如聊天工具)可以在此基础上调用它。

最近几年,因为 ProtonMail 的大力支持,一个叫做openpgp.js的 JavaScript 库出现了。 这是一个纯 JavaScript 实现的 OpenPGP 标准,可以给 ProtonMail 等网页服务提供在浏览器上的信息加解密功能。 在 NodeJS 上,也可以运行这个库。因此,可以围绕这个库编写一个命令行的调用程序,用openpgp.js代替gpg

有关技术标准

这个设想出现之后,作者注意到最近几年在互联网上也出现了类似的思考。 在 IETF,有一个名为《无状态的 OpenPGP 命令行标准》的草案已经更新到了第三版:最近更新于2020年3月6日。

查阅草案的内容,发现和本实验室的设想非常接近。这一草案定义了命令行的语法格式和应具有的功能。

这种命令行的格式类似git,是命令 子命令 参数...的形式。具体的子命令定义如下。用sop代表一个无状态 OpenPGP 命令行工具的名字。

sop version         显示工具版本
sop generate-key    生成私钥
sop extract-cert    从给定私钥导出公钥
sop sign            签署给定消息
sop verify          给定消息和签名,验证签名
sop encrypt         给定口令和/或公钥,加密消息
sop decrypt         给定口令和/或私钥,解密消息,按照需求验证签名
sop armor           给定PGP二进制数据,封装成文本格式输出
sop dearmor         给定文本格式封装的PGP信息,输出原始二进制数据
sop detach-inband-signature-and-message 从明文的数字签名消息中分离消息和签名

草案的作者概括列出了 OpenPGP 工具应当具有的基本功能。 这些功能,根据草案要求,基于很多简化的假设,例如:

  1. 处理输入时,不考虑多条 PGP 消息的情况。程序每次只处理一条 PGP 消息。
  2. PGP 消息支持很多复杂的格式,尤其是层层嵌套的情况,一概不考虑,以便程序简洁有效。
  3. 验证 PGP 消息签名时,只考虑已知的(相关的)签名者。
  4. sop工具传入的私钥,必须是已经解密的(没有密码保护)。

有关 scutum

当前,scutum v0.0.2(github, npm)已经大致实现了上述功能, 但工具的行为还不完全遵循标准,仍需完善。 另外,openpgp.js支持对数据流加密(例如STDIN),目前scutum还没有搞定这一特性,因此不适合处理非常巨大的文件。

使用 npm 可以方便地安装 scutum

$ npm i -g scutum
$ scutum
Usage:
 scutum version
 scutum generate-key [--no-armor] [--] [USERID...]
 scutum extract-cert [--no-armor]
 scutum encrypt [--as=binary|text|mime] [--no-armor] [--with-password=PASSWORD...] [--sign-with=KEY...] [--] [CERTS...]
 scutum sign [--no-armor] [--as=binary|text] [--] KEY [KEY...]
 scutum verify [--not-before=DATE] [--not-after=DATE] [--] SIGNATURES CERTS [CERTS...]
 scutum armor [--label=auto|sig|key|cert|message]
 scutum dearmor
 scutum decrypt [--session-key-out=SESSIONKEY] [--with-session-key=SESSIONKEY...] [--with-password=PASSWORD...] [--verify-out=VERIFICATIONS [--verify-with=CERTS...] [--verify-not-before=DATE] [--verify-not-after=DATE] ] [--] [KEY...]
$

scutum是一个严格的无状态命令行,其运行的每条命令都完全依赖用户的实时输入,不在磁盘上保存状态数据,但可以读取来自磁盘的文件作为输入,可以将一些结果写入文件(见sop decrypt命令的定义)。 在稍微修改之后,可以利用BrowserFS,让scutum在浏览器中运行,从虚拟的文件系统中读取输入。

未来如果需要实现一个有状态的版本,也可能取名scuta,然后通过兼容localStorage的接口,在网页上或者本地硬盘存储密钥环。

讨论:为什么要求输入的私钥必须已经解密

scutum和sop标准,要求私钥必须以解密之后的状态输入,并不意味着私钥需要在解密的状态下存储在系统中。 这样的设计,将管理密钥、解密私钥的责任,从工具转移给了用户或者调用工具的软件。

其他软件为了利用用户的私钥,应该实现一个密钥环,利用一个主密码加密整个私钥数据库,而在私钥导出和导入的过程中询问密码。

这样做有很多好处:

  1. 在操作scutum之前,只需要一个密钥环的解密密码就够了。只要密钥环已经解密,就无需在解密具体的消息之前挨个尝试私钥的密码。
  2. 私钥导出和导入密钥环时加密/解密,这样私钥在密钥环之外存放、传输时就一定处于加密的状态。
  3. 其他软件可以自己选择合适的方案,要求用户输入密码解密密钥环,而不局限于pinentry。 这些自定义的「密码输入器」上可以设置额外的机制,比如要求密码的长度或者复杂度、允许用户选择keyfile(解密文件)等来提升安全性。