内容


使用 OpenSSL API 进行安全编程,第 3 部分

提供安全服务

OpenSSL 提供必要的能力

Comments

系列内容:

此内容是该系列 # 部分中的第 # 部分: 使用 OpenSSL API 进行安全编程,第 3 部分

敬请期待该系列的后续内容。

此内容是该系列的一部分:使用 OpenSSL API 进行安全编程,第 3 部分

敬请期待该系列的后续内容。

本系列文章的前两部分讨论了使用 OpenSSL 来创建客户机端应用程序的内容。第 1 部分 讨论了使用 OpenSSL 创建基本安全客户机的问题,而 第 2 部分 则深入讨论了有关数字证书的问题。在阅读本文的读者给我发回很多 e-mail 和正面反馈之后,我非常清楚,接下来的一期理论介绍应该是有关服务器的。

服务器为网络和 Internet 提供了访问诸如文件和设备之类的资源的访问能力。有时我们必须要通过一个安全通道来提供这些服务。OpenSSL 让我们可以使用安全通道和开放通道来编写服务。

使用 OpenSSL 来创建基本的服务器应用程序从本质上来说几乎 等同于创建一个基本的客户机应用程序。二者之间的区别不多。显然,区别之一就是服务器将被设置为接收到达的连接,而不是建立外发的连接。并且,正如我们可以从本系列的第 2 部分有关数字证书的讨论中看到的一样,服务器还必须要在握手过程中提供安全证书。

等待

服务器基本上就是呆在那里等待到达的连接。毕竟,这就是服务器存在的原因。Web 服务器要等待浏览器请求页面,FTP 服务器要等待客户机请求文件,聊天服务器要等待聊天客户机所发出的连接。因此服务器要做的事情就是等待。

在客户机和服务器通信之间差别不大,惟一的差别就是对于握手来说,服务器就像是硬币的反面。其他东西都是相同的。

这让我们可以使用 OpenSSL 来编写安全的服务器应用程序,再次假设您已经了解如何使用 OpenSSL 来编写客户机应用程序。(如果您还不了解相关知识,请参阅本系列第 1 部分 “API 概述” 来学习如何设置 OpenSSL 库。)

两种形式的标识

也可以说是标识的两部分。

服务器要负责提供在握手过程中使用的安全证书。完整的服务器证书包括两个部分:公钥和私钥。公钥是发送给客户机的,而私钥则是保密的。

就像是信任证书必须要提供给客户机应用程序使用的库一样,服务器密钥也必须要提供给服务器应用程序使用的库。有几个函数都提供了这种功能:

清单 1. 加载服务器证书的函数
SSL_CTX_use_certificate(SSL_CTX *, X509 *)
SSL_CTX_use_certificate_ASN1(SSL_CTX *ctx, int len, unsigned char *d);
SSL_CTX_use_certificate_file(SSL_CTX *ctx, const char *file, int type);

这个函数的 ASN1 变种可以将指定内存位置处的使用 ASN1 编码的数字证书加载到 SSL 环境中。这个函数会加载给定内存结构中所提供的一个 X.509 证书;而最后一个函数,即带有 _file 的那个,会从文件中加载一个使用 PEM 编码的数字证书。这个函数的 type 参数让我们可以加载使用 DER 编码的证书。

要加载私钥,请使用下面函数之一:

清单 2. 加载私钥使用的函数
SSL_CTX_use_PrivateKey(SSL_CTX *ctx, EVP_PKEY *pkey);
SSL_CTX_use_PrivateKey_ASN1(int pk, SSL_CTX *ctx, unsigned char *d, long len);
SSL_CTX_use_PrivateKey_file(SSL_CTX *ctx, const char *file, int type);
SSL_CTX_use_RSAPrivateKey(SSL_CTX *ctx, RSA *rsa);
SSL_CTX_use_RSAPrivateKey_ASN1(SSL_CTX *ctx, unsigned char *d, long len);
SSL_CTX_use_RSAPrivateKey_file(SSL_CTX *ctx, const char *file, int type);

需要的交互

私钥最适合用来加密存储。不过,问题是加载证书的函数并没有请求使用加密证书的密码。相反,OpenSSL 为获得密码提供了一种回调机制。

回调格式如下:

清单 3. 回调格式
int password_callback(char *buf, int size, int rwflag, void *userdata);

就本文的目的来说,最后一个参数 userdata 是不需要的。缓冲区在调用这个函数之前被调用,因此对于这个缓冲区的大小我们无法控制。

参数 rwflag 是读/写标记。使用它的目的是使我们可以编程确定这个密码是正在用来加密信息(rwflag = 1)还是解密信息(rwflag = 0)。如果正在使用回调函数来请求对数据进行加密使用的密码,最好是以某种方式请求两次,这样可以多一次机会接收用户的输入。

在证书加载时,这个密码只会请求一次,这样它就可以进行解密并保存到内存中了。如何从用户那里获取密码,完全取决于您的实现。

一旦我们创建好密码回调函数,就可以按照下面的方法使用 SSL_CTX_set_default_passwd_cb 将其安装到 SSL 环境中:

清单 4. 安装回调函数
/* ctx is a pointer to a previously created SSL context, and cb is the pointer
 * to the callback function you created.
 */

SSL_CTX_set_default_passwd_cb(ctx, cb);

发动引擎

现在提示用户输入密码的回调函数已经创建好了,然后我们就可以使用真正导入证书的函数了。证书可以从现有的内存结构或文件中导入。

为了更加符合处理数字证书的常用情况,例如 Apache HTTP Server Project 项目所作的一样,我将展示如何从文件中加载证书。如果我们已经阅读了本系列文章的 第 1 部分,那么加载证书的的方式就非常类似于在前面文章中给出的加载信任存储的方式。

我们将从公用证书开始介绍,这个证书会发送给客户机。

清单 5. 加载公用证书
/**
 * ctx is the SSL context created earlier
 */

if(SSL_CTX_use_certificate_file(ctx, "/path/to/certificate.pem", SSL_FILETYPE_PEM) < 1)
{
    /* Handle failed load here */
}

在加载公用证书之后,就必须加载私有证书了。这是在握手过程中需要的,因为客户机在这个过程中正将这些信息发送给对公钥进行加密的服务器。这些数据只能使用私钥进行解密。同样,为了保持一致,我们也将从文件中加载密钥。

清单 6. 加载私钥
if(SSL_CTX_use_PrivateKey_file(ctx, "/path/to/private.key", SSL_FILETYPE_PEM) < 1)
{
    /* Handle failed load here */
}

完成设置

在设置好环境(请参看上面的 SSL 环境)和加载密钥之后,现在应该通过创建 BIO 对象来完成设置了。我们可以回想一下在 第 1 部分 中是如何使用 OpenSSL BIO 库来建立 SSL 和非 SSL 通信的。为了与这篇文章保持一致,我们在本文中也将实现同样的功能。

清单 7. BIO 指针
BIO *bio, *abio, *out;

3 个 BIO 对象?为什么我们需要使用 3 个 BIO 对象呢?这么做有一个目的,请相信我。(记住,信任和安全性的目标是一致的。)

第一个指针 bio 是主要的 BIO 对象,它可以从 SSL 环境中创建。第二个对象 abio 是接受连接使用的 BIO,用来接收到达的连接。第三个 BIO out,是服务器发往客户机使用的对象。

清单 8. 设置主要的 BIO 对象
bio = BIO_new_ssl(ctx, 0);
if(bio == NULL)
{
    /* Handle failure here */
}

/* Here, ssl is an SSL* (see Part 1) */

BIO_get_ssl(bio, &ssl);
SSL_set_mode(ssl, SSL_MODE_AUTO_RETRY);

此处对 BIO 对象的设置与客户机连接使用的 BIO 设置稍有不同。我们可以回想一下第 1 部分中客户机连接是使用 BIO_new_ssl_connect 建立的。

此处,设置是使用 BIO_new_ssl 加上两个参数建立的:一个指向 SSL_CTX 对象的指针和一个标记。这个标记告诉 OpenSSL 要创建哪种 BIO 对象:0 用于服务器,1 用于客户机。由于这段代码试图建立客户机连接,因此这个标记就应该设置为 0。

清单 9. 设置接收 BIO
abio = BIO_new_accept("4422");
BIO_set_accept_bios(abio, bio);

其中 BIO_do_connect 为客户机连接创建 BIO, BIO_new_accept 为服务器连接创建 BIO。它只需要一个参数,就是监听的端口,它被编码在字符串中。

由于这假设正在监听安全连接,因此我们需要将一个安全 BIO 链接到这个接收 BIO 上。这是第二个函数调用 BIO_set_accept_bios 使用的地方。她将前面创建的 SSL BIO 连接到接收 BIO 上。

这个函数调用不需要释放 SSL BIO。在销毁接收 BIO 之后,它会自动被释放。

现在坐下来等待吧

服务器就像是一个渔夫;它只需要坐在那里等待客户机上钩就好了。服务器玩的就是等待游戏,只需要等待客户机连接到达即可。

如果曾经有过使用 Winsock 或 BSD Socket 进行编程的经验,就很可能具备 accept 函数的使用经验。OpenSSL 中的对应部分是 BIO_do_accept,不过我们不是只调用一次 accept 然后等待,而是在等待之前,必须要调用 BIO_do_accept 两次。

清单 10. 告诉服务器 “坐下来”
/* First call to set up for accepting incoming connections... */

if(BIO_do_accept(abio) <= 0)
{
    /* Handle fail here */
}

/* Second call to actually wait */

if(BIO_do_accept(abio) <= 0)
{
    /* Handle fail here */
}

/* Any other call will cause it to wait automatically */

第一次调用 BIO_do_accept 会设置 BIO 来接收到达连接。第二次调用需要真正坐下来等待。此后任何时候都允许它等待。

响应到达连接

BIO_do_accept 在接收到到达连接时会返回 1。不过我们不能只通过接收 BIO 进行通信。相反,OpenSSL 会创建另外一个 BIO,它必须使用 BIO_pop 来弹出接收 BIO。

清单 11. 弹出连接进行通信
out = BIO_pop(abio);

if(BIO_do_handshake(out) <= 0)
{
    /* Handle fail here */
}

在弹出到达的连接到接收 BIO 之后,握手需要使用一个到 BIO_do_handshake 的调用进行处理。如果前面几节中的设置成功了,那么握手在这里也应该会成功。

服务器会通过 BIO 库的各种读写函数真正与客户机进行通信。在 第 1 部分 中我们已经讨论了有关这些问题的内容,因此我们可以在本文中找到更多讨论内容。

提供卓越的服务

总体来说,一旦理解这一切是如何工作的,使用 OpenSSL 创建安全的服务器应用程序就没什么困难了。从现在开始,我们就可以对所提供的样例代码进行扩展,从而创建一个完全版本的 SSL 服务器应用程序来满足我们的要求了。不过要预先警告一下,此处以及 下载一节 所提供的代码样例都进行了尽量简化,应该只适用于实验的目的。在真正创建一个完整的 SSL 服务器应用程序之前,请确保阅读并研究最新的安全建议。


下载资源


相关主题


评论

添加或订阅评论,请先登录注册

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Linux, Open source
ArticleID=173616
ArticleTitle=使用 OpenSSL API 进行安全编程,第 3 部分: 提供安全服务
publish-date=11092006