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

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

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

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

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

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

证书登录的主要优点有两个:(1)用户和服务器不用交换公钥,这更容易管理,也具有更好的可扩展性。(2)证书可以设置到期时间,而公钥没有到期时间。针对不同的情况,可以设置有效期很短的证书,进一步提高安全性。

摘自阮一峰的网络日志

证书认证主要包含主机证书和用户证书两部分。主机证书用于认证主机,用户在登陆使用受信 CA 签发证书的新主机时不需要手动确认新指纹。用户证书用于认证用户,主机可以直接信任使用受信 CA 签发证书的用户密钥登陆,而不需要在主机端手动添加用户公钥(authorized_keys)。这两个功能可以同时使用,也可以仅选用其中一种。

Plain Certificate Authentication

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

生成密钥

CA的本质是一对普通的密钥。

证书的本质是包含CA对用户的公钥的签名的公钥,所以在证书认证中,用户也是要创建密钥的。

虽然 CA 可以用同一对密码签发用户证书和服务器证书,但是出于安全性和灵活性,最好用不同的密钥分别签发。所以,CA 需要至少两对密钥,一对用于签发用户证书的密钥,即User CA,另一对用来签发服务器证书,即Host CA

创建密钥很简单,使用ssh-keygen程序即可。比如:

1
2
# 生成ed25519的Host CA
ssh-keygen -t ed25519 -f host_ca -C host_ca

-t 指定密钥算法

-f 生成私钥的位置

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

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

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

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

1
2
3
4
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 # 使用带FIDO双因素的ed25519的用户密钥

一般情况下,SSH 服务端会自动生成主机密钥,一般为/etc/ssh/ssh_host_xxx_key,可以先下载到本地后进行签名操作,也可以使用自己生成的密钥。在一些自动部署场景,比如 CI ,可以直接信任一个 Host CA 而不需要单独添加对应主机的指纹,减少不必要的操作。

签发公钥证书

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

主机证书

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

1
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)。

最后,为证书设置权限。

1
chmod 600 ssh_host_rsa_key-cert.pub

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

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

用户证书

同理:

1
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 用户公钥

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

最后,为证书设置权限:

1
chmod 600 user_key-cert.pub

分发CA公钥

主机安装证书 & CA 公钥

ssh_host_rsa_key-cert.pub证书传回主机(默认为/etc/ssh下),可以使用rsyncscpsftp等方法上传,喜欢哪种用哪种就行。

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

1
HostCertificate /etc/ssh/ssh_host_xxx_key-cert.pub

指定主机证书的位置。


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

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

添加一行至/etc/ssh/sshd_config

1
TrustedUserCAKeys /etc/ssh/user_ca.pub

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

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

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

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

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

最后重启sshd服务。

1
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的内容(即公钥)粘贴在后面,大概是这个样子:

1
@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添加:

1
RevokedKeys /etc/ssh/revoked_keys

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

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

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

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

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

生成自签名证书

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

1
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 验证。

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

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

-a 指定操作指令

-S 指定证书生成的信息

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

-i 指定公钥位置

-o 指定生成证书的位置

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

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

完成了。


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

1
openssl genrsa -out ca-key.pem 2048

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

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

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

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

-genkey 执行生成密钥操作

-noout 关闭输出

-out 指定生成私钥路径

生成自签证书:

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

-sha256 指定证书的散列算法

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

-key 指定密钥私钥位置

-out 指定导出证书的位置

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

1
openssl x509 -text < ca-crt.pem

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

1
2
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 对应的公钥:

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

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

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

-e 执行导出公钥操作

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

1
2
3
4
5
6
7
8
# 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

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

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

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

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

测试一下 Github 登陆:

1
ssh [email protected]

With X.509 PIV

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

创建 Root CA

创建私钥:

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

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

为 Root CA 生成自签证书:

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

限制证书链长度:

1
echo 01 > root.ca-crt.srl

创建 Sub CA

生成私钥:

1
2
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

生成 Certificate Request(CSR):

1
2
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 证书:

1
2
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 证书的信息:

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

将 Sub CA 导入 Yubikey:

1
2
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 导入到另一个插槽中:

1
2
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 的公钥:

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

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

生成用户和主机密钥。

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

签发主机&用户证书:

1
2
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 中可以看到。这个 Issue中可以看到 GPG-Agent 是可以支持 SSH-Agent 功能并提供证书支持的。

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

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

深入搜索资料后我发现了这篇 Issue,表明这个错误与卡片性能有关,更换其他密钥类型就可以解决问题。经测试,NIST256NIST384算法工作正常。NIST512算法未测试。brainpool系列算法不支持 SSH 认证。

参考链接

SSH 证书登录教程 - 阮一峰的网络日志

SSH user certificates - Yubico

PIV Walk-Through - Yubico

Certificate authority - Yubico

14.3.6. Signing an SSH Certificate Using a PKCS#11 Token Red Hat Enterprise Linux 6 | Red Hat Customer Portal

ssh-keygen(1) — Arch manual pages

T1756 gpg-agent doesn’t accept ssh certificates

T5041 gpg-agent/scdaemon/gnuk unable to sign ssh certificate (Couldn’t certify key … via agent: agent refused operation)