1. 程式人生 > >OpenSSL程式設計指引,第一部分(2)

OpenSSL程式設計指引,第一部分(2)

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中顯示。

列表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物件而不是檔案描述符。

列表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()類似,返回可用的資料,即使它少於請求的數量。另一方面,如果資料不可用,呼叫會阻塞。

清單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.

最後,我們需要銷燬我們建立的各種物件。由於程式將要退出,因此釋放物件並不熟非常必要,但是他是一般普通的程式的做法。