Я не могу понять, как правильно использовать команду SSL_Shutdown в OpenSSL. Подобные вопросы возникали несколько раз в разных местах, но я не смог найти решение, которое точно соответствовало бы моей ситуации. Я использую пакет libssl-dev 1.0.1f-1ubuntu2.15 (последний на данный момент) под Ubuntu в VirtualBox.
Я работаю с небольшой устаревшей оболочкой C++ над библиотекой OpenSSL с неблокирующим вводом-выводом для серверных и клиентских сокетов. Обертка работает нормально, за исключением следующего тестового случая (я не привожу код самого модульного теста, потому что он содержит много кода, не связанного с проблемой):
- Инициализировать сокет сервера с самозаверяющим сертификатом
- Подключиться к этому разъему. Подтверждение SSL завершается успешно, за исключением того, что я пока игнорирую возврат X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT SSL_get_verify_result.
- Успешно отправить/получить некоторые данные через соединение. Этот шаг является необязательным и не влияет на последующую проблему. Я упоминаю об этом только для того, чтобы показать, что соединение действительно установлено и установлено в правильное состояние.
- Попытка отключить SSL-соединение (серверное или клиентское, неважно, какое), что приводит к бесконечному ожиданию при выборе.
Все вызовы SSL_read и SSL_write синхронизированы, также установлен locking_callback. После шага 3 нет никаких других операций над сокетами, кроме выключения одного из них.
В приведенном ниже фрагменте кода я опускаю весь код обработки ошибок и отладки для ясности, ни один из вызовов OpenSSL/POSIX не завершается ошибкой (за исключением случаев, когда я оставил обработку ошибок на месте). Я также предоставляю функции подключения, если это важно:
void OpenSslWrapper::ConnectToHost( ErrorCode& ec )
{
ctx_ = SSL_CTX_new(SSLv23_client_method());
SSL_CTX_load_verify_locations(ctx_, NULL, config_.verify_locations.c_str());
if (config_.use_standard_verify_locations)
{
SSL_CTX_set_default_verify_paths(ctx_);
}
bio_ = BIO_new_ssl_connect(ctx_);
BIO_get_ssl(bio_, &ssl_);
SSL_set_mode(ssl_, SSL_MODE_AUTO_RETRY);
std::string hostname = config_.address + ":" + to_string(config_.port);
BIO_set_conn_hostname(bio_, hostname.c_str());
BIO_set_nbio(bio_, 1);
int res = 0;
while ((res = BIO_do_connect(bio_)) <= 0)
{
BIO_get_fd(bio_, &fd_);
if (!BIO_should_retry(bio_))
{ /* Never happens */}
WaitAfterError(res);
}
res = SSL_get_verify_result(ssl_);
if (res != X509_V_OK && res != X509_V_ERR_DEPTH_ZERO_SELF_SIGNED_CERT)
{ /* Never happens */ }
SSL_set_mode(ssl_, SSL_MODE_ENABLE_PARTIAL_WRITE);
SSL_set_mode(ssl_, SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER);
}
// config_.handle is a file descriptor which was got from
// accept function, stx_ is also set in advance
void OpenSslWrapper::ConnectAsServer( ErrorCode& ec )
{
ssl_ = SSL_new(ctx_);
int flags = fcntl(config_.handle, F_GETFL, 0);
flags |= O_NONBLOCK;
fcntl(config_.handle, F_SETFL, flags);
SSL_set_fd(ssl_, config_.handle);
while (true)
{
int res = SSL_accept(ssl_);
if( res > 0) {break;}
if( !WaitAfterError(res).isSucceded() )
{ /* never happens */ }
}
SSL_set_mode(ssl_, SSL_MODE_ENABLE_PARTIAL_WRITE);
SSL_set_mode(ssl_, SSL_MODE_ACCEPT_MOVING_WRITE_BUFFER);
}
// The trouble is here
void OpenSSLWrapper::Shutdown()
{
// ...
while (true)
{
int ret = SSL_shutdown(ssl_);
if (ret > 0) {break;}
else if (ret == 0) {continue;}
else {WaitAfterError(ret);}
}
// ...
}
ErrorCode OpenSSLWrapper::WaitAfterError(int res)
{
int err = SSL_get_error(ssl_, res);
switch (ret)
{
case SSL_ERROR_WANT_READ:
WaitForFd(fd_, k_WaitRead);
return ErrorCode::Success;
case SSL_ERROR_WANT_WRITE:
case SSL_ERROR_WANT_CONNECT:
case SSL_ERROR_WANT_ACCEPT:
WaitForFd(fd_, k_WaitWrite);
return ErrorCode::Success;
default:
return ErrorCode::Fail;
}
}
WaitForFd — это просто простая оболочка для выбора, которая бесконечно долго ожидает в заданном сокете указанного FD_SET для чтения или записи.
Когда клиент вызывает Shutdown, первый вызов SSL_Shutdown возвращает 0. После второго вызова он возвращает -1, а SSL_get_error возвращает SSL_ERROR_WANT_READ, но выбор дескриптора файла для чтения никогда не возвращается. Если указать время ожидания при выборе, SSL_Shutdown продолжит возвращать -1, а SSL_get_error продолжит возвращать SSL_ERROR_WANT_READ. Цикл никогда не выходит. После первого вызова SSL_Shutdown статус выключения всегда будет SSL_SENT_SHUTDOWN.
Неважно, закрываю ли я сервер или клиент: оба ведут себя одинаково.
Также возникает странная ситуация, когда я подключаюсь к какому-то внешнему хосту. Первый вызов SSL_Shutdown возвращает 0, второй -1 с SSL_ERROR_WANT_READ. Выбор сокета завершается успешно, но когда я в следующий раз вызываю SSL_Shutdown, я снова получаю -1 с ошибкой SSL_ERROR_SYSCALL и errno=0. Как я читал в других местах, это не имеет большого значения, хотя все же кажется странным и, возможно, это как-то связано, поэтому я упоминаю об этом здесь.
UPD. Портировал тот же код для Windows, поведение не изменилось.
P.S. Прошу прощения за ошибки в моем английском, буду признателен, если кто-то исправит мой язык.
ctx_ = SSL_CTX_new(SSLv23_client_method());
. Есть параметры контекста, которые следует настроить, и вы должны использовать Индикацию имени сервера (SNI). . См., например, Клиент SSL/TLS на вики OpenSSL. - person jww   schedule 06.10.2015