openssl rand 为何造不出 tls-crypt 需要的密钥
OpenSSL 只生成随机字节;tls-crypt 需要结构化的静态密钥文件,tls-crypt-v2 需要只有服务器才能包装的密钥。请使用 openvpn --genkey。
OpenSSL 已经装好,主机也有熵源,用它生成密钥看起来是条显而易见的捷径。输出会既随机又保密,却完全不能用;失败还会以控制通道错误的形式出现,而不是任何提到密钥格式的东西。这种误导才是代价高昂之处,因为它让人去检查自己的加密套件。VPN 读取的是一种特定的文件结构,而不是任意一段字节;而且这个功能有一个变体根本无法在服务器之外生成。
磁盘上的文件究竟必须是什么样
OpenVPN 的静态密钥文件是文本。它以一行声明自身用途的注释开头,以固定宽度的行承载十六进制数据,密钥材料合计 256 字节。OpenVPN 把它当作控制通道的共享秘密来读取,既不是证书,也不是口令短语,更不是 TLS 库能识别的任何东西。
同一种对象同时服务于控制通道的两项功能。用 tls-auth 时,每个对端还需要一个方向,这样一方的数据包就不会被反射回它自己。用 tls-crypt 时,单个共享秘密双向加密并认证控制通道,既隐藏握手,也保护握手。
这些都不是秘密知识,格式也不是重点。重点是文件必须与读取方的预期完全一致:头部、行宽、末尾换行和总长度。错开一行的文件会被当成垃圾载入,或者干脆失败。因此逐字节比较两个对端是正当的第一项测试,而且比读两个配置文件中的任何一个都快。
受支持的生成方式
用将要使用它的工具生成普通静态密钥,然后从两端引用它:
openvpn --genkey secret /etc/openvpn/tc-shared.key
tls-crypt /etc/openvpn/tc-shared.key
这个选项的拼写随时间变化:2.4 时代的旧安装使用在文件名之前带单独标志的形式,而当前的构建接受较短的形式。无论哪种,生成的文件都是同一种对象,所以检查的是两个对端是否持有完全相同的字节。
第二代加入了任何外部生成器都无法凭空发明的结构。服务器密钥只生成一次,每个客户端收到由它派生出的包装密钥:
openvpn --genkey tls-crypt-v2-server /etc/openvpn/tc-v2-server.key
openvpn --genkey tls-crypt-v2-client /etc/openvpn/laptop1.key --tls-crypt-v2 /etc/openvpn/tc-v2-server.key
客户端收到的并不是服务器自己密钥的副本。它是一个包装对象,携带只有服务器才能解开的元数据;正因为这层包装,按设备划分的文件才能 在不触碰任何其他设备的情况下被吊销。把服务器密钥留在服务器上,并把它当作这份按设备信任的根。
手工字节在哪里崩掉
在正确的头部下面把 256 个随机字节按十六进制打印出来,理论上能得到一个可载入的静态密钥文件。这仍然是个坏主意。每个格式细节都会变成出错的机会:要么载入时失败,要么更糟,让两个对端持有不同的值,而这只会在认证失败时才暴露。
v2 变体则完全做不到等价的事。包装后的客户端密钥只作为服务器密钥流程的输出存在,服务器保留解开它所需的材料。随机的数据块会被拒绝,或者干脆无法解密,而这两种提示都不会提到随机性。
版本兼容性叠在这一切之上。第一代需要两端都支持它的版本,第二代又需要更新的版本。在功能出现之前构建的客户端,无法与只接受包装材料的服务器完成握手,而它报告的失败指向控制通道,而不是版本。
轮换而不把所有人锁在外面
轮换单个共享秘密是协调工作,而不是技术工作。每个客户端都要在服务器撤回旧文件之前拿到替换文件,任何还持有旧值的机器都会在那一刻失败。不要在上班时间中途开始这次轮换。
采用 v2 方案时,轮换按设备进行:为一台机器签发新的包装密钥,并只为那台机器退役旧的。等所有客户端都用上支持它的版本之后再迁移,这就是主要理由。
文件权限也属于同一场讨论。密钥应当只对运行服务的账户可读,并且不应与它保护的配置文件放在同一个仓库里。OpenVPN 2.6 参考手册记录了生成形式和密钥材料章节,这比从论坛帖子里抄来的命令更可靠。