Одним из замечательных аспектов сообщества разработчиков Ethereum является то, что инженеры обычно находятся рядом, чтобы помочь друг другу. В этом сообщении в блоге я хотел бы продолжить эту тенденцию, объясняя процесс, который мы недавно прошли, создав что-то, казалось бы, простое, но в деталях, приведшее к довольно сложным инженерным усилиям, которые привели к PR как для Geth, так и для Metamask. »Кодовые базы.

Немного предыстории: мы активно работаем над системой управления идентификацией, в которой есть функция, требующая от пользователей подтверждения контроля над адресом Ethereum. Наша первоначальная мысль заключалась в том, чтобы использовать простой eth_sign API Metamask, однако мы быстро поняли, что это неправильный путь, учитывая, что метод приводит к появлению красного сообщения об ошибке в пользовательском интерфейсе Metamask:

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

  1. Бэкэнд Go генерирует сообщение EIP-712 и отправляет его пользователю.
  2. Пользователь подписывает сообщение с помощью Metamask
  3. Полученная подпись отправляется на серверную часть и проверяется, чтобы гарантировать ее подлинность.

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

Создание EIP-712 в Go

Основным преимуществом сборки на Go является использование встроенных библиотек Geth. К счастью для нас, поддержка EIP-712 была недавно объединена, что позволило нам использовать стандартную библиотеку для наших целей. Первым шагом является создание псевдослучайного запроса для подписи пользователя. Мы передадим запрос пользователю для подписи с помощью Metamask, а затем проверим отправленный пользователем результат на сервере.

Здесь происходит много всего, но давайте разберемся немного, начиная с определения TypedData:

Здесь мы определяем поля двух структур данных, которые мы передадим в Metamask для подписания: Challenge и EIP712Domain. Первый полностью определяется нами, второй следует предлагаемой структуре разделителя доменов из стандарта EIP-712. Разделитель домена предназначен для обеспечения того, чтобы сообщения, подписываемые клиентами, могли использоваться только для определенных приложений dApp (и их соответствующих контрактов в цепочке). Поскольку наш конкретный вариант использования не предполагает какой-либо проверки в цепочке, домен не был столь критичным, чтобы правильно его настроить. При этом, если вы разрабатываете dApp и планируете проверку подписей в смарт-контрактах, необходимо тщательно продумать структуру домена, чтобы обеспечить правильное пространство имен как для целей безопасности, так и для целей управления версиями.

Далее мы указываем первичный тип. Это просто имя структуры данных, которая содержит основу нашего сообщения EIP-712 - в нашем случае это наша ранее определенная структура Challenge.

Наконец, заполняем пробелы !. Ключ Domain устанавливает значения для каждого из полей, которые мы указали в определении структуры нашего домена, а ключ Message устанавливает значения для каждого из полей, которые мы указали в определении структуры нашего первичного типа (Challenge).

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

Обратите внимание, что мы игнорируем второе значение каждого из вызовов HashStruct (их ответ с ошибкой). В производственном коде мы настоятельно рекомендуем проверять эти значения ошибок и обрабатывать их соответствующим образом.

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

Первые две строки обрабатывают кодирование и хеширование структур домена и первичного типа. Это относительно просто, достаточно вызвать HashStruct с именем структуры и отображением ее данных.

После хеширования двух структур нам нужно будет отформатировать их в байтовую строку, совместимую с EIP-712. Если вам интересно узнать, зачем нужен \x19\x01, не стесняйтесь погрузиться в этот раздел спецификации.

Теперь, когда у нас есть байтовая строка, мы просто хэшируем ее и вуаля! Теперь у нас есть все необходимое для завершения процесса проверки. На этом этапе вы должны сериализовать структуру signerData в JSON и отправить ее своему клиенту для подписи. Кроме того, вам необходимо сохранить challengeHash в базе данных, чтобы его можно было извлечь позже, когда пользователь отправит свою подпись для проверки.

Подписание полезной нагрузки с помощью Metamask

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

Еще раз, давайте немного разберем этот фрагмент кода:

В этом первом пункте мы рассмотрим стандартный танец Metamask: убедимся, что у пользователя установлено расширение, запросим у него разрешение на подключение к Ethereum API и получим массив адресов их кошельков (переменная accounts) в случае успеха.

Обратите внимание, что более полная реализация предполагает, что у пользователей будет более одной доступной учетной записи, и предоставит пользователю возможность выбрать, с какой учетной записью они хотят подписать сообщение. Этот шаг следует выполнить до создания сообщения, так как вы хотите убедиться, что поле Challenge.address в нашем сообщении EIP-712 совпадает с адресом, который использовался для его подписи.

Выглядит знакомо? По сути, мы скопировали все данные, которые мы сгенерировали ранее в нашем бэкэнде Go, и перенесли их в похожий объект в Javascript, а затем передали эти данные в метод eth_signTypedData_v3 RPC.

Обратите внимание, что eth_signTypedData_v3 - это немного сложно на момент написания этой статьи. Для начала требуется, чтобы вызывающий объект отправлял строку в кодировке JSON в качестве второго параметра, а не простой объект Javascript. Это должно измениться в будущих версиях, поэтому, если вы читаете это и получаете сообщение об ошибке, вам следует дважды проверить самую последнюю документацию и / или попытаться вызвать ее без JSON-кодировки. объект data. Кроме того, мы заметили ошибку с полями типа string в определении структуры сообщения: если она начинается с 0x, библиотека подписи Metamask автоматически преобразует ее в байтовую строку без уведомления, отбрасывая вашу подпись и приводя к плохому времени для вас. . Этот PR решает проблему, но если вы или ваши пользователи используете более старую версию Metamask, вы должны знать об этой проблеме.

Последний шаг! Давайте посмотрим на пример реализации onSignatureComplete:

Эта реализация представляет собой своего рода образовательную демонстрацию, следуя примеру Metamask в их вводном сообщении в блоге. Самая важная часть этого - установка переменной signature, так как вы захотите отрезать первые два символа, прежде чем отправлять ее обратно в Go для проверки.

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

Проверка подписи в Go

Вернувшись к нашей серверной кодовой базе, я предполагаю, что у вас есть какие-то средства получения подписи:

Еще раз давайте разберем этот фрагмент:

Для начала мы конвертируем закодированную в шестнадцатеричном формате подпись (прямо из Metamask) в []byte срез, который является типом данных, который нам понадобится для правильного управления им. Затем мы запускаем две проверки подписи, проверяя, что она имеет правильную длину, и удостоверяясь, что ее идентификатор восстановления (последний байт) установлен на 27 или 28. Последняя проверка предназначена для того, чтобы убедиться, что подпись соответствует устаревшим причинам. 'указано в Ecrecover определении функции в Geth. Продолжая следовать спецификации, мы вычитаем 27 из идентификатора восстановления, чтобы преобразовать его в 0 или 1, еще одну странность функции Ecrecover.

Наконец-то здесь происходит волшебство! Мы используем функцию geth Ecrecover, чтобы получить открытый ключ из предоставленной подписи. Если адрес Ethereum этого открытого ключа совпадает с адресом Ethereum нашего пользователя, все готово! Сообщение было успешно проверено. Если открытый ключ отличается, мы знаем, что подпись недействительна либо из-за искаженной полезной нагрузки, либо из-за неправильного ключа подписи.

Подведение итогов

Мы надеемся, что это сообщение в блоге поможет вам в любом будущем путешествии, которое вы можете совершить через сложный мир проверки подписи EIP-712. Этот процесс оказался намного сложнее, чем мы изначально думали, когда только начинали, но мы надеемся, что документирование нашей боли поможет сэкономить время других.

Это все для этого руководства! Если у вас есть какие-либо отзывы, вопросы или исправления, не стесняйтесь обращаться ко мне в Twitter, мой идентификатор - @stevenleeg.

Особая благодарность Кумавису из Metamask за то, что он часами копался по телефону в кодовых базах Geth и Metamask, чтобы заставить его работать должным образом.

Посетите сайт Alpine

Следите за нами в Twitter

Ничто в этой статье не должно восприниматься как юридический или инвестиционный совет.