第一部分:協(xié)議概覽
整個通訊過程中,經(jīng)過下面幾個階段協(xié)商實現(xiàn)認(rèn)證連接。
第一階段:
由客戶端向服務(wù)器發(fā)出 TCP 連接請求。TCP 連接建立后,客戶端進(jìn)入等待,服務(wù)器向客戶端發(fā)送第一個報文,宣告自己的版本號,包括協(xié)議版本號和軟件版本號。協(xié)議版本號由主版本號和次版本號兩部分組成。它和軟件版本號一起構(gòu)成形如:
"SSH-<主協(xié)議版本號>.<次協(xié)議版本號>-<軟件版本號>\n"
的字符串。其中軟件版本號字符串的最大長度為40個字節(jié),僅供調(diào)試使用。客戶端接到報文后,回送一個報文,內(nèi)容也是版本號。客戶端響應(yīng)報文里的協(xié)議版本號這樣來決定:當(dāng)與客戶端相比服務(wù)器的版本號較低時,如果客戶端有特定的代碼來模擬,則它發(fā)送較低的版本號;如果它不能,則發(fā)送自己的版本號。當(dāng)與客戶端相比服務(wù)器的版本號較高時,客戶端發(fā)送自己的較低的版本號。按約定,如果協(xié)議改變后與以前的相兼容,主協(xié)議版本號不變;如果不相兼容,則主主協(xié)議版本號升高。
服務(wù)器接到客戶端送來的協(xié)議版本號后,把它與自己的進(jìn)行比較,決定能否與客戶端一起工作。如果不能,則斷開TCP 連接;如果能,則按照二進(jìn)制數(shù)據(jù)包協(xié)議發(fā)送第一個二進(jìn)制數(shù)據(jù)包,雙方以較低的協(xié)議版本來一起工作。到此為止,這兩個報文只是簡單的字符串,你我等凡人直接可讀。
第二階段:
協(xié)商解決版本問題后,雙方就開始采用二進(jìn)制數(shù)據(jù)包進(jìn)行通訊。由服務(wù)器向客戶端發(fā)送第一個包,內(nèi)容為自己的 RSA主機(jī)密鑰(host key)的公鑰部分、RSA服務(wù)密鑰(server key)的公鑰部分、支持的加密方法、支持的認(rèn)證方法、次協(xié)議版本標(biāo)志、以及一個 64 位的隨機(jī)數(shù)(cookie)。這個包沒有加密,是明文發(fā)送的。客戶端接收包后,依據(jù)這兩把密鑰和被稱為cookie的 64 位隨機(jī)數(shù)計算出會話號(session id)和用于加密的會話密鑰(session key)。隨后客戶端回送一個包給服務(wù)器,內(nèi)容為選用的加密方法、cookie的拷貝、客戶端次協(xié)議版本標(biāo)志、以及用服務(wù)器的主機(jī)密鑰的公鑰部分和服務(wù)密鑰的公鑰部分進(jìn)行加密的用于服務(wù)器計算會話密鑰的32 字節(jié)隨機(jī)字串。除這個用于服務(wù)器計算會話密鑰的 32字節(jié)隨機(jī)字串外,這個包的其他內(nèi)容都沒有加密。之后,雙方的通訊就是加密的了,服務(wù)器向客戶端發(fā)第二個包(雙方通訊中的第一個加密的包)證實客戶端的包已收到。
第三階段:
雙方隨后進(jìn)入認(rèn)證階段。可以選用的認(rèn)證的方法有:
(1) ~/.rhosts 或 /etc/hosts.equiv 認(rèn)證(缺省配置時不容許使用它);
(2) 用 RSA 改進(jìn)的 ~/.rhosts 或 /etc/hosts.equiv 認(rèn)證;
(3) RSA 認(rèn)證;
(4) 口令認(rèn)證。
如果是使用 ~/.rhosts 或 /etc/hosts.equiv 進(jìn)行認(rèn)證,客戶端使用的端口號必須小于1024。
認(rèn)證的第一步是客戶端向服務(wù)器發(fā) SSH_CMSG_USER 包聲明用戶名,服務(wù)器檢查該用戶是否存在,確定是否需要進(jìn)行認(rèn)證。如果用戶存在,并且不需要認(rèn)證,服務(wù)器回送一個SSH_SMSG_SUCCESS 包,認(rèn)證完成。否則,服務(wù)器會送一個 SSH_SMSG_FAILURE 包,表示或是用戶不存在,或是需要進(jìn)行認(rèn)證。注意,如果用戶不存在,服務(wù)器仍然保持讀取從客戶端發(fā)來的任何包。除了對類型為 SSH_MSG_DISCONNECT、SSH_MSG_IGNORE 以及 SSH_MSG_DEBUG 的包外,對任何類型的包都以 SSH_SMSG_FAILURE 包。用這種方式,客戶端無法確定用戶究竟是否存在。
如果用戶存在但需要進(jìn)行認(rèn)證,進(jìn)入認(rèn)證的第二步。客戶端接到服務(wù)器發(fā)來的 SSH_SMSG_FAILURE 包后,不停地向服務(wù)器發(fā)包申請用各種不同的方法進(jìn)行認(rèn)證,直到時限已到服務(wù)器關(guān)閉連接為止。時限一般設(shè)定為 5 分鐘。對任何一個申請,如果服務(wù)器接受,就以 SSH_SMSG_SUCCESS 包回應(yīng);如果不接受,或者是無法識別,則以 SSH_SMSG_FAILURE 包回應(yīng)。
第四階段:
認(rèn)證完成后,客戶端向服務(wù)器提交會話請求。服務(wù)器則進(jìn)行等待,處理客戶端的請求。在這個階段,無論什么請求只要成功處理了,服務(wù)器都向客戶端回應(yīng) SSH_SMSG_SUCCESS包;否則回應(yīng) SSH_SMSG_FAILURE 包,這表示或者是服務(wù)器處理請求失敗,或者是不能識別請求。會話請求分為這樣幾類:申請對數(shù)據(jù)傳送進(jìn)行壓縮、申請偽終端、啟動 X11、TCP/IP 端口轉(zhuǎn)發(fā)、啟動認(rèn)證代理、運行 shell、執(zhí)行命令。到此為止,前面所有的報文都要求 IP 的服務(wù)類型(TOS)使用選項 IPTOS_THROUGHPUT。
第五階段:
會話申請成功后,連接進(jìn)入交互會話模式。在這個模式下,數(shù)據(jù)在兩個方向上雙向傳送。此時,要求 IP 的服務(wù)類型(TOS)使用 IPTOS_LOWDELAY 選項。當(dāng)服務(wù)器告知客戶端自己的退出狀態(tài)時,交互會話模式結(jié)束。
(注意:進(jìn)入交互會話模式后,加密被關(guān)閉。在客戶端向服務(wù)器發(fā)送新的會話密鑰后,加密重新開始。用什么方法加密由客戶端決定。)
第二部分:數(shù)據(jù)包格式和加密類型
二進(jìn)制數(shù)據(jù)包協(xié)議:
包 = 包長域(4字節(jié):u_int32_t) + 填充墊(1-7字節(jié))
+ 包類型域(1字節(jié):u_char) + 數(shù)據(jù)域
+ 校驗和域(4字節(jié))
加密部分 = 填充墊 + 包類型 + 數(shù)據(jù) + 校驗和
包長 = 1(包類型) + 數(shù)據(jù)字節(jié)長度 + 4(校驗和)
數(shù)據(jù)包壓縮:
如果支持壓縮,包類型域和數(shù)據(jù)域用 gzip 壓縮算法進(jìn)行壓縮。壓縮時在兩個數(shù)據(jù)傳送方向的任何一個上,包的壓縮部分(類型域+數(shù)據(jù)域)被構(gòu)造得象是它連在一起,形成一個連續(xù)的數(shù)據(jù)流。在兩個數(shù)據(jù)傳送方向上,壓縮是獨立進(jìn)行的。
數(shù)據(jù)包加密:
現(xiàn)時支持的數(shù)據(jù)加密方法有這樣幾種:
SSH_CIPHER_NONE 0 不進(jìn)行加密
SSH_CIPHER_IDEA 1 IDEA 加密法(CFB模式)
SSH_CIPHER_DES 2 DES 加密法(CBC模式)
SSH_CIPHER_3DES 3 3DES 加密法(CBC模式)
SSH_CIPHER_ARCFOUR 5 Arcfour加密法)
SSH_CIPHER_BLOWFISH 6 Blowfish 加密法
協(xié)議的所有具體實現(xiàn)都要求支持3DES。
DES 加密:
從會話密鑰中取前8個字節(jié),每個字只用高7位,忽略最低位,這樣構(gòu)成56位的密鑰供加密使用。加密時使用CBC 模式,初使矢量被初始化為全零。
3DES 加密:
3DES 是 DES 的變體,它三次獨立地使用 CBC 模式的DES 加密法,每一次的初始矢量都是獨立的。第一次用DES 加密法對數(shù)據(jù)進(jìn)行加密;第二次對第一次加密的結(jié)果用 DES 加密法進(jìn)行解密;第三次再對第二次解密的
結(jié)用 DES 加密法進(jìn)行加密。注意:第二次解密的結(jié)果并不就是被加密的數(shù)據(jù),因為三次使用的密鑰和初始矢量都是分別不同的。與上面的 DES 加密采用的方法類似,第一次從會話密鑰中取起始的前8個字節(jié)生成加密密鑰,第二次取下一個緊跟著的8個字節(jié),第三次取再下一個緊跟著的8個字節(jié)。三次使用的初始矢量都初始化為零。
IDEA 加密:
加密密鑰取自會話密鑰的前16個字節(jié),使用 CFB 模式。初始矢量初始化為全零。
RC4 加密:
會話密鑰的前16個字節(jié)被服務(wù)器用作加密密鑰,緊接著的下一個16字節(jié)被客戶端用作加密密鑰。結(jié)果是兩個數(shù)據(jù)流方向上有兩個獨立的129位密鑰。這種加密算法非常快。
第二部分:密鑰的交換和加密的啟動
在服務(wù)器端有一個主機(jī)密鑰文件,它的內(nèi)容構(gòu)成是這樣的:
1. 私鑰文件格式版本字符串;
2. 加密類型(1 個字節(jié));
3. 保留字(4 個字節(jié));
4. 4 個字節(jié)的無符號整數(shù);
5. mp 型整數(shù);
6. mp 型整數(shù);
7. 注解字符串的長度;
8. 注解字符串;
9. 校驗字(4 個字節(jié));
10. mp 型整數(shù);
11. mp 型整數(shù);
12. mp 型整數(shù);
13. mp 型整數(shù);
其中 4、5、6 三個字段構(gòu)成主機(jī)密鑰的公鑰部分;10、11、12、13 四個字段構(gòu)成主機(jī)密鑰的私鑰部分。9、10、11、12、13 五個字段用字段 2 的加密類型標(biāo)記的加密方法進(jìn)行了加密。4 個字節(jié)的校驗字交叉相等,即第一個字節(jié)與第三個字節(jié)相等,第二個字節(jié)與第四個字節(jié)相等。在服務(wù)器讀取這個文件時進(jìn)行這種交叉相等檢查,如果不滿足這個條件,則報錯退出。
服務(wù)器程序運行的第一步,就是按照上面的字段劃分讀取主機(jī)密鑰文件。隨后生成一個隨機(jī)數(shù),再調(diào)用函數(shù)
void rsa_generate_key
(
RSAPrivateKey *prv,
RSAPublicKey *pub,
RandomState *state,
unsigned int bits
);
生成服務(wù)密鑰,服務(wù)密鑰也由公鑰和私鑰兩部分組成。上面的這個函數(shù)第一個指針參數(shù)指向服務(wù)密鑰的私鑰部分,第二個指向公鑰部分。然后把主機(jī)密鑰的公鑰部分和服務(wù)密鑰的公鑰部分發(fā)送給客戶端。在等到客戶端回應(yīng)的包后,服務(wù)器用自己的主機(jī)密鑰的私鑰部分和服務(wù)密鑰的私鑰部分解密得到客戶端發(fā)來的 32 字節(jié)隨機(jī)字串。然后計算自己的會話號,并用會話號的前 16字節(jié) xor 客戶端發(fā)來的 32 字節(jié)隨機(jī)字串的前 16 字節(jié),把它作為自己的會話密鑰。注意,服務(wù)器把8個字節(jié)的 cookie、主機(jī)密鑰的公鑰部分、和服務(wù)密鑰的公鑰部分作為參數(shù)來計算自己的會話號。
再來看客戶端。客戶端啟動后的第一步驟也是讀取主機(jī)密鑰。然后等待服務(wù)器主機(jī)密鑰、服務(wù)密鑰、和 8個字節(jié)的cookie。注意,服務(wù)器發(fā)送來的只是主機(jī)密鑰和服務(wù)密鑰的公鑰部分。接到包后,客戶端立即把從服務(wù)器端收到cookie、主機(jī)密鑰、和服務(wù)密鑰作為參數(shù)計算出會話號。從上面可以看出,服務(wù)器和客戶端各自計算出的會話號實際是一樣的。
隨后,客戶端檢查用戶主機(jī)列表和系統(tǒng)主機(jī)列表,查看從服務(wù)器收到的主機(jī)密鑰是否在列表中。如果不在列表中,則把它加入列表中。然后就生成 32 字節(jié)的隨機(jī)字串,這個32 字節(jié)的隨機(jī)字串就是客戶端的會話密鑰。客戶端用 16字節(jié)的會話密鑰 xor 它的前 16 字節(jié),把結(jié)果用服務(wù)器的主機(jī)密鑰和服務(wù)密鑰進(jìn)行雙重加密后發(fā)送給服務(wù)器。產(chǎn)生 32字節(jié)隨機(jī)字串時,隨機(jī)數(shù)種子由兩部分組成,其中一部分從系統(tǒng)隨機(jī)數(shù)種子文件中得到,這樣來避免會話密鑰被猜出。從上面服務(wù)器和客戶端各自計算會話密鑰的過程可以看出,服務(wù)器和客戶端計算出的會話密鑰是一樣的。
上面的這幾步,總結(jié)起來就要交換確定會話密鑰,因為無論是 des、idea、3des、arcfour、還是 blowfish 都是對稱加密方法,只有一把密鑰,雙方都知道了會話密鑰才能啟動加密。但會話密鑰不能在網(wǎng)絡(luò)上明文傳送,否則加密就失去意義了。于是使用 RSA 公鑰體系對會話密鑰進(jìn)行加密。
RSA 公鑰體系的辦法是用公鑰加密私鑰解密,它依據(jù)這樣的數(shù)學(xué)定理:
若 p、q 是相異的兩個質(zhì)數(shù),整數(shù) r 和 m 滿足
rm == 1 (mod (p-1)(q-1))
a 是任意的整數(shù),整數(shù) b、c 滿足 b == a^m (mod pq),
c == b^r (mod pq)。則
c == a (mod pq)。
具體實現(xiàn)是這樣的:
(1) 找三個正整數(shù) p、q、r,其中 p、q 是相異的質(zhì)數(shù),
r 是與(p-1)、(q-1)互質(zhì)的數(shù)。這三個數(shù) p、q、r
就是私鑰(private key)。
(2) 再找一個正整數(shù) m 滿足 rm == 1 (mod(p-1)(q-1))。
計算 n = pq,m、n 就是公鑰(public key)。
(3) 被加密對象 a 看成是正整數(shù),設(shè) a < n。若 a >= n,
將 a 表示成 s (s < n,通常取 s = 2^t) 進(jìn)制的,
然后對每一位分別編碼。
(4) 加密:計算 b == a^m (mod n) (0 <= b < n),b 為
加密結(jié)果。
(5) 解密:計算 c == b^r (mod n) (0 <= c < n),c 為
解密結(jié)果。
從上面的數(shù)學(xué)定理可知,最后結(jié)果 c = a。
計算 RSA 密鑰的方法及過程是,調(diào)用下面的函數(shù)計算 RSA公鑰和 RSA 私鑰:
_______________________________________________________
void rsa_generate_key
(
RSAPrivateKey *prv, RSAPublicKey *pub,
RandomState *state, unsigned int bits
)
{
MP_INT test, aux;
unsigned int pbits, qbits;
int ret;
mpz_init(&prv->q);
mpz_init(&prv->p);
mpz_init(&prv->e);
mpz_init(&prv->d);
mpz_init(&prv->u);
mpz_init(&prv->n);
mpz_init(&test);
mpz_init(&aux);
/* 計算質(zhì)數(shù) p、q 的位數(shù) */
pbits = bits / 2;
qbits = bits - pbits;
retry0:
fprintf(stderr, "Generating p: ");
/* 生成隨機(jī)質(zhì)數(shù) p */
rsa_random_prime(&prv->p, state, pbits);
retry:
fprintf(stderr, "Generating q: ");
/* 生成隨機(jī)質(zhì)數(shù) q */
rsa_random_prime(&prv->q, state, qbits);
/* 判斷是否 p == q,如果是返回重新生成 */
ret = mpz_cmp(&prv->p, &prv->q);
if (ret == 0)
{
fprintf(stderr,
"Generated the same prime twice!\n");
goto retry;
}
if (ret > 0)
{
mpz_set(&aux, &prv->p);
mpz_set(&prv->p, &prv->q);
mpz_set(&prv->q, &aux);
}
/* 確定 p、q 是否很接近 */
mpz_sub(&aux, &prv->q, &prv->p);
mpz_div_2exp(&test, &prv->q, 10);
if (mpz_cmp(&aux, &test) < 0)
{
fprintf(stderr,
"The primes are too close together.\n");
goto retry;
}
/* Make certain p and q are relatively prime (in case
one or both were false positives... Though this is
quite impossible). */
mpz_gcd(&aux, &prv->p, &prv->q);
if (mpz_cmp_ui(&aux, 1) != 0)
{
fprintf(stderr,
"The primes are not relatively prime!\n");
goto retry;
}
/* 從質(zhì)數(shù) p、q 導(dǎo)出私鑰 */
fprintf(stderr, "Computing the keys...\n");
derive_rsa_keys(&prv->n, &prv->e, &prv->d,
&prv->u, &prv->p, &prv->q, 5);
prv->bits = bits;
/* 從質(zhì)數(shù) p、q 導(dǎo)出公鑰 */
pub->bits = bits;
mpz_init_set(&pub->n, &prv->n);
mpz_init_set(&pub->e, &prv->e);
/* 測試公鑰和密鑰是否有效 */
fprintf(stderr, "Testing the keys...\n");
rsa_random_integer(&test, state, bits);
mpz_mod(&test, &test, &pub->n); /* must be less than n. */
rsa_private(&aux, &test, prv);
rsa_public(&aux, &aux, pub);
if (mpz_cmp(&aux, &test) != 0)
{
fprintf(stderr,
"**** private+public failed to decrypt.\n");
goto retry0;
}
rsa_public(&aux, &test, pub);
rsa_private(&aux, &aux, prv);
if (mpz_cmp(&aux, &test) != 0)
{
fprintf(stderr,
"**** public+private failed to decrypt.\n");
goto retry0;
}
mpz_clear(&aux);
mpz_clear(&test);
fprintf(stderr, "Key generation complete.\n");
}
_______________________________________________________
在上面的函數(shù)成一對密鑰時,首先調(diào)用函數(shù)
_______________________________________________________
void rsa_random_prime
(
MP_INT *ret, RandomState *state,
unsigned int bits
)
{
MP_INT start, aux;
unsigned int num_primes;
int *moduli;
long difference;
mpz_init(&start);
mpz_init(&aux);
retry:
/* 挑出一個隨機(jī)的足夠大的整數(shù) */
rsa_random_integer(&start, state, bits);
/* 設(shè)置最高的兩位 */
mpz_set_ui(&aux, 3);
mpz_mul_2exp(&aux, &aux, bits - 2);
mpz_ior(&start, &start, &aux);
/* 設(shè)置最低的兩位為奇數(shù) */
mpz_set_ui(&aux, 1);
mpz_ior(&start, &start, &aux);
/* 啟動小質(zhì)數(shù)的 moduli 數(shù) */
moduli = malloc(MAX_PRIMES_IN_TABLE * sizeof(moduli[0]));
if (moduli == NULL)
{
printf(stderr, "Cann't get memory for moduli\n");
exit(1);
}
if (bits < 16)
num_primes = 0;
/* Don\'t use the table for very small numbers. */
else
{
for (num_primes = 0;
small_primes[num_primes] != 0; num_primes++)
{
mpz_mod_ui(&aux, &start, small_primes[num_primes]);
moduli[num_primes] = mpz_get_ui(&aux);
}
}
/* 尋找一個數(shù),它不能被小質(zhì)數(shù)整除 */
for (difference = 0; ; difference += 2)
{
unsigned int i;
if (difference > 0x70000000)
{
fprintf(stderr, "rsa_random_prime: "
"failed to find a prime, retrying.\n");
if (moduli != NULL)
free(moduli);
else
exit(1);
goto retry;
}
/* 檢查它是否是小質(zhì)數(shù)的乘積 */
for (i = 0; i < num_primes; i++)
{
while (moduli[i] + difference >= small_primes[i])
moduli[i] -= small_primes[i];
if (moduli[i] + difference == 0)
break;
}
if (i < num_primes)
continue; /* Multiple of a known prime. */
/* 檢查通過 */
fprintf(stderr, ".");
/* Compute the number in question. */
mpz_add_ui(ret, &start, difference);
/* Perform the fermat test for witness 2.
This means: it is not prime if 2^n mod n != 2. */
mpz_set_ui(&aux, 2);
mpz_powm(&aux, &aux, ret, ret);
if (mpz_cmp_ui(&aux, 2) == 0)
{
/* Passed the fermat test for witness 2. */
fprintf(stderr, "+");
/* Perform a more tests. These are probably unnecessary. */
if (mpz_probab_prime_p(ret, 20))
break; /* It is a prime with probability 1 - 2^-40. */
}
}
/* Found a (probable) prime. It is in ret. */
fprintf(stderr, "+ (distance %ld)\n", difference);
/* Free the small prime moduli; they are no longer needed. */
if (moduli != NULL)
free(moduli);
else
exit(1);
/* Sanity check: does it still have the high bit set (we might have
wrapped around)? */
mpz_div_2exp(&aux, ret, bits - 1);
if (mpz_get_ui(&aux) != 1)
{
fprintf(stderr,
"rsa_random_prime: high bit not set, retrying.\n");
goto retry;
}
mpz_clear(&start);
mpz_clear(&aux);
}
_______________________________________________________
隨機(jī)產(chǎn)生一對大質(zhì)數(shù)(p,q)。這對隨機(jī)大質(zhì)數(shù)要符合的條件是p 必須小于 q。然后調(diào)用下面的函數(shù)來生成公鑰和私鑰對的其他組員:
static void derive_rsa_keys
(
MP_INT *n, MP_INT *e, MP_INT *d, MP_INT *u,
MP_INT *p, MP_INT *q,
unsigned int ebits
)
{
MP_INT p_minus_1, q_minus_1, aux, phi, G, F;
assert(mpz_cmp(p, q) < 0);
mpz_init(&p_minus_1);
mpz_init(&q_minus_1);
mpz_init(&aux);
mpz_init(&phi);
mpz_init(&G);
mpz_init(&F);
/* 計算 p-1 和 q-1. */
mpz_sub_ui(&p_minus_1, p, 1);
mpz_sub_ui(&q_minus_1, q, 1);
/* phi = (p - 1) * (q - 1) */
mpz_mul(&phi, &p_minus_1, &q_minus_1);
/* G is the number of "spare key sets" for a given
modulus n. The smaller G is, the better. The
smallest G can get is 2. */
mpz_gcd(&G, &p_minus_1, &q_minus_1);
if (mpz_cmp_ui(&G, 100) >= 0)
{
fprintf(stderr, "Warning: G=");
mpz_out_str(stdout, 10, &G);
fprintf(stderr,
" is large (many spare key sets); key may be bad!\n");
}
/* F = phi / G; the number of relative prime
numbers per spare key set. */
mpz_div(&F, &phi, &G);
/* Find a suitable e (the public exponent). */
mpz_set_ui(e, 1);
mpz_mul_2exp(e, e, ebits);
mpz_sub_ui(e, e, 1); /*make lowest bit 1, and substract 2.*/
/* Keep adding 2 until it is relatively prime
to (p-1)(q-1). */
do
{
mpz_add_ui(e, e, 2);
mpz_gcd(&aux, e, &phi);
}
while (mpz_cmp_ui(&aux, 1) != 0);
/* d is the multiplicative inverse of e, mod F.
Could also be mod (p-1)(q-1); however, we try to
choose the smallest possible d. */
mpz_mod_inverse(d, e, &F);
/* u is the multiplicative inverse of p, mod q,
if p < q. It is used when doing private key
RSA operations using the chinese remainder
theorem method. */
mpz_mod_inverse(u, p, q);
/* n = p * q (the public modulus). */
mpz_mul(n, p, q);
/* Clear auxiliary variables. */
mpz_clear(&p_minus_1);
mpz_clear(&q_minus_1);
mpz_clear(&aux);
mpz_clear(&phi);
mpz_clear(&G);
mpz_clear(&F);
}
_______________________________________________________
最后為檢驗所生成的一對密鑰的有效性,它調(diào)用下面的函數(shù)產(chǎn)生一個隨機(jī)整數(shù)。
_______________________________________________________
void rsa_random_integer(MP_INT *ret, RandomState *state,
unsigned int bits)
{
unsigned int bytes = (bits + 7) / 8;
char *str = xmalloc(bytes * 2 + 1);
unsigned int i;
/* 生成一個適當(dāng)大小的16進(jìn)制隨機(jī)數(shù),把它轉(zhuǎn)化成mp型整數(shù) */
for (i = 0; i < bytes; i++)
sprintf(str + 2 * i, "%02x", random_get_byte(state));
/* 轉(zhuǎn)化到內(nèi)部表示 */
if (mpz_set_str(ret, str, 16) < 0)
{
fprintf("Intenal error, mpz_set_str returned error");
exit(1);
}
/* Clear extra data. */
memset(str, 0, 2 * bytes);
if (str != NULL)
free(str);
else
exit(1);
/* Reduce it to the desired number of bits. */
mpz_mod_2exp(ret, ret, bits);
}
_______________________________________________________
服務(wù)密鑰生成后,服務(wù)器發(fā)送一個包把兩把密鑰發(fā)送給客戶端,一個是主機(jī)密鑰的公鑰,另一個是服務(wù)密鑰的公鑰。跟隨這個包一起發(fā)送的還有服務(wù)器支持的加密類型和8個字節(jié)即64位的隨機(jī)字串 cookie。客戶端依據(jù)這兩把密鑰計算會話號,會話號長16字節(jié)即128位。計算方法是:
會話號 = MD5(主機(jī)公鑰模數(shù) n || 服務(wù)公鑰模數(shù) n || cookie)
計算函數(shù)是:
void compute_session_id
(
unsigned char session_id[16],
unsigned char cookie[8],
unsigned int host_key_bits,
MP_INT *host_key_n,
unsigned int session_key_bits,
MP_INT *session_key_n
)
{
unsigned int bytes = (host_key_bits + 7) / 8 +
(session_key_bits + 7) / 8 + 8;
unsigned char *buf = xmalloc(bytes);
struct MD5Context md;
mp_linearize_msb_first(buf, (host_key_bits + 7 ) / 8, host_key_n);
mp_linearize_msb_first(buf + (host_key_bits + 7 ) / 8,
(session_key_bits + 7) / 8, session_key_n);
memcpy(buf + (host_key_bits + 7) / 8 + (session_key_bits + 7) / 8,
cookie, 8);
MD5Init(&md);
MD5Update(&md, buf, bytes);
MD5Final(session_id, &md);
xfree(buf);
}
void mp_linearize_msb_first
(
unsigned char *buf, unsigned int len,
MP_INT *value
)
{
unsigned int i;
MP_INT aux;
mpz_init_set(&aux, value);
for (i = len; i >= 4; i -= 4)
{
unsigned long limb = mpz_get_ui(&aux);
PUT_32BIT(buf + i - 4, limb);
mpz_div_2exp(&aux, &aux, 32);
}
for (; i > 0; i--)
{
buf[i - 1] = mpz_get_ui(&aux);
mpz_div_2exp(&aux, &aux, 8);
}
mpz_clear(&aux);
}
隨后客戶端計算會話密鑰,計算過程是首先生成32個字節(jié)即256位隨機(jī)字串:
for (i = 0; i < 32; i++)
session_key[i] = random_get_byte(state);
然后用16字節(jié)的會話號 xor 這32字的隨機(jī)字串的前16字節(jié),并安 msb 次序來排列構(gòu)成一個MP型整數(shù):
mpz_init_set_ui(&key, 0);
for (i = 0; i < 32; i++)
{
mpz_mul_2exp(&key, &key, 8);
if (i < 16)
mpz_add_ui(&key,&key, session_key[i]^session_id[i]);
else
mpz_add_ui(&key,&key, session_key[i]);
}
把結(jié)果發(fā)給服務(wù)器。在用服務(wù)器發(fā)來主機(jī)公鑰和服務(wù)公鑰對這個MP型整數(shù)作兩次 RSA 加密后,客戶端發(fā)一個包把這個MP型整數(shù)交給服務(wù)器。跟隨這個包一起還有客戶端選定的加密類型。注意,在客戶端,它用上面最初的32字節(jié)隨機(jī)串 session_key 來作為會話密鑰進(jìn)行加密,而不是發(fā)給服務(wù)器的會話密鑰 key。服務(wù)器接到上面MP型整數(shù)后,把它轉(zhuǎn)換成32字節(jié)即256位的字串。再用自己計算出的16字節(jié)的會話號xor 這個字串的前16字節(jié),把結(jié)果作為會話密鑰。服務(wù)器計算自己的16字節(jié)會話號時也是把發(fā)給客戶端的主機(jī)公鑰、服務(wù)公鑰、和16字節(jié)隨機(jī)串 cookie 作為輸入,因此它計算出的會話號與客戶端計算出的一樣。
在這之后,所有的數(shù)據(jù)傳輸都用選用客戶端指定的加密方法進(jìn)行加密了,加密時使用上面的會話密鑰。加密使用的代碼在 arcfour.c、des.c、idea.c、blowfish.c 中。
ssh 聲稱避免了 IP 欺騙,使用的方法在上面的密鑰交換中服務(wù)器給客戶端發(fā)了一個64位 cookie,要求客戶端原樣拷貝送回。看不出這能避免 IP 欺騙。
第三部分:認(rèn)證
RSA公鑰和RSA私鑰數(shù)據(jù)結(jié)構(gòu)為:
typedef struct
{
unsigned int bits; /* 模數(shù)大小 */
MP_INT e; /* 公鑰指數(shù) */
MP_INT n; /* 模數(shù) */
} RSAPublicKey;
typedef struct
{
unsigned int bits; /* 模數(shù)大小 */
MP_INT n; /* 模數(shù) */
MP_INT e; /* 公鑰指數(shù) */
MP_INT d; /* 私鑰指數(shù) */
MP_INT u; /* Multiplicative inverse of p mod q. */
MP_INT p; /* 質(zhì)數(shù) p */
MP_INT q; /* 質(zhì)數(shù) q */
} RSAPrivateKey;
RSA 認(rèn)證的過程是,客戶端向服務(wù)器提交自己 RSA公鑰的模數(shù)成員,服務(wù)器先讀取用戶 .ssh 目錄中的公鑰文件進(jìn)行有效性檢驗,再生成一個 256 位二進(jìn)制隨機(jī)數(shù) cookie。隨后把這個隨機(jī)數(shù) cookie 用從公鑰文件讀出的公鑰加密后傳給客戶端,客戶端接到 cookie 后,先用自己的私鑰解密,再對這個 cookie 和會話號計算出 16 字節(jié)的 md5水印,把兩個水印相加后發(fā)給服務(wù)器。服務(wù)器把它收到 md5水印和它自己對 cookie 和會話號計算出的水印和進(jìn)行比較,如果相等,則認(rèn)證通過。
第四部分:shell 和 X11 調(diào)用
ssh 提供的一個重要功能就是 X 轉(zhuǎn)發(fā)功能,它可以在客戶端的顯示屏上把服務(wù)器端 X 程序的運行結(jié)果以圖形形式顯示出來顯示在客戶端的顯示屏幕上。例如運行 xterm 程序啟動一個 X 終端,該 X 終端窗口顯示在客戶端的顯示屏上。
先來看看 X 窗口系統(tǒng)本身的情況。X 窗口系統(tǒng)是 UNIX的圖形用戶界面(GUI),它采用"客戶/服務(wù)器"模式,二者之間的通訊遵從 X 協(xié)議。每臺主機(jī)運行一個 X 服務(wù)器,且只能運行一個 X 服務(wù)器,但一個 X 服務(wù)器可以控制多個顯示屏幕(顯示器)。應(yīng)用程序要想進(jìn)行圖形顯示必須以客戶的方式向 X 服務(wù)器提交顯示請求,由 X 服務(wù)器統(tǒng)一控制進(jìn)行顯示。用戶運行 X 程序時,實際是調(diào)用 XOpenDisplay 庫函數(shù)打開一個 PF_UNIX 或 TCP socket 連接到 X 服務(wù)器,然后通過這個連接向它提交顯示請求。連接建立后, X 客戶所做的第一件事就是:按用戶的 $DISPLAY 環(huán)境變量的值讀取用戶配置文件 .Xauthority 中的顯示記錄,把這條記錄的有關(guān)內(nèi)容提交給 X 服務(wù)器進(jìn)行認(rèn)證。如果認(rèn)證通過,就可以提交顯示請求了,這個過程稱為打開一個 X 顯示。作為客戶的 X 程序在提交顯示請求時,實際上是把 X 顯示數(shù)據(jù)寫入上面打開的 socket。在打開 X 顯示時,必須提供協(xié)議號、認(rèn)證鑰(hexkey)、和屏幕號,如果 X 服務(wù)器不是在本地運行,還需要提供運行 X 服務(wù)器的遠(yuǎn)程主機(jī)名。這些都記錄在用戶配置文件 .Xauthority 中,所給的協(xié)議號、認(rèn)證鑰、和屏幕號從這個列表中取出。可以用 xauth 命令來查看顯示列表里的內(nèi)容:
[wangdb@ /home/wangdb]> /usr/openwin/bin/xauth list
***.***.***/unix:10 MIT-MAGIC-COOKIE-1 92b404e556588ced6c1acd4ebf053f68
***.***.***/unix:11 MIT-MAGIC-COOKIE-1 92b404e556588ced6c1acd4ebf053f68
***.***.***:10 MIT-MAGIC-COOKIE-1 92b404e556588ced6c1acd4ebf053f68
***.***.***/unix:10 MIT-MAGIC-COOKIE-1 92b404e556588ced6c1acd4ebf053f68
***.***.***:11 MIT-MAGIC-COOKIE-1 92b404e556588ced6c1acd4ebf053f68
***.***.***/unix:11 MIT-MAGIC-COOKIE-1 92b404e556588ced6c1acd4ebf053f68
[wangdb@ /home/wangdb]> echo $DISPLAY
***.***.***:10.0
[wangdb@ /home/wangdb]> /usr/openwin/bin/xauth
Using authority file /home/wangdb/.Xauthority
xauth> list ***.***.***:10.0
***.***.***:10 MIT-MAGIC-COOKIE-1 92b404e556588ced6c1acd4ebf053f68
xauth> quit
[wangdb@ /home/wangdb]>
.Xauthority 文件的顯示記錄里各個字段的含義如下,第一個字段的***.***.*** 是主機(jī)名,":"號后的"."前面的數(shù)字是 X 服務(wù)器標(biāo)號,"."后面的數(shù)字是顯示屏幕(顯示器)標(biāo)號。這個字段稱為顯示名,$DISPLAY 環(huán)境變量里填入這個字段。第二個字段是協(xié)議標(biāo)號,第三個字段是十六進(jìn)制的認(rèn)證鑰。認(rèn)證鑰是由系統(tǒng)給的,打開 X 顯示時如果認(rèn)證鑰給的不對,X 服務(wù)器拒絕處理顯示請求。
ssh 實現(xiàn) X 轉(zhuǎn)發(fā)的第一步是,客戶端調(diào)用 popen 函數(shù)執(zhí)行 "xauth list $DISPLAY" 命令,讀取 X 顯示的屏幕號、協(xié)議號、和認(rèn)證鑰,然后把協(xié)議號和認(rèn)證鑰保存在內(nèi)存中。客戶端并不把自己的認(rèn)證鑰發(fā)送給服務(wù)器,而是生成一個 8位二進(jìn)制隨機(jī)數(shù)序列,以十六進(jìn)制打印,把這個十六進(jìn)制數(shù)字串發(fā)送給服務(wù)器作為認(rèn)證鑰。等到服務(wù)器發(fā)來打開 X 顯示請求時,客戶端使用自己真正的認(rèn)證鑰打開 X 顯示。采用這種方法,客戶保證了自己的認(rèn)證鑰不會泄露給外界,安全性得到保證。
服務(wù)器接到客戶端的 X 轉(zhuǎn)發(fā)請求后,讀取客戶端發(fā)來的屏幕號、協(xié)議號、和認(rèn)證鑰,然后打開一個 socket 并綁定它,設(shè)置成偵聽模式,并用這個 socket 設(shè)置一個通道。隨后就從服務(wù)器自己的配置文件讀出 X 服務(wù)器標(biāo)號,調(diào)用gethostname函數(shù)獲取本機(jī)主機(jī)名,把這兩者和客戶發(fā)來的屏幕號結(jié)合在一起構(gòu)成顯示列表記錄的第一字段。
在服務(wù)器處理客戶端執(zhí)行命令或啟動 shell 的請求時,它用前面設(shè)置的通道接受一個 TCP 連接,返回一個 socket,再用這個 socket 設(shè)置一個新通道。然后發(fā)一個包給客戶端要求它打開一個 X 顯示。客戶端接到這個包后打開一個socket 與本地 X 服務(wù)器連接,即打開一個 X 顯示:
_____________________________________________________
int display_number, sock;
const char *display;
struct sockaddr_un ssun;
/* Try to open a socket for the local X server. */
display = getenv("DISPLAY");
if (!display)
{
error("DISPLAY not set.");
goto fail;
}
/* Now we decode the value of the DISPLAY variable
* and make a connection to the real X server.
*/
/* Check if it is a unix domain socket. Unix domain
* displays are in one of the following formats:
* unix:d[.s], :d[.s], ::d[.s]
*/
if (strncmp(display, "unix:", 5) == 0 ||
display[0] == ':')
{
/* Connect to the unix domain socket. */
if (sscanf(strrchr(display, ':') + 1,
"%d", &display_number) != 1)
{
error("Could not parse display number "
"from DISPLAY: %.100s", display);
goto fail;
}
/* Create a socket. */
sock = socket(AF_UNIX, SOCK_STREAM, 0);
if (sock < 0)
{
error("socket: %.100s", strerror(errno));
goto fail;
}
/* Connect it to the display socket. */
ssun.sun_family = AF_UNIX;
#ifdef HPSUX_NONSTANDARD_X11_KLUDGE
{
/* HPSUX release 10.X uses
* /var/spool/sockets/X11/0
* for the unix-domain sockets, while earlier
* releases stores the socket in
* /usr/spool/sockets/X11/0
* with soft-link from
* /tmp/.X11-unix/`uname -n`0
*/
struct stat st;
if (stat("/var/spool/sockets/X11", &st) == 0)
{
sprintf(ssun.sun_path, "%s/%d",
"/var/spool/sockets/X11", display_number);
}
else
{
if (stat("/usr/spool/sockets/X11", &st) == 0)
{
sprintf(ssun.sun_path, "%s/%d",
"/usr/spool/sockets/X11", display_number);
}
else
{
struct utsname utsbuf;
/* HPSUX stores unix-domain sockets in
* /tmp/.X11-unix/`hostname`0
* instead of the normal /tmp/.X11-unix/X0.
*/
if (uname(&utsbuf) < 0)
fatal("uname: %.100s", strerror(errno));
sprintf(ssun.sun_path, "%.20s/%.64s%d",
X11_DIR, utsbuf.nodename, display_number);
}
}
}
#else /* HPSUX_NONSTANDARD_X11_KLUDGE */
{
struct stat st;
if (stat("/var/X", &st) == 0)
{
sprintf(ssun.sun_path, "%.80s/X%d",
"/var/X/.X11-unix", display_number);
}
else if (stat(X11_DIR, &st) == 0)
{
sprintf(ssun.sun_path, "%.80s/X%d",
X11_DIR, display_number);
}
else
{
sprintf(ssun.sun_path, "%.80s/X%d",
"/tmp/.X11-unix", display_number);
}
}
#endif /* HPSUX_NONSTANDARD_X11_KLUDGE */
if (connect(sock, (struct sockaddr *)&ssun,
AF_UNIX_SIZE(ssun)) < 0)
{
error("connect %.100s: %.100s",
ssun.sun_path, strerror(errno));
close(sock);
goto fail;
}
/* OK, we now have a connection to the display. */
goto success;
}
success:
/* We have successfully obtained a connection to
* the real X display.
*/
#if defined(O_NONBLOCK) && !defined(O_NONBLOCK_BROKEN)
(void)fcntl(sock, F_SETFL, O_NONBLOCK);
#else /* O_NONBLOCK && !O_NONBLOCK_BROKEN */
(void)fcntl(sock, F_SETFL, O_NDELAY);
#endif /* O_NONBLOCK && !O_NONBLOCK_BROKEN */
______________________________________________________
隨后客戶端用這個 socket 設(shè)置一個新通道。注意,如果客戶端主機(jī)的本地沒有終端顯示器,在這一步,它也按自己的環(huán)境變量 $DISPLAY 的值,打開一個 TCP socket 與遠(yuǎn)程 X服務(wù)器連接。
最后服務(wù)器把前面已經(jīng)構(gòu)造出的顯示列表記錄第一字段和客戶端發(fā)送來的協(xié)議號與認(rèn)證鑰結(jié)合在一起構(gòu)成一條顯示記錄,置入用戶的.Xauthority 文件中。并把 $DIAPLAY 環(huán)境變量的值設(shè)置為這條記錄第一個字段的顯示名。
做了這些之后,就可以進(jìn)行 X 轉(zhuǎn)發(fā)了。服務(wù)器運行 X程序時使用這個虛擬的 X 顯示提交圖形顯示請求,把圖形顯示數(shù)據(jù)寫入這個虛擬的 X 顯示,也即寫入上面新建的通道發(fā)給客戶端。客戶端取得這些數(shù)據(jù)后再把它寫入自己剛剛建立的與 X 服務(wù)器連接的通道,也即向 X 服務(wù)器提交顯示請求。
為什么客戶端不直接把自己 .Xauthority 文件中一條顯示配置記錄交給服務(wù)器,由服務(wù)器按這條記錄直接打開 TCPsocket 與客戶端的 X 建立連接呢?ssh 的安全性也就在這里,如果這樣做,就把等于把自己的 X 服務(wù)器完全奉送給外界來使用,而 X 服務(wù)器本身又是問題多多的。前面?zhèn)卧煲粋€認(rèn)證鑰也是出于這個考慮,因為如果知道了認(rèn)證鑰,顯示記錄里別的幾個字段是很容易猜出的。
盡管做了這些,還是存在問題的。如果一個攻擊者侵入或掌握著 ssh 服務(wù)器運行的主機(jī),那么他/她發(fā)現(xiàn)一個 ssh連接并進(jìn)行 X 轉(zhuǎn)發(fā)服務(wù)時,設(shè)法獲取連接者的 $DISPLAY 環(huán)境變量值,再執(zhí)行一下 "xauth value_of_$DISPLAY" 命令,就得到顯示記錄了。隨后他/她用 "xauth add" 命令把這條記錄加入自己的 .Xauthority 文件中,再把自己的$DISPLAY環(huán)境變量設(shè)置成這條記錄的顯示名。這樣他/她就可以在 X轉(zhuǎn)發(fā)連接期間運行 X 程序,X 程序的顯示請求全部提交給客戶端的 X 服務(wù)器了。如果 X 服務(wù)器有什么漏洞的話,他/她可以自由運用了。