ssh登录的RSA鉴权相关及ssh-agent/ssh-add的意义
一、问题
当我们通过ssh登录一个远端服务器的时候,通常需要通过输入密码来验证是一个合法的、被授权(authentic)的用户,验证的方法其实就是通过密码来验证。这个密码从哪里来呢?很显然,密码是在远端机器上创建当前登录用户的时候设置的,也就是通过useradd -p设置的密码。这里的验证逻辑是:如果一个连接能够知道账户名和对应密码,那么它是可以通过远端登录该服务器的。
把密码每次输入就感觉有些繁琐,如果以明文的形式保存在机器上又不太安全,所以这个时候就可以使用公钥密码体系,例如地球人都知道的RSA算法。这里的逻辑是生成公钥/私钥之后,在服务器上保存一个公钥,而客户端保存一个私钥。如果能够将公钥保存在服务器上,就说明服务器有意愿让这个公钥对应的客户端来进行连接;也就是等价来说,带有私钥的客户端都可以登录到这个服务器。这里原始的逻辑就是这个公钥能够被放置到服务器的特定目录,这是最早授权(authority)的开始。
前面说的都是一些常识性的废话,在实际应用上的问题在于,一个服务器为了支持多个客户端,它需要保存多个公钥,当一个客户端连接过来的时候,如果知道使用哪个公钥进行验证?另一个被忽略的问题是,一个客户端同样也面临着这个问题,客户端可能为不同的服务器生成各自的公钥/私钥,当连接一个服务器的时候,怎么确定它使用哪个私钥呢?
二、服务器的问题
可以看到,服务器可以在authorized_keys文件中维护多个公钥文件,当收到客户端请求的时候,如何确定使用哪个公钥呢?从代码中可以看到,当客户端连接过来的时候,会带上签名的指纹(fingerprint),而服务器计算自己管理的所有公钥文件的指纹进行匹配,匹配之后使用该公钥进行验证。
1、生成key时指纹信息
其实在使用ssh-keygen生成RSA秘钥的时候,默认就会显示一个指纹信息,下面输出中的The key fingerprint is就引导了指纹信息
tsecer@harry: ssh-keygen -f tsecer -C tsecer@harry.com -t rsa
Generating public/private rsa key pair.
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in tsecer.
Your public key has been saved in tsecer.pub.
The key fingerprint is:
f5:ee:19:9b:68:e1:fe:b4:3d:a5:09:5b:f6:6b:d0:a9 tsecer@harry.com
The key's randomart image is:
+--[ RSA 2048]----+
| |
| |
| . |
| . . |
| S . . .|
| ....oo.|
| . .+=o= |
| o+.E+..|
| oo.B oo.|
+-----------------+
tsecer@harry:
2、RSA的指纹怎么计算
可以看到,对于RSA来说,签名是通过对公钥中最为关键的大数n和加密因子e拼接之后计算的hash值。这里顺便提一下,这两个信息在公钥和私钥中都是存在的,也就是客户端和服务器都可以方便的获得。当客户端连接过来的时候,服务器可以获得客户端传递过来的指纹,和服务器所有的公钥的指纹进行匹配,匹配成功则使用该公钥进行鉴权。
openssh-8.0p1\sshkey.c
static int
to_blob_buf(const struct sshkey *key, struct sshbuf *b, int force_plain,
enum sshkey_serialize_rep opts)
{
……
case KEY_RSA:
if (key->rsa == NULL)
return SSH_ERR_INVALID_ARGUMENT;
RSA_get0_key(key->rsa, &rsa_n, &rsa_e, NULL);
if ((ret = sshbuf_put_cstring(b, typename)) != 0 ||
(ret = sshbuf_put_bignum2(b, rsa_e)) != 0 ||
(ret = sshbuf_put_bignum2(b, rsa_n)) != 0)
return ret;
break;
……
}
三、客户端的问题
明显的,客户端在登录特定服务器的时候可以为某个特定服务器使用特定的标志文件,也即是在ssh的
-i identity_file
Selects a file from which the identity (private key) for public key authentication is read. The default is ~/.ssh/identity for protocol version 1, and ~/.ssh/id_dsa,
~/.ssh/id_ecdsa, ~/.ssh/id_ed25519 and ~/.ssh/id_rsa for protocol version 2. Identity files may also be specified on a per-host basis in the configuration file. It is possi‐
ble to have multiple -i options (and multiple identities specified in configuration files). ssh will also try to load certificate information from the filename obtained by
appending -cert.pub to identity filenames.
指定的文件。也就是说当用户选择连接一个特定服务器的时候,如果不同服务器使用的是不同的公钥/私钥文件,就需要自己维护这个关系。那么有没有更加简洁的方法呢?
四、多个标识尝试
可以看到,当使用ssh的时候,其实也并没有指定私钥是是和哪个公钥匹配的,那么它为什么就可以智能识别哪个服务器使用哪个呢?
这个其实是一个错觉,和其它计算机智能场景一样,如果穷举的足够快,那么看起来就好像有智能一样,其实通过ssh的 verbose选项就可以看到,其实并不存在智能识别的问题,只是简单的逐个尝试所有可能的标志文件
openssh-8.0p1\sshconnect2.c
static int
userauth_pubkey(struct ssh *ssh)
{
Authctxt *authctxt = (Authctxt *)ssh->authctxt;
Identity *id;
int sent = 0;
char *ident;
while ((id = TAILQ_FIRST(&authctxt->keys))) {
if (id->tried++)
return (0);
/* move key to the end of the queue */
TAILQ_REMOVE(&authctxt->keys, id, next);
TAILQ_INSERT_TAIL(&authctxt->keys, id, next);
……
}
debug1: kex: client->server cipher: chacha20-poly1305@openssh.com MAC: <implicit> compression: none
debug1: expecting SSH2_MSG_KEX_ECDH_REPLY
debug1: Server host key: ecdsa-sha2-nistp256 SHA256:JCf5IhPo/MfPVJNykuMIO2zB/t26rlZWVlZsYzjKe+c
debug1: Host '[127.0.0.1]:36000' is known and matches the ECDSA host key.
debug1: Found key in /home/harry/.ssh/known_hosts:1
debug2: set_newkeys: mode 1
debug1: rekey out after 134217728 blocks
debug1: SSH2_MSG_NEWKEYS sent
debug1: expecting SSH2_MSG_NEWKEYS
debug1: SSH2_MSG_NEWKEYS received
debug2: set_newkeys: mode 0
debug1: rekey in after 134217728 blocks
debug1: Will attempt key: /home/harry/.ssh/id_rsa RSA SHA256:77W7XKH2dS5qOOvr7zbGQHUHNKXFHZbDUD2vlI+Al4M agent
debug1: Will attempt key: /home/harry/.ssh/id_dsa
debug1: Will attempt key: /home/harry/.ssh/id_ecdsa
debug1: Will attempt key: /home/harry/.ssh/id_ed25519
debug1: Will attempt key: /home/harry/.ssh/id_xmss
debug2: pubkey_prepare: done
debug2: service_accept: ssh-userauth
debug1: SSH2_MSG_SERVICE_ACCEPT received
也就是当ssh启动的时候,会尝试从ssh-agent拉取所有的已经纳入ssh-agent管理的私钥
openssh-8.0p1\authfd.c
/*
* Fetch list of identities held by the agent.
*/
int
ssh_fetch_identitylist(int sock, struct ssh_identitylist **idlp)
{
u_char type;
u_int32_t num, i;
struct sshbuf *msg;
struct ssh_identitylist *idl = NULL;
int r;
/*
* Send a message to the agent requesting for a list of the
* identities it can represent.
*/
if ((msg = sshbuf_new()) == NULL)
return SSH_ERR_ALLOC_FAIL;
if ((r = sshbuf_put_u8(msg, SSH2_AGENTC_REQUEST_IDENTITIES)) != 0)
goto out;
……
……
}
五、那么ssh-agent又是什么呢
1、passphrase的引入
为了让私钥更加安全,在生成的时候添加了passphrase这个概念,整个内容使用这个passphrase再次进行加密,所以使用私钥本身的时候要输入passphrase进行解密。通过简单的搜索可以知道ssh-agent管理的内容来自ssh-add,也就是把一些私钥文件放给ssh-agent管理。这样的一个好处在于,如果私钥生成的时候有设置了passphrase,之后使用私钥的时候不用手动输入这个passphrase。
tsecer@harry: ssh-keygen -t rsa -C tsecer@harry.com -f tsecer
Generating public/private rsa key pair.
Enter passphrase (empty for no passphrase):
Enter same passphrase again:
Your identification has been saved in tsecer.
Your public key has been saved in tsecer.pub.
The key fingerprint is:
b5:94:ec:23:78:34:19:fa:3f:07:dd:3a:06:28:d6:80 tsecer@harry.com
The key's randomart image is:
+--[ RSA 2048]----+
| . |
| . . + . |
| E o + = |
| * * o . |
| + S * . . |
| . o o + . |
| o = |
| + . |
| |
+-----------------+
tsecer@harry: eval $(ssh-agent)
Agent pid 11605
tsecer@harry: ssh-add tsecer
tsecer tsecer.pub
tsecer@harry: ssh-add tsecer
Enter passphrase for tsecer:
Identity added: tsecer (tsecer)
tsecer@harry: ssh-add -L
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDQAJ/iP/RC1hUVJeleAipPqp8LIvG9b1qfeYSMhCS3peFZ9lpPv6l/IpBNn4lJRlMGgv4s1oVUvjg1Ggm3FJXR8PDDEWhKv2dtFFaKm2Krzv8gFiE3pOwLG4X1q9uOPIpWQ973xk1itDUJct4ar2sWo/QbOorBPRTcvcSXEpUW6S6URWRI9uQ1iJQuJR3Neyz/T3ALOzaKnV+9buDux2gkjRYTUw+eNf8de5VN3RFP45Qsl0P3X53xBXIZeAs5AUYVdjbVJn9WOK8VnyNlIGQ8JZBA58kIYG/Do4Pr4mBnRJNbftLLwY/OX6gIeSQqvclpR/bGA4OZNIQ35iulWEo3 tsecer
tsecer@harry:
2、ssh的-A选项的意义
这个从实现上看,是为了二次跳转而使用的。从前面可以看到,当需要公钥的时候会尝试从一个socket中读取所有的私钥列表,这个socket对于最原始的客户端来说就是本地的ssh-agent进程。现在假设通过client登录到serverA,其实执行的命令是在ServerA派生的shell中执行,如果通过这个shell再次派生一个ssh来连接serverB,那么此时这个ssh也会尝试从socket中读取公钥列表,那么这个是从serverA本机上的ssh-agent读取,还是从client上的ssh-agent读取呢?
如果在ssh上制定了-A选项,则在ServerA服务器上会创建一个socket,遮掩当ServerA上的ssh启动时,读取的socket就是ServerA上sshd派生的socket,这个socket再转发到客户端client上的ssh-agent,从而生成一个接力链。
openssh-8.0p1\session.c
static int
auth_input_request_forwarding(struct ssh *ssh, struct passwd * pw)
{
……
/* Allocate a channel for the authentication agent socket. */
nc = channel_new(ssh, "auth socket",
SSH_CHANNEL_AUTH_SOCKET, sock, sock, -1,
CHAN_X11_WINDOW_DEFAULT, CHAN_X11_PACKET_DEFAULT,
0, "auth socket", 1);
nc->path = xstrdup(auth_sock_name);
return 1;
……
}
sshd将socket设置到子进程的环境变量中
static char **
do_setup_env(struct ssh *ssh, Session *s, const char *shell)
{
……
if (auth_sock_name != NULL)
child_set_env(&env, &envsize, SSH_AUTHSOCKET_ENV_NAME,
auth_sock_name);
……
}
3、举例
当我们通过ssh -A登录到远端服务器之后,在ssh中执行
sleep 12345 &
通过查看进程环境变量可以看到有一个SSH_AUTH_SOCK环境变量
SSH_AUTH_SOCK=/tmp/ssh-xqUNcdHDxs/agent.13891
tsecer@harry: sleep 1234 &
[1] 14047
tsecer@harry: cat /proc/14047/environ
……:SSH_AUTH_SOCK=/tmp/ssh-xqUNcdHDxs/agent.13891……
tsecer@harry:
如果在启动ssh的时候不添加-A选项,在shll中执行该命令则没有这个环境变量
4、更加专业的说明
下面的网页更加详细的说明了forward的作用。