openssl實現雙向認證教程(服務端代碼+客戶端代碼+證書生成)

1、背景說明

1.1 面臨問題

最近一份產品檢測報告建議使用基於pki的認證方式,因爲產品已實現https,商量之下認爲其意思是使用雙向認證以處理中間人形式攻擊。html

《信息安全工程》中接觸過雙向認證,但有兩個問題。node

第一個是當時最終的課程設計客戶端是瀏覽器,服務端是tomcat雙向認證只須要對二者進行配置並不須要本身真的實現代碼。算法

第二個是雖然課程也有接近雙向認證的實現代碼,但當時是Java+JCE環境如今要用C+++OpenSSL環境,整體意思確實仍是差很少但具體函數和參數差異仍是很多。編程

因此眼下有的是:證書生成的思想+雙向認證明現的思想。對讀者而言,即要假定已對證書、SSL/TSL、socket編程等幾個概念有基本的瞭解,本文不作詳細介紹。ubuntu

基於此本文要解決的問題是:openssl具體如何生成證書+openssl如何實現雙向認證。centos

 

1.2 解決辦法

1.2.1 openssl具體生成證書解決辦法

參考https://blog.csdn.net/gengxiaoming7/article/details/78505107再加其餘一些文章總結而成api

 

1.2.2 openssl實現雙向認證解決辦法

使用https://blog.csdn.net/sardden/article/details/42705897代碼實SSL,在其基礎上實現雙向認證。瀏覽器

雙向認證的關鍵點在如下幾個函數(服務端和客戶端都同樣),其餘就不細說參看代碼註釋:tomcat

SSL_CTX_set_verify----配置啓用雙向認證安全

SSL_CTX_load_verify_locations----加載信任的根證書

SSL_CTX_use_certificate_file----加載本身的證書

SSL_CTX_use_PrivateKey_file----加載本身的私鑰

SSL_get_verify_result----真正進行驗證,必定要調用這個函數否則前面四個光配置而已並不會進行雙向驗證

 

2、雙向認證程序實現

2.1 安裝openssl及開發api

apt-get install libssl-dev

 

2.2 服務端代碼

#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 <openssl/ssl.h>
#include <openssl/err.h>

#define MAXBUF 1024

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

    cert = SSL_get_peer_certificate(ssl);
    // SSL_get_verify_result()是重點,SSL_CTX_set_verify()只是配置啓不啓用並無執行認證,調用該函數纔會真證進行證書認證
    // 若是驗證不經過,那麼程序拋出異常停止鏈接
    if(SSL_get_verify_result(ssl) == X509_V_OK){
        printf("證書驗證經過\n");
    }
    if (cert != NULL) {
        printf("數字證書信息:\n");
        line = X509_NAME_oneline(X509_get_subject_name(cert), 0, 0);
        printf("證書: %s\n", line);
        free(line);
        line = X509_NAME_oneline(X509_get_issuer_name(cert), 0, 0);
        printf("頒發者: %s\n", line);
        free(line);
        X509_free(cert);
    } else
        printf("無證書信息!\n");
}

int main(int argc, char **argv) {
    int sockfd, new_fd;
    socklen_t len;
    struct sockaddr_in my_addr, their_addr;
    unsigned int myport, lisnum;
    char buf[MAXBUF + 1];
    SSL_CTX *ctx;

    if (argv[1])
        myport = atoi(argv[1]);
    else
        myport = 7838;

    if (argv[2])
        lisnum = atoi(argv[2]);
    else
        lisnum = 2;

    /* 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);
    }

    // 雙向驗證
    // SSL_VERIFY_PEER---要求對證書進行認證,沒有證書也會放行
    // SSL_VERIFY_FAIL_IF_NO_PEER_CERT---要求客戶端須要提供證書,但驗證發現單獨使用沒有證書也會放行
    SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER|SSL_VERIFY_FAIL_IF_NO_PEER_CERT, NULL);
    // 設置信任根證書
    if (SSL_CTX_load_verify_locations(ctx, "ca.crt",NULL)<=0){
        ERR_print_errors_fp(stdout);
        exit(1);
    }

    /* 載入用戶的數字證書, 此證書用來發送給客戶端。 證書裏包含有公鑰 */
    if (SSL_CTX_use_certificate_file(ctx, argv[3], SSL_FILETYPE_PEM) <= 0) {
        ERR_print_errors_fp(stdout);
        exit(1);
    }
    /* 載入用戶私鑰 */
    if (SSL_CTX_use_PrivateKey_file(ctx, argv[4], 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);
    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;
        }
        ShowCerts(ssl);

        /* 開始處理每一個新鏈接上的數據收發 */
        bzero(buf, MAXBUF + 1);
        strcpy(buf, "server->client");
        /* 發消息給客戶端 */
        len = SSL_write(ssl, buf, strlen(buf));

        if (len <= 0) {
            printf("消息'%s'發送失敗!錯誤代碼是%d,錯誤信息是'%s'\n", buf, errno,
                    strerror(errno));
            goto finish;
        } else
            printf("消息'%s'發送成功,共發送了%d個字節!\n", buf, len);

        bzero(buf, MAXBUF + 1);
        /* 接收客戶端的消息 */
        len = SSL_read(ssl, buf, MAXBUF);
        if (len > 0)
            printf("接收消息成功:'%s',共%d個字節的數據\n", buf, len);
        else
            printf("消息接收失敗!錯誤代碼是%d,錯誤信息是'%s'\n",
            errno, strerror(errno));
        /* 處理每一個新鏈接上的數據收發結束 */
        finish:
        /* 關閉 SSL 鏈接 */
        SSL_shutdown(ssl);
        /* 釋放 SSL */
        SSL_free(ssl);
        /* 關閉 socket */
        close(new_fd);
    }
    /* 關閉監聽的 socket */
    close(sockfd);
    /* 釋放 CTX */
    SSL_CTX_free(ctx);
    return 0;
}
View Code

 

2.3 客戶端代碼

#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 <openssl/ssl.h>
#include <openssl/err.h>

#define MAXBUF 1024

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

    cert = SSL_get_peer_certificate(ssl);
    // SSL_get_verify_result()是重點,SSL_CTX_set_verify()只是配置啓不啓用並無執行認證,調用該函數纔會真證進行證書認證
    // 若是驗證不經過,那麼程序拋出異常停止鏈接
    if(SSL_get_verify_result(ssl) == X509_V_OK){
        printf("證書驗證經過\n");
    }
    if (cert != NULL) {
        printf("數字證書信息:\n");
        line = X509_NAME_oneline(X509_get_subject_name(cert), 0, 0);
        printf("證書: %s\n", line);
        free(line);
        line = X509_NAME_oneline(X509_get_issuer_name(cert), 0, 0);
        printf("頒發者: %s\n", line);
        free(line);
        X509_free(cert);
    } else
        printf("無證書信息!\n");
}

int main(int argc, char **argv)
{
    int sockfd, len;
    struct sockaddr_in dest;
    char buffer[MAXBUF + 1];
    SSL_CTX *ctx;
    SSL *ssl;

    if (argc != 5) {
        printf("參數格式錯誤!正確用法以下:\n\t\t%s IP地址 端口\n\t好比:\t%s 127.0.0.1 80\n此程序用來從某個"
             "IP 地址的服務器某個端口接收最多 MAXBUF 個字節的消息",
             argv[0], argv[0]);
        exit(0);
    }

    /* SSL 庫初始化,參看 ssl-server.c 代碼 */
    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);
    }

    // 雙向驗證
    // SSL_VERIFY_PEER---要求對證書進行認證,沒有證書也會放行
    // SSL_VERIFY_FAIL_IF_NO_PEER_CERT---要求客戶端須要提供證書,但驗證發現單獨使用沒有證書也會放行
    SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER|SSL_VERIFY_FAIL_IF_NO_PEER_CERT, NULL);
    // 設置信任根證書
    if (SSL_CTX_load_verify_locations(ctx, "ca.crt",NULL)<=0){
        ERR_print_errors_fp(stdout);
        exit(1);
    }

    /* 載入用戶的數字證書, 此證書用來發送給客戶端。 證書裏包含有公鑰 */
    if (SSL_CTX_use_certificate_file(ctx, argv[3], SSL_FILETYPE_PEM) <= 0) {
        ERR_print_errors_fp(stdout);
        exit(1);
    }
    /* 載入用戶私鑰 */
    if (SSL_CTX_use_PrivateKey_file(ctx, argv[4], 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 用於 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");

    /* 基於 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);
    }

    /* 接收對方發過來的消息,最多接收 MAXBUF 個字節 */
    bzero(buffer, MAXBUF + 1);
    /* 接收服務器來的消息 */
    len = SSL_read(ssl, buffer, MAXBUF);
    if (len > 0)
        printf("接收消息成功:'%s',共%d個字節的數據\n",
               buffer, len);
    else {
        printf
            ("消息接收失敗!錯誤代碼是%d,錯誤信息是'%s'\n",
             errno, strerror(errno));
        goto finish;
    }
    bzero(buffer, MAXBUF + 1);
    strcpy(buffer, "from client->server");
    /* 發消息給服務器 */
    len = SSL_write(ssl, buffer, strlen(buffer));
    if (len < 0)
        printf
            ("消息'%s'發送失敗!錯誤代碼是%d,錯誤信息是'%s'\n",
             buffer, errno, strerror(errno));
    else
        printf("消息'%s'發送成功,共發送了%d個字節!\n",
               buffer, len);

  finish:
    /* 關閉鏈接 */
    SSL_shutdown(ssl);
    SSL_free(ssl);
    close(sockfd);
    SSL_CTX_free(ctx);
    return 0;
}
View Code

 

2.4 證書生成

注意三點

第一點,注意將其中的私鑰加密密碼(-passout參數)修改爲本身的密碼;下邊都是以帶-passout參數生成私鑰,若是使用-nodes參數,則最後一步「將加密的RSA密鑰轉成未加密的RSA密鑰」不須要執行。

第二點,證書和密鑰給出了直接一步生成和分步生成兩種形式,兩種形式是等價的,這裏使用直接生成形式(分步生成形式被註釋)

第三點,注意將其中的證書信息改爲本身的組織信息的。其中證數各參數含義以下:

C-----國家(Country Name)

ST----省份(State or Province Name)

L----城市(Locality Name)

O----公司(Organization Name)

OU----部門(Organizational Unit Name)

CN----產品名(Common Name)

emailAddress----郵箱(Email Address)

# CA證書及密鑰生成方法一----直接生成CA密鑰及其自簽名證書
# 若是想之後讀取私鑰文件ca_rsa_private.pem時不須要輸入密碼,亦即不對私鑰進行加密存儲,那麼將-passout pass:123456替換成-nodes
openssl req -newkey rsa:2048 -passout pass:123456 -keyout ca_rsa_private.pem -x509 -days 365 -out ca.crt -subj "/C=CN/ST=GD/L=SZ/O=COM/OU=NSP/CN=CA/emailAddress=youremail@qq.com"
# CA證書及密鑰生成方法二----分步生成CA密鑰及其自簽名證書:
# openssl genrsa -aes256 -passout pass:123456 -out ca_rsa_private.pem 2048
# openssl req -new -x509 -days 365 -key ca_rsa_private.pem -passin pass:123456 -out ca.crt -subj "/C=CN/ST=GD/L=SZ/O=COM/OU=NSP/CN=CA/emailAddress=youremail@qq.com"

# 服務器證書及密鑰生成方法一----直接生成服務器密鑰及待簽名證書
# 若是想之後讀取私鑰文件server_rsa_private.pem時不須要輸入密碼,亦即不對私鑰進行加密存儲,那麼將-passout pass:server替換成-nodes
openssl req -newkey rsa:2048 -passout pass:server -keyout server_rsa_private.pem  -out server.csr -subj "/C=CN/ST=GD/L=SZ/O=COM/OU=NSP/CN=SERVER/emailAddress=youremail@qq.com"
# 服務器證書及密鑰生成方法二----分步生成服務器密鑰及待簽名證書
# openssl genrsa -aes256 -passout pass:server -out server_rsa_private.pem 2048
# openssl req -new -key server_rsa_private.pem -passin pass:server -out server.csr -subj "/C=CN/ST=GD/L=SZ/O=COM/OU=NSP/CN=SERVER/emailAddress=youremail@qq.com"
# 使用CA證書及密鑰對服務器證書進行簽名:
openssl x509 -req -days 365 -in server.csr -CA ca.crt -CAkey ca_rsa_private.pem -passin pass:123456 -CAcreateserial -out server.crt
# 將加密的RSA密鑰轉成未加密的RSA密鑰,避免每次讀取都要求輸入解密密碼
# 密碼就是生成私鑰文件時設置的passout、讀取私鑰文件時要輸入的passin,好比這裏要輸入「server」
openssl rsa -in server_rsa_private.pem -out server_rsa_private.pem.unsecure

# 客戶端證書及密鑰生成方法一----直接生成客戶端密鑰及待簽名證書
# 若是想之後讀取私鑰文件client_rsa_private.pem時不須要輸入密碼,亦即不對私鑰進行加密存儲,那麼將-passout pass:client替換成-nodes
openssl req -newkey rsa:2048 -passout pass:client -keyout client_rsa_private.pem -out client.csr -subj "/C=CN/ST=GD/L=SZ/O=COM/OU=NSP/CN=CLIENT/emailAddress=youremail@qq.com"
# 客戶端證書及密鑰生成方法二----分步生成客戶端密鑰及待簽名證書:
# openssl genrsa -aes256 -passout pass:client -out client_rsa_private.pem 2048
# openssl req -new -key client_rsa_private.pem -passin pass:client -out client.csr -subj "/C=CN/ST=GD/L=SZ/O=COM/OU=NSP/CN=CLIENT/emailAddress=youremail@qq.com"
# 使用CA證書及密鑰對客戶端證書進行簽名:
openssl x509 -req -days 365 -in client.csr -CA ca.crt -CAkey ca_rsa_private.pem -passin pass:123456 -CAcreateserial -out client.crt
# 將加密的RSA密鑰轉成未加密的RSA密鑰,避免每次讀取都要求輸入解密密碼
# 密碼就是生成私鑰文件時設置的passout、讀取私鑰文件時要輸入的passin,好比這裏要輸入「client」
openssl rsa -in client_rsa_private.pem -out client_rsa_private.pem.unsecure

 

2.5 開發環境配置

操做系統----kali-roaling。爲了使用如今虛擬機而已,使用ubuntu、centos等等應該都是沒差異的。

IDE----eclipse。直接在終端中編譯不經過沒深究,放eclipse編譯沒問題就直接使用eclipse。

 

2.5.1 在同一活動目錄下建了兩個project

myclient1----建src文件夾放客戶端代碼

myserver1----建src文件夾放服務端代碼

(其餘目錄要麼是自動生成的,要麼是編譯後自動生成的,不用管;若是項目有報錯試試多重啓幾回eclipse甚操做系統)

 

2.5.2 指定ssl和crypto

在項目文件夾上右鍵----Properties----指定ssl庫和crypto庫目錄否則編譯找不到ssl。兩個project都要配置

 

2.5.3 編譯

使用Ctrl+B快捷鍵進行編譯,eclipse會編譯全部project

 

2.5.4 證書複製

將前邊生成的ca證書(ca.crt)、客戶端證書(client.crt)、客戶端未加密私鑰文件()複製到myclient1項目的Debug目錄下

將前邊生成的ca證書、服務端證書、服務端端未加密私鑰文件複製到myclient1項目的Debug目錄下

 

2.5.5 運行程序

 先運行服務端後運行客戶端

./myserver1 7838 1 server.crt server_rsa_private.pem.unsecure
./myclient1 127.0.0.1 7838 client.crt client_rsa_private.pem.unsecure

運行結果以下,服務端:

客戶端:

 

參考:

https://blog.csdn.net/gengxiaoming7/article/details/78505107

https://blog.csdn.net/sardden/article/details/42705897

https://blog.csdn.net/Junkie0901/article/details/40402003

https://www.openssl.org/docs/man1.1.1/man3/

https://blog.csdn.net/wangsifu2009/article/details/7569566

http://www.javashuo.com/article/p-twvcoeae-cq.html

相關文章
相關標籤/搜索