一、問題
當我們通過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的作用。