烦恼一般都是想太多了。

0%

使用OpenSSL进行编程

本篇文章来自 使用 OpenSSL API 进程安全编程。因为突然有一个项目需要用到相关的东西,而之前都是使用的高级语言,如 Python 这些,所以需要从头来了解一下这个东西。更何况,提供的 API 太多,不是很清楚到底是用来干什么的。

前言

这篇文章对于 OpenSSL API 来说还是有点模糊的。在 OpenSSL API 的使用上,并没有太多的文档,因此想在一个应用程序里面使用它的时候可能会遇到麻烦。因此,你会如何使用 OpenSSL 来实现一个安全的连接呢?这边文章就是解决这个问题。

学习如何实现 OpenSSL 的一部分问题是:文档是不完全的。一个不完整的 API 文档会让开发者不会去使用那些 API,但 OpenSSL 还是这么的强壮和流行,为何?

OpenSSL 是最为知名的关于安全连接的开源库。它自 1998 年从 Eric Young 和 Tim Hudson 开发的 SSLeay 库开始开发。其他的 SLL 工具集包括 GNU TLS,使用 GPL 许可发布;还有 Mozilla 的 NSS。

那是什么东西让 OpenSSL 比 GNU TLS,Mozilla NSS,或者其他的库更好呢?许可证是一个原因。GNU TLS 只支持 TLS v1.0 和 SSL v3.0.

Mozilla NSS 在 Mozilla Public License 和 GPL 许可下发布,开着了可以自由选择。但是 NSS 需要其他更多的外部库,而 OpenSSL 完全是一个自包含的库。同时,和 OpenSSL 一样, NSS 的文档也是不完整的。 NSS 有 PKCS #11 的支持,这是用来加密加密 Token 的,OpenSSL 没有这个支持。

SSL 是什么

SSL 是 Secure Sockets Layer 的简写,也就是安全套接字层的意思。它是在互联网上进行安全传输的标准,其将数据加密在协议中。数据在离开主机前会被加密,同时只会数据到达目标主机前被解密一次。证书和加密算法在后面进行支持,使用 OpenSSL ,对于证书和加密我们都可以进行操作。

理论上,在数据到达目标主机前被拦截或者窃听,但没有什么机会破解这些数据。但随者计算机的速度越来越快,破解 SSL 加密数据的可能性也在增大。

SSL 和安全连接可以应用在网络上的任何协议,如 HTTP,POP3,FTP。SSL 也能用来加密 Telnet 会话。虽然任何连接都能使用 SSL,但并没有这个必要。只要在连接会携带敏感数据的时候才需要使用。

OpenSSL 是什么

OpenSSL 不止是 SSL。它是信息摘要 、文件加解密、数据证书、数字签名、随机数的综合体。想要把 OpenSSL 放在一篇文章中,那是非常困难的。

OpenSSL 也止是 API,它也有一个命令行工具。命令行工具可以做和 API 一样的事情,但是走得更远一点,允许测试 SSL 服务器和客户端。

头文件和初始化

本篇文章中只需要三个头文件:ssl.h, bio.h, err.h。所有三个文件都在 openssl 的子目录中。

/* OpenSSL headers */

# include "openssl/bio.h"
# include "openssl/ssl.h"
# include "openssl/err.h"

/* Initializing OpenSSL */

SSL_load_error_strings();
ERR_load_BIO_strings();
OpenSSL_add_all_algorithms();

不安全连接

OpenSSL 使用了一个抽象的库,BIO,来处理多中类型的通信,包括文件和套接字,安全与不安全的。它也可以被当做一个过滤器,比如 UU 或 Base64 编码。

BIO 完全解释起来很复杂,我们只会在需要的时候对其介绍逐步的介绍。首先,我们展示一下如何设置一个标准的套接字连接。这只需要几行 BSD 套接字库写几行代码。

当我们设置一个连接的时候,无论安全与否,一个指向 BIO 对象的指针都是必须的。这就像是标准 C 的 文件流对象 FILE 一样。

/* OpenSSL headers */

# include "openssl/bio.h"
# include "openssl/ssl.h"
# include "openssl/err.h"

/* Initializing OpenSSL */

SSL_load_error_strings();
ERR_load_BIO_strings();
OpenSSL_add_all_algorithms();

打开一个连接

我们需要使用 BIO_new_connect() 来打开一个新的连接。我们可以在这个调用中指定主机名和端口,这将会尝试打开连接。也可以将这两个步骤分开:BIO_new_connect 来建立一个连接和设置主机名,使用 BIO_set_conn_port(BIO_set_conn_int_port)来设置端口号。

bio = BIO_new_connect("hostname:port");
if(bio == NULL)
{
/ Handle the failure /
}



if(BIO_do_connect(bio) <= 0)
{
/ Handle failed connection /
}

无论怎么搞,只要主机名和端口都给 BIO 对象设置了,它就会尝试打开连接。没有其他方式。如果在建立 BIO 对象的过程中出现了问题,返回的指针会是 NULL。我们必须调用 BIO_do_connect 来验证连接是否成功。

在上面的例子中,第一行用特定的主机和端口来建立一个新的 BIO 对象。BIO_do_connect 检查连接是否成功建立,它会返回 0, -1 或者 错误。

与服务器通信

无论是套接字还是文件,当我们读写BIO 对象的时候,都会用到两个函数:BIO_read, BIO_write。很简单是吧。

BIO_read 会尝试从服务器读取指定长度的字节,返回独到的字节数,或者 0, -1。在一个阻塞的连接上,0 表示连接已经关闭,-1 表示发生了错误。在一个非阻塞的端口上,0 表示没有数据,-1 表示发生了错误。要判断错误是否是可恢复的,使用 BIO_should_retry

int x = BIO_read(bio, buf, len);
if(x == 0)
{
/ Handle closed connection /
}
else if(x < 0)
{
if(! BIO_should_retry(bio))
{
/ Handle failed read here /
}



/ Do something to handle the retry /
}

BIO_write 会将字节写到套接字,返回它真正写进去的字节数,0,或者 -1.和 BIO_read 一样,0 和 -1 并不必须用来表示一个错误。BIO_should_retry 才是正确的使用方式。如果写操作需要重做,那么参数必须和上一次相同。

关闭连接

关闭连接就很简单了。我们可以使用两者方式:BIO_reset, BIO_free_all。如果我们要重用 BIO 对象的话,那就使用第一种。

BIO_rese 会关闭连接,重置 BIO 对象的内部状态,以便复用。

BIO_free_all 则会释放所有的内部结构和内存,关闭相关的套接字。如果 BIO 是在 类里面使用,这应该放在析构函数里面。

/ To reuse the connection, use this line /



BIO_reset(bio);



/ To free it from memory, use this line /



BIO_free_all(bio);

安全连接

现在是开始关注在设置安全连接的时候需要什么了。不同之处在于我们如何设置和建立连接。其他都是一样的。

安全连接需要在连接建立后进行一个握手,handshake。在握手中,服务器发送证书到客户端,客户端使用一些受信任的证书进行验证。它也会检查证书是否过期。检查一个证书是否受信任的需要我们在建立连接前就加载一个受信任的证书集。

客户端只会在服务器需要的时候发送一个证书。这被叫做客户端验证。使用这些证书,加密参数在服务器和客户端传输来建立安全连接。尽管握手是在连接建立后进行,但是客户端和服务器都可以随时请求一个新的握手。

设置一个安全连接

设置一个安全连接就需要更多的代码了。另外一个需要的指针就是 SSL_CTX。这是一个用来引用 SSL 信息的结构。它也被用来通过 BIO 库设置 SSL 连接。这个结构通过 SSL_CTX_new来建立,同时还需要给他提供一个 SSL 方法函数,典型的是 SSLv23_client_method

另外一个指针,SSL 用来引用 SSL 连接结构(这在某些会在段时间完成的事情是必须的)。这个 SSL 后面可以用来测试连接的信息或者设置 额外的SSL参数。

SSL_CTX  ctx = SSL_CTX_new(SSLv23_client_method());
SSL ssl;

设置受信任证书

在 SSL_CTX 建立后,就必须加载受信任的证书集了。这对成功验证对端证书来说是非常必要的。如果证书不能被验证或者信任,OpenSSL 将会将把证书标记为不可用的(但是连接依然继续)。

OpenSSL 本身就有一个受信任的证书集。他们在 certs 目录中。每个证书都是一个独立的文件,因此我们必须单独加载每个证书。在certs 目录下有一个目录用来保存过期的证书。加载他们会报错。

如果我们愿意,我们可以单独加再每个证书,但是为了简单, OpenSSL 发布的时候将证书打包在了一个文件中,TrustStore.pem。如果我们已经有一个受信任的证书集文件了,那我们简单的替换它就是了。

SSL_CTX_load_verify_locations 用来加载信任存储文件。需要三个参数: CTX 指针,信任存储路径,证书的目录。后两个参数我们必须指定一个。成功返回 1, 有问题返回 0.

SSL_CTX_load_verify_locations

如果我们使用一个目录来存储信任存储,那么这些文件必须用一个特定的方式命名。OpenSSL 文档中说明了是什么方式,但是这里有一个工具,用来将一个目录准备为一个路径参数。

/ Use this at the command line /



c_rehash /path/to/certfolder



/ Then call this from within the application /



if(! SSL_CTX_load_verify_locations(ctx, NULL, "/path/to/certfolder"))
{
/ Handle error here /
}

建立连接

BIO 对象使用 BIO_new_ssl_connect 来建立,使用 CTX作为它的唯一参数。 SSL 结构的指针需要进行获取。在这个文章中,SSL 指针只是在 SSL_set_mode 函数中用到。这个函数用来设置 SSL_MODE_AUTO_RETRY 标志。设置了这个标志后,如果服务器需要一个新的握手, OpenSSL 会在后台进行处理。没有这个选项,那么在服务器需要一个新的握手时,在设置重试标志时,任何读写操作都会返回一个错误

bio = BIO_new_ssl_connect(ctx);
BIO_get_ssl(bio, & ssl);
SSL_set_mode(ssl, SSL_MODE_AUTO_RETRY);

在 CTX 结构建立后,连接可以被建立。主机使用 BIO_set_conn_hostname函数。主机和端口与上面相同的方式来指定。这个函数也会打开到主机的连接。BIO_do_connect 也是必须的。

/ Attempt to connect /



BIO_set_conn_hostname(bio, "hostname:port");



/ Verify the connection opened and perform the handshake /



if(BIO_do_connect(bio) <= 0)
{
/ Handle failed connection /
}

连接一旦建立,就需要验证证书是否有效。实际上,OpenSSL 给我们做了这个事情。如果证书有严重问题——比如,hash 值无效——这个连接就不会发生。但是如果不是严重的错误——比如当过期—那连接依然可用。

我们可以使用 SSL_get_verify_result 来看看证书检查是否 OK。如果证书通过了 OpenSSL 的内部检查,包括检查是否信任,那么就会返回 X509_V_OK。如果有什么错误发生,就会返回一个错误代码。

需要注意的是,一个失败的校验并不代表着连接不可用。连接是否可用要看校验的结果和安全性设置。例如,证书信任校验可能只是简单的意味着受信任证书不可用。连接依然可用,只是有安全问题。

if(SSL_get_verify_result(ssl) != X509_V_OK)
{
/ Handle the failed verification /
}

这就是全部了。所有的通信现在都可以使用 BIO_read, BIO_write 进行了。关闭也是。

最后, CTX 结构也必须释放

SSL_CTX_free(ctx);

错误检查

OpenSSL 会抛出某些类型 的错误。它是什么意思?首先,我们需要获取错误码,ERR_get_error 做这个事情。然后,我们要让这个错误码转换成一个字符串,这些字符已经被 SSL_load_error_strings or ERR_load_BIO_strings 加载到内存了。

ERR_reason_error_string Returns a pointer to a static string, which can then be displayed on the screen, written to a file, or whatever you wish to do with it.
ERR_lib_error_string Tells in which library the error occurred.
ERR_func_error_string Returns the OpenSSL function that caused the error.
printf("Error: %s\n", ERR_reason_error_string(ERR_get_error()));

我们也可以使用库给我们一个预格式化后的字符串,调用 ERR_error_string 来做这个事情。

printf("%s\n", ERR_error_string(ERR_get_error(), NULL));

我们也可以 dump 所有的错误队列到一个文件或者 BIO。这通过 ERR_print_errors or ERR_print_errors_fp

[pid]:error:[error code]:[library name]:[function name]:[reason string]:[file name]:[line]:[optional text message]
ERR_print_errors_fp(FILE );
ERR_print_errors(BIO );

完整例子