2017-2018-1 20155306 实验五 通信协议设计
2017-2018-1 20155306 20155315 实验五 通信协议设计
实验内容:
通讯协议设计-1
在Ubuntu中完成 http://www.cnblogs.com/rocedu/p/5087623.html 中的作业:
1. 两人一组
基于Socket实现TCP通信,一人实现服务器,一人实现客户端
2. 研究OpenSSL算法,测试对称算法中的AES,非对称算法中的RSA,Hash算法中的MD5
3. 选用合适的算法,基于混合密码系统实现对TCP通信进行机密性、完整性保护。
4. 学有余力者,对系统进行安全性分析和改进。
提交运行结果截图
通讯协议设计-2
在Ubuntu中实现对实验二中的“wc服务器”通过混合密码系统进行防护
提交测试截图
实验步骤:
任务一:
1. 下载OpenSSL 1.1.0alpha
2.解压源代码:
tar xzvf openssl-1.1.0-pre1.tar.gz
3.然后进入源代码目录:
cd openssl-1.1.0-pre1
4.编译安装:
./configure
make
sudo make install
5. 测试代码test_openssl.c:
#include <stdio.h>
#include <openssl/evp.h>
int main(){
OpenSSL_add_all_algorithms();
return 0;
}
6. 用下面命令编译:【编译后出现“致命错误”,在问题中有相应解决】
gcc -o to test_openssl.c -I /usr/local/ssl/inlcude /usr/local/ssl/lib -ldl -lpthread
7. 执行:【如下图,成功返回0,安装成功】
./to;echo $?
8. 基于Socket实现TCP通信
- 基于C的Socket编程相关函数和数据类型
1.socket()函数
该函数用于根指定的地址族、数据类型和协议来分配一个套接字的描述 字及其所用的资源。 Socket 函数原型为:
int socket( int domain , int type , int protocol )
a、 参数 domain 指定地址描述,一般为 AP_INET;
b、 参数 type 指定 socket 类型:SOCK_STREAM 和 SOCK_DGRAM;
- bind()函数
该函数用于将一个本地地址与一个套接字绑定在一起。
int bind( int sockfd , struct sockadd* my_addr , int addrlen)
sockfd:socket 描述符,使用 socket 函数返回值,将该 socket 与本机上的一个端口相关联。
3.connect()函数
与远程服务器建立一个 TCP 连接。
int connect(int sockfd, struct sockaddr* serv_addr, int addrlen);
a. sockfd:目的服务器的 socket 描述符
b. connect 函数返回值:为-1 表示遇到错误,并且 errno 中包含相应的错误码,进行服务器端程序设计时不需调用 connect 函数。
4.listen()函数
在服务器端程序中,当 socket 与某一端口绑定后,需要监听该端口, 及时处理到达该端口上的服务请求。
int listen(int sockfd, int backlog);
backlog:指定在请求队列中允许的最大请求数,进入的连接请求将在队列中等待接收 backlog 限制了队列中等待服务的请求数目,系统缺省值为 20。
5.accept()函数
当某个客户端试图与服务器监听的端口连接时, 该连接请求将排队等待 服务器用 accept 接收它并为其建立一个连接。
int a ccept(int sockfd, s truct s ockaddr* addr, i nt* addrlen);
accept 函数返回值:为-1 表示遇到错误,并且 errno 中包含相应的错误码。
6.sent()和recv()函数
int send(int sockfd, const void* msg, int len, int flags)
int recv(int sockfd, void* buf, int len, unsigned int flags);
7.close()函数:
在服务器与客户端建立连接之后,会进行一些读写操作,完成了读写操作就要关闭相应的socket描述字,好比操作完打开的文件要调用fclose关闭打开的文件。
close(int sockfd);
- 实现的客户端和服务器核心代码如下:
服务器:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/time.h>
#define BUFLEN 1024
#define PORT 6666
#define LISTNUM 20
int main()
{
int sockfd, newfd;
struct sockaddr_in s_addr, c_addr;
char buf[BUFLEN];
socklen_t len;
unsigned int port, listnum;
fd_set rfds;
struct timeval tv;
int retval,maxfd;
/*建立socket*/
if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1){
perror("socket");
exit(errno);
}else
printf("socket create success!\n");
memset(&s_addr,0,sizeof(s_addr));
s_addr.sin_family = AF_INET;
s_addr.sin_port = htons(PORT);
s_addr.sin_addr.s_addr = htons(INADDR_ANY);
/*把地址和端口帮定到套接字上*/
if((bind(sockfd, (struct sockaddr*) &s_addr,sizeof(struct sockaddr))) == -1){
perror("bind");
exit(errno);
}else
printf("bind success!\n");
/*侦听本地端口*/
if(listen(sockfd,listnum) == -1){
perror("listen");
exit(errno);
}else
printf("the server is listening!\n");
while(1){
printf("*****************聊天开始***************\n");
len = sizeof(struct sockaddr);
if((newfd = accept(sockfd,(struct sockaddr*) &c_addr, &len)) == -1){
perror("accept");
exit(errno);
}else
printf("正在与您聊天的客户端是:%s: %d\n",inet_ntoa(c_addr.sin_addr),ntohs(c_addr.sin_port));
while(1){
FD_ZERO(&rfds);
FD_SET(0, &rfds);
maxfd = 0;
FD_SET(newfd, &rfds);
/*找出文件描述符集合中最大的文件描述符*/
if(maxfd < newfd)
maxfd = newfd;
/*设置超时时间*/
tv.tv_sec = 6;
tv.tv_usec = 0;
/*等待聊天*/
retval = select(maxfd+1, &rfds, NULL, NULL, &tv);
if(retval == -1){
printf("select出错,与该客户端连接的程序将退出\n");
break;
}else if(retval == 0){
printf("waiting...\n");
continue;
}else{
/*用户输入信息了*/
if(FD_ISSET(0, &rfds)){
/******发送消息*******/
memset(buf,0,sizeof(buf));
/*fgets函数:从流中读取BUFLEN-1个字符*/
fgets(buf,BUFLEN,stdin);
/*打印发送的消息*/
//fputs(buf,stdout);
if(!strncasecmp(buf,"quit",4)){
printf("server 请求终止聊天!\n");
break;
}
len = send(newfd,buf,strlen(buf),0);
if(len > 0)
printf("\t消息发送成功:%s\n",buf);
else{
printf("消息发送失败!\n");
break;
}
}
/*客户端发来了消息*/
if(FD_ISSET(newfd, &rfds)){
/******接收消息*******/
memset(buf,0,sizeof(buf));
/*fgets函数:从流中读取BUFLEN-1个字符*/
len = recv(newfd,buf,BUFLEN,0);
if(len > 0)
printf("客户端发来的信息是:%s\n",buf);
else{
if(len < 0 )
printf("接受消息失败!\n");
else
printf("客户端退出了,聊天终止!\n");
break;
}
}
}
}
/*关闭聊天的套接字*/
close(newfd);
/*是否退出服务器*/
printf("服务器是否退出程序:y->是;n->否? ");
bzero(buf, BUFLEN);
fgets(buf,BUFLEN, stdin);
if(!strncasecmp(buf,"y",1)){
printf("server 退出!\n");
break;
}
}
/*关闭服务器的套接字*/
close(sockfd);
return 0;
}
客户端:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/time.h>
#define BUFLEN 1024
#define PORT 6666
int main(int argc, char **argv)
{
int sockfd;
struct sockaddr_in s_addr;
socklen_t len;
unsigned int port;
char buf[BUFLEN];
fd_set rfds;
struct timeval tv;
int retval, maxfd;
/*建立socket*/
if((sockfd = socket(AF_INET, SOCK_STREAM, 0)) == -1){
perror("socket");
exit(errno);
}else
printf("socket create success!\n");
/*设置服务器ip*/
memset(&s_addr,0,sizeof(s_addr));
s_addr.sin_family = AF_INET;
s_addr.sin_port = htons(PORT);
if (inet_aton(argv[1], (struct in_addr *)&s_addr.sin_addr.s_addr) == 0) {
perror(argv[1]);
exit(errno);
}
/*开始连接服务器*/
if(connect(sockfd,(struct sockaddr*)&s_addr,sizeof(struct sockaddr)) == -1){
perror("connect");
exit(errno);
}else
{printf("conncet success!\n");
printf("*****************聊天开始***************\n");}
while(1){
FD_ZERO(&rfds);
FD_SET(0, &rfds);
maxfd = 0;
FD_SET(sockfd, &rfds);
if(maxfd < sockfd)
maxfd = sockfd;
tv.tv_sec = 6;
tv.tv_usec = 0;
retval = select(maxfd+1, &rfds, NULL, NULL, &tv);
if(retval == -1){
printf("select出错,客户端程序退出\n");
break;
}else if(retval == 0){
printf("waiting...\n");
continue;
}else{
/*服务器发来了消息*/
if(FD_ISSET(sockfd,&rfds)){
/******接收消息*******/
bzero(buf,BUFLEN);
len = recv(sockfd,buf,BUFLEN,0);
if(len > 0)
printf("服务器发来的消息是:%s\n",buf);
else{
if(len < 0 )
printf("接受消息失败!\n");
else
printf("服务器退出了,聊天终止!\n");
break;
}
}
/*用户输入信息了,开始处理信息并发送*/
if(FD_ISSET(0, &rfds)){
/******发送消息*******/
bzero(buf,BUFLEN);
fgets(buf,BUFLEN,stdin);
if(!strncasecmp(buf,"quit",4)){
printf("client 请求终止聊天!\n");
break;
}
len = send(sockfd,buf,strlen(buf),0);
if(len > 0)
printf("\t消息发送成功:%s\n",buf);
else{
printf("消息发送失败!\n");
break;
}
}
}
}
/*关闭连接*/
close(sockfd);
return 0;
}
9. 研究OpenSSL算法,测试对称算法中的AES,非对称算法中的RSA,Hash算法中的MD5
openssl支持在命令行进行各种基本加密算法的操作。这些操作过程无需编程,其命令参数与程序函数调用加密的参数有着很好的直接对应关系。openssl提供了N多的对称加密算法指令,enc就是把这些N多的对称的加密算法指令统一集成到enc指令中。当用户使用时,只需使用enc,指定加密算法,就是完成单独的加密算法指令完成的操作。而且,enc中可以指定的对称加密算法指令可能并没有以单独指令的形式存在。推荐使用enc的方式。所以使用man命令查看帮助文档,如下图:
通过自己上网查询和翻译man文档,明白了各参数的含义:
[in/out]
这两个参数指定输入文件和输出文件,加密是输入文件是明文,输出文件是密文;解密时输入文件是密文,输出文件是明文。
[pass]
指定密码的输入方式,共有五种方式:命令行输入(stdin)、文件输入(file)、环境变量输入(var)、文件描述符输入(fd)、标准输入(stdin)。默认是标准输入,及从键盘输入。
[e/d]
e:加密, d:解密 默认是加密
[-a/-base64]
由于文件加密后是二进制形式,不方便查看,使用该参数可以使加密后的内容经过base64编码,使其可读;同样,解密时需要先进行base64解编码,然后进行解密操作。
[-k/-kfile]
兼容以前版本,指定密码输入方式,现已被pass参数取代
[md]
指定密钥生成的摘要算法,用户输入的口令不能直接作为文件加密的密钥,而是经过摘要算法做转换,此参数指定摘要算法,默认md5
[-S]
为了增强安全性,在把用户密码转换成加密密钥的时候需要使用盐值,默认盐值随机生成。使用该参数,则盐值由用户指定。也可指用-nosalt指定不使用盐值,但降低了安全性,不推荐使用。
[K/IV]
默认文件的加密密钥的Key和IV值是有用户输入的密码经过转化生成的,但也可以由用户自己指定Key/IV值,此时pass参数不起作用
[pP]
加上p参数会打印文件密钥Key和IV值,加上P参数也会打印文件密钥Key和IV值,但不进行真正的加解密操作
[bufsize]
读写文件的I/O缓存,一般不需要指定
[-nopad]
不使用补齐,这就需要输入的数据长度是使用加密算法的分组大小的倍数
[engine]
指定三方加密设备,没有环境,暂不实验
- RSA加解密
生成一个密钥:【这里-out指定生成文件的。需要注意的是这个文件包含了公钥和密钥两部分,也就是说这个文件即可用来加密也可以用来解密。后面的1024是生成密钥的长度。】
openssl genrsa -out test.key 1024
openssl可以将这个文件中的公钥提取出来:【-in指定输入文件,-out指定提取生成公钥的文件名】
openssl rsa -in test.key -pubout -out test_pub.key
利用此前生成的公钥加密文件:【-in指定要加密的文件,-inkey指定密钥,-pubin表明是用纯公钥文件加密,-out为加密后的文件】
openssl rsautl -encrypt -in hello -inkey test_pub.key -pubin -out hello.en
解密文件:【-in指定被加密的文件,-inkey指定私钥文件,-out为解密后的文件。】
openssl rsautl -decrypt -in hello.en -inkey test.key -out hello.de
- AES加解密
加密:
openssl enc -aes-128-cbc -e -a -in in.txt -out dj.txt -K c286696d887c9aa0611bbb3e2025a45a
解密:
openssl enc -aes-128-cbc -d -a -in dj.txt -out dedj.txt -K c286696d887c9aa0611bbb3e2025a45a
密钥也可以这样输入:
openssl enc -aes-128-cbc -in plain.txt -out out.txt -pass pass:123456
上述命令各参数含义如下:
-aes-128-ecb 表示使用128位AES算法,EBC工作模式;
-K 0001......0F 表示加密使用的密钥为十六进制000102030405060708090A0B0C0D0E0F;
-in和-out指明输入和输出文件名;
-p表示屏幕打印加密所使用的密钥key、初始向量iv、盐值salt等信息。
MD5加解密:
openssl dgst -md5 20155339.txt
任务二:
混合密码系统如图所示:
通过查阅资料,基于openSSl实现客户端服务器,大概就需要以下步骤:
(1) OpenSSL初始化
在使用OpenSSL之前,必须进行相应的协议初始化工作,这可以通过下面的函数实现:
void SSLeay_add_ssl_algorithms();
该函数实际是int SSL_library_init(void),在ssl.h中时行了宏定义;
(2) 选择会话协议
在利用OpenSSL开始SSL会话之前,需要为客户端和服务器制定本次会话采用的协议,目前能够使用的协议包括TLSv1.0、SSLv2、SSLv3、SSLv2/v3。
需要注意的是,客户端和服务器必须使用相互兼容的协议,否则SSL会话将无法正常进行。
SSL_METHOD *SSLv23_client_method();
(3 ) 创建会话环境
在OpenSSL中创建的SSL会话环境称为CTX,使用不同的协议会话,其环境也不一样的。
申请SSL会话环境的OpenSSL函数是:
SSL_CTX *SSL_CTX_new(SSL_METHOD * method);
当SSL会话环境申请成功后,还要根据实际的需要设置CTX的属性,通常的设置是指定SSL握手阶段证书的验证方式和加载自己的证书。
制定证书验证方式的函数是:
int SSL_CTX_set_verify(SSL_CTX *ctx,int mode,int(*verify_callback),int(X509_STORE_CTX *));
为SSL会话环境加载CA证书的函数是:
SSL_CTX_load_verify_location(SSL_CTX *ctx,const char *Cafile,const char *Capath);
为SSL会话加载用户证书的函数是:
SSL_CTX_use_certificate_file(SSL_CTX *ctx, const char *file,int type);
为SSL会话加载用户私钥的函数是:
SSL_CTX_use_PrivateKey_file(SSL_CTX *ctx,const char* file,int type);
在将证书和私钥加载到SSL会话环境之后,就可以调用下面的函数来验证私钥和证书是否相符:
int SSL_CTX_check_private_key(SSL_CTX *ctx);
(4) 建立SSL套接字
SSL套接字是建立在普通的TCP套接字基础之上,在建立SSL套接字时可以使用下面的一些函数:
SSL *SSl_new(SSL_CTX *ctx);
//申请一个SSL套接字
int SSL_set_fd(SSL *ssl,int fd);)
//绑定读写套接字
int SSL_set_rfd(SSL *ssl,int fd);
//绑定只读套接字
int SSL_set_wfd(SSL *ssl,int fd);
//绑定只写套接字
(5) 完成SSL握手
在成功创建SSL套接字后,客户端应使用函数SSL_connect( )替代传统的函数connect( )来完成握手过程:
int SSL_connect(SSL *ssl);
而对服务器来讲,则应使用函数SSL_ accept ( )替代传统的函数accept ( )来完成握手过程:
int SSL_accept(SSL *ssl);
握手过程完成之后,通常需要询问通信双方的证书信息,以便进行相应的验证,这可以借助于下面的函数来实现:
X509 *SSL_get_peer_certificate(SSL *ssl);
该函数可以从SSL套接字中提取对方的证书信息,这些信息已经被SSL验证过了。
X509_NAME *X509_get_subject_name(X509 *a);
该函数得到证书所用者的名字。
(6) 进行数据传输
当SSL握手完成之后,就可以进行安全的数据传输了,在数据传输阶段,需要使用SSL_read( )和SSL_write( )来替代传统的read( )和write( )函数,来完成对套接字的读写操作:
int SSL_read(SSL *ssl,void *buf,int num);
int SSL_write(SSL *ssl,const void *buf,int num);
(7 ) 结束SSL通信
当客户端和服务器之间的数据通信完成之后,调用下面的函数来释放已经申请的SSL资源:
int SSL_shutdown(SSL *ssl);
//关闭SSL套接字
void SSl_free(SSL *ssl);
//释放SSL套接字
void SSL_CTX_free(SSL_CTX *ctx);
//释放SSL会话环境
客户端程序的框架为:
meth = SSLv23_client_method();
ctx = SSL_CTX_new (meth);
ssl = SSL_new(ctx);
fd = socket();
connect();
SSL_set_fd(ssl,fd);
SSL_connect(ssl);
SSL_write(ssl,"Hello world",strlen("Hello World!"));
服务端程序的框架为:
meth = SSLv23_server_method();
ctx = SSL_CTX_new (meth);
ssl = SSL_new(ctx);
fd = socket();
bind();
listen();
accept();
SSL_set_fd(ssl,fd);
SSL_connect(ssl);
SSL_read (ssl, buf, sizeof(buf));
实验中遇到的问题和解决方案
-
问题一:任务一,进行gcc编译时,出现如下错误:
-
解决:查阅资料以后,发现尝试编译的程序使用OpenSSL,但是需要和OpenSSL链接的文件(库和头文件)在你Linux平台上缺少。
使用下面的命令后,重新编译。
sudo apt-get install libssl-dev
参考资料
利用openssl实现SSL安全通讯协议(一)
安全通信系统--OpenSSL服务器和客户端
使用openssl命令进行加密解密及散列运算的命令行