Одним из замечательных аспектов сообщества разработчиков Ethereum является то, что инженеры обычно находятся рядом, чтобы помочь друг другу. В этом сообщении в блоге я хотел бы продолжить эту тенденцию, объясняя процесс, который мы недавно прошли, создав что-то, казалось бы, простое, но в деталях, приведшее к довольно сложным инженерным усилиям, которые привели к PR как для Geth, так и для Metamask. »Кодовые базы.
Немного предыстории: мы активно работаем над системой управления идентификацией, в которой есть функция, требующая от пользователей подтверждения контроля над адресом Ethereum. Наша первоначальная мысль заключалась в том, чтобы использовать простой eth_sign
API Metamask, однако мы быстро поняли, что это неправильный путь, учитывая, что метод приводит к появлению красного сообщения об ошибке в пользовательском интерфейсе Metamask:
Вместо этого мы решили работать с новым стандартом EIP-712, который предоставляет пользователю гораздо более удобный интерфейс и легко проверяется в сети, если нам когда-нибудь понадобится эта возможность в будущем. К концу этого руководства мы покажем вам, как построить следующий поток:
- Бэкэнд Go генерирует сообщение EIP-712 и отправляет его пользователю.
- Пользователь подписывает сообщение с помощью Metamask
- Полученная подпись отправляется на серверную часть и проверяется, чтобы гарантировать ее подлинность.
Довольно просто, правда? Не совсем! Как всегда, дьявол кроется в деталях, и в этом случае детали были довольно задействованы из-за зарождающейся документации, которую мы надеемся улучшить с помощью этого поста.
Создание 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
Ничто в этой статье не должно восприниматься как юридический или инвестиционный совет.