Ответ 1
Для криптографически безопасного отключения обе стороны должны выполнить операции останова на boost::asio::ssl::stream
путем вызова shutdown()
или async_shutdown()
и запуска <io_service
, Если операция завершена с помощью error_code
, которая не имеет категории SSL и не была отменена до того, как часть завершения может произойти, соединение было надежно отключено, а базовый транспорт может быть повторно использован или закрыт. Простое закрытие самого низкого уровня может сделать сеанс уязвимым для атаки усечения.
Протокол и Boost.Asio API
В стандартизованном протоколе TLS и нестандартном протоколе SSLv3 безопасное завершение включает в себя обмен сторонами close_notify
. В терминах API Boost.Asio любая из сторон может инициировать выключение путем вызова shutdown()
или async_shutdown()
, в результате чего сообщение close_notify
отправляется другой стороне, сообщая получателю, что инициатор не отправит больше сообщений по SSL-соединению. По спецификациям получатель должен ответить сообщением close_notify
. Boost.Asio автоматически не выполняет это поведение и требует, чтобы получатель явно вызывал shutdown()
или async_shutdown()
.
Спецификация позволяет инициатору выключения закрыть свою сторону чтения соединения до получения ответа close_notify
. Это используется в тех случаях, когда протокол приложения не желает повторно использовать базовый протокол. К сожалению, Boost.Asio в настоящее время (1.56) не обеспечивает прямую поддержку этой возможности. В Boost.Asio операция shutdown()
считается завершенной при ошибке или если сторона отправила и получила сообщение close_notify
. По завершении операции приложение может повторно использовать базовый протокол или закрыть его.
Сценарии и коды ошибок
Как только соединение SSL установлено, во время выключения возникают следующие коды ошибок:
- Одна сторона инициирует выключение, а удаленная сторона закрывает или уже закрыла базовый транспорт, не отключая протокол:
- Операция инициатора
shutdown()
завершится с ошибкой короткого чтения SSL.
- Операция инициатора
- Одна сторона инициирует выключение и ждет, когда удаленная сторона выключит протокол:
- Операция завершения инициализации завершится со значением ошибки
boost::asio::error::eof
. - Операция удаленной стороны
shutdown()
завершается с успехом.
- Операция завершения инициализации завершится со значением ошибки
- Одна сторона инициирует выключение, затем закрывает базовый протокол, не дожидаясь, когда удаленная сторона выключит протокол:
- Операция инициатора
shutdown()
будет отменена, что приведет к ошибкеboost::asio::error::operation_aborted
. Это результат обходного пути, описанного ниже. - Операция удаленной стороны
shutdown()
завершается с успехом.
- Операция инициатора
Эти различные сценарии описаны ниже. Каждый сценарий проиллюстрирован диаграммой, подобной плаванию, и указывает, что делает каждая сторона в тот же момент времени.
PartyA вызывает shutdown()
после PartyB закрывает соединение без согласования выключения.
В этом случае PartyB нарушает процедуру выключения, закрывая базовый транспорт без первого вызова shutdown()
в потоке. Когда основной транспорт закрыт, PartyA пытается инициировать shutdown()
.
PartyA | PartyB
-------------------------------------+----------------------------------------
ssl_stream.handshake(...); | ssl_stream.handshake(...);
... | ssl_stream.lowest_layer().close();
ssl_stream.shutdown(); |
PartyA попытается отправить сообщение close_notify
, но запись в базовый транспорт завершится с boost::asio::error::eof
. Boost.Asio явно сопоставляет лежащую в основе транспортную ошибку eof
с ошибкой короткого чтения SSL, поскольку PartyB нарушает процедуру выключения SSL.
if ((error.category() == boost::asio::error::get_ssl_category())
&& (ERR_GET_REASON(error.value()) == SSL_R_SHORT_READ))
{
// Remote peer failed to send a close_notify message.
}
PartyA вызывает shutdown()
, затем PartyB закрывает соединение без согласования выключения.
В этом случае PartyA инициирует выключение. Тем не менее, в то время как PartyB получает сообщение close_notify
, PartyB нарушает процедуру выключения, никогда явно не отвечая на shutdown()
перед закрытием основного транспорта.
PartyA | PartyB
-------------------------------------+---------------------------------------
ssl_stream.handshake(...); | ssl_stream.handshake(...);
ssl_stream.shutdown(); | ...
| ssl_stream.lowest_layer().close();
Поскольку операция Boost.Asio shutdown()
считается завершенной после того, как a close_notify
был отправлен и принят или произошла ошибка, PartyA отправит close_notify
, а затем ждет ответа. PartyB закрывает базовый транспорт, не отправляя close_notify
, нарушая протокол SSL. Ошибка PartyA с ошибкой boost::asio::error::eof
, а Boost.Asio сопоставляет его с короткой ошибкой чтения SSL.
PartyA инициирует shutdown()
и ждет PartyBдля ответа с помощью shutdown()
.
В этом случае PartyA инициирует завершение работы и ждет ответа PartyB с отключением.
PartyA | PartyB
-------------------------------------+----------------------------------------
ssl_stream.handshake(...); | ssl_stream.handshake(...);
ssl_stream.shutdown(); | ...
... | ssl_stream.shutdown();
Это довольно простое завершение работы, когда обе стороны отправляют и получают сообщение close_notify
. После завершения переговоров стороны обе стороны, базовый транспорт может быть повторно использован или закрыт.
- Операция завершения операции
- PartyA будет иметь значение ошибки
boost::asio::error::eof
. Операция закрытия - PartyB завершится успешно.
PartyA инициирует shutdown()
, но не ждет ответа PartyB.
В этом случае PartyA инициирует завершение работы и сразу же закрывает базовый транспорт после отправки close_notify
. PartyA не ожидает ответа PartyB сообщением close_notify
. Этот тип согласованного выключения разрешен по спецификации и довольно распространен среди реализаций.
Как упоминалось выше, Boost.Asio напрямую не поддерживает этот тип выключения. Операция Boost.Asio shutdown()
ожидает, пока удаленный одноранговый узел отправит свой close_notify
. Тем не менее, можно реализовать обходной путь, сохраняя при этом спецификацию.
PartyA | PartyB
-------------------------------------+---------------------------------------
ssl_stream.handshake(...); | ssl_stream.handshake(...)
ssl_stream.async_shutdown(...); | ...
const char buffer[] = ""; | ...
async_write(ssl_stream, buffer, | ...
[](...) { ssl_stream.close(); }) | ...
io_service.run(); | ...
... | ssl_stream.shutdown();
PartyA инициирует асинхронную операцию выключения, а затем инициирует асинхронную операцию записи. Буфер, используемый для записи, должен иметь ненулевую длину (нулевой символ используется выше); в противном случае Boost.Asio оптимизирует запись в no-op. Когда выполняется операция shutdown()
, она отправит close_notify
в PartyB, заставив SSL закрыть сторону записи потока PartyA SSL, а затем асинхронно ждать PartyB close_notify
. Однако по мере того, как поток записи в потоке SSL PartyA закрыт, операция async_write()
завершится с ошибкой SSL, указывающей, что протокол был отключен.
if ((error.category() == boost::asio::error::get_ssl_category())
&& (SSL_R_PROTOCOL_IS_SHUTDOWN == ERR_GET_REASON(error.value())))
{
ssl_stream.lowest_layer().close();
}
Неисправная операция async_write()
затем явно закрывает базовый транспорт, в результате чего операция async_shutdown()
, ожидающая отмены PartyB close_notify
.
- Хотя PartyA выполнила процедуру выключения, разрешенную спецификацией SSL, операция
shutdown()
была явно отменена, когда основной транспорт был закрыт. Следовательно, код ошибкиshutdown()
будет иметь значениеboost::asio::error::operation_aborted
. Операция закрытия - PartyB завершится успешно.
Таким образом, операции отключения Boost.Asio SSL немного сложны. Неисправности между кодами ошибок инициатора и удаленного однорангового соединения при правильном отключении могут сделать обработку немного неудобной. Как правило, до тех пор, пока категория кода ошибки не является категорией SSL, протокол был надежно отключен.