阅读 222

Openssl实现双向认证教程(附服务端客户端代码)

这篇文章主要介绍了Openssl实现双向认证教程(附服务端客户端代码),文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下

一、背景说明

1.1 面临问题

最近一份产品检测报告建议使用基于pki的认证方式,由于产品已实现https,商量之下认为其意思是使用双向认证以处理中间人形式攻击。

《信息安全工程》中接触过双向认证,但有两个问题。

第一个是当时最终的课程设计客户端是浏览器,服务端是tomcat双向认证只需要对两者进行配置并不需要自己真的实现代码。

第二个是虽然课程也有接近双向认证的实现代码,但当时是Java+JCE环境现在要用C+++OpenSSL环境,总体意思确实还是差不多但具体函数和参数差别还是不少。

所以眼下有的是:证书生成的思想+双向认证实现的思想。对读者而言,即要假定已对证书、SSL/TSL、socket编程等几个概念有基本的了解,本文不做详细介绍。

基于此本文要解决的问题是:openssl具体如何生成证书+openssl如何实现双向认证。

双向认证的关键点在以下几个函数(服务端和客户端都一样),其他就不细说参看代码注释:

SSL_CTX_set_verify----配置启用双向认证

SSL_CTX_load_verify_locations----加载信任的根证书

SSL_CTX_use_certificate_file----加载自己的证书

SSL_CTX_use_PrivateKey_file----加载自己的私钥

SSL_get_verify_result----真正进行验证,一定要调用这个函数不然前面四个光配置而已并不会进行双向验证

二、双向认证程序实现

2.1 安装openssl及开发api

apt-get install libssl-dev

2.2 服务端代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
#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;
}

2.3 客户端代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
#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;
}

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)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
# 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

运行结果如下,服务端:

客户端:

以上就是本文的全部内容,希望对大家的学习有所帮助



文章分类
后端
版权声明:本站是系统测试站点,无实际运营。本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 XXXXXXo@163.com 举报,一经查实,本站将立刻删除。
相关推荐