证书认证(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 支持 RSA1024
、RSA2048
、ECCP256
和 ECCP384
。
-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 ,只能使用 1024
和 2048
。
如果需要生成 ECC 证书,执行:
openssl ecparam -name secp384r1 -genkey -noout -out ca-key.pem
-name
指定使用的椭圆曲线算法,可以使用 openssl ecparam -list_curves
查看可用曲线。Yubikey 5 仅支持 secp384r1
和 secp256k1
。
-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,对于普通用户可以说是基本无解,只能换用其他算法。
经测试,NIST256
、NIST384
、NIST512
算法工作正常。brainpool
系列算法不支持 SSH 认证。