Авторизация узла Пандоры

в 9:32, , рубрики: captcha, p2p, Peer-to-Peer, авторизация, криптография, хэш-алгоритм, эцп

После статьи о системе доверия, как и после статьи о Пандоре, мне снова пишут люди. Особенно приятно, когда практикующие программисты задают конкретные вопросы. Чтобы в личной переписке не повторяться, я решил написать цикл статей об алгоритмах. Сегодня поговорим о 3-х (уже реализованных в коде) механизмах авторизации: хэш-загадке, которая ограждает от DoS-атак, электронной подписи, которая идентифицирует узел-собеседник, и картинке-загадке, которая отсеивает спамеров и ботов.

Авторизация узла Пандоры - 1

Поговорим о запланированных: бан-листе, системе штрафов, а также ускоренной авторизации по сеансовому ключу после обрыва связи. Для начала взглянем на общую диаграмму сеанса связи:

Авторизация узла Пандоры - 2

При авторизации охотник, как инициатор соединения, подвергается большему числу проверок, нежели слушатель. Стадии авторизации проходят под диктовку слушателя, а охотник должен строго отвечать на запросы слушателя. На этапе авторизации идёт синхронный обмен.

Сеанс начинается с фразы приветствия охотника:

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 (на период сеанса связи). Такой механизм даёт право писать новичкам с неподтверждёнными ключами, но при этом ограждает от ботов и спам-рассылок.

Авторизация узла Пандоры - 3

Если же длина капчи задана 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х неавторизованных сессий

При нарушении в бан-лист заносятся адрес и время разбанивания (текущее время плюс срок нарушения).
Если нарушение повторяется, то время разбанивания отодвигается вперёд на дополнительный штрафной срок.

Авторизация узла Пандоры - 4

Если приходит подключение с адреса, который находится в бан-листе и текущее время не достигло времени разбанивания, то соединение разрывается. Также в unix-системах возможна связка Пандоры и фаервола (iptables, например), при которой отброс (reject) ip будет происходить на системном уровне, избавляя приложение от инициализации tcp (или udp) сокета.

5. Ускроренное восстановление сеанса

В настоящий момент авторизация производится на открытых ключах ассиметричного алгоритма RSA. Но в будущем запланирована генерация закрытых сеансовых ключей для симметричного алгоритма, например Blowfish.

Сеансовый ключ будет использоваться для быстрого шифрования трафика между узлами. А в целях быстрой авторизации после обрыва охотник может предложить панхэш последнего сеансового ключа. Симметричная криптография работает на порядок быстрее, а следовательно в сокращённой авторизации можно пропустить проверку хэш-загадки, основных ключей и капчи, мгновенно восстанавливая утерянную сессию. Кроме того, панхэш сессионого ключа меняется гораздо чаще (так как сессионные ключи периодически меняются, например один раз в час), а это затруднит злоумышленнику представлять себя от имени известного узла, так как панхэши сессионных ключей нигде не публикуются, в отличие от публичных, которые общедоступны в сети.

6. После авторизации

После прохождения всех стадий авторизации, узлы становятся равноправными и обмен продолжается асинхронно. Каждая сторона формирует запросы, следуя своей внутренне логике. Другая сторона принимает запросы и отвечает. Узлы запрашивают друг у друга записи и открывают медиа-каналы. Но об этом в следующих статьях.

Автор: robux

Источник


* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js