Смарт-контракты и идентификация пользователей
Простота и доступность этого паттерна позволяет использовать для авторизации пользователей почти любой блокчейн, даже самый простой, так как абсолютно любой блокчейн использует приватные и публичные ключи для идентификации своих пользователей. Но при использовании полноценных смарт-контрактов, эта схема может быть доработана до любого уровня сложности и иметь любые административные функции, присущие централизованным identity системам. Поэтому в статье я буду описывать Ethereum, как наиболее развитый блокчейн с поддержкой смарт-контрактов и всеми важными механизмами обеспечения безопасности сети, так как с ним сравнивают все новые решения, а алгоритмы, используемые в этой сети давно отвечают за миллиарды долларов криптоактивов пользователей и давно доказали свою состоятельность. Поехали...
Идентификация пользователей
В любых децентрализованных сетях идентифицировать пользователя или сетевой узел можно с помощью электронной подписи. Пользователь или узел (будем называть их просто “участниками”) создает ключевую пару - секретный и публичный ключи, а затем информация, созданная на основе публичного ключа, позволяет в дальнейшем идентифицировать участника. Например, в блокчейнах Ethereum адрес (можете называть его “аккаунтом”) - это 160bit от хеша публичного ключа, т.е. чтобы создать свой адрес в Ethereum нужно:
создать private_key/public_key keypair для использования со схемой ECDSA (в Ethereum сейчас используется эллиптическая кривая secp256k1)
взять последние 160 бит от хеша публичного ключа, SHA-3(pubkey) (в Etheruem используется функция хеширования Keccak-256
полученное значение - адрес в сети Ethereum выглядит примерно так: 0xDC25EF3F5B8A186998338A2ADA83795FBA2D695E
Каждый раз, владельцу адреса нужно подтвердить его право владения этим адресом - он подписывает данные с помощью своего секретного ключа. Например, любая криптовалютная транзакция с какого-то адреса включает в себя публичный ключ и электронную подпись, которые доказывают, что эту транзакцию мог создать только тот, кто имеет доступ к секретному ключу, от которого “наследуется” публичный ключ, от которого “наследуется” адрес.
Этот способ “подтвердить владение информацией, наследуемой от секретного ключа” крайне распространен. Например - выдача и проверка сертификатов HTTPS - это та же самая схема, сертификат HTTPS - это подписанный хеш публичного ключа с дополнительными данными (именем домена, датой истечения). Также, близкой процедурой является работа с “квалифицированной ЭЦП”, в которой контролирующий орган регистрирует публичный ключ организации вместе с информацией о ее названии, ИНН и т.п., а организация, отправляющая отчетность в электронном виде в дальнейшем подписывает ее с помощью своего секретного ключа.
Блокчейн, как identity-система
Учитывая вышеописанное, публичный блокчейн, который для пользователя может быть представлен как облачный сервис, хранящий адреса и сопутствующую им информацию, является прекрасным образцом системы, которая может хранить информацию, идентифицирующую пользователей.
Аутентификация всех пользователей в этой схеме проводится по приватному ключу, а публичные ключи (точнее их хеши) - хранятся “в блокчейне”. Схема эта привлекательна тем, что именно хеши публичных ключей являются “истинными” адресами в блокчейне и абсолютно любое действие в блокчейне, любая валидная транзакция является доказательством владения определенным адресом (определенным публичным ключом).
При этом все важные свойства блокчейнов, такие как устойчивость к атакам, невозможность ограничения доступа, полное отсутствие секретной информации делают его идеально подходящим видом систем для хранения информации, необходимой для идентификации пользователей. Блокчейн доступен отовсюду, получить доказательство существования адреса и любые подтверждения его валидности можно с любой блокчейн-ноды, в любой точке мира, заблокировать все блокчейн-ноды в мире не представляется возможным, а изменение информации о ключах невозможно провести незаметно, оно публично проверяемо.
Поэтому любые алгоритмы, требующие “доказательства владения адресом” отлично ложатся на смарт-контракты, а адрес становится уникальным иентификатором (uid) пользователя, не требующим хранения хешей паролей и иных видов shared secrets. Также, доступность и проверяемость такой “базы uid” позволяет сервису обойтись без backup-ов security информации, и в случае сбоев легко перестроить базу аккаунтов. Для публичных блокчейнов сервер, отдающий проверяемую информацию можно запустить где угодно без всяких регистраций, без необходимости иметь криптовалюту - она просто будет получать обновления от других нод по p2p сети.
Чтобы продемонстрировать удобство этой схемы опишем один из вариантов построения авторизации на сайте с использованием публичной блокчейн-сети Ethereum. Эта схема широко используется и требует от пользователя простейших действий в один клик, схожих с “login with Google”, но без отправки каких либо данных куда либо кроме сайта.
Регистрация
Регистрация в этой системе - это помещение в блокчейн информации, связывающей публичный адрес в блокчейне с идентификатором и любыми доп. данными. Если совсем просто, то “в блокчейне” создается запись в key-value БД, где:
key: адрес пользоватея (f.e. 0x13668Ecf257cC15c381b461B9fEDaB5D451c8F7F)
value: {login: “vasya”, age: 18, … }
В самом минимальном варианте достаточно просто наличия адреса в контракте. Вот пример кода такого мини-контракта, который "регистрирует" любого обратившегося:
contract Users {
mapping (address => bool) users; // storage of user addresses
event UserRegistered(address indexed user); // event, fired each time user is registered
constructor() {
register(); // register creator of contract as first user
}
function register() public {
users[msg.sender] = true;
emit UserRegistered(msg.sender);
}
}
Теперь, у любой ноды блокчейна можно получить эту информацию, запросив у смарт-контракта, хранящего адреса зарегистрированных пользователей данные по ключу (f.e. 0x13668Ecf257cC15c381b461B9fEDaB5D451c8F7F). Внутри данных, привязанных к адресу пользователя можно предусмотреть любую внешнюю логику - подтверждение KYC со специального адреса, принадлежащего внешнему KYC-провайдеру, указание nickname, под которым пользователь будет работать на сайте и т.п. Хранить в БЧ много данных дорого и бессмыссленно, обычно экономится каждый байт, поэтому в этом месте имеет смысл хранить только необходимый минимум. В нашем варианте это просто bool, но обычно используют struct с нужными onchain данными, например bitmask роли пользователя.
Чтобы зарегистрировать адрес в смарт-контракте, пользователь самостоятельно вызывает фукнцию register() со своего адреса. Именно здесь кроется главная проблема таких систем на текущий момент - чтобы зарегистрироваться, пользователю придется оплатить транзакцию в нативной криптовалюте Ethereum - ETH, и, хотя здесь немного данных и комиссия невелика, она все равно ненулевая. Поэтому onboarding пользователей в блокчейн проектах - это отдельная боль.Но, есть и хорошее - во-первых, пользователя не удастся взломать, получив доступ к сайту - в блокчейнах каждый сам отвечает за свою безопасность. Во-вторых - регистрационные действия происходят крайне редко, и один раз заплатив, пользователь может пользоваться этой учетной записью сколько хочет, информация о ней не пропадет и отвечает за нее он сам, а не ваш проект. Это и есть Self-Sovereign Identity.
P.S. вообще, в мире блокчейна не принято считать пользователя пользователем, пока он не сделал хотя бы одно значимое действие, поэтому можно вообще не использовать контракт, а просто проверить в блокчейне, есть ли у этого адреса на балансе нужная криптовалюта ( например, токен вашего проекта)
Авторизация
Для прохождения авторизации на сайте:
пользователь предъявляет свой адрес, под которым хочет авторизоваться
сайт убеждается в блокчейне, что этот адрес “зарегистрирован”
сайт предлагает пользователю подписать одноразовую строку (challenge)
пользователь подписывает challenge, предоставляя сайту ЭЦП
сайт проверяет подпись, и, если она верна - выдает пользователю обычный авторизационный JWT токен
Пользователи, использующие Ethereum в обязательном порядке используют ПО, которое подписывает транзакции, поэтому, если пользователь регистрировался ранее, или пользовался Ethereum в браузере, то он умеет подписывать данные своим секретным ключом. Например, при помощи расширения браузера Metamask это можно делать в один клик, одним и тем же способом отправляя криптовалюту, и точно так же подписывая challenge. При вызове функций JS библиотеки web3, которые предлагают подписать challenge, у пользователя всплывает уведомление расширения Metamask, предлагающее подписать строку. Выглядит в браузере это примерно так:
Строка, которую нужно подписать предоставляется бэкендом, который временно запоминает ее, чтобы исключить replay атаки, если атакующий перехватит подписанное сообщение и попробует его перепослать. Далее, challenge вместе с подписью отправляется на backend, который, проверив подпись удаляет одноразовый challenge из своей базы(исключая replay) и выдает пользователю авторизационный JWT токен. Дальше все работает как обычно в web. Примерный кусок кода, реализующий такую схему в Django приведен ниже (это обычный view, использующий POST запрос):
Код крайне простой, и, что важно, в базе проекта нет никакой security critical информации, кроме временных challenge - никаких хешированных и соленых паролей. Даже в случае утечки SQL базы c backend нет угрозы атаки на identity пользователей. Если угонят аккаунт - то у самого пользователя, а не через ваш сайт.
Такую аутентификацию можно проводить используя совершенно разные блокчейны, контракты, разные типы подписей, с помощью разного ПО, выполняющего подпись (хотя софт удобней и защищенней криптовалютных кошельков вам придется поискать).
Нужен ли блокчейн?
Внимательный читатель в схеме выше заметил, что в этой схеме, собственно блокчейн-то и не нужен, достаточно возможности подписать приватным ключом данные на стороне клиента. Это действительно так - и авторизация по приватному ключу сейчас активно развивается, пруф тут: https://www.w3.org/TR/webauthn-2/, так что скоро мы увидим эти схемы в наших браузерах без всяких блокчейнов. Эта схема крайне удобна тем, что сервису теперь не нужно отвечать за учетные записи пользователей, реализовывать схемы восстановления паролей, ведь каждый из этих механизмов несет в себе риски безопасности. Гораздо проще позволить пользователям самим заботиться о безопасности своих ключей - те, кто хотят мощной защиты заведут аппаратные ключи, внешние signer-ы (типа Parity Signer), будут хранить seed phrase в правильных менеджерах паролей, использовать схемы разделения секрета. Для тех, кто хранит пароли в текстовом файле на рабочем столе, правда, ничего не изменится.
Помяните мое слово - фразу "Sign Up" и "Забыл пароль" лет через пять вы увидите лишь на бородатых сайтах, тоскующих по Web 2.0.
Если всё таки нужен блокчейн
Но, вернемся к блокчейнам. Публичный блокчейн и смарт-контракты для данной схемы - это крайне удобное неубиваемое облако для security-critical информации и реализации более сложных схем. Например, вашему сервису требуется, чтобы identity пользователя подтвердил какой-нибудь внешний KYC-провайдер, или в сети был бы всегда доступный master public key для распространения обновлений софта (актуально для IoT). В этом случае смарт-контракт несложно дополняется административными аккаунтами с различными ролями, которые могут устанавливать адресам пользователей различные признаки, типа “прошёл KYC”, “вакцинирован от COVID”. Эти действия в правильных системах защищены мультиподписями, чтобы взлом одного из аккаунтов был бы недостаточен для атаки на систему. А использование смарт-контрактов означает, что доступ к такому “API” возможен откуда угодно без всяких авторизаций и лишних действий - вся защита обеспечивается за счет все той авторизации по приватному ключу. Identity-cистема на базе смарт-контрактов не требует почти никакой работы по обеспечению безопасности хранения данных, защита инфраструктуры в ней тривиальна, и с архитектурной точки зрения системы на базе смарт-контрактов не сложнее традиционных, а намного проще, а значит, надежней.
Такой функционал, разумеется, не бесплатен, и, реализуя такую систему необходимо всегда помнить про то, что в правильных блокчейнах не бывает бесплатных транзакций, а значит, любые важные административные действия в identity системе будут платными. Также, специфика кода смарт-контрактов исключает наличие в них функций, работающих сразу с большим числом сущностей, все функции должны отрабатывать за O(1) по про процессору, памяти, storage, объем хранимых данных - минимален, любые циклы должны быть ограничены сверху.
Синхронизация с блокчейном
Вышеупомянутые моменты сильно затрудняют решение задач типа “показать список пользователей, которые ожидают подтверждения identity” или “вывести все аккаунты с nickname, начинающимся со строки vasya...”. Поэтому любые, более менее сложные блокчейн-проекты, обязательно имеют backend, задачей которого является хранение и отображение данных, получить которые из напрямую из блокчейн-ноды нельзя. Это могут быть поисковые индексы по сущностям в блокчейне, какие-то большие offchain данные, которые сами не хранятся в блокчейне, а в контрактах представлены лишь их криптографические хеши, различные агрегированные данные.
Для организации такого backend-а и его синхронизации с блокчейном разумно использовать отдельный сервис синхронизации, который умеет накатывтаь обновления данных из блокчейна на свою SQL базу, параллельно проводя агрегацию и строя необходимые индексы. Такой сервис обычно использует subscription модель, подписываясь на события интересующих его контрактов. Каждый раз, когда в хранилище контракта происходит обновление (появляется новый зарегистрированный адрес, изменяются данные некоторого аккаунта) сервис видит эти изменения и изменяет свою БД в соответствии с ними.
В Ethereum, Substrate и многих других блокчейнах важной частью контрактов являются Events (event UserRegistered в примере приведенном выше). Очень часто нет необходимости постоянно держать на ноде информацию о произошедших событиях, ведь ее всегда можно “перепроиграть”, просматривая блоки один за другим, и наполнить базу на backend-е информацией из событий, экономя память и ресурсы ноды и не платя за занимаемое место в state database. В вышеприведенном примере сервису синхронизации достаточно подписаться на события контракта, и, каждый раз, когда встретится событие UserRegistered обновить список адресов пользователей в сервисе. События надежно с точностью до бита фиксируются в блокчейне и не могут быть изменены позже, поэтому им можно доверять. Также, генерация события стоит в разы дешевле, чем запись значений в state database, и, технически, вышеприведенный пример можно было бы сделать вообще без хранилища, просто фиксируя каждую регистрацию с помощью событий. Апофеозом упрощения "регистрации в блокчейне" можно считать просто отправку регистрирующимся любой суммы на адрес проекта, сам факт такой транзакции вполне можно считать регистрацией, если никакие другие метаданные не нужны, то перевод нативной криптовалюты - это самая дешевая из операций в любом блокчейне, информация о которой никогда не будет удалена.
Крайне важным фактором для определения того, какие события считать зафиксированными в блокчейне является финализация блоков. Для Ethereum 1.0, работающего на базе консенсуса proof-of-work, “надежными” событиями считаются те, которые произошли несколько блоков назад, т.е. имеют очень низкую вероятность того, что цепочка будет заменена на другую, более сложную, и “победит” другая история транзакций. Т.е. здесь "финальность" - условная, вероятностная, хотя и довольно надежная. В сетях с другими консенсусами следует крайне внимательно относиться к тому, что для данной сети является признаком финальности транзакции.
В блокчейнах с консенсусами proof-of-stake и proof-of-authority атаки типа “double spend” могут быть проведены без использования огромных мощностей, или, перестроение цепочки может быть вызвано разделением сети. В большинстве современных блокчейнов, использующих proof-of-stake и proof-of-authority используется детерминированная финальность, которая позволяет со 100% вероятностью утверждать, что финализированный блок никогда не будет откачен. Это очень удобно для сервиса синхронизации, и, пускай финализация обычно немного отстает от производства блоков (блок производит один компьютер, а финализируют его - несколько), зато, получив финализированный блок можно быть уверенным в том, что он не будет откачен (только в случае крайне серьезных сбоев в логике работы блокчейн-нод). Увы, я не могу кратко написать этот абзац, это тема отдельных статей и докладов, просто запомните - если у вас в сети не proof-of-work (нет майнинга) и блок не финализирован, значит информация еще "не в блокчейне", хотя уже включена в блок а сам блок опубликован в сети.
Заметки на полях
По моему скромному мнению, identity протоколы в связке с публичными блокчейнами в итоге повлияют на IT-инфраструктуру сильнее чем криптовалюты. Перенос security critical операций в блокчейн и управление публичными ключами имеют огромное значение. Эти операции мы видим в электронном документообороте, в доступе IoT устройств к облакам производителей, в выдаче verified credentials, вообще в любой области, где существуют цифровое право владения некой информацией. Публичные блокчейны позволяют избавиться от необходимости поддерживать различные виды API, контролировать доступ к серверам, убирая необходимость решать эти проблемы, точно так же как электромобили избавлены от проблем с зажиганием, подачей топлива и воздуха - их дизайн избавлен от этих инженерных проблем.
Хранение identity в смарт-контрактах - это крайне простой и надежный способ организации Public Key Infrastructure для проектов, именно поэтому я решил написать статью именно об этом несложном паттерне. В следующих статьях мы поговорим о других широко используемых алгоритмах в смарт-контрактах.
Greendq
Подобные решения требуют от пользователя дополнительных танцев с бубном - да, Metamask сильно упрощает жизнь, но всё же... И в вашей статье вообще ничего не говорится о варианте "забыл пароль" :) Этот вариант попросту не работает без единого центра, чего в БЧ как бы и нет.
BoogerWooger Автор
Ну, любые текущие системы требуют также танцев с бубуном - например владения почтовым ящиком, который может исчезнуть, или начать класть в спам письма, или телефона.
Я в конце написал, что никаких "забыл пароль" в этих схемах не будет - этот функционал изначально хромой - как был хромым функционал "выслать пароль на email". "Менять пароль" должен сам пользователь, а не сервис, без малейших дейсттвий на стороне сервиса, это его аккаунт и только ему им управлять. Проблема угнанных аккаунов решается созданием двухуровневых identity - master ключа и рабочих аккаунтов, которые master всегда может заменить.
Это долгая дискуссия, если кратко - это да, другая схема, которая сильно отличается от привычной. Но почему-то W3C ее принимает и развивает, а SSI и VC - это уже существующие тренды развития интернета. Ну и как то в DeFi крутятся десятки млрд $ в день без всяких восстановлений паролей, здесь первым делом в любых сервисах пишут огромными буквами "нет seed phrase, нет и мультиков"