После статьи о системе доверия, как и после статьи о Пандоре, мне снова пишут люди. Особенно приятно, когда практикующие программисты задают конкретные вопросы. Чтобы в личной переписке не повторяться, я решил написать цикл статей об алгоритмах. Сегодня поговорим о 3-х (уже реализованных в коде) механизмах авторизации: хэш-загадке, которая ограждает от DoS-атак, электронной подписи, которая идентифицирует узел-собеседник, и картинке-загадке, которая отсеивает спамеров и ботов.
Поговорим о запланированных: бан-листе, системе штрафов, а также ускоренной авторизации по сеансовому ключу после обрыва связи. Для начала взглянем на общую диаграмму сеанса связи:
При авторизации охотник, как инициатор соединения, подвергается большему числу проверок, нежели слушатель. Стадии авторизации проходят под диктовку слушателя, а охотник должен строго отвечать на запросы слушателя. На этапе авторизации идёт синхронный обмен.
Сеанс начинается с фразы приветствия охотника:
hello = {:version=>0, :mode=>0, :mykey=>my_key, :tokey=>out_key, :addr=>my_callback}
В которой присутствует версия протокола, опции обмена, панхэш ключа на этом узле, панхэш ключа на том узле, ip-адрес для обратного подключения.
Слушатель проверяет приветствие. Если параметры приветствия допустимы (протокол согласован, режимы поддерживаются, панхэш ключа охотника не значится, как имеющий отрицательное доверие), то можно переходить к шагам авторизации.
1. Хэш-загадка (puzzle)
Первым делом нужно заставить охотника совершить некоторый объём работы (концепция Proof-of-work). Это снижает способности охотника к DoS-атаке слушателя, которая, как вы знаете, заключается в ассиметричном расходовании ресурсов. С помощью хэш-загадки слушатель смещает баланс в свою сторону.
Для этого слушатель генерирует случайную 256-символьную фразу, в которой последний байт заменяется на длину в битах требуемой «отгадки», например, 14 бит.
Задача охотника – добавить такое число к фразе, первые 14 бит суммарного хэша sha2 которого совпадут с фразой-загадкой. В виде формулы можно выразить так:
F & M = sha2(F+A) & M,
где F – фраза,
M – битовая маска требуемой длины, например 011111111111111b,
A – блок (число), который необходимо найти, чтобы выполнилось тождество.
Так как аналитического решения у уравнения с хэшем нет, то охотнику приходится перебором подставлять числа и высчитывать хэш для каждого блока (фраза+число) до тех пор, пока решение не будет найдено. Время поиска зависит от требуемой длины маски и удачи. При 14 битах на среднем ноутбуке среднее время поиска равняется около 2 секунд (иногда больше, иногда меньше). Вы можете увеличить или уменьшить размер маски, регулируя этим самым требовательность к охотнику.
Также в предпоследнем байте фразы задаётся минимальное время задержки, которое охотник должен ждать, если нашёл отгадку слишком быстро.
По умолчанию в Пандоре заданы такие параметры:
puzzle_bit_length = 14
puzzle_sec_delay = 2
Если охотник честно отработал и нашёл отгадку, а слушатель проверил отгадку, то авторизация переходит на следующий этап. Если охотник отказывается выполнять работу или присылает неверную отгадку, то штрафуется добавлением во временный бан-лист. Вот часть кода, которая отвечает за генерацию загадки, нахождение (простым перебором) и проверку отгадки:
# Generate random phrase
# RU: Сгенерировать случайную фразу
def get_sphrase(init=false)
phrase = params['sphrase'] if not init
if init or (not phrase)
phrase = OpenSSL::Random.random_bytes(256)
params['sphrase'] = phrase
init = true
end
[phrase, init]
end
phrase, init = get_sphrase(true)
phrase[-1] = puzzle_bit_length.chr
phrase[-2] = puzzle_sec_delay.chr
# Find sha1-solution
# RU: Находит sha1-загадку
def self.find_sha1_solution(phrase)
res = nil
lenbit = phrase[phrase.size-1].ord
len = lenbit/8
puzzle = phrase[0, len]
tailbyte = nil
drift = lenbit - len*8
if drift>0
tailmask = 0xFF >> (8-drift)
tailbyte = (phrase[len].ord & tailmask) if tailmask>0
end
i = 0
while (not res) and (i<0xFFFFFFFF)
add = PandoraUtils.bigint_to_bytes(i)
hash = Digest::SHA1.digest(phrase+add)
offer = hash[0, len]
if (offer==puzzle) and ((not tailbyte) or ((hash[len].ord & tailmask)==tailbyte))
res = add
end
i += 1
end
res
end
# Check sha1-solution
# RU: Проверяет sha1-загадку
def self.check_sha1_solution(phrase, add)
res = false
lenbit = phrase[phrase.size-1].ord
len = lenbit/8
puzzle = phrase[0, len]
tailbyte = nil
drift = lenbit - len*8
if drift>0
tailmask = 0xFF >> (8-drift)
tailbyte = (phrase[len].ord & tailmask) if tailmask>0
end
hash = Digest::SHA1.digest(phrase+add)
offer = hash[0, len]
if (offer==puzzle) and ((not tailbyte) or ((hash[len].ord & tailmask)==tailbyte))
res = true
end
res
end
Заметьте, затраты на проверку через единственный вызов хэш-функции требуют гораздо меньше вычислительных ресурсов, чем многократный вызов хэш-функций для поиска отгадки. Но это в «нормальном» режиме.
Охотник-злоумышленник может подсунуть левую отгадку, желая нагрузить слушателя. В этом случае ресурсные потери слушателя будут больше, чем у охотника. Но важно понимать, что потери на генерацию фразы (которую, кстати, можно генерять не чаще одной в минуту для всех) и одиночный вызов хэш-функции не сопоставимы с теми потерями, которые бы понёс слушатель от охотника на следующих этапах авторизации. Поэтому хэш-загадка является первой проверкой охотника «на вшивость», при этом требует минимум «крови» слушателя.
Прохождение хэш-загадки может быть пропущено, если соединение поступает с известного ip-адреса или с узла, приславшего приветствие с заданными критериями (например, с панхэшем известного ключа). Все перепетии момента могут быть заданы на узле.
Хэш-загадка, например, вообще может быть отключена, но ЭЦП обоюдо обязательна: как для охотника, так и для слушателя.
2. Электронная подпись (sign)
В этом месте нужно заметить, что хэш-загадка была придумана, чтобы безопасно подойти к шагу обмена ключами, анкетами и началу «взрослой» криптографии. Ведь принятие Ключа, регистрация Узла и непосредственно сама проверка подписи требуют ощутимые ресурсы слушателя. Дополнительной мерой защиты может служить ограничение на число принимаемых неизвестных (не просчитываемых в системе доверия слушателя) ключей с одного ip-адреса за заданную единицу времени. Например, в сутки не более 3-х неизвестных ключей с одного ip.
Проверка ключа происходит так. Слушатель посылает охотнику фразу (по умолчанию, если была стадия хэш-загадки, то фраза не высылается, а используется та же самая), охотник берёт хэш sha2-384bit от фразы, подписывает своим ключом и высылает подпись слушателю:
rphrase = OpenSSL::Digest::SHA384.digest(rphrase)
sign = PandoraCrypto.make_sign(@rkey, rphrase)
Слушатель тоже берёт хэш от фразы и сверяет подпись:
res = PandoraCrypto.verify_sign(@skey, OpenSSL::Digest::SHA384.digest(params['sphrase']), rsign)
Если совпала, то происходит обратная проверка. Теперь уже охотник шлёт фразу слушателю, а слушатель подписывает её хэш и высылает охотнику.
Обратите внимание: подписывается не сама фраза, а хэш от фразы!
Дело в том, что ключи могут использоваться не только для авторизации узлов, но и для подписания записей. В этом случае, охотник (или слушатель) под видом «случайной» фразы может подсунуть какую-нибудь рабочую запись (Ключ, Человек, Договор, Проект и т. д.) и таким образом подставить владельца ключа (слушателя или охотника), который даже не будет подозревать, что его правом подписи воспользовались втёмную! В случае же когда подписывается хэш, а не сама фраза, такая манипуляция невозможна. В то же время обеспечивается надёжная проверка на владение закрытым ключом.
Можно написать больше о криптографии в Пандоре (например, о формате надёжного хранения ключей), но это выходит за рамки данной статьи. В остальном же на этапе авторизации Пандора использует базовые вызовы OpenSSL.
3. Картинка-загадка (captcha)
Если ключ охотника из предыдущей стадии не имеет доверия слушателя, то дальнейший ход сеанса связи зависит от настроек для капчи:
captcha_length = 4
captcha_attempts = 2
trust_for_captchaed = true
Если длина капчи captcha_length больше нуля (по умолчанию равна 4), то слушатель, используя библиотеки Cairo и Gdk, генерирует картинку с заданным числом символов и предлагает охотнику её отгадать. Если человек на узле-охотнике отгадал картинку, то ему присваивается временное доверие trust = 0.01 (на период сеанса связи). Такой механизм даёт право писать новичкам с неподтверждёнными ключами, но при этом ограждает от ботов и спам-рассылок.
Если же длина капчи задана 0, то охотнику будет выдано сообщение:
«Ключ на подтверждении»
До тех пор, пока на слушателе не проставят доверие ключу охотника, сеанс связи будет обрываться на данном этапе.
4. Активный бан-лист
В настоящий момент бан-лист не реализован в коде, но видится он примерно так:
узел ведёт список недавно подключавшихся ip-адресов и отмечает, кто как себя вёл.
Если недавно авторизация прошла нормально, то при повторном подключении этапы хэш-загадки и капчи могут быть пропущены, потребуется только подпись.
Если ip-адрес вел себя «неприлично», то накапливается длительность бана:
1) подключился и отключился без трафика — 1 мин
2) подключился, отправил приветствие, но не ответил на хэш-загадку — 2 мин
3) прислал отгадку раньше срока — 30 сек
4) прислал неверную отгадку — 10 мин
5) прислал отгадку в срок и верную, но отключился, не прислав подписи — 2 мин
6) прислал неверную подпись — 10 мин
7) прислал неверную капчу, исчерпав все (2 по умочанию) попытки — 2 мин
8) подключился повторно раньше чем через 5 мин, хотя был ответ «ключ на рассмотрении» — 5 мин
9) замолчал во время связи на этапе авторизации и не отвечает более 1 мин — 20 сек
10) прислал неадекватный сегмент для текущей стадии — 5 мин
11) авторизация с одного ip со вторым неизвестным ключом с интервалом менее 2 ч — 15 мин
12) с одного ip может быть не более 2х неавторизованных сессий
При нарушении в бан-лист заносятся адрес и время разбанивания (текущее время плюс срок нарушения).
Если нарушение повторяется, то время разбанивания отодвигается вперёд на дополнительный штрафной срок.
Если приходит подключение с адреса, который находится в бан-листе и текущее время не достигло времени разбанивания, то соединение разрывается. Также в unix-системах возможна связка Пандоры и фаервола (iptables, например), при которой отброс (reject) ip будет происходить на системном уровне, избавляя приложение от инициализации tcp (или udp) сокета.
5. Ускроренное восстановление сеанса
В настоящий момент авторизация производится на открытых ключах ассиметричного алгоритма RSA. Но в будущем запланирована генерация закрытых сеансовых ключей для симметричного алгоритма, например Blowfish.
Сеансовый ключ будет использоваться для быстрого шифрования трафика между узлами. А в целях быстрой авторизации после обрыва охотник может предложить панхэш последнего сеансового ключа. Симметричная криптография работает на порядок быстрее, а следовательно в сокращённой авторизации можно пропустить проверку хэш-загадки, основных ключей и капчи, мгновенно восстанавливая утерянную сессию. Кроме того, панхэш сессионого ключа меняется гораздо чаще (так как сессионные ключи периодически меняются, например один раз в час), а это затруднит злоумышленнику представлять себя от имени известного узла, так как панхэши сессионных ключей нигде не публикуются, в отличие от публичных, которые общедоступны в сети.
6. После авторизации
После прохождения всех стадий авторизации, узлы становятся равноправными и обмен продолжается асинхронно. Каждая сторона формирует запросы, следуя своей внутренне логике. Другая сторона принимает запросы и отвечает. Узлы запрашивают друг у друга записи и открывают медиа-каналы. Но об этом в следующих статьях.
Автор: robux