Невозможно проверить сообщение, подписанное адаптером-кошельком.

Создав подписанное сообщение, я не уверен, как использовать полученную подпись для проверки сообщения с помощью publicKey.

Мой вариант использования: я хочу использовать кошелек Solana для входа на сервер API с таким шаблоном, как:

  1. GET message: String (from API server)
  2. sign message with privateKey
  3. POST signature (to API server)
  4. verify signature with stored publicKey

Я попытался использовать nodeJS crypto.verify чтобы декодировать подписанное сообщение на стороне API, но я немного не в себе вникаю в буферы и эллиптические кривые:

      // Front-end code
const toHexString = (buffer: Buffer) =>
  buffer.reduce((str, byte) => str + byte.toString(16).padStart(2, "0"), "");

const data = new TextEncoder().encode('message to verify');
const signed = await wallet.sign(data, "hex");
await setLogin({ // sends API post call to backend
  variables: {
    publicAddress: walletPublicKey,
    signature: toHexString(signed.signature),
  },
});

// Current WIP for backend code
const ALGORITHM = "ed25519";
const fromHexString = (hexString) =>
  new Uint8Array(hexString.match(/.{1,2}/g).map((byte) => parseInt(byte, 16)));
const signature = fromHexString(args.signature);
const nonceUint8 = new TextEncoder().encode('message to verify');
const verified = crypto.verify(
  ALGORITHM,
  nonceUint8,
  `-----BEGIN PUBLIC KEY-----\n${user.publicAddress}\n-----END PUBLIC KEY-----`,
  signature
);
console.log("isVerified: ", verified);

Я почти уверен, что поступаю неправильно, и должен быть очевидный метод, который мне не хватает.

По мере того, как пространство созревает, я ожидаю, что функция verify или lib будет потреблять вывод const signed = await wallet.sign(data, "hex");

Что-то вроде:

      import { VerifyMessage } from '@solana/web3.js';

const verified = VerifyMessage(message, publicKey, signature, 'hex');

Но после 3 дней упорных усилий я начинаю выходить за пределы своих возможностей, и мой мозг не выдерживает. Любая помощь или направление, куда смотреть, приветствуются 🙏

5 ответов

Solved with input from the fantastic Project Serum discord devs. High level solution is to use libs that are also used in the sol-wallet-adapter repo, namely and bs58:

      const signatureUint8 = base58.decode(args.signature);
const nonceUint8 = new TextEncoder().encode(user?.nonce);
const pubKeyUint8 = base58.decode(user?.publicAddress);

nacl.sign.detached.verify(nonceUint8, signatureUint8, pubKeyUint8)
// true

Для любых разработчиков, которые читают это и могут реализовывать подобную функциональность, может быть предпочтительнее использовать https://www.npmjs.com/package/elliptic в качестве формата uint8Array, представленного tweetnacl означает, что для отправки подписей через JSON требуется кодирование / декодирование с использованием библиотеки bs58, когда elliptic по умолчанию обрабатывает шестнадцатеричные строки.

I recommend staying in solana-labs trail and use tweetnacl

spl-token-wallet (sollet.io) signs an arbitrary message with nacl.sign.detached(message, this.account.secretKey)

https://github.com/project-serum/spl-token-wallet/blob/9c9f1d48a589218ffe0f54b7d2f3fb29d84f7b78/src/utils/walletProvider/localStorage.js#L65-L67

on the other end, verify is done with

in @solana/web3.jshttps://github.com/solana-labs/solana/blob/master/web3.js/src/transaction.ts#L560

Use nacl.sign.detached.verify in your backend and you should be good. I also recommend avoiding any data format manipulation, I am not sure what you were trying to do but if you do verify that each step is correct.

Для iOS solana.request вызовет ошибку. Используйте solana.signMessage и base58 для кодирования подписи.

      var _signature = ''; 
try {
  signedMessage = await window.solana.request({
    method: "signMessage",
    params: {
      message: encodedMessage
    },
  }); 
  _signature = signedMessage.signature; 
} catch (e) { 
  try {
    signedMessage = await window.solana.signMessage(encodedMessage); 
    _signature = base58.encode(signedMessage.signature); 
  } catch (e1) {
    alert(e1.message);
  }
}
// 
try {
  signIn('credentials',
    {
      publicKey: signedMessage.publicKey,
      signature: _signature,
      callbackUrl: `${window.location.origin}/`
    }
  )
} catch (e) {
  alert(e.message);
}

Подписание и кодировка base64:

      const data = new TextEncoder().encode(message);
const signature = await wallet.signMessage(data); // Uint8Array
const signatureBase64 = Buffer.from(signature).toString('base64')

Декодирование и проверка Base64:

      const signatureUint8 = new Uint8Array(atob(signature).split('').map(c => c.charCodeAt(0)))
const messageUint8 = new TextEncoder().encode(message)
const pubKeyUint8 = wallet.publicKey.toBytes() // base58.decode(publicKeyAsString)
const result = nacl.sign.detached.verify(messageUint8, signatureUint8, pubKeyUint8) // true or false

Пример полного кода: https://github.com/enginer/solana-message-sign-verify-example

Мне нужно было преобразовать Uint8Array в строку и преобразовать его обратно в Uint8Array для связи по HTTP. я нашел toLocaleStringметод Uint8Array полезен в этом случае. Он выводит целые числа, разделенные запятыми, в виде строки.

      const signedMessage = await window.solana.signMessage(encodedMessage, "utf8");
const signature = signedMessage.signature.toLocaleString();

И затем вы можете преобразовать его обратно в Uint8Array с помощью следующего кода.

      const signatureUint8 = new Uint8Array(signature.split(",").map(Number));

Редактировать

Приведенное выше решение работало на рабочем столе, но когда я попробовал свой код в браузере iOS кошелька Phantom, он выдал ошибку. Я предполагаю, что метод toLocaleString недоступен в этом браузере. Я нашел более надежное решение для преобразования Uint8Array в строку с разделителями-запятыми.

      Array.apply([], signedMessage.signature).join(",")
Другие вопросы по тегам