无校验
$ openssl genrsa -out privkey.pem 2048
$ openssl req -new -x509 -key privkey.pem -out cacert.pem -days 1095
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <openssl/ssl.h>
#include <openssl/err.h>
#define MAXBUF 1024
int main(void)
{
SSL *ssl;
SSL_CTX *ctx;
socklen_t len;
char buff[MAXBUF + 1];
int sockfd, clifd, rc;
struct sockaddr_in svraddr, cliaddr;
SSL_library_init();
//OpenSSL_add_all_algorithms();
SSL_load_error_strings();
ctx = SSL_CTX_new(SSLv23_server_method()); /* support both V2 and V3 */
if (ctx == NULL) {
ERR_print_errors_fp(stderr);
exit(1);
}
/* load certificate file, which will send public key to client. */
if (SSL_CTX_use_certificate_file(ctx, "cacert.pem", SSL_FILETYPE_PEM) <= 0) {
ERR_print_errors_fp(stderr);
exit(1);
}
/* and also private key file */
if (SSL_CTX_use_PrivateKey_file(ctx, "privkey.pem", SSL_FILETYPE_PEM) <= 0) {
ERR_print_errors_fp(stderr);
exit(1);
}
sockfd = socket(PF_INET, SOCK_STREAM, 0);
if (sockfd < 0) {
fprintf(stderr, "create socket failed, %s.\n", strerror(errno));
exit(1);
}
memset(&svraddr, 0, sizeof(svraddr));
svraddr.sin_family = PF_INET;
svraddr.sin_port = htons(8080);
svraddr.sin_addr.s_addr = INADDR_ANY;
if (bind(sockfd, (struct sockaddr *)&svraddr, sizeof(struct sockaddr)) < 0) {
fprintf(stderr, "bind socket failed, %s.\n", strerror(errno));
close(sockfd);
exit(1);
}
if (listen(sockfd, 128) < 0) {
fprintf(stderr, "listen socket failed, %s.\n", strerror(errno));
close(sockfd);
exit(1);
}
len = sizeof(struct sockaddr);
while (1) {
clifd = accept(sockfd, (struct sockaddr *)&cliaddr, &len);
if (clifd < 0) {
fprintf(stderr, "listen socket failed, %s.\n", strerror(errno));
continue;
}
fprintf(stdout, "got connection #%d from [%s:%d]\n", clifd,
inet_ntoa(cliaddr.sin_addr), ntohs(cliaddr.sin_port));
ssl = SSL_new(ctx);
if (ssl == NULL) {
fprintf(stderr, "create SSL context failed.\n");
close(clifd);
continue;
}
SSL_set_fd(ssl, clifd);
if (SSL_accept(ssl) < 0) {
fprintf(stderr, "accept SSL failed.\n");
close(clifd);
continue;
}
rc = SSL_read(ssl, buff, sizeof(buff) - 1); /* for '\0' */
if (rc < 0) {
fprintf(stderr, "read from SSL failed.\n");
close(clifd);
continue;
}
buff[rc] = 0;
fprintf(stdout, "got data(%2d): %s\n", rc, buff);
SSL_shutdown(ssl);
SSL_free(ssl);
close(clifd);
}
SSL_CTX_free(ctx);
return 0;
}
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <arpa/inet.h>
#include <openssl/ssl.h>
#include <openssl/err.h>
void show_certs(SSL *ssl)
{
X509 *cert;
char *line;
cert = SSL_get_peer_certificate(ssl);
if (cert == NULL) {
printf("No certificate information!\n");
return;
}
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);
}
int main(void)
{
SSL *ssl;
SSL_CTX *ctx;
int sockfd, rc;
struct sockaddr_in dst;
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(stderr);
exit(1);
}
sockfd = socket(AF_INET, SOCK_STREAM, 0);
if (sockfd < 0) {
fprintf(stderr, "create socket failed, %s.\n", strerror(errno));
exit(1);
}
memset(&dst, 0, sizeof(dst));
dst.sin_family = PF_INET;
dst.sin_port = htons(8080);
inet_aton("127.0.0.1", (struct in_addr *)&dst.sin_addr.s_addr);
rc = connect(sockfd, (struct sockaddr *)&dst, sizeof(dst));
if (rc < 0) {
fprintf(stderr, "connect to server failed, %s.\n", strerror(errno));
close(sockfd);
exit(1);
}
ssl = SSL_new(ctx);
SSL_set_fd(ssl, sockfd);
if (SSL_connect(ssl) < 0) {
ERR_print_errors_fp(stderr);
exit(1);
}
printf("Connected with %s encryption\n", SSL_get_cipher(ssl));
show_certs(ssl);
SSL_write(ssl, "Hello!", strlen("Hello!"));
SSL_shutdown(ssl);
SSL_free(ssl);
close(sockfd);
SSL_CTX_free(ctx);
return 0;
}
双向认证
同步方式
在创建上下文时使用的是 SSLv23_method()
函数,这并不意味着只使用 TLSv1.2 以及 TLSv1.3 版本的协议,这是由于历史原因导致,实际的意思是指服务端和客户端协商使用双方都兼容最高版本,在 1.1.0 版本之后会修改为 TLS_method()
。
如果要屏蔽部分版本,可以调用 SSL_CTX_set_options()
函数,例如参数 SSL_OP_NO_SSLv2
SSL_OP_NO_TLSv1_1
等等。
会话复用
将上次通过握手计算出来的对称密钥复用,可以通过一次 RTT 完成握手。
<Client> <Server>
[Client Hello] -------------------------------->
* Supported Ciphers
* Random Number
* Session ID(**)
* SNI
<-------------------------------- [Server Hello]
* Session ID(reuse) [Change Cipher Spec
Finished(encrypt)]
[Change Cipher Spec
Finished(encrypt)]
[Application Data] <-------------------------------> [Application Data]
客户端在发送 Client Hello
请求的时候,会将上次握手过程中 Server 发送的 SessionID 带上,如果在服务端可以匹配到相关的信息,那么就直接返回成功,然后就可以交换应用数据。
SessionID 存在一些问题,如果服务端采用了分布式,当同一个客户端的请求没有落到上次的服务器时,会实效;服务端在使用 SessionID 时,很难判断其保存时间的长短。
SessionTicket
而 Session Ticket 是在服务端加密之后的会话信息,然后保存在客户端中,下次请求会带上 Session Ticket ,只要服务端可以解密成功,那么就直接完成了握手。
可以使用 github.com 中提供的工具测试支持情况。
OpenSSL 会将会话信息保存在上下文中,在 TLSv1.2 之前是在握手阶段发送会话 ID ,
其它
TLSv1.3
对于 TLSv1.3 版本之后,Session Ticket 会在握手成功之后发送,此时需要调用 SSL_read()
函数,对于 OpenSSL 来说才会完成 Session 信息的接收。
这同样也意味着,如果客户端检查证书时发现异常,如果此时直接强制关闭文件描述符,而再触发 SSL_read()
时就可能会发生 EPIPE
报错,而且关闭时如果没有很好处理,那么就可能会导致资源泄漏。
关闭
需要通过 SSL_shutdown()
函数关闭,详细可以查看 man 3 SSL_shutdown 帮助文档
可以强制关闭链接,但可能会导致部分资源的泄漏,所以最好的方式是 Two-Way Shutdown ,也就是客户端可能仍然需要通过 SSL_read()
读取报文,例如上述的未完全读取会话时。
参考
- SSL Programming Tutorial 比较简单清晰介绍各个流程,不过有点老。