现在的位置: 首页 > 综合 > 正文

OpenSSL编程指引,第一部分(2)

2018年03月15日 ⁄ 综合 ⁄ 共 13742字 ⁄ 字号 评论关闭
The Client
客户端

Once the client has initialized the SSL context, it's ready to connect to the server. OpenSSL requires us to create a TCP connection between client and server on our own and then use the TCP socket to create an SSL socket. For convenience, we've isolated the
creation of the TCP connection to the tcp_connect() function (which is not shown here but is available in the downloadable source).

一旦客户端初始化了SSL上下文,它准备连接到服务器。OpenSSL需要我们自己创建一个客户端与服务器的TCP连接,然后使用TCP套接字创建一个SSL套接字。为了方便,我们把TCP连接放在tcp_connect()函数中(没有在这里展示,但是可以在可下载的源码那里看到)。

Once the TCP connection has been created, we create an SSL object to handle the connection. This object needs to be attached to the socket. Note that we don't directly attach the SSL object to the socket. Rather, we create a BIO object using the socket and
then attach the SSL object to the BIO.

一旦TCP连接创建,我们创建SSL对象来处理连接。这个对象需要附加在socket上。注意我们并不是直接把SSL对象附加给socket。而是使用socket创建一个BIO对象然后把SSL对象附加在BIO上。

This abstraction layer allows you to use OpenSSL over channels other than sockets, provided you have an appropriate BIO. For instance, one of the OpenSSL test programs connects an SSL client and server purely through memory buffers. A more practical use would
be to support some protocol that can't be accessed via sockets. For instance, you could run SSL over a serial line.

这个抽象层提供给你一个合适的BIO,允许你在通道而不是socket之上使用OpenSSL。例如,一个OpenSSL测试程序通过内存缓存连接SSL客户端和服务器。更典型的情况是可以支持那些不经过socket访问的协议。比如,你可能在串口上运行SSL协议。

The first step in an SSL connection is to perform the SSL handshake. The handshake authenticates the server (and optionally the client) and establishes the keying material that will be used to protect the rest of the traffic. The SSL_connect() call is used
to perform the SSL handshake. Because we're using blocking sockets, SSL_connect() will not return until the handshake is completed or an error has been detected. SSL_connect() returns 1 for success and 0 or negative for an error. This call is shown below:

一个SSL连接的第一步是执行SSL握手。握手验证服务器(以及可选的客户端)并建立加密材料来保护接下来的通讯。调用SSL_connect()来执行SSL握手。由于我们使用阻塞的套接字,在握手完成之后或者出现错误,SSL_connect()才会返回。SSL_connect()返回1表示成功,0或者负数表示失败。这个调用如下所示:

/* Connect the TCP socket*/
sock=tcp_connect(host,port);
/* Connect the SSL socket */
ssl=SSL_new(ctx);
sbio=BIO_new_socket(sock,BIO_NOCLOSE);
SSL_set_bio(ssl,sbio,sbio);
if(SSL_connect(ssl)<=0)
  berr_exit("SSL connect error");
if(require_server_auth)
  check_cert(ssl,host);

When we initiate an SSL connection to a server, we need to check the server's certificate chain. OpenSSL does some of the checks for us, but unfortunately others are application specific, and so we have to do those ourselves. The primary test that our sample
application does is to check the server identity. This check is performed by the check_cert function, shown in Listing 2.

当我们初始化一个SSL连接到服务器,我们需要检查服务器的证书链。OpenSSL为我们做了一些检查,但是不幸的是其他的是应用程序特有的,因此我们必须自己检查。我们示例程序的主要测试时检查服务器id。检查有check_cert函数执行,在列表2中显示。

Listing 2. check_cert() Function

列表2.check_cert()函数

Once you've established that the server's certificate chain is valid, you need to verify that the certificate you're looking at matches the identity that you expect the server to have. In most cases, this means that the server's DNS name appears in the certificate,
either in the Common Name field of the Subject Name or in a certificate extension. Although each protocol has slightly different rules for verifying the server's identity, RFC 2818 contains the rules for HTTP over SSL/TLS. Following RFC 2818 is generally a
good idea unless you have some explicit reason to do otherwise.

一旦你已经建立了服务端验证链是有效的连接,你需要验证你期望服务器拥有的匹配id的证书。在大多数情况下,这意味着服务器的DNS名出现在证书中,不是持有名的常用名域就是在证书扩展域中。尽管每个协议对验证服务器标示的方法有轻微不同,RFC2818包含通过SSL/TLS协议的HTTP服务器的规则。追随RFC 2818一般是个好主意,除非你有一些特殊的原因。

Because most certificates still contain the domain name in the Common Name field rather than in an extension, we show only the Common Name check. We simply extract the server's certificate using SSL_get_peer_certificate() and then compare the common name to
the hostname we're connecting to. If they don't match, something is wrong and we exit.

由于大多数证书仍然包含域名是在常用名域而不是在扩展名中,我们仅仅做常用名检测。我们使用SSL_get_peer_certificate()解析服务器证书然后把常用名与我们要连接的主机名做比较。如果他们不匹配,事情就不对,我们退出。

Before version 0.9.5, OpenSSL was subject to a certificate extension attack. To understand this, consider the case where a server authenticates with a certificate signed by Bob, as shown in Figure 1. Bob isn't one of your CAs, but his certificate is signed
by a CA you trust.

在0.9.5版本之前,OpenSSL受证书扩展名攻击。要理解它,考虑这个情况,一个服务器有一个Bob签名的证书,像图1显示的那样。Bob不是你CA的一员,但是这个证书被你信任的CA签名了。

Figure 1. An Extended Certificate Chain

If you accept this certificate you're going to be in a lot of trouble. The fact that the CA signed Bob's certificate means that the CA believes that it has verified Bob's identity, not that Bob can be trusted. If you know that you want to do business with Bob,
that's fine, but it's not very useful if you want to do business with Alice, and Bob (of whom you've never heard) is vouching for Alice.

如果你接受这个证书你将有很多麻烦。事实是,CA签名了Bob的证书表示CA相信它有一个信任的Bob的id,并不是表示Bob可以被信任。如果你知道你在与Bob交易,这很好,但是在你不想与Alice交易,而Bob是Alice的凭据的时候,这就没什么用了。

Originally, the only way to protect yourself against this sort of attack was to restrict the length of certificate chains so that you knew that the certificate you were looking at was signed by the CA. The X.509 version 3 contains a way for a CA to label certain
certificates as other CAs. This permits a CA to have a single root that then certifies a bunch of subsidiary CAs.

起初,保护你不受这种攻击的唯一方法是限制证书链的长度,这样你可以知道你寻找的证书被CA签名。X。509版本3包含一个方法,可以让CA标签一个为其他的CA的证书。这允许CA拥有一个单独的根来约束一群子CA。

Modern versions of OpenSSL (0.9.5 and later) check these extensions, so you're automatically protected against extension attacks whether or not you check chain length. Versions prior to 0.9.5 do not check the extensions at all, so you have to enforce the chain
length if using an older version. 0.9.5 has some problems with checking, so if you're using 0.9.5, you should probably upgrade. The #ifdefed code in initialize_ctx() provides chain-length checking with older versions. We use the SSL_CTX_set_verify_depth()
to force OpenSSL to check the chain length. In summary, it's highly advisable to upgrade to 0.9.6, particularly because longer (but properly constructed) chains are becoming more popular. The absolute latest (and best) version of OpenSSL is .0.9.66.

OpenSSL的流行版本(0.9.5以及以后)检查这些扩展,因此你检查链长度来自动保护不受扩展攻击。0.9.5以前的版本没有检查扩展,因此如果你用老版本你必须强制设定链的长度。0.9.5也有一些问题,如果你正在使用0.9.5,你应该尽可能的升级。在_ctx()方法的初始化代码中的#ifdefed语句部分提供老版本检查链长度的功能。我们使用SSL_CTX_set_verify_depth()
来强制检查证书链长度。总的来说,强烈建议升级到0.9.6,特别是在长链变得越来越流行的时候。OpenSSL最新的(并且最好的)版本是0.9.66.

We use the code shown in Listing 3 to write the HTTP request. For demonstration purposes, we use a more-or-less hardwired HTTP request found in the REQUEST_TEMPLATE variable. Because the machine to which we're connecting may change, we do need to fill in the
Host header. This is done using snprintf(). We then use SSL_write() to send the data to the server. SSL_write()'s API is more or less the same as write(), except that we pass in the SSL object instead of the file descriptor.

我们使用列表3展示的代码来写一个HTTP请求。为演示目的,我们或多或少使用在REQUEST_TEMPLATE 变量的硬编码的HTTP请求。由于我们链接的机器可能改变,我们需要填充主机名。这由snprintf()完成。我们使用SSL_write()来发送数据给服务器。SSL_write()API或多或少与write()类似,出了我们传递SSL对象而不是文件描述符。

Listing 3. Writing the HTTP Request 

列表3.写一个HTTP请求

Experienced TCP programmers will notice that instead of looping around the write, we throw an error if the return value doesn't equal the value we're trying to write. In blocking mode, SSL_write() semantics are all or nothing; the call won't return until all
the data is written or an error occurs, whereas write() may only write part of the data. The SSL_MODE_ENABLE_PARTIAL_WRITE flag (not used here) enables partial writes, in which case you'd need to loop.

有经验的TCP程序员知道,如果一个值不等于我们想写入的值,就抛出一个错误,而不是尝试循环的写。在阻塞模式中,SSL_write()全有或全无。这个调用在数据完全写入或者出现错误之前不会返回,而write()仅写入部分数据。SSL_MODE_ENABLE_PARTIAL_WRITE 标记启用部分写入,这个模式你需要循环。

In old-style HTTP/1.0, the server transmits its response and then closes the connection. In later versions, persistent connections that allow multiple sequential transactions on the same connection were introduced. For convenience and simplicity we will not
use persistent connections. We omit the header that allows them, causing the server to use a connection close to signal the end of the response. Operationally, this means that we can keep reading until we get an end of file, which simplifies matters considerably.

在老版本的HTTP/1.0中,服务器传输他的响应然后关闭连接。后面的版本,引进了保持连接并允许在同一个连接上多个队列的事物。为了便利和简单,我们不使用保持连接。我们省略头,来使服务器关闭连接通知响应结束。操作上,这意味着我们可以读完整个文件,这相当的省事。

OpenSSL uses the SSL_read() API call to read data, as shown in Listing 4. As with read(), we simply choose an appropriate-sized buffer and pass it to SSL_read(). Note that the buffer size isn't really that important here. The semantics of SSL_read(), like the
semantics of read(), are that it returns the data available, even if it's less than the requested amount. On the other hand, if no data is available, then the call to read blocks.

OpenSSL使用SSL_read()API来读取数据,如4显示的那样。使用read(),我们简单的选择一个合适尺寸的缓存来传递给SSL_read()。注意缓存大小在这里并不是十分重要。SSL_read()的语义,与read()类似,返回可用的数据,即使它少于请求的数量。另一方面,如果数据不可用,调用会阻塞。

Listing 4. Reading the Response

清单4.读取响应

The choice of BUFSIZZ, then, is basically a performance trade-off. The trade-off is quite different here from when we're simply reading from normal sockets. In that case, each call to read() requires a context switch into the kernel. Because context switches
are expensive, programmers try to use large buffers to reduce them. However, when we're using SSL the number of calls to read(), and hence context switches, is largely determined by the number of records the data was written in rather than the number of calls
to SSL_read().

BUFSIZE的选择基本上是与性能相关的。当我们简单的从正常的套接字读取的时候,权衡是很不同的。在这种情况下,每个read()需要切换内核上下文。由于上下文切换是昂贵的,程序员尝试使用更大的缓存来减少损耗。然而,当使用SSL的read调用而产生上下文切换的时候,大部分决定于要写入数据记录的数量而不是SSL_read()的调用数量。

For instance, if the client wrote a 1000-byte record and we call SSL_read() in chunks of 1 byte, then the first call to SSL_read() will result in the record being read in, and the rest of the calls will just read it out of the SSL buffer. Thus, the choice of
buffer size is less significant when we're using SSL than with normal sockets. If the data were written in a series of small records, you might want to read all of them at once with a single call to read(). OpenSSL provides a flag, SSL_CTRL_SET_READ_AHEAD,
that turns on this behavior.

例如,客户端写一个1000字节的记录并且调用SSL_read读取每次1字节块,这样第一次调用SSL_read()将读入所有记录,接下来的调用将从SSL缓存读取出来。这样,对于使用SSL的普通套接字来说缓存大小没什么意义。如果记录以小记录序列的形式写入,你可能调用一次read()来读取所有数据。OpenSSL提供SSL_CTRL_SET_READ_AHEAD标记来打开这个功能。

Note the use of the switch on the return value of SSL_get_error(). The convention with normal sockets is that any negative number (typically -1) indicates failure, and that one then checks errno to determine what actually happened. Obviously errno won't work
here because that only shows system errors, and we'd like to be able to act on SSL errors. Also, errno requires careful programming in order to be threadsafe.

注意开启SSL_get_error()函数的返回值的使用。socket的正常约定是任何负数的返回值(典型的-1)指示失败,然后检查错误码来决定真正的原因。显然这里错误码没用,因为那仅仅指示系统错误,而我们想要SSL错误。而且,为了线程安全,错误码需要仔细的编程。

Instead of errno, OpenSSL provides the SSL_get_error() call. This call lets us examine the return value and figure out whether an error occurred and what it was. If the return value was positive, we've read some data, and we simply write it to the screen. A
real client would parse the HTTP response and either display the data (e.g., a web page) or save it to disk. However, none of this is interesting as far as OpenSSL is concerned, so we won't show any of it here.

出了错误码,OpelSSL提供SSL_get_error()调用。这个函数让我们检测返回值是否是错误并且是什么错误。如果返回值是整数,我们读一些数据,然后我们简单的打印到屏幕。一个真正的客户端可能解析HTTP响应并线束数据(例如 web浏览器)或者保存到磁盘。然而,目前他们都与OpenSSL无关,因此我们这里不做说明。

If the return value was zero, this does not mean that there was no data available. In that case, we would have blocked, as discussed above. Rather, it means that the socket is closed, and there never will be any data available to read. Thus, we exit the loop.

如果返回值是0,这并不是意味着没有数据可用。在这个情况下,我们可能阻塞了,就像前面说过的。这表示套接字已经关闭了,我们再也不可能读取任何数据。这样,我们推出循环。

If the return value was something negative, then some kind of error occurred. There are two kinds of errors we're concerned with: ordinary errors and premature closes. We use the SSL_get_error() call to determine which kind of error we have. Error handling
in our client is pretty primitive, so with most errors we simply call berr_exit() to print an error message and exit. Premature closes have to be handled specially.

如果返回值是一些负数,表示某些错误出现了。我们关心两种错误:普通错误和提前关闭。我们使用SSL_get_error()来决定出现了什么错误。我们客户端的错误处理是非常原始的,因此我们对于大部分错误简单的调用berr_exit()来打印错误消息然后退出。提前关闭必须特殊处理。

TCP uses a FIN segment to indicate that the sender has sent all of its data. SSL version 2 simply allowed either side to send a TCP FIN to terminate the SSL connection. This allowed for a truncation attack; the attacker could make it appear that a message was
shorter than it was simply by forging a TCP FIN. Unless the victim had some other way of knowing what message length to expect, he or she would simply believe the length was correct.

TCP使用FIN片段来指示发送者是否发送完了所有数据。SSL的第二个版本简单的允许任何一方发送一个TCP FIN来终止SSL连接。这可能遭受截断攻击;攻击者可能简单的强制一个TCP FIN使一个消息比实际更短。除非受害人知道他期望的消息的长度,否则他或她就只能相信这个长度是正确的。

In order to prevent this security problem, SSLv3 introduced a “close_notify” alert. The close_notify is an SSL message (and therefore secured) but is not part of the data stream itself and so is not seen by the application. No data may be transmitted after
the close_notify is sent.

为了防止安全问题,SSLv3引进了“关闭_通知(close_notify)”警告。close_notify是一个SSL消息(因此也是安全的)但是不熟数据流的一部分,因此对应用程序透明。在close_notify消息发送之后就不会再发送任何消息。

Thus, when SSL_read() returns 0 to indicate that the socket has been closed, this really means that the close_notify has been received. If the client receives a FIN before receiving a close_notify, SSL_read() will return with an error. This condition is called
a premature close.

这样,SSL_read()返回0指示套接字已经关闭,这真的表示close_notify已经接收了。如果客户端在close_notify之前接受一个FIN,SSL_read()将返回一个错误。这种情况叫做提前关闭。

A naïve client might decide to report an error and exit whenever it received a premature close. This is the behavior that is implied by the SSLv3 specification. Unfortunately, sending premature closes is a rather common error, particularly common with clients.
Thus, unless you want to be reporting errors all the time, you often have to ignore premature closes. Our code splits the difference. It reports the premature close on stderr but doesn't exit with an error.

当接受到一个提前关闭消息错误的时候,客户端决定报告错误和退出。这是SSLv3规范的默认的行为。不幸的是,发送提前关闭时一个常见错误,在客户端特别常见。因此,除非你想在任何时候报告错误,你通常必须忽略提前关闭。我们的代码分开区别。它在stderr端输出错误但是不退出。

If we read the response without any errors, then we need to send our own close_notify to the server. This is done using the SSL_shutdown() API call. We'll cover SSL_shutdown() more completely when we talk about the server, but the general idea is simple: it
returns 1 for a complete shutdown, 0 for an incomplete shutdown and -1 for an error. Since we've already received the server's close_notify, about the only thing that can go wrong is that we have trouble sending our close_notify. Otherwise SSL_shutdown() will
succeed (returning 1).

如果我们没有任何错误的读取响应,然后我们需要发送我们自己的close_notify消息给服务器。这使用SSL_shutdown()API完成。在讨论服务器的时候我们会完整的说明SSL_shutdown(),但是想法很简单:完整关闭返回1,0是一个不完整的关闭,-1指示错误。既然我们已经接受了服务器的close_notify,唯一肯能有麻烦的地方导致出错是发送我们的close_notify的时候。另外SSL_shutdowm()将成功(返回1)。

Finally, we need to destroy the various objects we've allocated. Since this program is about to exit, thus freeing the objects, this isn't strictly necessary, but it would be in a more general program.

最后,我们需要销毁我们创建的各种对象。由于程序将要退出,因此释放对象并不熟非常必要,但是他是一般普通的程序的做法。

抱歉!评论已关闭.