C 语言使用 SSL

2018-07-08 security c/cpp tls/ssl

无校验

$ 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() 读取报文,例如上述的未完全读取会话时。

参考