证书认证(Certificate Authentication)和密钥认证(Publickey Authentication)不是一回事。

摘自 阮一峰的网络日志

密码登录和密钥登录,都有各自的缺点。

密码登录需要输入服务器密码,这非常麻烦,也不安全,存在被暴力破解的风险。

密钥登录需要服务器保存用户的公钥,也需要用户保存服务器公钥的指纹。这对于多用户、多服务器的大型机构很不方便,如果有员工离职,需要将他的公钥从每台服务器删除。

证书登录就是为了解决上面的缺点而设计的。它引入了一个证书签发机构(Certificate Authority ,简称 CA ),对信任的服务器签发服务器证书,对信任的用户签发用户证书。

登录时,用户和服务器不需要提前知道彼此的公钥,只需要交换各自的证书,验证是否可信即可。

证书登录的主要优点有两个:

(1)用户和服务器不用交换公钥,这更容易管理,也具有更好的可扩展性。

(2)证书可以设置到期时间,而公钥没有到期时间。针对不同的情况,可以设置有效期很短的证书,进一步提高安全性。

证书认证包含主机证书和用户证书两部分。

主机证书用于认证主机。用户在登陆使用受信证书的新主机时不需要手动确认指纹(自动信任)。

用户证书用于认证用户。主机可以直接信任使用受信证书的用户登陆,不需要在主机端反复添加公钥(authorized_keys)。

这两个功能可以同时使用,也可以只用一种。

Plain Certificate Authentication

基础版的使用方法非常简单,主要分为生成 CA & 私钥、签发公钥证书、分发 CA 公钥三个步骤。

生成密钥

CA的本质是一对密钥。

虽然一个 CA 可以同时签发用户证书和服务器证书,但是出于安全性和灵活性,建议用不同的 CA 签发不同证书。

所以,CA 需要至少两对密钥,一对用来签发用户证书,即 User CA ,另一对用来签发服务器证书,即 Host CA 。

创建密钥很简单,使用 ssh-keygen

# 生成ed25519的HostCA
ssh-keygen -t ed25519 -f host_ca -C host_ca

-t 指定密钥算法

-f 生成私钥的位置

-C 指定密钥的识别字符串,即公钥末尾的明文字符串,相当于注释,可以随便设置

因为 ed25519 算法不需要指定密钥位数,所以没有 -b 参数。

上面的命令会在当前目录生成一对密钥:host_ca(私钥)和 host_ca.pub(公钥)。

SSH 对密钥的类型并没有限制,CA 和用户的密钥类型不需要一致,喜欢什么用什么即可,可以使用传统的 RSA ,也可以使用更先进(?)的 ed25519

分别对 CA(用户 & 主机 CA)、用户和主机创建密钥对:

ssh-keygen -t ed25519 -f host_ca -C host_ca # ed25519的Host CA
ssh-keygen -t rsa -b 4096 -f user_ca -C user_ca # 4096位RSA的User CA
ssh-keygen -t ed25519 -f host_001 -C host_001 # 主机密钥
ssh-keygen -t ed25519_sk -f user_alice -C user_alice # 支持双因素认证的ed25519的用户密钥

一般情况,主机会自动生成主机密钥,为 /etc/ssh/ssh_host_xxx_key 。可以将主机公钥下载到本地签名后再上传,也可以使用自己生成的密钥签名后一并上传(不安全)。

在一些自动化场景中,可以让主机信任一个 Host CA ,而不需要单独添加对应主机的指纹,减少操作步骤的同时还能确保安全性。

颁发公钥证书

需要分别签发主机和用户的证书。

主机证书

签发主机证书,需要使用 Host CA 。

ssh-keygen -s host_ca -I host.example.com -h -n host.example.com -V +52w ssh_host_rsa_key.pub

-s 指定 CA 的私钥。

-I 身份字符串,可以随便设置,相当于注释,方便区分证书,将来可以使用这个字符串撤销证书。非明文,包含在签名中

-h 指定该证书是服务器证书,而非用户证书。

-n 指定服务器的域名,表示证书仅对该域名有效。如果有多个域名,则使用逗号分隔。用户登录该域名服务器时,SSH 通过证书的这个值,分辨应该使用哪张证书发给用户,用来证明服务器的可信性。

-V +52w 指定证书的有效期,这里为 52 周(一年)。默认情况下,证书是永远有效的。建议使用该参数指定有效期,并且有效期最好短一点,最长不超过 52 周。

ssh_host_rsa_key.pub 服务器公钥。

上面的命令会生成主机证书 ssh_host_rsa_key-cert.pub(公钥名加后缀 -cert)。

最后,为证书设置权限。

chmod 600 ssh_host_rsa_key-cert.pub

生成证书以后,可以使用下面的命令,查看证书的细节:

ssh-keygen -L -f ssh_host_rsa_key-cert.pub

用户证书

同理:

ssh-keygen -s user_ca -I [email protected] -n user -V +1d user_key.pub

-s 指定 CA 签发证书的密钥

-I 身份字符串,可以随便设置,相当于注释,方便区分证书,将来可以使用这个字符串撤销证书。

-n user 指定用户名,表示证书仅对该用户名有效。如果有多个用户名,使用逗号分隔。用户以该用户名登录服务器时,SSH 通过这个值,分辨应该使用哪张证书,证明自己的身份,发给服务器

-V +1d 指定证书的有效期,这里为1天,强制用户每天都申请一次证书,提高安全性。默认情况下,证书永久有效

user_key.pub 用户公钥

默认生成用户证书,所以这里不需要指定其他参数。

最后,为证书设置权限:

chmod 600 user_key-cert.pub

分发 CA 公钥

主机安装证书 & CA 公钥

ssh_host_rsa_key-cert.pub 证书传回主机(默认为 /etc/ssh 下),喜欢用什么方法都行。

添加SSH配置。在 /etc/ssh/sshd_config 中添加:

HostCertificate /etc/ssh/ssh_host_xxx_key-cert.pub

指定主机证书的位置。


为了让主机信任用户证书,需要将 User CA 的公钥 user_ca.pub 也上传到主机中。

有两种方法添加主机对用户证书的信任,一种是全局方法,对主机中所有用户都有效,

添加一行至 /etc/ssh/sshd_config

TrustedUserCAKeys /etc/ssh/user_ca.pub

还有一种是将证书添加到某个用户的 authorized_key 中,只让该用户信任这个 CA 。

在该用户的 .ssh/authorized_keys 中追加一行:

cert-authority principals="用户名" ...

行尾添加 user_ca.pub 的内容,大概是这个样子:

cert-authority principals="user" ssh-ed25519 AAAAC3Nza......HeJ

最后重启 sshd 服务。

sudo systemctl restart sshd

客户端安装证书 & CA 公钥

客户端安装用户证书很简单。将用户证书 user_key-cert.pub 与用户的密钥 user_key 保存在同一个位置即可。


为了让客户端信任服务器证书,必须将 Host CA 的公钥 host_ca.pub ,添加到用户 /etc/ssh/ssh_known_hosts(全局)或者 ~/.ssh/known_hosts 文件(仅用户)。

打开文件,追加一行,开头为 cert-authority *.example.com ,然后将 host_ca.pub 的内容(即公钥)粘贴在后面,大概是这个样子:

cert-authority *.example.com ssh-rsa AAAAB3Nz...XNRM1EX2gQ==

*.example.com 是域名的模式匹配,表示只要服务器符合该模式的域名,且证书的 CA 匹配该公钥就可以信任。如果没有域名限制,这里可以写成 * 。如果有多个域名模式,可以使用逗号分隔;如果服务器没有域名,可以用主机名(比如 host1,host2,host3)或者IP地址(比如 11.12.13.14,21.22.23.24)。

然后就可以使用证书登录远程服务器了。

撤销证书

主机证书的撤销只需要在 known_hosts 文件里,修改或删除对应的 cert-authority

用户证书的撤销,需要在主机中新建 /etc/ssh/revoked_keys 文件,然后在 sshd_config 添加:

RevokedKeys /etc/ssh/revoked_keys

revoked_keys 文件保存不再信任的用户公钥,由下面的命令生成。

ssh-keygen -kf /etc/ssh/revoked_keys -z 1 ~/.ssh/user1_key.pub

上面命令中,-z 参数用来指定用户公钥保存在 revoked_keys 文件的哪一行,这里保存在第 1 行。

如果以后需要撤销其他的用户公钥,可以用下面的命令保存在第 2 行。

ssh-keygen -ukf /etc/ssh/revoked_keys -z 2 ~/.ssh/user2_key.pub

如果这篇文章只有这些内容,那就和引用的博客重复,这篇博客就没有存在的意义了。

With PIV

在查阅 ssh-keygen 的 manual 时,发现签名语句的用法中还有一个可选参数 -D

ssh-keygen -I certificate_identity -s ca_key [-hU] [-D pkcs11_provider] [-n principals] [-O option] [-V validity_interval] [-z serial_number] file ...

pkcs11 即智能卡使用的接口。是否可以用 PIV 卡代替 CA 的文件密钥呢,答案是可以的,不过在此之前,先学习一下如何使用 PKCS#11 密钥登陆 SSH 。

生成自签名证书

先生成 OpenSSL 格式的私钥。如果直接在 Yubikey 中生成可以使用 yubico-piv-tool 工具:

yubico-piv-tool -a generate -s 9a -A RSA2048 -o public.pem

-a 指定操作指令。

-s 指定 PIV 插槽。要进行 SSH 认证,只能选择 9a(Authentication)和 9e(Card Authentication)插槽。如果选择其他插槽,虽然可以读取到插槽的 SSH 公钥,但实际上并不能使用。

-A 指定密钥算法,Yubikey 5 支持 RSA1024RSA2048ECCP256ECCP384

-o 指定公钥导出路径。卡上生成不支持导出私钥,如果需要备份私钥,必须本机生成后导入。

同时可以设置插槽的安全策略,可以添加 --pin-policy--touch-policy 参数设置使用时是否需要触摸或是否需要输入 PIN 验证。

然后生成自签名证书。这一步也可以在卡上进行。

yubico-piv-tool -a selfsign-certificate -s 9a -S "/CN=SSH key/" -i public.pem -o cert.pem

-a 指定操作指令。

-S 指定证书生成的信息。

-s 指定使用的 PIV 插槽,即上一步使用的插槽。

-i 指定公钥位置。

-o 指定生成证书的位置。

生成证书之后还需要导入:

yubico-piv-tool -a import-certificate -s 9a -i cert.pem

完成了。


本地生成需要使用 OpenSSL 的工具,执行:

openssl genrsa -out ca-key.pem 2048

这条指令会生成一个 RSA2048 格式的 OpenSSL 密钥。末尾的数字指定算法的位数,如果需要导入 Yubikey ,只能使用 10242048

如果需要生成 ECC 证书,执行:

openssl ecparam -name secp384r1 -genkey -noout -out ca-key.pem

-name 指定使用的椭圆曲线算法,可以使用 openssl ecparam -list_curves 查看可用曲线。Yubikey 5 仅支持 secp384r1secp256k1

-genkey 执行生成密钥操作。

-noout 关闭输出。

-out 指定生成私钥路径。

生成自签证书:

openssl req -new -sha256 -x509 -set_serial 1 -days 1000000 -key ca-key.pem -out ca-crt.pem

-sha256 指定证书的散列算法。

-days 指定证书的到期时间,必须设置,默认为一个月。可以设置一个足够长的天数,比如 36500(100 年)。

-key 指定密钥私钥位置。

-out 指定导出证书的位置。

生成后可以查看证书的详细信息:

openssl x509 -text < ca-crt.pem

生成证书之后需要导入 Yubikey :

yubico-piv-tool -a import-key -s 9a -i ca-key.pem # 导入私钥
yubico-piv-tool -a import-certificate -s 9a -i ca-crt.pem # 导入证书

完成了。

使用 PIV 登陆 SSH

先导出 PIV 对应的公钥:

ssh-keygen -D /usr/lib/libykcs11.so -e

-D 指定提供 PKCS#11 支持的库,这里使用的是 Yubico 的 Yubikey 专用库,如果是 Canokey 等其他智能卡,可以使用 opensc 的开源库:

ssh-keygen -D /usr/lib/opensc-pkcs11.so -e

-e 执行导出公钥操作。

应该能得到类似如下的输出:

# libykcs11.so
ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItb...7VudzedZFcvas8lw== Public key for PIV Authentication
ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItb...52qNzqTj+d9fzT4g== Public key for Digital Signature
ssh-rsa AAAAB3NzaC1y...V+aSELY1qkYox Public key for PIV Attestation

# opensc-pkcs11.so
ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItb...7VudzedZFcvas8lw== PIV AUTH pubkey
ecdsa-sha2-nistp384 AAAAE2VjZHNhLXNoYTItb...52qNzqTj+d9fzT4g== CARD AUTH pubkey

两个公钥来自两个插槽 9a 和 9e ,如果同时导入了两个插槽,请注意区分公钥的用途。

libykcs11.so 会多输出一个其他用途的 RSA 公钥,该密钥并不能用于 SSH 认证。

这里的公钥可用于密钥登陆,比如 GitHub 登陆。

也可以使用ssh-add -L查看公钥。

测试一下 GitHub 登陆:

With X.509 PIV

现在你已经学会 PIV 登陆了,接下来让我们结合简单的 SSL 知识1构建一条简易的证书链,来签发我们的 SSH 证书。

创建 Root CA

创建私钥:

openssl ecparam -name secp384r1 -genkey -noout -out root.ca-key.pem 

一般来说,为了保证安全性,Root CA 的加密算法需要尽可能复杂,并且私钥需要离线存储,所以这里可以选择一些超出 Yubikey 可存储要求的加密算法,比如 RSA3744 ,不过其他的算法强度作学习用也足够了。

为 Root CA 生成自签证书:

openssl req -new -sha256 -x509 -set_serial 1 -days 365000 -key root.ca-key.pem -out root.ca-crt.pem

限制证书链长度:

echo 01 > root.ca-crt.srl

创建 Sub CA

生成私钥:

openssl ecparam -name secp384r1 -genkey -noout -out users.ca-key.pem # User CA
openssl ecparam -name secp384r1 -genkey -noout -out hosts.ca-key.pem # Host CA

生成 CertificateRequest(CSR):

openssl req -sha256 -new -key users.ca-key.pem -nodes -out users.ca-csr.pem
openssl req -sha256 -new -key hosts.ca-key.pem -nodes -out hosts.ca-csr.pem

生成 CA 证书:

openssl x509 -sha256 -CA root.ca-crt.pem -CAkey root.ca-key.pem -req -in users.ca-csr.pem -out users.ca-crt.pem
openssl x509 -sha256 -CA root.ca-crt.pem -CAkey root.ca-key.pem -req -in hosts.ca-csr.pem -out hosts.ca-crt.pem

你可能想看一下 Sub CA 证书的信息:

openssl x509 -text < users.ca-crt.pem
openssl x509 -text < hosts.ca-crt.pem

将 Sub CA 导入 Yubikey :

yubico-piv-tool -a import-key -s 9a -i users.ca-key.pem
yubico-piv-tool -a import-certificate -s 9a -i users.ca-crt.pem

这里只把 User CA 导入到 9a 插槽,也可以将 Host CA 导入到另一个插槽中:

yubico-piv-tool -a import-key -s 9e -i hosts.ca-key.pem
yubico-piv-tool -a import-certificate -s 9e -i hosts.ca-crt.pem

签发主机&用户证书

首先需要导出卡中 User CA 和 Host CA 的公钥:

ssh-keygen -D /usr/lib/opensc-pkcs11.so -e

将输出的公钥分别保存为 User CA 公钥 user.CA.pub 和 Host CA 公钥 host.CA.pub

生成用户和主机密钥:

ssh-keygen -t ed25519 -f id_ed25519
ssh-keygen -t ed25519 -f ssh_host_ed25519_key

签发主机&用户证书:

ssh-keygen -s user.CA.pub -D /usr/lib/opensc-pkcs11.so -I User_test id_ed25519.pub
ssh-keygen -s host.CA.pub -D /usr/lib/opensc-pkcs11.so -I Host_test ssh_host_ed25519_key.pub

即可得到用户证书 id_ed25519-cert.pub 和主机证书 ssh_host_ed25519_key-cert.pub

分发过程可参考第二节

With GPG-Agent

既然只需要签名公钥就可以使用证书登陆,那如果给 GPG 导出的公钥签名是否可以让 Yubikey 中的 GPG 证书支持证书登陆。经过一番探索,答案是:

理论上可行。

相关讨论在 GPG 的 issues 中。GPG-Agent 是可以支持 SSH-Agent 功能并提供证书支持的2

但是我的 Yubikey 5 使用 ed25519 密钥在搭配证书使用时失败并提示如下错误:

sign_and_send_pubkey: signing failed for ED25519 "/home/nomad/.ssh/id_ed25519": agent refused operation

深入搜索资料后我发现这个错误与卡片性能有关3,更换其他密钥类型就可以解决问题。

这个错误与 Yubikey 以及 OpenSSH 协议本身的设计有关4,对于普通用户可以说是基本无解,只能换用其他算法。

经测试,NIST256NIST384NIST512 算法工作正常。brainpool 系列算法不支持 SSH 认证。

参考资料

  1. https://developers.yubico.com/PIV/Guides/Certificate_authority.html

  2. https://dev.gnupg.org/T1756

  3. https://dev.gnupg.org/T5041

  4. https://dev.gnupg.org/T6250