Подпись сообщений — EIP-191
Как подписать текстовое сообщение через MetaMask и ethers.js v6, проверить подпись через EIP-191 и восстановить адрес подписанта.
Что такое подпись сообщения
Подпись сообщения доказывает, что пользователь контролирует приватный ключ адреса.
Это не транзакция. Сообщение не попадает в блокчейн, gas не тратится, State не меняется. Такой механизм используют для входа через кошелёк, подтверждения владения адресом и off-chain действий.
Подписываем сообщение и проверяем адрес
import { BrowserProvider, verifyMessage } from "ethers";
import type { Eip1193Provider } from "ethers";
declare global {
interface Window {
ethereum?: Eip1193Provider;
}
}
const app = document.createElement("main");
app.innerHTML = `
<button id="sign-message">Подписать сообщение</button>
<pre id="signature-info">Сообщение не подписано</pre>
`;
document.body.appendChild(app);
const button = document.querySelector<HTMLButtonElement>("#sign-message");
const output = document.querySelector<HTMLPreElement>("#signature-info");
if (!button || !output) {
throw new Error("Не удалось найти элементы интерфейса");
}
async function signEip191Message() {
if (!window.ethereum) {
output.textContent = "MetaMask не найден. Установите расширение браузера.";
return;
}
const provider = new BrowserProvider(window.ethereum);
await provider.send("eth_requestAccounts", []);
const signer = await provider.getSigner();
const address = await signer.getAddress();
const message = `Вход на kitaev.tech
Адрес: ${address}
Nonce: 2026-06-20T14:00:00.000Z`;
const signature = await signer.signMessage(message);
const recoveredAddress = verifyMessage(message, signature);
const isValid = recoveredAddress.toLowerCase() === address.toLowerCase();
output.textContent = [
`Адрес: ${address}`,
`Сообщение:`,
message,
``,
`Подпись: ${signature}`,
`Восстановленный адрес: ${recoveredAddress}`,
`Подпись валидна: ${isValid}`,
].join("\n");
}
button.addEventListener("click", () => {
signEip191Message().catch((error: unknown) => {
const message = error instanceof Error ? error.message : "Неизвестная ошибка";
output.textContent = `Ошибка подписи: ${message}`;
});
});MetaMask покажет пользователю текст сообщения и попросит подтвердить подпись. До подтверждения сайт не получает подпись.
Как это работает
provider.getSigner() — получает signer для аккаунта, выбранного в MetaMask.
signer.signMessage(message) — подписывает сообщение по EIP-191. ethers добавляет служебный префикс перед подписью.
verifyMessage(message, signature) — восстанавливает адрес, приватный ключ которого создал подпись.
recoveredAddress === address — проверяет, что подпись действительно принадлежит ожидаемому адресу.
Nonce — одноразовое значение. В реальном логине backend должен выдавать новый nonce для каждой попытки входа.
Что именно подписывает EIP-191
Кошелёк не подписывает строку напрямую. Для обычной personal message подписи к сообщению добавляется префикс:
\x19Ethereum Signed Message:
<длина сообщения>
<сообщение>Этот префикс защищает пользователя от ситуации, где обычный текст можно выдать за транзакцию или структурированные данные другого формата.
Если строка выглядит как "0x1234", signMessage() подпишет её как шесть UTF-8 символов. Чтобы подписать байты 0x12 и 0x34, нужно передать Uint8Array.
Частые ошибки
Использовать подпись как транзакцию → EIP-191 подпись ничего не отправляет в сеть. Она только доказывает контроль адреса.
Подписывать один и тот же текст для логина → добавляйте nonce, домен и срок действия. Иначе подпись можно переиспользовать.
Проверять подпись без исходного сообщения → для verifyMessage() нужны и сообщение, и подпись. Если текст изменится хотя бы на один символ, адрес не восстановится корректно.
Что дальше
- Подключение MetaMask через BrowserProvider — получить signer из браузерного кошелька
- Провайдер vs Сайнер — понять, почему подпись делает signer, а не provider
- Смена сети — addChain и switchChain — проверить сеть пользователя перед транзакциями