1. 程式人生 > >SSL握手通訊詳解及linux下c/c++ SSL Socket(另附SSL雙向認證客戶端程式碼)

SSL握手通訊詳解及linux下c/c++ SSL Socket(另附SSL雙向認證客戶端程式碼)

SSL(Secure Sockets Layer 安全套接層),及其繼任者傳輸層安全(Transport Layer Security,TLS)是為網路通訊提供安全及資料完整性的一種安全協議。TLS與SSL在傳輸層對網路連線進行加密。

  安全證書既包含了用於加密資料的金鑰,又包含了用於證實身份的數字簽名。安全證書採用公鑰加密技術。公鑰加密是指使用一對非對稱的金鑰進行加密或解密。每一對金鑰由公鑰和私鑰組成。公鑰被廣泛釋出。私鑰是隱祕的,不公開。用公鑰加密的資料只能夠被私鑰解密。反過來,使用私鑰加密的資料只能被公鑰解密。這個非對稱的特性使得公鑰加密很有用。在安全證書中包含著一對非對稱的金鑰。只有安全證書的所有者才知道私鑰。當通訊方A將自己的安全證書傳送給通訊方B時,實際上發給通訊方B的是公開金鑰,接著通訊方B可以向通訊方A傳送用公鑰加密的資料,只有通訊方A才能使用私鑰對資料進行解密,從而獲得通訊方B傳送的原始資料。安全證書中的數字簽名部分是通訊方A的電子身份證。數字簽名告訴通訊方B該資訊確實由通訊方A發出,不是偽造的,也沒有被篡改。

  客戶與伺服器通訊時,首先要進行SSL握手,SSL握手主要完成以下任務:

  1)協商使用的加密套件。加密套件中包括一組加密引數,這些引數指定了加密演算法和金鑰的長度等資訊。

  2)驗證對方的身份,此操作是可選的。

  3)確定使用的加密演算法。

  4)SSL握手過程採用非對稱加密方法傳遞資料,由此來建立一個安全的SSL會話。SSL握手完成後,通訊雙方將採用對稱加密方法傳遞實際的應用資料。

   以下是SSL握手的具體流程:

(1)客戶將自己的SSL版本號、加密引數、與SSL會話有關的資料及其他一些必要資訊傳送到伺服器。

(2)伺服器將自己的SSL版本號、加密引數、與SSL會話有關的資料及其他一些必要資訊傳送給客戶,同時發給客戶的還有伺服器的證書。如果伺服器需要驗證客戶身份,伺服器還會發出要求客戶提供安全證書的請求。

(3)客戶端驗證伺服器證書,如果驗證失敗,就提示不能建立SSL連線。如果成功,那麼繼續下一步驟。

(4)客戶端為本次SSL會話生成預備主密碼(pre-master secret),並將其用伺服器公鑰加密後傳送給伺服器。

(5)如果伺服器要求驗證客戶身份,客戶端還要對另外一些資料簽名後,將其與客戶端證書一起傳送給伺服器。

(6)如果伺服器要求驗證客戶身份,則檢查簽署客戶證書的CA(Certificate Authority,證書機構)是否可信。如果不在信任列表中,結束本次會話。如果檢查通過,伺服器用自己的私鑰解密收到的預備主密碼(pre-master secret),並用它通過某些演算法生成本次會話的主密碼(master secret)。

(7)客戶端與伺服器端均使用此主密碼(master secret)生成此次會話的會話金鑰(對稱金鑰)。在雙方SSL握手結束後傳遞任何訊息均使用此會話金鑰。這樣做的主要原因是對稱加密比非對稱加密的運算量要低一個數量級以上,能夠顯著提高雙方會話時的運算速度。

(8)客戶端通知伺服器此後傳送的訊息都使用這個會話金鑰進行加密,並通知伺服器客戶端已經完成本次SSL握手。

(9)伺服器通知客戶端此後傳送的訊息都使用這個會話金鑰進行加密,並通知客戶端伺服器已經完成本次SSL握手。

(10)本次握手過程結束,SSL會話已經建立。在接下來的會話過程中,雙方使用同一個會話金鑰分別對傳送和接收的資訊進行加密和解密。

以下為 linux c/c++ SSL socket Client和Server的程式碼參考。

/*File:client.c
 *Auth:sjin
 *Date:2014-03-11
 *
 */


#include <stdio.h>
#include <string.h>
#include <errno.h>
#include <sys/socket.h>
#include <resolv.h>
#include <stdlib.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <openssl/ssl.h>
#include <openssl/err.h>

#define MAXBUF 1024

void ShowCerts(SSL * ssl)
{
  X509 *cert;
  char *line;

  cert = SSL_get_peer_certificate(ssl);
  if (cert != NULL) {
    printf("Digital certificate information:\n");
    line = X509_NAME_oneline(X509_get_subject_name(cert), 0, 0);
    printf("Certificate: %s\n", line);
    free(line);
    line = X509_NAME_oneline(X509_get_issuer_name(cert), 0, 0);
    printf("Issuer: %s\n", line);
    free(line);
    X509_free(cert);
  }
  else
    printf("No certificate information!\n");
}

int main(int argc, char **argv)
{
  int i,j,sockfd, len, fd, size;
  char fileName[50],sendFN[20];
  struct sockaddr_in dest;
  char buffer[MAXBUF + 1];
  SSL_CTX *ctx;
  SSL *ssl;

  if (argc != 3)
  {
    printf("Parameter format error! Correct usage is as follows:\n\t\t%s IP Port\n\tSuch as:\t%s 127.0.0.1 80\n", argv[0], argv[0]); exit(0);
  }

  /* SSL 庫初始化 */
  SSL_library_init();
  OpenSSL_add_all_algorithms();
  SSL_load_error_strings();
  ctx = SSL_CTX_new(SSLv23_client_method());
  if (ctx == NULL)
  {
    ERR_print_errors_fp(stdout);
    exit(1);
  }

  /* 建立一個 socket 用於 tcp 通訊 */
  if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0)
  {
    perror("Socket");
    exit(errno);
  }
  printf("socket created\n");

  /* 初始化伺服器端(對方)的地址和埠資訊 */
  bzero(&dest, sizeof(dest));
  dest.sin_family = AF_INET;
  dest.sin_port = htons(atoi(argv[2]));
  if (inet_aton(argv[1], (struct in_addr *) &dest.sin_addr.s_addr) == 0)
  {
    perror(argv[1]);
    exit(errno);
  }
  printf("address created\n");

  /* 連線伺服器 */
  if (connect(sockfd, (struct sockaddr *) &dest, sizeof(dest)) != 0)
  {
    perror("Connect ");
    exit(errno);
  }
  printf("server connected\n\n");

  /* 基於 ctx 產生一個新的 SSL */
  ssl = SSL_new(ctx);
  SSL_set_fd(ssl, sockfd);
  /* 建立 SSL 連線 */
  if (SSL_connect(ssl) == -1)
    ERR_print_errors_fp(stderr);
  else
  {
    printf("Connected with %s encryption\n", SSL_get_cipher(ssl));
    ShowCerts(ssl);
  }

  /* 接收使用者輸入的檔名,並開啟檔案 */
  printf("\nPlease input the filename of you want to load :\n>");
  scanf("%s",fileName);
  if((fd = open(fileName,O_RDONLY,0666))<0)
  {
    perror("open:");
    exit(1);
  }
    
  /* 將使用者輸入的檔名,去掉路徑資訊後,發給伺服器 */
  for(i=0;i<=strlen(fileName);i++)
  {
    if(fileName[i]=='/')
    {
      j=0;
      continue;
    }
    else {sendFN[j]=fileName[i];++j;}
  }
  len = SSL_write(ssl, sendFN, strlen(sendFN));
  if (len < 0)
    printf("'%s'message Send failure !Error code is %d,Error messages are '%s'\n", buffer, errno, strerror(errno));

  /* 迴圈傳送檔案內容到伺服器 */
  bzero(buffer, MAXBUF + 1); 
  while((size=read(fd,buffer,1024)))
  {
    if(size<0)
    {
      perror("read:");
      exit(1);
    }
    else
    {
      len = SSL_write(ssl, buffer, size);
      if (len < 0)
        printf("'%s'message Send failure !Error code is %d,Error messages are '%s'\n", buffer, errno, strerror(errno));
    }
    bzero(buffer, MAXBUF + 1);
  }
  printf("Send complete !\n");

  /* 關閉連線 */
  close(fd);
  SSL_shutdown(ssl);
  SSL_free(ssl);
  close(sockfd);
  SSL_CTX_free(ctx);
  return 0;
}

/*File:server.c
 *Auth:sjin
 *Date:2014-03-11
 *
 */
#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <sys/wait.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <openssl/ssl.h>
#include <openssl/err.h>

#define MAXBUF 1024

int main(int argc, char **argv)
{
  int sockfd, new_fd, fd;
  socklen_t len;
  struct sockaddr_in my_addr, their_addr;
  unsigned int myport, lisnum;
  char buf[MAXBUF + 1];
  char new_fileName[50]="/newfile/";
  SSL_CTX *ctx;
  mode_t mode;
  char pwd[100];
  char* temp;

  /* 在根目錄下建立一個newfile資料夾 */
  mkdir("/newfile",mode);

  if (argv[1])
    myport = atoi(argv[1]);
  else
  {
    myport = 7838;
    argv[2]=argv[3]=NULL;
  }

  if (argv[2])
    lisnum = atoi(argv[2]);
  else
  {
    lisnum = 2;
    argv[3]=NULL;
  }

  /* SSL 庫初始化 */
  SSL_library_init();
  /* 載入所有 SSL 演算法 */
  OpenSSL_add_all_algorithms();
  /* 載入所有 SSL 錯誤訊息 */
  SSL_load_error_strings();
  /* 以 SSL V2 和 V3 標準相容方式產生一個 SSL_CTX ,即 SSL Content Text */
  ctx = SSL_CTX_new(SSLv23_server_method());
  /* 也可以用 SSLv2_server_method() 或 SSLv3_server_method() 單獨表示 V2 或 V3標準 */
  if (ctx == NULL)
  {
    ERR_print_errors_fp(stdout);
    exit(1);
  }
  /* 載入使用者的數字證書, 此證書用來發送給客戶端。 證書裡包含有公鑰 */
  getcwd(pwd,100);
  if(strlen(pwd)==1)
    pwd[0]='\0';
  if (SSL_CTX_use_certificate_file(ctx, temp=strcat(pwd,"/cacert.pem"), SSL_FILETYPE_PEM) <= 0)
  {
    ERR_print_errors_fp(stdout);
    exit(1);
  }
  /* 載入使用者私鑰 */
  getcwd(pwd,100);
  if(strlen(pwd)==1)
    pwd[0]='\0';
  if (SSL_CTX_use_PrivateKey_file(ctx, temp=strcat(pwd,"/privkey.pem"), SSL_FILETYPE_PEM) <= 0)
  {
    ERR_print_errors_fp(stdout);
    exit(1);
  }
  /* 檢查使用者私鑰是否正確 */
  if (!SSL_CTX_check_private_key(ctx))
  {
    ERR_print_errors_fp(stdout);
    exit(1);
  }

  /* 開啟一個 socket 監聽 */
  if ((sockfd = socket(PF_INET, SOCK_STREAM, 0)) == -1)
  {
    perror("socket");
    exit(1);
  }
  else
    printf("socket created\n");

  bzero(&my_addr, sizeof(my_addr));
  my_addr.sin_family = PF_INET;
  my_addr.sin_port = htons(myport);
  if (argv[3])
    my_addr.sin_addr.s_addr = inet_addr(argv[3]);
  else
    my_addr.sin_addr.s_addr = INADDR_ANY;

  if (bind(sockfd, (struct sockaddr *) &my_addr, sizeof(struct sockaddr)) == -1)
  {
    perror("bind");
    exit(1);
  }
  else
    printf("binded\n");

  if (listen(sockfd, lisnum) == -1)
  {
    perror("listen");
    exit(1);
  }
  else
    printf("begin listen\n");

  while (1)
  {
    SSL *ssl;
    len = sizeof(struct sockaddr);
    /* 等待客戶端連上來 */
    if ((new_fd = accept(sockfd, (struct sockaddr *) &their_addr, &len)) == -1)
    {
      perror("accept");
      exit(errno);
    }
    else
      printf("server: got connection from %s, port %d, socket %d\n", inet_ntoa(their_addr.sin_addr), ntohs(their_addr.sin_port), new_fd);

    /* 基於 ctx 產生一個新的 SSL */
    ssl = SSL_new(ctx);
    /* 將連線使用者的 socket 加入到 SSL */
    SSL_set_fd(ssl, new_fd);
    /* 建立 SSL 連線 */
    if (SSL_accept(ssl) == -1)
    {
      perror("accept");
      close(new_fd);
      break;
    }

    /* 接受客戶端所傳檔案的檔名並在特定目錄建立空檔案 */
    bzero(buf, MAXBUF + 1);
    bzero(new_fileName+9, 42);
    len = SSL_read(ssl, buf, MAXBUF);
    if(len == 0)
      printf("Receive Complete !\n");
    else if(len < 0)
      printf("Failure to receive message ! Error code is %d,Error messages are '%s'\n", errno, strerror(errno));
    if((fd = open(strcat(new_fileName,buf),O_CREAT | O_TRUNC | O_RDWR,0666))<0)
    {
      perror("open:");
      exit(1);
    }

    /* 接收客戶端的資料並寫入檔案 */
    while(1)
    {
      bzero(buf, MAXBUF + 1);
      len = SSL_read(ssl, buf, MAXBUF);
      if(len == 0)
      {
        printf("Receive Complete !\n");
        break;
      }
      else if(len < 0)
      {
        printf("Failure to receive message ! Error code is %d,Error messages are '%s'\n", errno, strerror(errno));
        exit(1);
      }
      if(write(fd,buf,len)<0)
      {
        perror("write:");
        exit(1);
      }
    }

    /* 關閉檔案 */
    close(fd);
    /* 關閉 SSL 連線 */
    SSL_shutdown(ssl);
    /* 釋放 SSL */
    SSL_free(ssl);
    /* 關閉 socket */
    close(new_fd);
  }

  /* 關閉監聽的 socket */
  close(sockfd);
  /* 釋放 CTX */
  SSL_CTX_free(ctx);
  return 0;
}

Openssl雙向認證客戶端程式碼
openssl是一個功能豐富且自包含的開源安全工具箱。它提供的主要功能有:SSL協議實現(包括SSLv2、SSLv3和TLSv1)、大量軟演算法(對稱/非對稱/摘要)、大數運算、非對稱演算法金鑰生成、ASN.1編解碼庫、證書請求(PKCS10)編解碼、數字證書編解碼、CRL編解碼、OCSP協議、數字證書驗證、PKCS7標準實現和PKCS12個人數字證書格式實現等功能。
本文主要介紹openssl進行客戶端-伺服器雙向驗證的通訊,客戶端應該如何設定。包括瞭如何使用openssl指令生成客戶端-服務端的證書和金鑰,以及使用openssl自帶server端來實現簡單的ssl雙向認證,client端程式碼中也做了相應的標註和說明,提供編譯的Makefile.希望對開始學習如何使用openssl進行安全連線的朋友有益。


1.首先介紹如何生成客戶和服務端證書(PEM)及金鑰。
在linux環境下下載並安裝openssl開發包後,通過openssl命令來建立一個SSL測試環境。
1)建立自己的CA
 在openssl安裝目錄的misc目錄下,執行指令碼:./CA.sh -newca,出現提示符時,直接回車。  執行完畢後會生成一個demonCA的目錄,裡面包含了ca證書及其私鑰。
2)生成客戶端和服務端證書申請:
 openssl  req  -newkey  rsa:1024  -out  req1.pem  -keyout  sslclientkey.pem
openssl  req  -newkey  rsa:1024  -out  req2.pem  -keyout  sslserverkey.pem
3)簽發客戶端和服務端證書
openssl  ca  -in  req1.pem  -out  sslclientcert.pem
openssl  ca  -in  req2.pem  -out  sslservercert.pem
4)執行ssl服務端:
openssl s_server -cert sslservercert.pem -key sslserverkey.pem -CAfile demoCA/cacert.pem -ssl3 
當然我們也可以使用openssl自帶的客戶端:
openssl s_client -ssl3 -CAfile demoCA/cacert.pem
但這裡我們為了介紹client端的設定過程,還是從一下程式碼來看看如何書寫吧。

#include <openssl/ssl.h>
#include <openssl/crypto.h>
#include <openssl/err.h>
#include <openssl/bio.h>
#include <openssl/pkcs12.h>

#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <errno.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>

#define IP "172.22.14.157"
#define PORT 4433
#define CERT_PATH "./sslclientcert.pem"
#define KEY_PATH  "./sslclientkey.pem"
#define CAFILE "./demoCA/cacert.pem"
static SSL_CTX *g_sslctx = NULL;


int connect_to_server(int fd ,char* ip,int port){
	struct sockaddr_in svr;
	memset(&svr,0,sizeof(svr));
	svr.sin_family = AF_INET;
	svr.sin_port = htons(port);
	if(inet_pton(AF_INET,ip,&svr.sin_addr) <= 0){
		printf("invalid ip address!\n");
		return -1;
	}
	if(connect(fd,(struct sockaddr *)&svr,sizeof(svr))){
		printf("connect error : %s\n",strerror(errno));
		return -1;
	}
	
	return 0;
}

//客戶端證書內容輸出
void print_client_cert(char* path)
{
	X509 *cert =NULL;
	FILE *fp = NULL;
	fp = fopen(path,"rb");
	//從證書檔案中讀取證書到x509結構中,passwd為1111,此為生成證書時設定的
	cert = PEM_read_X509(fp, NULL, NULL, "1111");
	X509_NAME *name=NULL;
	char buf[8192]={0};
	BIO *bio_cert = NULL;
	//證書持有者資訊
	name = X509_get_subject_name(cert);
	X509_NAME_oneline(name,buf,8191);
	printf("ClientSubjectName:%s\n",buf);
	memset(buf,0,sizeof(buf));
	bio_cert = BIO_new(BIO_s_mem());
	PEM_write_bio_X509(bio_cert, cert);
	//證書內容
	BIO_read( bio_cert, buf, 8191);
	printf("CLIENT CERT:\n%s\n",buf);
	if(bio_cert)BIO_free(bio_cert);
	fclose(fp);
	if(cert) X509_free(cert);
}
//在SSL握手時,驗證服務端證書時會被呼叫,res返回值為1則表示驗證成功,否則為失敗
static int verify_cb(int res, X509_STORE_CTX *xs)
{
	printf("SSL VERIFY RESULT :%d\n",res);
	switch (xs->error)
	{
		case X509_V_ERR_UNABLE_TO_GET_CRL:
			printf(" NOT GET CRL!\n");
			return 1;
		default :
			break;
	}
	return res;
}

int sslctx_init()
{
#if 0
	BIO *bio = NULL;
	X509 *cert = NULL;
	STACK_OF(X509) *ca = NULL;
	EVP_PKEY *pkey =NULL;
	PKCS12* p12 = NULL;
	X509_STORE *store =NULL;
	int error_code =0;
#endif

	int ret =0;
	print_client_cert(CERT_PATH);
	//registers the libssl error strings
	SSL_load_error_strings();
	
	//registers the available SSL/TLS ciphers and digests
	SSL_library_init();
	
	//creates a new SSL_CTX object as framework to establish TLS/SSL
	g_sslctx = SSL_CTX_new(SSLv23_client_method());
	if(g_sslctx == NULL){
		ret = -1;
		goto end;
	}
	
	//passwd is supplied to protect the private key,when you want to read key
	SSL_CTX_set_default_passwd_cb_userdata(g_sslctx,"1111");
	
	//set cipher ,when handshake client will send the cipher list to server
	SSL_CTX_set_cipher_list(g_sslctx,"HIGH:MEDIA:LOW:!DH");
	//SSL_CTX_set_cipher_list(g_sslctx,"AES128-SHA");
	
	//set verify ,when recive the server certificate and verify it
	//and verify_cb function will deal the result of verification
	SSL_CTX_set_verify(g_sslctx, SSL_VERIFY_PEER, verify_cb);
	
	//sets the maximum depth for the certificate chain verification that shall
	//be allowed for ctx
	SSL_CTX_set_verify_depth(g_sslctx, 10);

	//load the certificate for verify server certificate, CA file usually load
	SSL_CTX_load_verify_locations(g_sslctx,CAFILE, NULL);
	
	//load user certificate,this cert will be send to server for server verify
	if(SSL_CTX_use_certificate_file(g_sslctx,CERT_PATH,SSL_FILETYPE_PEM) <= 0){
		printf("certificate file error!\n");
		ret = -1;
		goto end;
	}
	//load user private key
	if(SSL_CTX_use_PrivateKey_file(g_sslctx,KEY_PATH,SSL_FILETYPE_PEM) <= 0){
		printf("privatekey file error!\n");
		ret = -1;
		goto end;
	}
	if(!SSL_CTX_check_private_key(g_sslctx)){
		printf("Check private key failed!\n");
		ret = -1;
		goto end;
	}

end:
	return ret;
}

void sslctx_release()
{
	EVP_cleanup();
	if(g_sslctx){
		SSL_CTX_free(g_sslctx);
	}
	g_sslctx= NULL;
}
//列印服務端證書相關內容
void print_peer_certificate(SSL *ssl)
{
	X509* cert= NULL;
	X509_NAME *name=NULL;
	char buf[8192]={0};
	BIO *bio_cert = NULL;
	//獲取server端證書
	cert = SSL_get_peer_certificate(ssl);
	//獲取證書擁有者資訊
	name = X509_get_subject_name(cert);
	X509_NAME_oneline(name,buf,8191);
	printf("ServerSubjectName:%s\n",buf);
	memset(buf,0,sizeof(buf));
	bio_cert = BIO_new(BIO_s_mem());
	PEM_write_bio_X509(bio_cert, cert);
	BIO_read( bio_cert, buf, 8191);
	//server證書內容
	printf("SERVER CERT:\n%s\n",buf);
	if(bio_cert)BIO_free(bio_cert);
	if(cert)X509_free(cert);
}

int main(int argc, char** argv){
	int fd = -1 ,ret = 0;
	SSL *ssl = NULL;
	char buf[1024] ={0};
	//初始化SSL
	if(sslctx_init()){
		printf("sslctx init failed!\n");
		goto out;
	}
	//客戶端socket建立tcp連線
	fd = socket(AF_INET,SOCK_STREAM,0);
	if(fd < 0){
		printf("socket error:%s\n",strerror(errno));
		goto out;
	}
	
	if(connect_to_server(fd ,IP,PORT)){
		printf("can't connect to server:%s:%d\n",IP,PORT);
		goto out;
	}
	ssl = SSL_new(g_sslctx);
	if(!ssl){
		printf("can't get ssl from ctx!\n");
		goto out;
	}
	SSL_set_fd(ssl,fd);
	//建立與服務端SSL連線
	ret = SSL_connect(ssl);
	if(ret != 1){
		int err = ERR_get_error();
		printf("Connect error code: %d ,string: %s\n",err,ERR_error_string(err,NULL));
		goto out;
	}
	//輸入服務端證書內容
	print_peer_certificate(ssl);

	//SSL_write(ssl,"sslclient test!",strlen("sslclient test!"));
	//SSL_read(ssl,buf,1024);
	//關閉SSL連線
	SSL_shutdown(ssl);


out:
	if(fd >0)close(fd);
	if(ssl != NULL){
		SSL_free(ssl);
		ssl = NULL;
	}
	if(g_sslctx != NULL) sslctx_release();
	return 0;
}


參考資料:

http://www.169it.com/article/3215130236.html