OpenSSLのソースコードを読むメモ。
ここで記載するのはOpenSSL-1.1.0系(1.1.0h)です。
OpenSSLのソース構成
OpenSSLのソースを展開すると、以下のディレクトリに分かれています。
SSL/TLSの動作を追う場合は、基本的にapps、crypto、sslのディレクトリを見ます。
- VMS
- apps (opensslコマンドのソース)
- crypto (各種のハッシュ、暗号方式、証明書等のソース)
- demos
- doc
- engines
- external
- fuzz
- include
- ms
- os-dep
- ssl (SSL/TLS処理関連のソース)
- test
- tools
- util
色々ありますが、まずはOpenSSLで通信を行う場合の流れを追います。
必要なところを掻い摘んでの説明のため、処理内容をすべて書くわけではありません。
処理の入口
サーバ側のTLSの処理を行う場合、SSL_CTX、SSLを生成して、ソケットFDをくっつけて、SSL_acceptを呼び出すと思います。
この時の処理フローを追います。
SSL_accept
SSL_accept関数の実体はssl/ssl_lib.cにあります。
SSL/TLS処理の公開関数は、大体ssl/ssl_lib.cに書かれています。
int SSL_accept(SSL *s)
{
if (s->handshake_func == NULL) {
/* Not properly initialized yet */
SSL_set_accept_state(s);
}
return SSL_do_handshake(s);
}
短いコードですが、まず最初のif判定は、まだこのSSLコネクションがクライアントとして動作するのか、サーバとして動作するのか決定されていない場合、サーバとして動作するようにしています。
事前に決める場合はSSL_set_accept_stateあるいはSSL_set_connect_stateを呼び出しておきます。
最後にSSL_do_handshakeを呼び出し、サーバ側としてSSL/TLS処理を行います。
そのため、予めSSL_set_accept_stateを呼び出している場合は、SSL_acceptではなく、SSL_do_handshakeを呼び出してネゴシエーションを開始することもできます。
SSL_do_handshake
int SSL_do_handshake(SSL *s)
{
int ret = 1;
if (s->handshake_func == NULL) {
SSLerr(SSL_F_SSL_DO_HANDSHAKE, SSL_R_CONNECTION_TYPE_NOT_SET);
return -1;
}
s->method->ssl_renegotiate_check(s);
if (SSL_in_init(s) || SSL_in_before(s)) {
if ((s->mode & SSL_MODE_ASYNC) && ASYNC_get_current_job() == NULL) {
struct ssl_async_args args;
args.s = s;
ret = ssl_start_async_job(s, &args, ssl_do_handshake_intern);
} else {
ret = s->handshake_func(s);
}
}
return ret;
}
SSL_do_handshakeではハンドシェイク処理の呼び出しを行います。
初回のネゴシエーションの場合SSL_in_init(s)が真になるため、if文内の処理が行われます。
SSL_MODE_ASYNCは、暗号処理を専用ハードウェアを利用して行う場合に、ハードからの戻り待ちを非同期化して動作させるようにするためのモードになります。このモードはsetcontextやlongjumpを利用していて難解です。
SSL_MODE_ASYNCではない場合は、SSLコネクションに紐づけられたハンドシェイク関数(handshake_func)を呼び出します。
handshake_funcは、SSL_CTXの生成時などで指定したTLSv1_2_methodなどに設定されている、ssl_acceptあるいはssl_connect関数です。
これは、ssl/methods.c内で、ssl/ssl_locl.hの IMPLEMENT_tls_meth_func マクロなどを使って設定されます。accept関数は大体ossl_statem_acceptになります。
ossl_statem_accpet
OpenSSL-1.1.0系から、ネゴシエーション周りが、statemに書かれるようになっています。
このため、OpenSSL-1.0.2系とは異なるため、注意してください。
int ossl_statem_accept(SSL *s)
{
return state_machine(s, 1);
}
state_machine関数を1(サーバサイド)で呼び出しているだけです。
connect側は0で呼び出します。
state_machine
このステートマシンでは、SSL/TLSのネゴシエーション状況をMSG_FLOW_STATEで管理しています。
/* Message flow states */
typedef enum {
/* No handshake in progress */
MSG_FLOW_UNINITED,
/* A permanent error with this connection */
MSG_FLOW_ERROR,
/* We are about to renegotiate */
MSG_FLOW_RENEGOTIATE,
/* We are reading messages */
MSG_FLOW_READING,
/* We are writing messages */
MSG_FLOW_WRITING,
/* Handshake has finished */
MSG_FLOW_FINISHED
} MSG_FLOW_STATE;
- MSG_FLOW_UNINITED: ネゴシエーション中ではない
- MSG_FLOW_ERROR: エラー発生(ネゴシエーションを行わない)
- MSG_FLOW_RENEGOTIATE: 再ネゴシエーション中
- MSG_FLOW_READING: ネゴシエーションパケット読み込み中
- MSG_FLOW_WRITING: ネゴシエーションパケット書き込み中
- MSG_FLOW_FINISHED: ネゴシエーション完了
この状態を参照して、state_machineの処理を行います。
static int state_machine(SSL *s, int server)
{
BUF_MEM *buf = NULL;
unsigned long Time = (unsigned long)time(NULL);
void (*cb) (const SSL *ssl, int type, int val) = NULL;
OSSL_STATEM *st = &s->statem;
int ret = -1;
int ssret;
if (st->state == MSG_FLOW_ERROR) {
/* Shouldn't have been called if we're already in the error state */
return -1;
}
エラー状態のときは、ネゴシエーションを行わずに終わります。
最初のネゴシエーションの場合や、再ネゴシエーション時は下記の条件の処理で、各バッファ等の確保や初期化を行います。
if (st->state == MSG_FLOW_UNINITED || st->state == MSG_FLOW_RENEGOTIATE) {
if (st->state == MSG_FLOW_UNINITED) {
st->hand_state = TLS_ST_BEFORE;
}
ネゴシエーションの中心となる処理は次のwhileループの処理です。
while (st->state != MSG_FLOW_FINISHED) {
if (st->state == MSG_FLOW_READING) {
ssret = read_state_machine(s);
if (ssret == SUB_STATE_FINISHED) {
st->state = MSG_FLOW_WRITING;
init_write_state_machine(s);
} else {
/* NBIO or error */
goto end;
}
} else if (st->state == MSG_FLOW_WRITING) {
ssret = write_state_machine(s);
if (ssret == SUB_STATE_FINISHED) {
st->state = MSG_FLOW_READING;
init_read_state_machine(s);
} else if (ssret == SUB_STATE_END_HANDSHAKE) {
st->state = MSG_FLOW_FINISHED;
} else {
/* NBIO or error */
goto end;
}
} else {
/* Error */
ossl_statem_set_error(s);
goto end;
}
}
この処理では、ネゴシエーションの段階に合わせて、相手からのネゴシエーションパケットを読み込むか、こちらからネゴシエーションパケットを送信するかで、read_state_machine/write_state_machineのどちらかを呼び出します。
つまりサーバ側の場合は、最初は相手からのClientHelloを読み込むので、read_state_machineを呼び出し、それが完了した場合、次はこちらからServerHello,Certificate,ServerKeyExchange,ServerHelloDoneの送信となるため、MSG_FLOW_WRITINGに切り替え、write_state_machineを呼び出す、という動作になります。
次回へ
steate machine周りはまだ長くなるかもしれないのでここで一旦切って次にします。
短いかもしれないですが。
次はread_state_machine/write_state_machineからやります。