An Introduction to OpenSSL Programming, Part II of II

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

The basic problem we're facing is that SSL is a record-oriented protocol. Thus, even if we want to read only one byte from the SSL connection, we still need to read the entire record containing that byte into memory. Without the entire record in hand, OpenSSL can't check the record MAC, so we can't safely deliver the data to the programmer. Unfortunately, this behavior interacts poorly with select(), as shown in Figure 1.

Figure 1. Read Interaction with SSL

The left-hand side of Figure 1 shows the situation when the machine has received a record but it's still waiting in the network buffers. The arrow represents the read pointer that is set at the beginning of the buffer. The bottom row represents data decoded by OpenSSL but not yet read by the program (the SSL buffer). This buffer is currently empty so we haven't shown a box. If the program calls select() at this point, it will return immediately, indicating that a call to read() will succeed. Now, imagine that the programmer calls SSL_read() requesting one byte. This takes us to the situation at the right side of the figure.

As we said earlier, the OpenSSL has to read the entire record in order to deliver even a single byte to the program. In general, the application does not know the size of records, and so its reads will not match the records. Thus, the box in the upper right-hand corner shows that the read pointer has moved to the end of the record. We've read all the data in the network buffer. When the implementation decrypts and verifies the record, it places the data in the SSL buffer. Then it delivers the one byte that the program asked for in SSL_read(). We show the SSL buffer in the lower right-hand corner. The read pointer points somewhere in the buffer, indicating that some of the data is available for reading but some has already been read.

Consider what happens if the programmer calls select() at this point. select() is concerned solely with the contents of the network buffer, and that's empty. Thus, as far as select() is concerned there's no data to read. Depending on the exact arguments it's passed, it will either return and say that there's nothing to read or wait for some more network data to become available. In either case we wouldn't read the data in the SSL buffer. Note that if another record arrived, select() would indicate that the socket was ready to read and, we'd have an opportunity to read more data.

Thus, select() is an unreliable guide to whether there is SSL data ready to read. We need some way to determine the status of the SSL buffer. This can't be provided by the operating system because it has no access to the SSL buffers. It must be provided OpenSSL. OpenSSL provides exactly such a function. The function SSL_pending() tells us whether there is data in the SSL buffer for a given socket. Listing 8 shows SSL_pending() in action.

Listing 8. Reading Data Using SSL_Pending()

49        /* Now check if there's data to read */
50        if((FD_ISSET(sock,&readfds) && !write_blocked_on_read) ||
51          (read_blocked_on_write && FD_ISSET(sock,&writefds))){
52          do {
53            read_blocked_on_write=0;
54            read_blocked=0;
55
56            r=SSL_read(ssl,s2c,BUFSIZZ);
57
58            switch(SSL_get_error(ssl,r)){
59              case SSL_ERROR_NONE:
60                /* Note: this call could block, which blocks the
61                   entire application. It's arguable this is the
62                   right behavior since this is essentially a terminal
63                   client. However, in some other applications you
64                   would have to prevent this condition */
65                fwrite(s2c,1,r,stdout);
66                break;
67              case SSL_ERROR_ZERO_RETURN:
68                /* End of data */
69                if(!shutdown_wait)
70                  SSL_shutdown(ssl);
71                goto end;
72                break;
73              case SSL_ERROR_WANT_READ:
74                read_blocked=1;
75                break;
76
77                /* We get a WANT_WRITE if we're
78                   trying to rehandshake and we block on
79                   a write during that rehandshake.
80
81                   We need to wait on the socket to be
82                   writeable but reinitiate the read
83                   when it is */
84              case SSL_ERROR_WANT_WRITE:
85                read_blocked_on_write=1;
86                break;
87              default:
88                berr_exit("SSL read problem");
89            }
90
91            /* We need a check for read_blocked here because
92               SSL_pending() doesn't work properly during the
93               handshake. This check prevents a busy-wait
94               loop around SSL_read() */
95          } while (SSL_pending(ssl) && !read_blocked);
96        }

The logic of this code is fairly straightforward. select() has been called earlier, setting the variable readfds with the sockets that are ready to read. If the SSL socket is ready to read, we go ahead and try to fill our buffer, unless the variable write_blocked_on_read is set (this variable is used when we're rehandshaking and we'll discuss it later). Once we've read some data, we write it to the console. Then we check with SSL_pending() to see if the record was longer than our buffer. If it was, we loop back and read some more data.

Note that we've added a new branch to our switch statement: a check for SSL_ERROR_WANT_READ. What's going on here is that we've set the socket for nonblocking operation. Recall that we said that if you called read() when the network buffers were empty, it would simply block (wait) until they weren't. Setting the socket to nonblocking causes it to return immediately, saying that it would have blocked.

To understand why we've done this, consider what happens if an SSL record arrives in two pieces. When the first piece arrives, select() will signal that we're ready to read. However, we need to read the entire record in order to return any data, so this is a false positive. Attempting to read all that data will block, leading to exactly the deadlock we were trying to avoid. Instead, we set the socket to non-blocking and catch the error, which OpenSSL translates to SSL_ERROR_WANT_READ.

It's worth noting that the call to fwrite() that we use to write to the console can block. This will cause the entire application to stall. This is reasonable behavior in a terminal client--if the user isn't looking at the screen we want the server to wait for him--but in other applications we might have to make this file descriptor non-blocking and select() on it as well. This is left as an exercise for the reader.

______________________

Comments

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?

White Paper
Linux Management with Red Hat Satellite: Measuring Business Impact and ROI

Linux has become a key foundation for supporting today's rapidly growing IT environments. Linux is being used to deploy business applications and databases, trading on its reputation as a low-cost operating environment. For many IT organizations, Linux is a mainstay for deploying Web servers and has evolved from handling basic file, print, and utility workloads to running mission-critical applications and databases, physically, virtually, and in the cloud. As Linux grows in importance in terms of value to the business, managing Linux environments to high standards of service quality — availability, security, and performance — becomes an essential requirement for business success.

Learn More

Sponsored by Red Hat

White Paper
Private PaaS for the Agile Enterprise

If you already use virtualized infrastructure, you are well on your way to leveraging the power of the cloud. Virtualization offers the promise of limitless resources, but how do you manage that scalability when your DevOps team doesn’t scale? In today’s hypercompetitive markets, fast results can make a difference between leading the pack vs. obsolescence. Organizations need more benefits from cloud computing than just raw resources. They need agility, flexibility, convenience, ROI, and control.

Stackato private Platform-as-a-Service technology from ActiveState extends your private cloud infrastructure by creating a private PaaS to provide on-demand availability, flexibility, control, and ultimately, faster time-to-market for your enterprise.

Learn More

Sponsored by ActiveState