Skip to content

Instantly share code, notes, and snippets.

@Jxck
Created May 24, 2016 08:21
Show Gist options
  • Save Jxck/b211a12423622fe304d2370b1f1d30d5 to your computer and use it in GitHub Desktop.
Save Jxck/b211a12423622fe304d2370b1f1d30d5 to your computer and use it in GitHub Desktop.
OpenSSL DTLS API

OpenSSL DTLS API

The API used for DTLS is mostly the same as for TLS, because of the mapping of generic functions to protocol specifc ones. Some additional functions are still necessary, because of the new BIO objects and the timer handling for handshake messages. The generic concept of the API is described in the following sections. Examples of applications using DTLS are available at [9].

DTLS の API は TLS とほぼ同じ。 BIO オブジェクトの生成とタイマのために追加でいくつか必要。 詳細は以下、サンプルはここ。

http://sctp.fh-muenster.de/dtls-samples.html

Prerequisites

Every program using OpenSSL has to start with initializing the library by calling 最初はライブラリの初期化で始まるのは同じ。

SSL_load_error_strings(); /* readable error messages */
SSL_library_init(); /* initialize library */

before any other action can be done. The DTLS specifc context can be created thereafter, from which SSL objects for each connection can be derived. The context is diferent for the client and server, and several parameters, including certifcates and keys, have to be set:

これをまず最初にやる。 DTLS のためのコネクションごとのコンテキストが作られる。 コンテキストは server/client で違い、鍵や証明書などの引数が必要。

/***** SERVER *****/
ctx = SSL_CTX_new(DTLSv1_server_method());

/***** CLIENT *****/
ctx = SSL_CTX_new(DTLSv1_client_method());

/***** BOTH *****/
/* Load certificates and key */
SSL_CTX_use_certificate_chain_file(ctx, "cert.pem");
SSL_CTX_use_PrivateKey_file(ctx, "key.pem", SSL_FILETYPE_PEM);

/* Server: Client has to authenticate */
/* Client: verify server's certificate */
SSL_CTX_set_verify(ctx, SSL_VERIFY_PEER, verify_cert);
SSL_CTX_set_cookie_generate_cb(ctx, generate_cookie);
SSL_CTX_set_cookie_verify_cb(ctx, verify_cookie);

Note that three callback functions have been used, that is verify_cert(), generate_cookie() and verify_cookie().

verify_cert(), generate_cookie() and verify_cookie() 三つのコールバックが必要。

The frst function, verify_cert(), is called every time a certifcate has been received. This function has to verify the certifcate and returns 1 if trusted or 0 otherwise. Usually the program will print certifcate details and ask the user if he trusts it, or maintains a database of known certifcates. In case the certifcate is not trusted, the handshake and therefore the connection setup will fail.

verify_cert() は、証明書を受け取るたびに呼ばれる。検証成功で 1 そうでなければ 0 を返す。 通常、ユーザに承認を求めるか、 DB を引いて証明書管理を行う。 証明書が信用されなかった場合、 handshake とコネクション生成が失敗する。

The other callback functions, generate_cookie() and verify_cookie(), are used for the cookie handling. When a cookie has to be generated for a HelloVerifyRequest, the generate_cookie() function is called and after receiving a cookie attached to a ClientHello the verify_cookie() function. The content is arbitrary, but for security reasons it should contain the client's address, a timestamp and should be signed.

generate_cookie() and verify_cookie() はクッキーのハンドリングに使う。 HelloVerifyRequest に対してクッキーを生成する必要がある場合 generate_cookie() が呼ばれ ClientHello にクッキーが付与されてきた場合、 verify_cookie() が呼ばれる。 値は任意だが、セキュリティ上の問題で client adderess, timestamp が追加され、署名されている必要がある。

The signatures of the callback functions are as follows:

関数の型は以下。

/* Certificate verification. Returns 1 if trusted, else 0 */
int verify_cert(int ok, X509_STORE_CTX *ctx);

/* Generate cookie. Returns 1 on success, 0 otherwise */
int generate_cookie(SSL *ssl, unsigned char *cookie, unsigned int *cookie_len);

/* Verify cookie. Returns 1 on success, 0 otherwise */
int verify_cookie(SSL *ssl, unsigned char *cookie, unsigned int cookie_len);

Connection setup

The server needs a socket for awaiting incoming connections. For this socket a BIO object has to be created, which can then be used with an SSL object to respond to connection attempts. To prevent DOS attacks, the server should use the HelloVerifyRequest to verify the client's address. Since this is unique to DTLS, there are newly added functions to realize this.

サーバは接続を受けるためにソケットが必要。 このソケットに対し SSL で使うための BIO オブジェクトを生成する。 DOS を防ぐため、 HelloVerifyRequest で client address をチェックするが、このために 追加の関数を呼ぶ必要がある。

int fd = socket(AF_INET6, SOCK_DGRAM, 0);
bind(fd, &server_addr, sizeof(struct sockaddr_in6));

while(1) {
  BIO *bio = BIO_new_dgram(fd, BIO_NOCLOSE);
  SSL *ssl = SSL_new(ctx);
  SSL_set_bio(ssl, bio, bio);

  /* Enable cookie exchange */
  SSL_set_options(ssl, SSL_OP_COOKIE_EXCHANGE);

  /* Wait for incoming connections */
  while (!DTLSv1_listen(ssl, &client_addr));

  /* Handle client connection */
  // snip
}

At frst, BIO_new_dgram() is used instead of BIO_new() to create a UDP specifc BIO. BIO_new_dgram() が UDP 専用で BIO_new() の代替。

Then a new SSL object is created using the previously set up context, to which the BIO object is assigned. すでに作ったコンテキストで BIO がアサインされた SSL オブジェクトを作る。

The cookie exchange is not enabled by default and has to be enabled with the corresponding option. クッキー交換はデフォルトでは無効なので、オプションで有効にする。

The new function DTLSv1_listen() waits for incoming ClientHellos on the listening socket, responds with a HelloVerifyRequest and returns 0, which indicates that no client has been verifed yet and it needs to be called again to continue listening.

DTLSv1_listen() がソケットでの ClientHello の受信を待ち、 HelloVerifyRequest を返信して 0 を返す。 クッキーのチェックが通ったクライアントがあるまで、これを繰り返す。

When the client repeats its ClientHello with a valid cookie attached, the function will return 1 and the sockaddr structure of the verifed client. The sockaddr structure can be used to create a new socket, connected to this client, which is used to replace the listening socket in the BIO object.

正しいクッキーをつけた ClientHello が送られてきたら、 1 と client の sockaddr 構造体を返して抜ける。 この sockeaddr でソケットを作り、クライアントと connect する、これが BIO で socket を listen する代わりとなる。

Hereafter the SSL object can be used for this connection, preferably in a new thread, while new BIO and SSL objects have to be created for the listening socket, to continue listening.

これで SSL オブジェクトがコネクションに対して使用でき、それが別スレッドにわかれるのが望ましく、 BIO と SSL オブジェクトがリスニングソケットに対して使えるように、 listen し続ける。

/* Handle client connection */
int client_fd = socket(AF_INET6, SOCK_DGRAM, 0);
bind(client_fd, &server_addr, sizeof(struct sockaddr_in6));
connect(client_fd, &client_addr, sizeof(struct sockaddr_in6));

/* Set new fd and set BIO to connected */
BIO *cbio = SSL_get_rbio(ssl);
BIO_set_fd(cbio, client_fd, BIO_NOCLOSE);
BIO_ctrl(cbio, BIO_CTRL_DGRAM_SET_CONNECTED, 0, &client_addr);

/* Finish handshake */
SSL_accept(ssl);

Since the handshake has only been performed until the repeated ClientHello, SSL_accept() to complete the handshake still has to be called, before sending and receiving data.

ClientHello を繰り返している間はハンドシェイクが終わってないため、 ハンドシェイクを終わらせるために SSL_accept() を呼ぶ必要があります。

Connecting the client to a server is rather straightforward. A socket connected to the server has to be created and put into a corresponding BIO object, which itself is used by an SSL object.

client から server への接続はもっと直感的です。 ソケットを作り、 BIO に入れ、 SSL オブジェクトを作るだけです。

int fd = socket(AF_INET6, SOCK_DGRAM, 0);
connect(fd, &server_addr, sizeof(struct sockaddr_in6));
BIO *bio = BIO_new_dgram(fd, BIO_NOCLOSE);
BIO_ctrl(cbio, BIO_CTRL_DGRAM_SET_CONNECTED, 0, &server_addr);
SSL *ssl = SSL_new(ctx);
SSL_set_bio(ssl, bio, bio);
/* Perform handshake */
SSL_connect(ssl);

Sending & Receiving

Sending and receiving with DTLS is just the same as with TLS. The functions used are SSL_write() for sending and SSL_read() for receiving. Both return the number of bytes sent and received, respectively. In case -1 is returned, an error handling is necessary, because there are several reasons why this could have happened.

DTLS における send/receive は TLS と同じく、 SSL_write()SSL_read() を呼ぶ。 戻り値が byte で、 -1 の場合は、エラーの内容に応じてエラー処理が必要。

The function SSL_get_error() determines if and what kind of error occurred. This is the same for sending and receiving, and should be done after every SSL_read() and SSL_write() call.

SSL_get_error() でエラーの詳細が取得できるため、全ての SSL_read() / SSL_Write() の後に行う。

Return value Description
SSL_ERROR_NONE No error.
SSL_ERROR_ZERO_RETURN Transport connection closed.
SSL_ERROR_WANT_READ,SSL_ERROR_WANT_WRITE Reading/Writing had to be interrupted, just try again.
SSL_ERROR_WANT_CONNECT,SSL_ERROR_WANT_ACCEPT Connecting/Accepting had to be interrupted, just try again.
SSL_ERROR_WANT_X509_LOOKUP Interrupt for certifcate lookup. Try again.
SSL_ERROR_SYSCALL Socket error.
SSL_ERROR_SSL SSL protocol error, connection failed.

The return value SSL_ERROR_SYSCALL indicates that a problem occurred while calling recvfrom() or sendto() internally. The kind of error can be determined with the errno variable.

SSL_ERROR_SYSCALL は、 recvfrom() / sendto() の内部呼び出しの結果を示す。 エラーの内容は errno でわかる。

Usually, a socket error is fatal and the connection cannot be continued, for example after ENOMEM, that is no memory left. However, some errors, like ECONNRESET ("Connection reset by peer"), may be ignored.

通常、ソケットのエラーは fatal でコネクションは継続できない、例えば ENOMEM ではメモリが足りない。 しかし、 ECONNRESET("Connection reset by peer") のような幾つかのエラーは、無視される。

This error only occurs when the peer closed its port, thus dropped a packet and notifes this with an Internet Control Message Protocol (ICMP) message. Such a message can easily be faked by an attacker to shut down the connection. Instead, the Heartbeat Extension should be used to check the peer's availability.

このエラーはピアがポートを閉じた時に、パケットが落ちて ICMP で通知された時に発生する。 こうしたメッセージは簡単に改竄できる。 代わりに、 Heartbeat Extension でピアをチェックできる。

Timer and Socket Timeout Handling

To set socket timeouts, the function BIO_ctrl() should be used with the corresponding BIO object:

BIO_ctrl() で BIO にタイムアウトを設定する。

struct timeval timeout;
timeout.tv_sec = 5;
timeout.tv_usec = 0;
BIO_ctrl(bio, BIO_CTRL_DGRAM_SET_RECV_TIMEOUT, 0, &timeout);

Whenever a socket timeout occurs, that is EAGAIN or EWOULDBLOCK is returned, the SSL_read() or SSL_write() call will return SSL_ERROR_WANT_READ or SSL_ERROR_WANT_WRITE. So to determine if this error was really caused by a socket timeout, the BIO object has to be asked:

タイムアウトが発生したら、 EAGAIN か EWOULDBLOCK が戻り、 SSL_read() / SSL_write() が SSL_ERROR_WANT_READ か SSL_ERROR_WANT_WRITE を返す。 なので、エラーが本当に socket timeout でのものかは、 BIO に確認しないとわからない。

int len = SSL_read(ssl, buffer, sizeof(buffer));
switch (SSL_get_error(ssl, len)) {
  ...
  case SSL_ERROR_WANT_READ:
  /* Handle socket timeouts */
  if (BIO_ctrl(bio, BIO_CTRL_DGRAM_GET_RECV_TIMER_EXP, 0, NULL)) {
    num_timeouts++;
  }
  break;
  ...
}

Besides the handling of socket timeouts, DTLS has also handshake timers which have to be considered. socket timeout のハンドリングに並べ、 DTLS では気にすべきハンドリングが他にもある。

When socket timeouts are set, DTLS will automatically adjust them while handshaking if they expire too late, so the blocking call will return and retransmissions can be performed. socket timeout がセットされた時、 DTLS はハンドシェイクにそのタイムアウトを設定するため、ブロックしなくなり再試行が実行される。

After the handshake has been done, the socket timeouts are reset to the previous values. ハンドシェイク完了後、タイムアウト値は元に戻る。

However, this does not work with non-blocking sockets, because no DTLS function will be called if there is no incoming or outgoing trafc. しかし、 DTLS でトラフィックが無いと関数が呼ばれないため、 ノンブロッキングなソケットではこれは動かない。

So when using non-blocking calls with select(), its timeout has to be set accordingly with the function DTLSv1_get_timeout(), which will return the time until the next timer expires, if any is running.

なのでノンブロッキングで select() を使う場合は、 DTLSv1_get_timeout() で取得した、次の期限までの値をセットする必要がある。

In that case, DTLSv1_handle_timeout() must be called to perform retransmissions:

その場合、 DTLSv1_handle_timeout() が再接続のために呼ばれる。

struct timeval timeout;
DTLSv1_get_timeout(ssl, &timeout);
int num = select(FD_SETSIZE, &rsocks, NULL, NULL, &timeout) {
  /* Handle timeouts */
  if (num == 0) {
    DTLSv1_handle_timeout(ssl);
  }
  ...
}

For simplicity, no socket timeouts should be set before the initial handshake is done with SSL_connect() and SSL_accept(), because if the socket timeouts expire earlier than the handshake timeouts, additional error handling will be necessary to resume the handshake in that case.

単純にするため、 SSL_connect()SSL_accept() の前にソケットタイムアウトを設定しない方がいい、 ハンドシェイクタイムアウトの前に、ソケットタイムアウトが怒ると、 再試行のために追加のエラー処理が必要となる。

@darconeous
Copy link

Nice writeup!

@zaidbepari
Copy link

Where can I find the complete source code file of client as well as server?

@brjoha
Copy link

brjoha commented Jan 11, 2019

Is this example specific to Windows, BSD, or Mac OS X? The reason I ask is that this code...

/* Handle client connection */
int client_fd = socket(AF_INET6, SOCK_DGRAM, 0);
bind(client_fd, &server_addr, sizeof(struct sockaddr_in6));
connect(client_fd, &client_addr, sizeof(struct sockaddr_in6));

...fails on the bind() call under Linux with an EADDRINUSE (98). If we set the SO_REUSEADDR option, then bind() causes the new descriptor to take over the port from the original descriptor. This means the original descriptor will get no new datagrams until the new descriptor is closed. Please reference...

https://medium.com/uckey/the-behaviour-of-so-reuseport-addr-1-2-f8a440a35af6
https://medium.com/uckey/so-reuseport-addr-2-2-how-packets-forwarded-to-multiple-sockets-ce4b83cd0fd2

I've been hunting high and low for a way to get OpenSSL to work with a UDP server that has multiple UDP clients, but it seems to not be possible (at least under Linux).

[edit]
After a little more testing, it appears that the connect() call lets new datagrams from other clients pass through to the original descriptor. So the caveat seems to be that this example code will work under Linux provided that the SO_REUSEADDR option is set on the sockets.
[/edit]

[edit2]
However, I think there are still a potential issue with this solution. Between the bind() and connect() calls, any new datagrams from other clients will be delivered to the new descriptor. Likely, these will be interpreted by OpenSSL as garbage and authentication will fail. It's a small window of opportunity, and the clients will retry, but still...
[/edit2]

@neoxic
Copy link

neoxic commented Dec 24, 2019

[edit2]
However, I think there are still a potential issue with this solution. Between the bind() and connect() calls, any new datagrams from other clients will be delivered to the new descriptor. Likely, these will be interpreted by OpenSSL as garbage and authentication will fail. It's a small window of opportunity, and the clients will retry, but still...
[/edit2]

That's exactly what I have encountered and was quite surprised that there's no way to bind() and connect() the new socket atomically. Though it seems macOS Mavericks onwards has a new syscall connectx() which does the job well. Otherwise, it's overwhelmingly unsatisfactory. Moreover, the whole idea of connect() for UDP seems flawed because it really works only for an unbound socket that is when connect() will automatically bind to an ephemeral port while connecting to a remote address.

The reliable solution to implement a custom BIO with only one underlying UDP socket is tempting yet looks not quite easy adding to the overall frustration with DTLS and UDP support.

EDIT: Please check out this gist that illustrates the problem: https://gist.github.com/neoxic/0d9314ec756d37ca4303bced49b94543

@neoxic
Copy link

neoxic commented Dec 24, 2019

I found a solution how to properly manage the race between bind() and connect(). Before passing the new socket to SSL_accept() for further possessing, it must first be exhausted with DTLSv1_listen() in non-blocking mode by checking for possible pending requests. This obviously introduces a small recursion since pending requests will require new sockets and so on. But it doesn't look like a problem since the number of file descriptors has a per process limit anyway.

EDIT: Please also note that it will not work with OpenSSL 1.1.0 or lower: openssl/openssl#6934

EDIT2: Unfortunately, the above solution does not work. It greatly reduces the possibility of racing packets, but it appears that there's still no guarantee (surprise!) that after returning from connect() and draining the socket until EAGAIN, there will be no packets in its queue other than those from the assigned address. This is why the correct solution is to receive packets on a connected socket via recvfrom() always checking a peer's address and if it doesn't match, treat the packet as though it has been received on the main (unconnected) socket.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment