Comments 10
Есть несколько мыслей по оптимизации вашей схемы. У вас получается что клиент сможет отправить первый байты полезных данных (например имя api endpoint-a) только после полного раундтрипа? А что насчет сервера? Если сервер вычислил ключ шифрования уже на первом upd-пакете от клиента то значит он имеет теоретическую возможность отправить зашифрованные данные тут же не дожидаясь запроса от клиента, правильно? То есть сервер генерирует "ephemeral server public key" записывает его в udp-пакет который предназначен клиенту и тут же внутри этого же udp пакета следом записывает уже зашифрованные данные и тогда клиент получив кей-шару от сервера вычисляет симметрический ключ шифрования и дальше сможет расшифровать уже полученные данные от сервера.
В общем это довольно интересная техника которая позволяет избежать лишнего сетевого раундтрипа в случае если серверу нужно безусловно отправить клиенту какие-то данные (например свою версию api-ручек чтобы клиент увидев что у него старая версия бинарника которая не совместима с ними попросил юзера обновить приложение).
И дальше появляются мысли как можно еще сильнее зарыться в оптимизацию сетевых раундтрипов - если имя api-ручки сервера не является таким уж и секретным то клиент может включить это имя (в незашифрованном виде) в тот свой первый udp-пакет и тогда сервер сможет выполнить полученный запрос и отправить уже зашифрованные данные клиенту уже в том первом udp-пакете который клиент получит от сервера.
И кстати тут же напрашивается идея что можно сэкономить драгоценное место в upd-пакете (1472 байт) и закодировать имя api-ручки в номере udp-порта - то есть сервер будет слушать не один порт а например тысячу сетевых портов по одному на каждую api-ручку, правда для этого нужно чтобы это зашифрованное соединение не было привязано не только к ip-аддресу и порту клиента (что в принципе и так является хорошей практикой чтобы например избежать установки нового соединения при смене клиентом wi-fi сети на мобильную сеть и наоборот) но и к порту сервера.
Ну и наконец появляется такая идея что этот "ephemeral server public key" можно вообще включить в бинарник приложения и тогда клиент сможет вычислить ключ для симметричного шифрования вообще без общения с сервером и тогда он сможет в своем первом upd-пакете (включив сначала свой "ephemeral public key" ) отправить серверу в зашифрованном виде как имя api-endpoint-а так и соответствующие данные.
Спасибо за размернутый фидбэк и советы.
Как сказано в начале статьи этот протокол в первую очередь пишется для месенджера, где будет создаваться долгоживущее соединение и все данные будут перегоняться через него.
Идея кодирования API ручки в номере порта довольно интересная и поможет сэкономить байты в UDP пакете, но поскольку в моем сценарии предполагается долгоживущее соединение, использование одного порта для всей сессии будет более выгодным и простым в менеджменте. Однако, этот подход можно добавить в виде опциональной возможности, которая будет выгодна сервисам, создающим единичные, короткие get запросы. Не знаю на сколько целесообразно смешивать два подхода в рамках одного протокола и это внесет сложность в реализацию, по этому на данный момент реализовывать эту возможность я не стану, но обязательно подумаю над этим позже.
Оптимизация раундтипов хорошая возможность для улучшения. Это очень важно для скорости запуска и бесшовных переподключений, но внедрение нулевого раундтипа приведет к сложностям в виде защиты от reply атак, что может быть критично. К тому же я описывал механизм востановления сессии, который не успел реализовать, который должен значительно сократить время повторного подключения. И действительно нужен ли нулевой раундтип в добавку к механизму возобновления сессии нужно будет проверять эксперементально, после его реализации. В первой версии протокола я оставлю текущую схему, но если эксперемент покажет критические зависания связанные с 1-RTT, придется его оптимизировать.
А запекание ephemeral ключа приведет к проблемам с PFS, а в данном случае для меня безопасность приоритетнее скорости
Оптимизация раундтипов хорошая возможность для улучшения. Это очень важно для скорости запуска и бесшовных переподключений, но внедрение нулевого раундтипа приведет к сложностям в виде защиты от reply атак, что может быть критично. К тому же я описывал механизм востановления сессии, который не успел реализовать, который должен значительно сократить время повторного подключения.
А почему бы просто не установить на сервере таймаут соединения в бесконечность (если там есть этот таймаут) а дальше на клиенте просто сохранять на диск контекст соединения и таким образом никогда не разрывать соединение? То есть даже если юзер выходит из приложения то при следующем запуске приложение просто загрузит контекст соединения из диска и просто продолжит отправлять и получать данные по этому уже установленному соединению.
который должен значительно сократить время повторного подключения
Таким образом решается задача возобновления сессий - раз соединение практически никогда не будет разрываться то нет такой необходимости пытаться оптимизировать установку нового соединения и время повторного подключения
А запекание ephemeral ключа приведет к проблемам с PFS, а в данном случае для меня безопасность приоритетнее скорости
Если мы запекаем в бинарнике приложения ephemeral ключ то на сервере нужно будет хранить приватную часть этого ключа не все время а только до выхода новой версии приложения (а если приложение активно развивается то выход минорной версии происходит обычно не реже чем раз в месяц). Это конечно не идеальный perfect-forward-security но уже лучше чем в протоколе TLS 1.2 когда часто использовалась схема с использованием RSA и публичного ключа из сертификата для дальнейшей передачи симметричного ключа шифрования - там действительно утечка приватного ключа сертификата позволит расшифровать все перехваченные ранее сообщения за все время действия сертификата (год и больше) и поэтому в TLS 1.3 полностью отказались от этой варианта и оставили только схемы c согласованием ключа через DHE или ECDHE (diffie-hellman и аналог на эллиптических кривых).
Вообще "идеального" PFS не сущестсвует так как это всегда trade-off между удобством для пользователя (отсутствие необходимости делать полноценный хендшейк на каждый чих) и безопасностью. Вот скажем ради безопасности вы решили что зашивать ephemeral ключ в бинарнике приложения (с необходимостью хранить приватную часть на сервере до выхода новой версии приложения) это небезопасно с точки зрения PFS поэтому вы решаете генерировать приватную часть и согласовывать новый ключ с каждым соединением. Но проблема в том что если вы решаете организовать общение клиента с сервером не через установку нового соединения на каждый запрос к серверу а через установку одного долгоживущего соединения (с обменом сообщений внутри этого соединения) то это уже в какой-то степени жертвование безопасностью и уход от PFS. А если вы решаете добавить механизм возобновления сессии (ну или же мое предложение в виде сохранения контекста соединения на диск) то это еще больший уход и пожертвование perfect-forward-security ради увеличения эффективности и удобства.
В общем для себя я решил что проблемы PFS несколько преувеличены и по сути являются сомнительной (на мой взгляд) попыткой сгладить потенциальный взлом сервера (а иначе как этот долговременный приватный ключ может утечь?) и я такой себе дальше думаю - если уж произошел такой взлом с утечкой приватного ключа то мне кажется что возможность расшифровать перехваченный ранее обмен сообщениями это будет не самая большая проблема в сравнении с потенциальным доступом (в случае взлома) ко всем данным пользователей (то есть ко всему что этот сервер может хранить или обрабатывать)
Оптимизация раундтипов хорошая возможность для улучшения. Это очень важно для скорости запуска и бесшовных переподключений, но внедрение нулевого раундтипа приведет к сложностям в виде защиты от reply атак, что может быть критично
Сложности в виде защиты от reply атак возникают не из-за нулевого рандтрипа а из-за отсуствия долговременного хранения этого счетчика (которого вы передаете с каждым сообщением) на сервере и на клиенте. Вот у вашего протокола уже есть такая защита но внутри установленного соединения. А что происходит если у пользователя на некоторое время отключается интернет? Если соединение разрывавется (например из-за таймаута) то повторная попытка установить новое соединение с повторной отправкой сообщения как раз и приведет к replay багам. И решением этой проблемы как раз и будет долговременное хранение как на сервере так и на клиенте либо этого счетчика либо вообще контекст всего соединения
Нет формального обоснования - алгоритм в помойку
Несколько я знаю, в криптографии основное правило: если ты не эксперт, то пользуйся готовыми алгоритмами и протоколами
Я вот этот момент никогда понять не мог
Дело в том, что
S— это результат математической операции, и использовать его «как есть» в качестве ключа небезопасно.
S мы можем получить, только если как-то утекли приватные ключи или как-то взломали сам алгоритм. А если у нас уже есть S, то мы точно так же можем применить KDF и получить нужные сессионные ключи. Тогда какая разница?
В начале тоже не понимал в чем вообще смысл этого "Лишнего шага", пока писал свой протокол как раз и разабрался в этом.
Суть в том, что S - результат заранее известной математической операции, по этому оно может иметь предсказуему структуру, иметь какой-то паттерн. Например, каждый второй бит S обязательно 0 (пример взят из воздуха). Из-за чего криптоанализ упрощается и надежность падает. Деревация ключа делает его чуть более непредсказуемым.
А еще S может быть просто слишком длинным или слишком коротким для использования в качестве ключа шифрования, деревация помогает привести его к нужной длине используя весь ключ (а не просто отсечь лишние байты, или заполнить недостающие нулями)
// 3. Подписываем свой эфемерный публичный ключ своим долгосрочным приватным ключом.
// Это доказательство для клиента, что мы — именно тот сервер, за кого себя выдаем.
Мне кажется или здесь зарыта дыра в вашем алгоритме? Что мешает "человеку посередине" перехватить это сообщение а потом выдать себя за сервер просто отправив эту копию? Приватного ключа у него нет но он ему и не нужен так как он просто отправит тот же эфемерный публичный ключ который был сформирован сервером и прикрепит ту же самую подпись и таким образом успешно выдаст себя за сервера. А чтобы предотвратить такую возможность нужно чтобы сервер подписал/зашифровал своим приватным ключом не свой эфемерный ключ а некое рандомное число которое клиент сгенерирует и пришлет серверу в своем первом сообщении (и дальше клиент получив ответ расшифрует число публичным ключом сервера и проверит совпадает ли оно с тем числом которое клиент отправил)
Как я свой гибридный протокол шифрования за выходные написал