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;

  1. 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命令进行加密解密及散列运算的命令行

posted on 2017-12-17 19:01  20155306  阅读(481)  评论(0编辑  收藏  举报

导航