在 Linux 上,最常见的 OpenPGP 工具是 GnuPG,但是这个工具一直有一些问题为人(我)诟病,比如:
- GnuPG 似乎主要面对人而非程序。需要使用 PGP 功能的程序,很难调用它的各种功能。
- 即使可以,GnuPG 也会试图自己来完成一些复杂的幕后工作,比如管理本地密钥环。
- 还比如用
pinentry
询问用户密码。即使调用它的程序本身并非设计成用于桌面环境, 也不容易通过标准的进程间通讯来完成这个步骤。
- GnuPG 在新技术上过于保守,比如 gpg2 在支持
curve25519
曲线方面, 到现在也非常顽固,不在默认调用方式中提供这类选项。 - 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 工具应当具有的基本功能。 这些功能,根据草案要求,基于很多简化的假设,例如:
- 处理输入时,不考虑多条 PGP 消息的情况。程序每次只处理一条 PGP 消息。
- PGP 消息支持很多复杂的格式,尤其是层层嵌套的情况,一概不考虑,以便程序简洁有效。
- 验证 PGP 消息签名时,只考虑已知的(相关的)签名者。
- 给
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标准,要求私钥必须以解密之后的状态输入,并不意味着私钥需要在解密的状态下存储在系统中。
这样的设计,将管理密钥、解密私钥的责任,从工具转移给了用户或者调用工具的软件。
其他软件为了利用用户的私钥,应该实现一个密钥环,利用一个主密码加密整个私钥数据库,而在私钥导出和导入的过程中询问密码。
这样做有很多好处:
- 在操作
scutum
之前,只需要一个密钥环的解密密码就够了。只要密钥环已经解密,就无需在解密具体的消息之前挨个尝试私钥的密码。 - 私钥导出和导入密钥环时加密/解密,这样私钥在密钥环之外存放、传输时就一定处于加密的状态。
- 其他软件可以自己选择合适的方案,要求用户输入密码解密密钥环,而不局限于
pinentry
。 这些自定义的「密码输入器」上可以设置额外的机制,比如要求密码的长度或者复杂度、允许用户选择keyfile
(解密文件)等来提升安全性。