Надежный, безопасный и универсальный бэкап для U2F

Мне действительно нравится уровень безопасности, предоставляемый U2F, но вместе с безопасностью, необходимо продумать и план восстановления. Потеря доступа к своим самым важным аккаунтам, если с основным U2F токеном что-то случится — серьезная проблема. В то же время, хотелось бы избежать использования бэкапа, который ставит под угрозу безопасность, предоставляемую U2F.

yubikey

Популярные методы бэкапа

На сегодняшний день, образцовая практика — держать второй независимый U2F токен для бэкапа; этот токен должен быть добавлен вручную на каждый сервис и храниться в «безопасном» месте. Другая общепринятая практика — использовать не-U2F метод в качестве бэкапа (OTP, коды восстановления). Честно говоря, оба этих метода оставляют желать лучшего.

Независимый U2F токен

Это означает, что каждый раз при регистрации на каком-нибудь новом сервисе, мне нужно добавить оба моих токена. Это обстоятельство порождает ряд проблем:

  • Бэкап-токен должен быть довольно легкодоступным. Несмотря на то, что я не буду носить его с собой на связке ключей, я должен иметь возможность быстро до него добраться, так что я вряд ли смогу придумать что-то лучше, чем держать его у себя дома. Насколько это реально безопасно, даже если используется сейф — можно долго говорить;
  • Когда я вынужден регистрироваться на каком-нибудь сервисе находясь вне дома, я не могу добавить бэкап-токен. Так что нужно постараться запомнить, что нужно добавить его позднее, и пока это не произошло, бэкапа нет. В худшем случае, я могу и вообще забыть про него;
  • Когда я дома, оба моих токена находятся в одном и том же месте. Такой метод бэкапа далек от идеала: оба токена могут оказаться недоступны вследствие одного происшествия (быть уничтожены или украдены) ;
  • Тот факт, что бэкап-токен хранится дома, совершенно очевиден. Если кто-то действительно хочет добраться до моего токена, он уже знает, где его искать;
  • Неуниверсальный метод: не все сервисы позволяют добавить более одного ключа в аккаунт.

На мой взгляд, эта «образцовая практика» не очень надежна, и довольно обременительна. Давайте посмотрим на другую распространенную практику.

Не-U2F метод в качестве бэкапа

OTP:

  • Использовать OTP в качестве бэкапа — это лучше, чем использовать его как основной метод 2FA, но факт наличия OTP так или иначе открывает дополнительный вектор атаки;
  • Телефоны ломаются, теряются и крадутся, и если после его утери есть вероятность, что он окажется у посторонних лиц, то нужно вручную отозвать этот бэкап на всех аккаунтах;
  • Я всегда ношу телефон и U2F токен с собой, так что, опять же, подобный метод бэкапа далек от идеала: вероятность утери сразу и того, и другого, значительно выше, чем если бы бэкап хранился отдельно. Но этот пункт можно немного компенсировать, используя, например, Authy, который хранит зашифрованный бэкап у себя на сервере;
  • Неуниверсальный метод: к сожалению, есть достаточное количество сервисов, предлагающих только кастомные приложения, и не поддерживающих стандартный TOTP.

Коды восстановления:

  • Коды восстановления нужно хранить в безопасном месте. Опять же, это «безопасное место» будет, скорее всего, моим домом, с почти такими же проблемами, как и у отдельного U2F токена;
  • Опять-таки, неуниверсальный метод: у каждого сервиса свой подход к бэкапу

Так что, подводя итоги, все эти методы неуниверсальны, обременительны и не слишком безопасны.

Оптимальный метод бэкапа

Теперь, после того как я достаточно раскритиковал существующее положение дел, я, наконец, скажу, что же я на самом деле хочу. Я действительно хочу иметь два U2F токена: основной и бэкапный, но они должны быть настроены определенным образом:

  • Когда я регистрирую основной токен на каком-либо устройстве, бэкап-токен автоматически становится рабочим для этого сервиса;
  • Как только я использую бэкап-токен на каком-либо сервисе, основной токен оказывается недействительным для этого сервиса.

Прежде, чем мы обсудим техническую возможность этого в рамках U2F, я объясню почему это здорово, и как я это использую.

Почему это здорово

Если мы посмотрим на критику независимого бэкап-токена, изложенную выше, мы можем увидеть, что все недостатки этого метода оказываются устранены:

  • Бэкап-токен больше не должен быть легкодоступным. Экстремальными примерами могут быть: замуровать токен внутрь кирпичной стены, или закопать на полтора метра в саду или еще где-нибудь. Без шуток, я вполне готов пойти на это;
  • Вне зависимости от того, где я нахожусь, если я регистрируюсь на каком-либо сервисе, мне не нужно делать ровно ничего, чтобы добавить бэкап-токен на этот сервис. Я просто использую мой основной токен, и нахожусь в душевном спокойствии, зная, что у меня есть бэкап;
  • Для посторонних лиц совершенно неочевидно, где находится мой бэкап-токен. Даже зная, что он существует, пытаться его найти самостоятельно вряд ли имеет смысл;
  • Это достаточно безопасно. Даже если что-то плохое случится с моим основным токеном, крайне маловероятно, что это же происшествие затронет и бэкап-токен;
  • Это универсально. Такой метод бэкапа будет работать на любом сервисе с поддержкой U2F, независимо от того, что еще поддерживает этот сервис.

И если с основным токеном действительно случится что-то плохое, то я делаю следующее:

  • Откапываю / размуровываю бэкап-токен;
  • Аутентифицируюсь на всех своих сервисах с U2F, тем самым аннулируя основной токен;
  • Заказываю новую пару токенов, и после получения, добавляю новый основной токен на всех сервисах, и отзываю старый.

По крайней мере, для меня лично эта стратегия — отличный компромисс для высокого уровня безопасности и необременительного бэкапа. Это более безопасно и более надежно, чем любой другой метод.

Реализация

Краткий обзор протокола U2F

Прежде чем мы можем говорить о реализации, мы должны на определенном уровне понимать, как работает U2F. Большинство производителей реализуют его следующим образом (не все из нижеприведенного присутствует в стандарте; некоторые вещи являются деталями реализаций, но большинство существующих реализаций, насколько мне известно, работает именно так):

В U2F-токене запрограммирован device_secret, вместе с 32-битным counter, который может быть только инкрементирован. Когда мы регистрируем U2F-токен на каком-либо сервисе, происходит следующее:

  • Браузер отправляет U2F-устройству AppID (фактически, доменное имя);
  • Устройство генерирует случайное число (nonce), объединяет его с его с AppID, пропускает все это через HMAC-SHA256 используя device_secret в качестве ключа, и результирующий хеш становится приватным ключом для этого конкретного сервиса: service_private_key;
  • Из service_private_key, генерируется публичный ключ service_public_key;
  • Устройство берет AppID снова, объединяет его с service_private_key, и снова пропускает через HMAC-SHA256 используя device_secret в качестве ключа. Результат (MAC), вместе с nonce который был сгенерирован ранее, становится key_handle;
  • Устройство отправляет key_handle и service_public_key обратно браузеру, и браузер передает сервису, который сохраняет эти данные для будущих аутентификаций.

Последующая аутентификация проходит следующим образом:

  • Сервис генерирует challenge (случайно сгенерированные данные) и отправляет их браузеру вместе с key_handle (который состоит из nonce и MAC). Браузер передает все это устройству, вместе с AppID (т.е. доменным именем);
  • Устройство, имея nonce и AppID, генерирует service_private_key тем же самым образом, которым он был сгенерирован при регистрации;
  • Устройство генерирует MAC тем же самым образом как и при регистрации, и сравнивая его с MAC полученным от браузера, удостоверяется что nonce не подменен, и следовательно, service_private_key достоверен;
  • Устройство инкрементирует counter;
  • Устройство подписывает challenge, AppID и counter с помощью service_private_key, и отправляет результирующую подпись (signature) и counter браузеру, который передает эти данные далее на сервис;
  • Сервис проверяет signature с помощью имеющегося у него после регистрации service_public_key. Также, большинство сервисов проверяют, что counter больше, чем предыдущее значение (если это не первая аутентификация). Цель этой проверки — сделать недоступным клонирование U2F-устройств. В итоге, если signature совпадает и counter больше, чем предыдущее значение, аутентификация считается успешно законченной, и сервис сохраняет новое значение counter.

Теперь давайте обозначим детали, которые имеют прямое отношение к дискуссии.

Детали, представляющие интерес

Первое — это то, что устройство не хранит service_private_key для каждого сервиса: вместо этого, оно выводит service_private_key каждый раз используя HMAC-SHA256. Это очень важно для нас: очевидно, если бы каждое устройство хранило бы уникальные ключи отдельно для каждого сервиса, то только это устройство могло бы осуществлять аутентификацию впоследствии.

Это, между прочем, не является требованием U2F: U2F не указывает, как именно ключи должны храниться, и некоторые ранние реализации U2F, действительно, хранили ключи для каждого сервиса в отдельности. Этот подход обладает тем недостатком, что количество сервисов, для которых устройство может быть использовано, ограничено. Деривация service_private_key устраняет этот недостаток.

И второе — устройство имеет counter, чтобы предотвратить клонирование.

На первый взгляд, может показаться, что этот counter не позволяет нам реализовать обсуждаемую бэкап-стратегию (как минимум, мне так казалось, когда я пытался найти решение), однако на самом деле, он только помогает нам! Сейчас объясню.

Основная идея

Идея заключается в следующем: на этапе производства, запрограммировать два токена таким образом, что они оба имеют одинаковый device_secret, но бэкап-токен нуждается в некоторой коррекции: вместо того, чтобы использовать counter в чистом виде (как делают обычные токены), он должен добавить некоторую большую константу к counter. Например, половина 32-битного диапазона, т.е. примерно 2 000 000 000, выглядит разумно: я вряд ли исчерпаю такое количество аутентификаций за всю жизнь.

Фактически, это все. Просто и эффективно.

Имея два токена запрограммированных таким образом, я прячу бэкап-токен в какое-нибудь реально труднодоступное место, и никогда его не трогаю. Если что-то ужасное случается и я утрачиваю доступ к основному токену, я таки добираюсь до бэкап-токена, и сразу могу использовать его на всех сервисах, где я регистрировал основной токен, т.к. бэкап имеет тот же самый device_secret, и его counter начинается с реально большого числа, до которого я не доберусь на протяжение всей жизни.

Также, обращаю внимание, что я не предлагаю делать токены клонируемыми. Два токена, хотя и обладают одинаковым device_secret, имеют разные счетчики, и после программирования device_secret не должно быть способа получить его обратно из устройства или каким-либо другим образом создать клон.

Замечание насчет counter

Внимательный читатель может заметить, что имеется следующая проблема безопасности: что если злоумышленник получает доступ к основному токену и как-либо инициирует 2 000 000 000 аутентификаций? Тогда он получает доступ к сервису даже после того, как бэкап-токен был использован на этом сервисе.

К счастью, эта проблема имеет простое решение. В любом случае, счетчик должен быть реализован аппаратно (предположительно, на некотором криптопроцессоре), и для безопасной реализации этот аппаратный счетчик должен иметь диапазон меньше, чем 32 бита. Например, на ATECC508A счетчики могут считать только до 2097151, так что, устанавливая константу, добавляемую к счетчику, в любое значение большее чем максимальное значение счетчика, мы можем быть уверены, что основной токен никогда не сможет досчитать до счетчика в бэкап-токене.

Для пояснения: допустим, что на нашем U2F-токене используется ATECC508A, и обозначим счетчик внутри ATECC508A как hw_counter. Тогда:

  • В основном токене, мы используем для вычислений: hw_counter;
  • В бэкап-токене, мы используем для вычислений: hw_counter + 2000000000.

Обратите внимание, что мы не модифицируем реальный hw_counter внутри криптопроцессора; он по-прежнему будет считать от 0 до 2097151. Вместо этого, каждый раз когда нам нужно получить значение счетчика, мы считываем hw_counter из ATECC508A, потом добавляем нашу константу, и возвращаем (для дальнейших вычислений для U2F).

Таким образом, диапазон значений счетчика в основном токене будет [0, 2097151], тогда как диапазон значение счетчика в бэкап-токене будет [2000000000, 2002097151]. Тот факт, что эти диапазоны не пересекаются, обеспечивает аннулирование основного токена при использовании бэкапа (если сервис использует counter; основные сервисы, которые я проверил, используют его).

Фактическая реализация

Ни один из производителей U2F-токенов, о которых я знаю, не поддерживает требуемой кастомизации на сегодняшний день. Но к счастью, существует open-source реализация U2F-токена: SoloKeys.

Я писал свою оригинальную статью (на англ) год назад, и эта часть несколько устарела: тогда SoloKeys был на стадии прототипирования, а я использовал предыдущую итерацию проекта: u2f-zero. Поэтому переводить эту часть я сейчас не буду, поскольку единственный способ получить u2f-zero устройство — это спаять его самостоятельно, и заниматься этим вряд ли целесообразно (хотя на гитхабе есть инструкции).

Тем не менее, все подробности необходимой модификaции u2f-zero приведены в моей оригинальной статье на англ.

Когда дойдут руки до solokeys, я напишу и инструкцию по его модификации.

Так или иначе, это — единственный известный мне на сегодня способ получить рабочий U2F-токен с надежным бэкапом. Проверка нескольких сервисов (как минимум google и github) показала, что он работает: зарегистрировав основной токен на сервисе, мы можем также использовать бэкап, и после первого использования бэкапа, основной токен перестает работать. Awwwwwww. <3

Предупреждение

Несмотря на то, что эта бэкап-стратегия крута, я не настолько уверен в ее конкретной реализации, посредством u2f-zero или solokey. Этот путь — единственный способ получить желаемое, так что этим путем я и пошел;, но если предположить, что злоумышленник получает физический доступ к U2F-устройству, я не уверен, что взлом устройства (т.е. получение device_secret из него) будет настолько же сложным, каким бы он был в случае Yubikey или других основных производителей. Авторы solokey заявляют, что «уровень безопасности такой же, как в современном автомобильном ключе», но я не проводил экспертиз чтобы это подтвердить.

Тем не менее, честно говоря, я не очень об этом беспокоюсь. Если злоумышленник просто крадет токен без намерения его вернуть, то тогда сложность его взлома не играет роли, т.к. злоумышленник может просто использовать этот токен для доступа в аккаунт и, например, просто отозвать этот токен и добавить другой. Однако, для этого я должен иметь и другие серьезные проблемы безопасности, т.к. U2F-токен — это только второй фактор.

Так что, единственный сценарий, при котором solokey может быть менее безопасным, чем что-то другое — это когда злоумышленник пытается получить доступ к устройству в течение краткого промежутка времени, получить device_secret из него, и вернуть устройство обратно, незаметно для меня. Для этого, ему необходимо прочитать содержимое флеши микроконтроллере (или RAM в правильный момент), и это не очень тривиально.

Принимая во внимание все факторы, я считаю, что для меня лично иметь надежный бэкап — это гораздо более важно, чем иметь сверхбезопасную аппаратную реализацию U2F-устройства. Вероятность проблем с такой безопасной реализацией и отсутствием хорошего бэкапа выше, чем вероятность проблем с u2f-zero (solokey) и бэкапом.

Заключение

Рассмотренная стратегия бэкапа выигрывает у альтернатив на всех измерениях: она универсальна, более безопасна и более надежна, чем любые другие способы.

Я буду рад, если хотя бы один из основных производителей реализует это в своих продуктах, но уверенности пока нет. Один парень из поддержки Yubico, James A., даже сказал мне, что реализовать бэкап так, как мне нужно, «is not possible with the way U2F is designed», и после того, как я изложил детали реализации, он просто перестал отвечать.

К счастью, это оказалось не настолько невозможно, насколько считает Yubico.


Моя оригинальная статья на английском: Reliable, Secure and Universal Backup for U2F Token. Т.к. автор оригинальной статьи — я сам, то, с вашего позволения, я не стал помещать эту статью в категорию «перевод».

© Habrahabr.ru