An Introduction to OpenSSL Programming, Part II of II

Part two of the series that started in the September 2001 issue.
Client Authentication

Most modes of SSL only authenticate the server (and some infrequently used anonymous modes authenticate neither the client nor the server). However, the server can request that the client authenticate using a certificate. OpenSSL provides the SSL_CTX_set_verify() and SSL_set_verify() API calls, which allow you to configure OpenSSL to require client authentication. The only difference between the calls is that SSL_CTX_set_verify() sets the verification mode for all SSL objects derived from a given SSL_CTX--as long as they are created after SSL_CTX_set_verify() is called--whereas SSL_set_verify() only affects the SSL object it is called on.

SSL_CTX_set_verify() takes three arguments: the SSL_CTX to change, the certificate verification mode and a verification callback. The verification callback is called by OpenSSL for each certificate that is verified. This allows fine control over the verification process but is too complicated to discuss here. Check the OpenSSL man pages for more detail.

We're primarily concerned with the verification mode. The mode is an integer consisting of a series of logically or'ed flags. On the server, these flags have the following effect (on the client the effect is somewhat different):

SSL_VERIFY_NONE--don't do certificate-based client authentication

SSL_VERIFY_PEER--attempt to do certificate-based client but don't require it. Note that you must not set SSL_VERIFY_PEER and SSL_VERIFY_NONE together.

SSL_VERIFY_FAIL_IF_NO_PEER_CERT--fail if the client doesn't provide a valid certificate. This flag must be used with SSL_VERIFY_PEER.

SSL_VERIFY_CLIENT_ONCE--if you renegotiate a connection where the client has authenticated, don't requires client authentication (we won't use this flag but it's mentioned for completeness).

wserver2 offers three client authentication options, set in the switch statement shown in Listing 3. The -c switch requests client auth but doesn't require it, using SSL_VERIFY_PEER only. The -C switch requires client auth using SSL_VERIFY_PEER | SSL_VERIFY_FAIL_IF_NO_PEER_CERT. The third option is more complicated. If the -x switch is specified, we allow the client to connect without requesting client authentication. Once the client has sent its request, we then force renegotiation with client authentication. Thus, this branch of the switch does nothing.

Listing 3. Setting Client Authentication Mode

158      switch(client_auth){
159        case CLIENT_AUTH_REQUEST:
160          SSL_CTX_set_verify(ctx,SSL_VERIFY_PEER,0);
161          break;
162        case CLIENT_AUTH_REQUIRE:
163          SSL_CTX_set_verify(ctx,SSL_VERIFY_PEER |
164            SSL_VERIFY_FAIL_IF_NO_PEER_CERT,0);
165          break;
167          /* Do nothing */
168          break;
169      }

At this point you might ask, "Why not just require client authentication from the beginning?". For some applications doing so would be just as good but for a real web server it might not be. Imagine that you have a web server that requires SSL for all requests but has a super-secure section for which you want to require certificate-based authentication. Because the SSL handshake occurs before the HTTP request is transmitted, there's no way to tell in advance which part of the web site the client wants to access, and therefore whether client authentication is required.

The workaround is to allow the client to connect without client authentication. The server then reads the client's request and determines whether or not client authentication is required. If it is, it requests a new SSL handshake and requires client authentication. However, since this code is demonstration code, we don't bother to actually examine the client request. We simply force a rehandshake if the -x flag is specified.

Rehandshaking on the Server

The OpenSSL rehandshake API is unfortunately pretty complicated. In order to understand it you first need to understand a little bit about how SSL rehandshake works. Ordinarily the client initiates the SSL handshake by sending a ClientHello message to the server. The client can initiate a rehandshake simply by sending a new ClientHello. If the server wishes to initiate a rehandshake, it sends a HelloRequest message. When it receives a HelloRequest the client may--or may not--initiate a new handshake. Moreover, handshake messages are part of a different stream of data, so data can be flowing while the handshake is happening.

Listing 4 shows the code necessary to implement the rehandshake. The first thing we do is use SSL_set_verify() to tell the server to require the client to perform client authentication. Unfortunately, this setting only controls what OpenSSL does on new handshakes. If a client were to attempt a resumed handshake, this setting would be ignored. To prevent this problem we use SSL_set_session_id_context() to set the resumption context to s_server_auth_session_id_context, instead of s_server_session_id_context.

It's worth taking a minute to understand why changing the session ID context works. Whenever an SSL object stores a session in the session cache, it tags it with the current ID context. Before resuming a session, OpenSSL checks that the current ID context matches the one stored with the cache--but only if SSL_VERIFY_PEER is set. Therefore, changing our ID context ensures that the client can't simply resume the session it just created, because it was created under a different ID context. However, if the client had previously communicated with the server and established a client authenticated session under s_server_auth_session_id_context, then it could resume that session.

Only checking the session ID context if SSL_VERIFY_PEER is set is somewhat counterintuitive; the intent is to ensure that you don't resume sessions in one context that were client authenticated in another context, perhaps with different certificate checking rules. Presumably the idea is that aside from client authentication, one session is rather like another. This assumption is somewhat questionable; one might similarly wish to prevent a session established with one set of cipher preferences from being resumed in an SSL object with a different set of cipher preferences. The only way to accomplish this separation with OpenSSL is to use a totally different SSL_CTX for each cipher suite policy.

Next, we call SSL_negotiate() to move the connection into renegotiate state. Note that this does not cause the connection to be renegotiated; it merely sets the renegotiate flag in the object so that when we call SSL_do_handshake(), the server sends a HelloRequest. Note that the server doesn't do the entire handshake at this point. That requires a separate call to SSL_do_handshake().

Listing 4. Rehandshake with Client Authentication

50      /* Now perform renegotiation if requested */
51      if(client_auth==CLIENT_AUTH_REHANDSHAKE){
52        SSL_set_verify(ssl,SSL_VERIFY_PEER |
55        /* Stop the client from just resuming the
56           un-authenticated session */
57        SSL_set_session_id_context(ssl,
58          (void *)&s_server_auth_session_id_context,
59          sizeof(s_server_auth_session_id_context));
61        if(SSL_renegotiate(ssl)<=0)
62          berr_exit("SSL renegotiation error");
63       if(SSL_do_handshake(ssl)<=0)
64          berr_exit("SSL renegotiation error");
65        ssl->state=SSL_ST_ACCEPT;
66        if(SSL_do_handshake(ssl)<=0)
67          berr_exit("SSL renegotiation error");
68      }

The only remaining piece to explain is explicitly setting the state to SSL_ST_ACCEPT in line 58. Recall we said that the client isn't required to start a rehandshake when it gets the HelloRequest. Moreover, OpenSSL allows the server to keep sending data while the handshake is happening. However, in this case the whole point of the rehandshake is to get the client to authenticate, so we want to finish the handshake before proceeding. Explicitly setting the state forces the server to wait for the client rehandshake. If we don't set the state, the second call to SSL_do_handshake() will return immediately, and the server will send its data before doing the rehandshake. Having to set internal variables of the SSL object is rather ugly, but it's the standard practice in HTTPS servers such as mod_ssl and ApacheSSL, and it has the virtue causing the server to send an unexpected_message alert if the client does not renegotiate as requested.

OpenSSL does offer a SSL_set_accept_state() call, but it isn't useful here because it clears the current crypto state. Since we're already encrypting data, clearing the crypto state causes MAC errors.

For simplicity's sake we've decided to always force a rehandshake when wserver2 is invoked with the -x flag. However, this can create an unnecessary performance load on the server. What if the client has already authenticated? In that case there's no point in authenticating it again. This situation can occur if the client is resuming a previous session in which client authentication occurred. (It cannot happen if this is the first time the client is connecting because client authentication only happens when the server requests it, and we only request it on the rehandshake).

An enhanced version of wserver2 could check to see if the client already provided a certificate and skip the rehandshake if it had. However, great care must be taken when doing this because of the way OpenSSL resumes sessions. Consider the case of a server that has two different sets of certificate verification rules, each associated with a separate ID context. Because OpenSSL only checks the ID context when SSL_VERIFY_PEER is set--which it is not for our initial handshake--the client could resume a session associated with either context. Thus, in addition to getting the certificate we would need to check that the session being resumed came from the right session ID context. If it didn't, we'd still have to rehandshake to be sure that we get the right kind of client authentication.



Comment viewing options

Select your preferred way to display the comments and click "Save settings" to activate your changes.

Block socket with SSL_MODE_AUTO_RETRY

Anonymous's picture

If I use block socket with SSL_MODE_AUTO_RETRY
The flag SSL_MODE_AUTO_RETRY will cause read/write operations to only return after the handshake and successful completion.

Do I have to handle the retrying in SSL_Read and SSL_Write? Isn't it easier to do this way? I know it hurts throughput a little, but is it a big deal?