上周幫一個童鞋做一個數字認證的實驗,要求是編程實現一個基於X.509證書認證的過程,唉!可憐我那點薄弱的計算機網絡安全的知識啊!只得惡補一下了。
首先來看看什麼是X.509。所謂X.509其實是一種非常通用的證書,什麼是證書?唉!這麼說吧!當兩個人需要進行遠程通信而又不想讓第三個人知道時就必須建立一種安全措施,因為看不到對方的臉,又不能通過電話直接詢問對方,就得想點別的辦法,比如我設計一個密碼,讓後發短信告訴你,這樣當我們在網上交流之前就可以對一下密碼,暗號之類的。確認後就可以證明你的身份了。這個證書就相當於這裡的密碼,只是確實比密碼要復雜一些,想了解的朋友可以google之~~
那麼什麼是SSL呢?SSL是一種網絡通信的安全協議,在傳輸層對網絡連接進行加密,簡單點說就是提過一種方法使我們通過網絡進行通信的安全性得到提高。而X.509證書認證的過程就可以通過這個協議來實現。
好了,知道了這些就然我們來開始寫這個程序吧!慢著,如果實現這個SSL和X.509呢?我們貌似還是不知道啊!好吧,既然是一種加密通信過程肯定和加密分不開吧,隨便查了一下,SSL涉及到的加密方法就不下數十種,而且極其艱深復雜,難道我要自己實現這個過程?自己構建X.509證書?當然不會,互聯網就是好啊!讓我發現了存在OpenSSL這麼個東西,讓我們感謝Eric A. Young和Tim J. Hudson這兩個人吧!因為他們在1995年就開始做我上面想做的事情了,而且寫出了OpenSSL這個沒有太多限制的開放源代碼的軟件包。有了這玩意什麼事都好辦了。
我先從網上下了openssl-0.9.8的壓縮包,然後琢磨了兩個多小時終於明白如何安裝這麼個軟件包了。我把自己的安裝心得記錄下來大家一起分享。
在使用這個軟件包之前需要安裝PERL組件——ActivePerl-5.8.0.806-MSWin32-x86PERL和OpenSSL網上有很多免費下載的,這裡我就不給出下載地址了),這個安裝倒很簡單,直接使用安裝包就行了。判斷perl是否安裝成功的方法是通過命令行進入到perl安裝目錄下的\eg目錄裡,執行perl example.pl命令,如果顯示“Hello from ActivePerl!”,則說明Perl安裝成功。然後我們開始安裝OpenSSL。
1、將openssl.0.9.8.tar.gz解壓到d:盤下。
2、打開命令行窗口進入d: \openssl-0.9.8目錄下鍵入以下命令:perl Configure VC-WIN32。
3、然後鍵入ms\do_ms命令。
4、然後鍵入nmake –f ms\ntdll.mak會成功編譯很多c文件,如果不成功的話一般就是提示“cl”無法找到之類的錯誤,這時就需要在系統環境變量PATH裡加入VC的bin路徑,這樣才能找到“cl”命令。
5、接著鍵入mkdir c:\caroot,這樣就會在C盤根目錄下生成一個caroot文件夾。
6、然後需要在系統環境變量PATH路徑中加入openssl-0.9.8\out32dll路徑,並在VC我使用的是VC編譯器)中把include路徑增加一個:D:\openssl-0.9.8\include,把library路徑增加一個:D:\openssl-0.9.8\out32dll,將D:\openssl-0.9.8\apps下的openssl.cnf文件拷貝到c:\caroot目錄下。
到這裡可以說我們的安裝已經完成了,下面我們需要生成幾個證書和密鑰,密鑰是用來加密的,證書是用來認證的。比如服務器證書、密鑰,客戶端證書、密鑰。但這裡我們漏了一個最重要的證書和密鑰,就是CA證書和密鑰。CA是個什麼東東呢?這個解釋起來很麻煩,他就像一個中間證明人,證明兩方的身份都是真實可信的,這樣雙方拿著證書才能相互信任,詳細解釋可以參看這裡:http://baike.baidu.com/view/23691.htm#7,維基百科的解釋更加詳細,不過是英文的我覺得喜歡英文的朋友可以看看:http://en.wikipedia.org/wiki/Certificate_authority。
好吧,讓我們把這些該死的證書密鑰都生成然後開始編碼吧:
在上面C:\caroot目錄下其實可以不在這目錄下)輸入如下:
#產生CA自簽名證書
openssl.exe genrsa -out private\ca.key -rand private\.rnd -des 2048
openssl.exe req -new -x509 -days 3650 -key private\ca.key -out private\ca.crt -config openssl.cnf
openssl.exe x509 -in private\ca.crt -noout -text
#產生server的證書過程
openssl.exe genrsa -out private\server.key 1024
openssl.exe req -new -key private\server.key -out newcerts\server.csr -config openssl.cnf
openssl.exe ca -in newcerts\server.csr -cert private\ca.crt -keyfile private\ca.key
-config openssl.cnf -policy policy_anything -out certs\server.crt
openssl.exe x509 -in certs\server.crt -noout -text
#產生proxy的證書過程
openssl.exe genrsa -out private\proxy.key 1024
openssl.exe req -new -key private\proxy.key -out newcerts\proxy.csr -config openssl.cnf
openssl.exe ca -in newcerts\proxy.csr -cert private\ca.crt -keyfile private\ca.key -config openssl.cnf -policy policy_anything -out certs\proxy.crt
openssl.exe x509 -in certs\proxy.crt -noout -text
#產生client的證書過程
openssl.exe genrsa -out private\client.key 1024
openssl.exe req -new -key private\client.key -out newcerts\client.csr -config openssl.cnf
openssl.exe ca -in newcerts\client.csr -cert private\ca.crt -keyfile private\ca.key -config openssl.cnf -policy policy_anything -out certs\client.crt
openssl.exe x509 -in certs\client.crt -noout -text
上面的命令中會給出提示讓用戶輸入一些證書的信息,只要正常輸入就可以了。執行上述操作後C:\caroot目錄如下:
private目錄如下:
newcerts目錄如下:
certs目錄如下:
其中證書:
ca.crt為自簽名證書;
server.crt,server.key為服務器端的證書和私鑰文件;
proxy.crt,proxy.key為代理服務器端的證書和私鑰文件;
client.crt,client.key為客戶端的證書和私鑰文件。
亂七八糟的事情終於做完了,下面總算可以開始編碼了。看了一下實驗要求,要編寫簡單的Client程序和Server程序,實現Client程序與Server程序之間基於X509證書和SSL協議身份認證和通信加密,服務器能夠接收並且顯示客戶端發送來的文字消息。
看來需要兩個程序,一個服務端的,一個客戶端的。
具體編程大家直接看代碼就行了,涉及到socket通信,其實相當簡單,懂一點點就行了,我這裡就不要王婆賣瓜了,直接上代碼吧。
客戶端程序代碼:
- //client
- #include <winsock2.h>
- #include <conio.h>
- #include <stdio.h>
- #include "openssl/x509.h"
- #include "openssl/ssl.h"
- #include "openssl/err.h"
- #include "openssl/rand.h"
- #define PORT 1111
- #define SERVER "127.0.0.1"
- #define CACERT "ca.crt"
- #define MYCERTF "yuliding.crt"
- #define MYKEYF "yuliding.key"
- #define MSGLENGTH 1024
- int main()
- {
- WSADATA wsadata;
- WSAStartup(MAKEWORD(2,2), &wsadata);
- sockaddr_in sin;
- int seed_int[100]; /*存放隨機序列*/
- SSL*ssl;
- SSL_METHOD *meth;
- SSL_CTX *ctx;
- //SSL初始化
- OpenSSL_add_ssl_algorithms();
- //SSL錯誤信息初始化
- SSL_load_error_strings();
- //創建本次會話所使用的協議
- meth = TLSv1_client_method();
- //申請SSL會話的環境
- ctx = SSL_CTX_new(meth);
- if (NULL == ctx)
- exit(1);
- //設置會話的握手方式並加載CA證書
- SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER, NULL);
- SSL_CTX_load_verify_locations(ctx, CACERT, NULL);
- //加載自己的證書
- if (0 >= SSL_CTX_use_certificate_file(ctx, MYCERTF, SSL_FILETYPE_PEM)) {
- ERR_print_errors_fp(stderr);
- exit(1);
- }
- //加載自己的私鑰
- if (0 >= SSL_CTX_use_PrivateKey_file(ctx, MYKEYF, SSL_FILETYPE_PEM)) {
- ERR_print_errors_fp(stderr);
- exit(1);
- }
- //檢查自己的證書和私鑰是否匹配
- if (!SSL_CTX_check_private_key(ctx)) {
- printf("Private key does not match the certificate public key\n");
- exit(1);
- }
- /*構建隨機數生成機制,WIN32平台必需*/
- srand((unsigned)time(NULL));
- for (int i = 0; i < 100; i++)
- seed_int[i] = rand();
- RAND_seed(seed_int, sizeof(seed_int));
- //加密方式
- SSL_CTX_set_cipher_list(ctx, "RC4-MD5");
- //處理握手多次
- SSL_CTX_set_mode(ctx, SSL_MODE_AUTO_RETRY);
- /*以下是正常的TCP socket建立過程 .............................. */
- SOCKET sock;
- printf("Begin tcp socket...\n");
- sock = socket(AF_INET, SOCK_STREAM, 0);
- if (sock == INVALID_SOCKET) {
- printf("SOCKET有問題. \n");
- }
- memset(&sin, '\0', sizeof(sin));
- sin.sin_family = AF_INET;
- sin.sin_addr.s_addr = inet_addr(SERVER); /* Server IP */
- sin.sin_port = htons(PORT); /* Server Port number */
- int icnn = connect(sock, (sockaddr *)&sin, sizeof(sin));
- if (icnn == SOCKET_ERROR) {
- printf("連不上服務器\n", GetLastError());
- exit(1);
- }
- /* TCP 鏈接已建立.開始 SSL 握手過程.......................... */
- //綁定套接字
- ssl = SSL_new(ctx);
- if (NULL == ssl)
- exit(1);
- if (0 >= SSL_set_fd(ssl, sock)) {
- printf("Attach to Line fail!\n");
- exit(1);
- }
- //SSL握手
- //SSL_connect(ssl);
- int k = SSL_connect(ssl);
- if (0 >= k) {
- printf("%d\n", k);
- printf("SSL connect fail!\n");
- exit(1);
- }
- printf("連接服務器成功\n");
- char sendmsg[MSGLENGTH] = "\0";
- char revmsg[MSGLENGTH] = "\0";
- int err = SSL_read(ssl, revmsg, sizeof(revmsg));
- revmsg[err] = '\0';
- printf("%s\n", revmsg);
- while (1) {
- printf("請輸入所要發送的數據:\n");
- scanf("%s", sendmsg);
- SSL_write(ssl, sendmsg, strlen(sendmsg));
- printf("發送消息“ %s ”成功!\n", sendmsg);
- }
- //關閉套接字
- SSL_shutdown(ssl);
- SSL_free(ssl);
- SSL_CTX_free(ctx);
- closesocket(sock);
- WSACleanup();
- getch();
- return 0;
- }
服務端程序代碼:
- //server
- #include <winsock2.h>
- #include <conio.h>
- #include <stdio.h>
- #include <winsock.h>
- #include "openssl/x509.h"
- #include "openssl/ssl.h"
- #include "openssl/err.h"
- #define MSGLENGTH 1024
- #define PORT 1111
- #define CACERT "ca.crt"
- #define SVRCERTF "server.crt"
- #define SVRKEYF "server.key"
- int main()
- {
- WSADATA wsaData;
- WSAStartup(MAKEWORD(2,2), &wsaData);
- SOCKET sock;
- SSL_METHOD *meth;
- SSL_CTX* ctx;
- SSL* ssl;
- //SSL初始化
- OpenSSL_add_ssl_algorithms();
- //SSL錯誤信息初始化
- SSL_load_error_strings();
- //創建本次會話所使用的協議
- meth = TLSv1_server_method();
- //申請SSL會話的環境
- ctx = SSL_CTX_new(meth);
- if (NULL == ctx)
- exit(1);
- //設置會話的握手方式並加載CA證書
- SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER, NULL);
- SSL_CTX_load_verify_locations(ctx, CACERT, NULL);
- //加載服務器端的證書
- if (0 >= SSL_CTX_use_certificate_file(ctx, SVRCERTF, SSL_FILETYPE_PEM)) {
- ERR_print_errors_fp(stderr);
- exit(1);
- }
- //加載服務器端的私鑰
- if (0 >= SSL_CTX_use_PrivateKey_file(ctx, SVRKEYF, SSL_FILETYPE_PEM)) {
- ERR_print_errors_fp(stderr);
- exit(1);
- }
- //檢查服務器端的證書和私鑰是否匹配
- if (!SSL_CTX_check_private_key(ctx)) {
- printf("Private key does not match the certificate public key\n");
- exit(1);
- }
- //加密方式
- SSL_CTX_set_cipher_list(ctx, "RC4-MD5");
- //處理握手多次
- SSL_CTX_set_mode(ctx, SSL_MODE_AUTO_RETRY);
- /*以下是正常的TCP socket建立過程 .............................. */
- printf("Begin tcp socket...\n");
- sock = socket(AF_INET, SOCK_STREAM, 0);
- if (sock == INVALID_SOCKET) {
- printf("SOCKET有問題. \n");
- return 0;
- }
- sockaddr_in addr;
- memset(&addr, '\0', sizeof(addr));
- addr.sin_family = AF_INET;
- addr.sin_port = htons(PORT); /* Server Port number */
- addr.sin_addr.s_addr = INADDR_ANY;
- //綁定sock
- int nResult = bind(sock, (sockaddr *)&addr, sizeof(addr));
- if (nResult == SOCKET_ERROR) {
- printf("綁定SOCKET有問題. \n");
- return 0;
- }
- printf("服務器啟動成功,端口:%d\n正在等待連接\n", PORT);
- /*接受TCP鏈接*/
- sockaddr_in sa_cli;
- int err = listen(sock, 5);
- if (-1 == err)
- exit(1);
- int client_len = sizeof(sa_cli);
- int ss = accept(sock, (struct sockaddr *) &sa_cli, &client_len);
- if (ss == -1) {
- exit(1);
- }
- closesocket(sock);
- printf("Connection from %d, port %d\n", sa_cli.sin_addr.s_addr, sa_cli.sin_port);
- /* TCP 鏈接已建立.開始 SSL 握手過程.......................... */
- //綁定套接字
- ssl = SSL_new(ctx);
- if (NULL == ssl)
- exit(1);
- if (0 >= SSL_set_fd(ssl, ss)) {
- printf("Attach to Line fail!\n");
- exit(1);
- }
- //SSL握手
- //SSL_accept(ssl);
- int k = SSL_accept(ssl);
- if (0 >= k) {
- printf("%d\n", k);
- printf("SSL connect fail!\n");
- exit(1);
- }
- //進行信息驗證
- X509 *client_cert;
- client_cert = SSL_get_peer_certificate(ssl);
- printf("發現客戶端嘗試連接\n");
- if (client_cert != NULL) {
- printf ("Client certificate:\n");
- //讀取證書subject名並顯示
- char *str = X509_NAME_oneline(X509_get_subject_name(client_cert), 0, 0);
- if (NULL == str) {
- printf("認證出錯!\n");
- exit(1);
- }
- printf("subject: %s\n", str);
- //讀取證書的issuer名並顯示
- str = X509_NAME_oneline(X509_get_issuer_name(client_cert), 0, 0);
- if (NULL == str) {
- printf("證書名為空\n");
- exit(1);
- }
- printf("issuer: %s\n", str);
- printf("連接成功\n");
- X509_free (client_cert);/*如不再需要,需將證書釋放 */
- OPENSSL_free(str);
- }
- else {
- printf("找不到客戶端的認證證書\n");
- exit(1);
- }
- char buf[MSGLENGTH];
- SSL_write(ssl, "Server is connect to you!\n", strlen("Server is connect to you!\n"));
- printf("Listen to the client: \n");
- while (1) {
- err = SSL_read(ssl, buf, sizeof(buf));
- buf[err] = '\0';
- printf("%s\n", buf);
- }
- //關閉套接字
- SSL_shutdown(ssl);
- SSL_free(ssl);
- SSL_CTX_free(ctx);
- WSACleanup();
- getch();
- return 0;
- }
編寫完後後首先運行服務端程序,然後運行客戶端程序,效果如下:
連接成功後服務端的狀態如下:
紅色部分為我在生成證書文件時輸入的一些信息,具體的你可以通過我上面講解建立證書時試驗一下。這裡我通過代碼把這些證書信息打印出來是為了判斷獲取的證書是否正確。
連接成功後客戶端的狀態:
這樣,就可以實現通過客戶端向服務端發送信息,如下:
細心的朋友可以發現上面我顯示Connection from後面貌似是一串亂碼,其實不然,那是一串IP地址,只不過是反序排列的,我曾經在做SIP TRUNK項目時也遇到過這個問題,感興趣的朋友可以自己寫個轉義程序把他轉成IP地址試試看。
最後提醒一點,因為我的代碼中把證書和密鑰文件的目錄設為當前目錄了,所以想要程序正確運行必須把證書文件和密鑰文件拷貝到當前工程的目錄下,如下:
客戶端是一樣的。或者你可以把目錄改到你生成證書和密鑰的目錄下。
可以看見,上面的程序有眾多漏洞和缺陷,如果有朋友想使用的話請一定仔細debug一下,把很多判斷的條件加上,這裡我就偷下懶羅~~分享快樂~~
本文出自 “菜鳥浮出水” 博客,請務必保留此出處http://rangercyh.blog.51cto.com/1444712/430652